mirror of
https://github.com/element-hq/element-web
synced 2024-11-28 04:21:57 +03:00
Merge branch 'develop' into rav/update_status_bar
This commit is contained in:
commit
b087157855
21 changed files with 229 additions and 182 deletions
|
@ -92,6 +92,7 @@ class ContentMessages {
|
|||
this.inprogress.push(upload);
|
||||
dis.dispatch({action: 'upload_started'});
|
||||
|
||||
var error;
|
||||
var self = this;
|
||||
return def.promise.then(function() {
|
||||
upload.promise = matrixClient.uploadContent(file);
|
||||
|
@ -103,11 +104,10 @@ class ContentMessages {
|
|||
dis.dispatch({action: 'upload_progress', upload: upload});
|
||||
}
|
||||
}).then(function(url) {
|
||||
dis.dispatch({action: 'upload_finished', upload: upload});
|
||||
content.url = url;
|
||||
return matrixClient.sendMessage(roomId, content);
|
||||
}, function(err) {
|
||||
dis.dispatch({action: 'upload_failed', upload: upload});
|
||||
error = err;
|
||||
if (!upload.canceled) {
|
||||
var desc = "The file '"+upload.fileName+"' failed to upload.";
|
||||
if (err.http_status == 413) {
|
||||
|
@ -128,6 +128,12 @@ class ContentMessages {
|
|||
break;
|
||||
}
|
||||
}
|
||||
if (error) {
|
||||
dis.dispatch({action: 'upload_failed', upload: upload});
|
||||
}
|
||||
else {
|
||||
dis.dispatch({action: 'upload_finished', upload: upload});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -17,13 +17,15 @@ limitations under the License.
|
|||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var ReactDOMServer = require('react-dom/server')
|
||||
var sanitizeHtml = require('sanitize-html');
|
||||
var highlight = require('highlight.js');
|
||||
|
||||
var sanitizeHtmlParams = {
|
||||
allowedTags: [
|
||||
'font', // custom to matrix. deliberately no h1/h2 to stop people shouting.
|
||||
'font', // custom to matrix for IRC-style font coloring
|
||||
'del', // for markdown
|
||||
// deliberately no h1/h2 to stop people shouting.
|
||||
'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
|
||||
'nl', 'li', 'b', 'i', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div',
|
||||
'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre'
|
||||
|
@ -56,24 +58,17 @@ class Highlighter {
|
|||
this._key = 0;
|
||||
}
|
||||
|
||||
applyHighlights(safeSnippet, highlights) {
|
||||
applyHighlights(safeSnippet, safeHighlights) {
|
||||
var lastOffset = 0;
|
||||
var offset;
|
||||
var nodes = [];
|
||||
|
||||
// XXX: when highlighting HTML, synapse performs the search on the plaintext body,
|
||||
// but we're attempting to apply the highlights here to the HTML body. This is
|
||||
// never going to end well - we really should be hooking into the sanitzer HTML
|
||||
// parser to only attempt to highlight text nodes to avoid corrupting tags.
|
||||
// If and when this happens, we'll probably have to split his method in two between
|
||||
// HTML and plain-text highlighting.
|
||||
|
||||
var safeHighlight = this.html ? sanitizeHtml(highlights[0], sanitizeHtmlParams) : highlights[0];
|
||||
var safeHighlight = safeHighlights[0];
|
||||
while ((offset = safeSnippet.toLowerCase().indexOf(safeHighlight.toLowerCase(), lastOffset)) >= 0) {
|
||||
// handle preamble
|
||||
if (offset > lastOffset) {
|
||||
var subSnippet = safeSnippet.substring(lastOffset, offset);
|
||||
nodes = nodes.concat(this._applySubHighlights(subSnippet, highlights));
|
||||
nodes = nodes.concat(this._applySubHighlights(subSnippet, safeHighlights));
|
||||
}
|
||||
|
||||
// do highlight
|
||||
|
@ -85,15 +80,15 @@ class Highlighter {
|
|||
// handle postamble
|
||||
if (lastOffset != safeSnippet.length) {
|
||||
var subSnippet = safeSnippet.substring(lastOffset, undefined);
|
||||
nodes = nodes.concat(this._applySubHighlights(subSnippet, highlights));
|
||||
nodes = nodes.concat(this._applySubHighlights(subSnippet, safeHighlights));
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
_applySubHighlights(safeSnippet, highlights) {
|
||||
if (highlights[1]) {
|
||||
_applySubHighlights(safeSnippet, safeHighlights) {
|
||||
if (safeHighlights[1]) {
|
||||
// recurse into this range to check for the next set of highlight matches
|
||||
return this.applyHighlights(safeSnippet, highlights.slice(1));
|
||||
return this.applyHighlights(safeSnippet, safeHighlights.slice(1));
|
||||
}
|
||||
else {
|
||||
// no more highlights to be found, just return the unhighlighted string
|
||||
|
@ -131,7 +126,7 @@ module.exports = {
|
|||
*
|
||||
* content: 'content' of the MatrixEvent
|
||||
*
|
||||
* highlights: optional list of words to highlight
|
||||
* highlights: optional list of words to highlight, ordered by longest word first
|
||||
*
|
||||
* opts.onHighlightClick: optional callback function to be called when a
|
||||
* highlighted word is clicked
|
||||
|
@ -143,26 +138,42 @@ module.exports = {
|
|||
|
||||
var safeBody;
|
||||
if (isHtml) {
|
||||
safeBody = sanitizeHtml(content.formatted_body, sanitizeHtmlParams);
|
||||
} else {
|
||||
safeBody = content.body;
|
||||
}
|
||||
|
||||
var body;
|
||||
// XXX: We sanitize the HTML whilst also highlighting its text nodes, to avoid accidentally trying
|
||||
// to highlight HTML tags themselves. However, this does mean that we don't highlight textnodes which
|
||||
// are interrupted by HTML tags (not that we did before) - e.g. foo<span/>bar won't get highlighted
|
||||
// by an attempt to search for 'foobar'. Then again, the search query probably wouldn't work either
|
||||
try {
|
||||
if (highlights && highlights.length > 0) {
|
||||
var highlighter = new Highlighter(isHtml, "mx_EventTile_searchHighlight", opts.onHighlightClick);
|
||||
body = highlighter.applyHighlights(safeBody, highlights);
|
||||
var safeHighlights = highlights.map(function(highlight) {
|
||||
return sanitizeHtml(highlight, sanitizeHtmlParams);
|
||||
});
|
||||
// XXX: hacky bodge to temporarily apply a textFilter to the sanitizeHtmlParams structure.
|
||||
sanitizeHtmlParams.textFilter = function(safeText) {
|
||||
return highlighter.applyHighlights(safeText, safeHighlights).map(function(span) {
|
||||
// XXX: rather clunky conversion from the react nodes returned by applyHighlights
|
||||
// (which need to be nodes for the non-html highlighting case), to convert them
|
||||
// back into raw HTML given that's what sanitize-html works in terms of.
|
||||
return ReactDOMServer.renderToString(span);
|
||||
}).join('');
|
||||
};
|
||||
}
|
||||
safeBody = sanitizeHtml(content.formatted_body, sanitizeHtmlParams);
|
||||
}
|
||||
finally {
|
||||
delete sanitizeHtmlParams.textFilter;
|
||||
}
|
||||
return <span className="markdown-body" dangerouslySetInnerHTML={{ __html: safeBody }} />;
|
||||
} else {
|
||||
safeBody = content.body;
|
||||
if (highlights && highlights.length > 0) {
|
||||
var highlighter = new Highlighter(isHtml, "mx_EventTile_searchHighlight", opts.onHighlightClick);
|
||||
return highlighter.applyHighlights(safeBody, highlights);
|
||||
}
|
||||
else {
|
||||
if (isHtml) {
|
||||
body = <span className="markdown-body" dangerouslySetInnerHTML={{ __html: safeBody }} />;
|
||||
}
|
||||
else {
|
||||
body = safeBody;
|
||||
return safeBody;
|
||||
}
|
||||
}
|
||||
|
||||
return body;
|
||||
},
|
||||
|
||||
highlightDom: function(element) {
|
||||
|
|
|
@ -182,6 +182,9 @@ var Notifier = {
|
|||
if (state === "PREPARED" || state === "SYNCING") {
|
||||
this.isPrepared = true;
|
||||
}
|
||||
else if (state === "STOPPED" || state === "ERROR") {
|
||||
this.isPrepared = false;
|
||||
}
|
||||
},
|
||||
|
||||
onRoomTimeline: function(ev, room, toStartOfTimeline) {
|
||||
|
|
|
@ -352,11 +352,12 @@ module.exports = {
|
|||
},
|
||||
|
||||
getCommandList: function() {
|
||||
// Return all the commands plus /me which isn't handled like normal commands
|
||||
// Return all the commands plus /me and /markdown which aren't handled like normal commands
|
||||
var cmds = Object.keys(commands).sort().map(function(cmdKey) {
|
||||
return commands[cmdKey];
|
||||
})
|
||||
cmds.push(new Command("me", "<action>", function(){}));
|
||||
cmds.push(new Command("markdown", "<on|off>", function(){}));
|
||||
|
||||
return cmds;
|
||||
}
|
||||
|
|
|
@ -76,6 +76,7 @@ var cached = false;
|
|||
function calcCssFixups() {
|
||||
for (var i = 0; i < document.styleSheets.length; i++) {
|
||||
var ss = document.styleSheets[i];
|
||||
if (!ss) continue; // well done safari >:(
|
||||
// Chromium apparently sometimes returns null here; unsure why.
|
||||
// see $14534907369972FRXBx:matrix.org in HQ
|
||||
// ...ah, it's because there's a third party extension like
|
||||
|
|
|
@ -30,6 +30,7 @@ class UserActivity {
|
|||
* Start listening to user activity
|
||||
*/
|
||||
start() {
|
||||
document.onmousedown = this._onUserActivity.bind(this);
|
||||
document.onmousemove = this._onUserActivity.bind(this);
|
||||
document.onkeypress = this._onUserActivity.bind(this);
|
||||
// can't use document.scroll here because that's only the document
|
||||
|
@ -46,6 +47,7 @@ class UserActivity {
|
|||
* Stop tracking user activity
|
||||
*/
|
||||
stop() {
|
||||
document.onmousedown = undefined;
|
||||
document.onmousemove = undefined;
|
||||
document.onkeypress = undefined;
|
||||
window.removeEventListener('wheel', this._onUserActivity.bind(this), true);
|
||||
|
|
|
@ -175,7 +175,7 @@ module.exports = React.createClass({
|
|||
guest: true
|
||||
});
|
||||
}, function(err) {
|
||||
console.error(err.data);
|
||||
console.error("Failed to register as guest: " + err + " " + err.data);
|
||||
self._setAutoRegisterAsGuest(false);
|
||||
});
|
||||
},
|
||||
|
@ -316,9 +316,6 @@ module.exports = React.createClass({
|
|||
});
|
||||
break;
|
||||
case 'view_room':
|
||||
// by default we autoPeek rooms, unless we were called explicitly with
|
||||
// autoPeek=false by something like RoomDirectory who has already peeked
|
||||
this.setState({ autoPeek : payload.auto_peek === false ? false : true });
|
||||
this._viewRoom(payload.room_id, payload.show_settings, payload.event_id);
|
||||
break;
|
||||
case 'view_prev_room':
|
||||
|
@ -880,7 +877,6 @@ module.exports = React.createClass({
|
|||
eventId={this.state.initialEventId}
|
||||
highlightedEventId={this.state.highlightedEventId}
|
||||
eventPixelOffset={this.state.initialEventPixelOffset}
|
||||
autoPeek={this.state.autoPeek}
|
||||
key={this.state.currentRoom}
|
||||
ConferenceHandler={this.props.ConferenceHandler} />
|
||||
);
|
||||
|
@ -974,7 +970,9 @@ module.exports = React.createClass({
|
|||
onRegisterClick={this.onRegisterClick}
|
||||
homeserverUrl={this.props.config.default_hs_url}
|
||||
identityServerUrl={this.props.config.default_is_url}
|
||||
onForgotPasswordClick={this.onForgotPasswordClick} />
|
||||
onForgotPasswordClick={this.onForgotPasswordClick}
|
||||
onLoginAsGuestClick={this.props.enableGuest && this.props.config && this.props.config.default_hs_url ? this._registerAsGuest: undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,7 +64,10 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
// we may have entirely lost our client as we're logging out before clicking login on the guest bar...
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("sync", this.onSyncStateChange);
|
||||
}
|
||||
},
|
||||
|
||||
onSyncStateChange: function(state, prevState) {
|
||||
|
|
|
@ -60,12 +60,11 @@ module.exports = React.createClass({
|
|||
displayName: 'RoomView',
|
||||
propTypes: {
|
||||
ConferenceHandler: React.PropTypes.any,
|
||||
roomId: React.PropTypes.string,
|
||||
autoPeek: React.PropTypes.bool, // Now unused, left here temporarily to avoid merge conflicts with @richvdh's branch.
|
||||
|
||||
roomId: React.PropTypes.string.isRequired,
|
||||
|
||||
// id of an event to jump to. If not given, will use the read-up-to-marker.
|
||||
// id of an event to jump to. If not given, will go to the end of the
|
||||
// live timeline.
|
||||
eventId: React.PropTypes.string,
|
||||
|
||||
// where to position the event given by eventId, in pixels from the
|
||||
|
@ -76,14 +75,6 @@ module.exports = React.createClass({
|
|||
// ID of an event to highlight. If undefined, no event will be highlighted.
|
||||
// Typically this will either be the same as 'eventId', or undefined.
|
||||
highlightedEventId: React.PropTypes.string,
|
||||
|
||||
autoPeek: React.PropTypes.bool, // should we try to peek the room on mount, or has whoever invoked us already initiated a peek?
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
autoPeek: true,
|
||||
}
|
||||
},
|
||||
|
||||
/* properties in RoomView objects include:
|
||||
|
@ -155,11 +146,6 @@ module.exports = React.createClass({
|
|||
// We can /peek though. If it fails then we present the join UI. If it
|
||||
// succeeds then great, show the preview (but we still may be able to /join!).
|
||||
if (!this.state.room) {
|
||||
if (!this.props.autoPeek) {
|
||||
console.log("No room loaded, and autopeek disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Attempting to peek into room %s", this.props.roomId);
|
||||
|
||||
roomProm = MatrixClientPeg.get().peekInRoom(this.props.roomId).then((room) => {
|
||||
|
@ -193,11 +179,6 @@ module.exports = React.createClass({
|
|||
|
||||
_initTimeline: function(props) {
|
||||
var initialEvent = props.eventId;
|
||||
if (!initialEvent) {
|
||||
// go to the 'read-up-to' mark if no explicit event given
|
||||
initialEvent = this.state.readMarkerEventId;
|
||||
}
|
||||
|
||||
var pixelOffset = props.eventPixelOffset;
|
||||
return this._loadTimeline(initialEvent, pixelOffset);
|
||||
},
|
||||
|
@ -486,20 +467,6 @@ module.exports = React.createClass({
|
|||
readMarkerEventId: readMarkerEventId,
|
||||
readMarkerGhostEventId: readMarkerGhostEventId,
|
||||
});
|
||||
|
||||
|
||||
// if the scrollpanel is following the timeline, attempt to scroll
|
||||
// it to bring the read message up to the middle of the panel. This
|
||||
// will have no immediate effect (since we are already at the
|
||||
// bottom), but will ensure that if there is no further user
|
||||
// activity, but room activity continues, the read message will
|
||||
// scroll up to the middle of the window, but no further.
|
||||
//
|
||||
// we do this here as well as in sendReadReceipt to deal with
|
||||
// people using two clients at once.
|
||||
if (this.refs.messagePanel && this.state.atEndOfLiveTimeline) {
|
||||
this.refs.messagePanel.scrollToToken(readMarkerEventId);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -585,14 +552,6 @@ module.exports = React.createClass({
|
|||
window.addEventListener('resize', this.onResize);
|
||||
this.onResize();
|
||||
|
||||
if (this.refs.roomView) {
|
||||
var roomView = ReactDOM.findDOMNode(this.refs.roomView);
|
||||
roomView.addEventListener('drop', this.onDrop);
|
||||
roomView.addEventListener('dragover', this.onDragOver);
|
||||
roomView.addEventListener('dragleave', this.onDragLeaveOrEnd);
|
||||
roomView.addEventListener('dragend', this.onDragLeaveOrEnd);
|
||||
}
|
||||
|
||||
this._updateTabCompleteList();
|
||||
|
||||
// XXX: EVIL HACK to autofocus inviting on empty rooms.
|
||||
|
@ -630,6 +589,16 @@ module.exports = React.createClass({
|
|||
// separate component to avoid this ridiculous dance.
|
||||
if (!this.refs.messagePanel) return;
|
||||
|
||||
if (this.refs.roomView) {
|
||||
var roomView = ReactDOM.findDOMNode(this.refs.roomView);
|
||||
if (!roomView.ondrop) {
|
||||
roomView.addEventListener('drop', this.onDrop);
|
||||
roomView.addEventListener('dragover', this.onDragOver);
|
||||
roomView.addEventListener('dragleave', this.onDragLeaveOrEnd);
|
||||
roomView.addEventListener('dragend', this.onDragLeaveOrEnd);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.refs.messagePanel.initialised) {
|
||||
this._initialiseMessagePanel();
|
||||
}
|
||||
|
@ -1159,19 +1128,6 @@ module.exports = React.createClass({
|
|||
// it failed, so allow retries next time the user is active
|
||||
this.last_rr_sent_event_id = undefined;
|
||||
});
|
||||
|
||||
// if the scrollpanel is following the timeline, attempt to scroll
|
||||
// it to bring the read message up to the middle of the panel. This
|
||||
// will have no immediate effect (since we are already at the
|
||||
// bottom), but will ensure that if there is no further user
|
||||
// activity, but room activity continues, the read message will
|
||||
// scroll up to the middle of the window, but no further.
|
||||
//
|
||||
// we do this here as well as in onRoomReceipt to cater for guest users
|
||||
// (which do not send out read receipts).
|
||||
if (this.state.atEndOfLiveTimeline) {
|
||||
this.refs.messagePanel.scrollToToken(lastReadEvent.getId());
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -1275,11 +1231,19 @@ module.exports = React.createClass({
|
|||
self.setState({
|
||||
rejecting: false
|
||||
});
|
||||
}, function(err) {
|
||||
console.error("Failed to reject invite: %s", err);
|
||||
}, function(error) {
|
||||
console.error("Failed to reject invite: %s", error);
|
||||
|
||||
var msg = error.message ? error.message : JSON.stringify(error);
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Failed to reject invite",
|
||||
description: msg
|
||||
});
|
||||
|
||||
self.setState({
|
||||
rejecting: false,
|
||||
rejectError: err
|
||||
rejectError: error
|
||||
});
|
||||
});
|
||||
},
|
||||
|
@ -1473,14 +1437,14 @@ module.exports = React.createClass({
|
|||
);
|
||||
}
|
||||
else {
|
||||
var joinErrorText = this.state.joinError ? "Failed to join room!" : "";
|
||||
return (
|
||||
<div className="mx_RoomView">
|
||||
<RoomHeader ref="header" room={this.state.room} simpleHeader="Join room"/>
|
||||
<div className="mx_RoomView_auxPanel">
|
||||
<RoomPreviewBar onJoinClick={ this.onJoinButtonClicked }
|
||||
canJoin={ true } canPreview={ false }/>
|
||||
<div className="error">{joinErrorText}</div>
|
||||
canJoin={ true } canPreview={ false }
|
||||
spinner={this.state.joining}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx_RoomView_messagePanel"></div>
|
||||
</div>
|
||||
|
@ -1506,10 +1470,6 @@ module.exports = React.createClass({
|
|||
} else {
|
||||
var inviteEvent = myMember.events.member;
|
||||
var inviterName = inviteEvent.sender ? inviteEvent.sender.name : inviteEvent.getSender();
|
||||
// XXX: Leaving this intentionally basic for now because invites are about to change totally
|
||||
// FIXME: This comment is now outdated - what do we need to fix? ^
|
||||
var joinErrorText = this.state.joinError ? "Failed to join room!" : "";
|
||||
var rejectErrorText = this.state.rejectError ? "Failed to reject invite!" : "";
|
||||
|
||||
// We deliberately don't try to peek into invites, even if we have permission to peek
|
||||
// as they could be a spam vector.
|
||||
|
@ -1522,9 +1482,9 @@ module.exports = React.createClass({
|
|||
<RoomPreviewBar onJoinClick={ this.onJoinButtonClicked }
|
||||
onRejectClick={ this.onRejectButtonClicked }
|
||||
inviterName={ inviterName }
|
||||
canJoin={ true } canPreview={ false }/>
|
||||
<div className="error">{joinErrorText}</div>
|
||||
<div className="error">{rejectErrorText}</div>
|
||||
canJoin={ true } canPreview={ false }
|
||||
spinner={this.state.joining}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx_RoomView_messagePanel"></div>
|
||||
</div>
|
||||
|
@ -1588,13 +1548,17 @@ module.exports = React.createClass({
|
|||
else if (this.state.guestsCanJoin && MatrixClientPeg.get().isGuest() &&
|
||||
(!myMember || myMember.membership !== "join")) {
|
||||
aux = (
|
||||
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked} canJoin={true} />
|
||||
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked} canJoin={true}
|
||||
spinner={this.state.joining}
|
||||
/>
|
||||
);
|
||||
}
|
||||
else if (this.state.canPeek &&
|
||||
(!myMember || myMember.membership !== "join")) {
|
||||
aux = (
|
||||
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked} canJoin={true} />
|
||||
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked} canJoin={true}
|
||||
spinner={this.state.joining}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1714,15 +1678,22 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
);
|
||||
} else {
|
||||
// it's important that stickyBottom = false on this, otherwise if somebody hits the
|
||||
// bottom of the loaded events when viewing historical messages, we get stuck in a
|
||||
// loop of paginating our way through the entire history of the room.
|
||||
// give the messagepanel a stickybottom if we're at the end of the
|
||||
// live timeline, so that the arrival of new events triggers a
|
||||
// scroll.
|
||||
//
|
||||
// Make sure that stickyBottom is *false* if we can paginate
|
||||
// forwards, otherwise if somebody hits the bottom of the loaded
|
||||
// events when viewing historical messages, we get stuck in a loop
|
||||
// of paginating our way through the entire history of the room.
|
||||
var stickyBottom = !this._timelineWindow.canPaginate(EventTimeline.FORWARDS);
|
||||
|
||||
messagePanel = (
|
||||
<ScrollPanel ref="messagePanel" className="mx_RoomView_messagePanel"
|
||||
onScroll={ this.onMessageListScroll }
|
||||
onFillRequest={ this.onMessageListFillRequest }
|
||||
style={ hideMessagePanel ? { display: 'none' } : {} }
|
||||
stickyBottom={ false }>
|
||||
stickyBottom={ stickyBottom }>
|
||||
<li className={scrollheader_classes}></li>
|
||||
{this.getEventTiles()}
|
||||
</ScrollPanel>
|
||||
|
|
|
@ -315,6 +315,16 @@ module.exports = React.createClass({
|
|||
onFinished={this.onPasswordChanged} />
|
||||
);
|
||||
}
|
||||
var notification_area;
|
||||
if (!MatrixClientPeg.get().isGuest()) {
|
||||
notification_area = (<div>
|
||||
<h2>Notifications</h2>
|
||||
|
||||
<div className="mx_UserSettings_section">
|
||||
<Notifications/>
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_UserSettings">
|
||||
|
@ -364,11 +374,7 @@ module.exports = React.createClass({
|
|||
{accountJsx}
|
||||
</div>
|
||||
|
||||
<h2>Notifications</h2>
|
||||
|
||||
<div className="mx_UserSettings_section">
|
||||
<Notifications/>
|
||||
</div>
|
||||
{notification_area}
|
||||
|
||||
<h2>Advanced</h2>
|
||||
|
||||
|
|
|
@ -35,7 +35,8 @@ module.exports = React.createClass({displayName: 'Login',
|
|||
// login shouldn't know or care how registration is done.
|
||||
onRegisterClick: React.PropTypes.func.isRequired,
|
||||
// login shouldn't care how password recovery is done.
|
||||
onForgotPasswordClick: React.PropTypes.func
|
||||
onForgotPasswordClick: React.PropTypes.func,
|
||||
onLoginAsGuestClick: React.PropTypes.func,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -167,6 +168,13 @@ module.exports = React.createClass({displayName: 'Login',
|
|||
var LoginFooter = sdk.getComponent("login.LoginFooter");
|
||||
var loader = this.state.busy ? <div className="mx_Login_loader"><Loader /></div> : null;
|
||||
|
||||
var loginAsGuestJsx;
|
||||
if (this.props.onLoginAsGuestClick) {
|
||||
loginAsGuestJsx =
|
||||
<a className="mx_Login_create" onClick={this.props.onLoginAsGuestClick} href="#">
|
||||
Login as guest
|
||||
</a>
|
||||
}
|
||||
return (
|
||||
<div className="mx_Login">
|
||||
<div className="mx_Login_box">
|
||||
|
@ -188,6 +196,7 @@ module.exports = React.createClass({displayName: 'Login',
|
|||
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
|
||||
Create a new account
|
||||
</a>
|
||||
{ loginAsGuestJsx }
|
||||
<br/>
|
||||
<LoginFooter />
|
||||
</div>
|
||||
|
|
|
@ -115,6 +115,9 @@ module.exports = React.createClass({
|
|||
onProcessingRegistration: function(promise) {
|
||||
var self = this;
|
||||
promise.done(function(response) {
|
||||
self.setState({
|
||||
busy: false
|
||||
});
|
||||
if (!response || !response.access_token) {
|
||||
console.warn(
|
||||
"FIXME: Register fulfilled without a final response, " +
|
||||
|
@ -126,7 +129,7 @@ module.exports = React.createClass({
|
|||
if (!response || !response.user_id || !response.access_token) {
|
||||
console.error("Final response is missing keys.");
|
||||
self.setState({
|
||||
errorText: "There was a problem processing the response."
|
||||
errorText: "Registration failed on server"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
@ -136,9 +139,6 @@ module.exports = React.createClass({
|
|||
identityServerUrl: self.registerLogic.getIdentityServerUrl(),
|
||||
accessToken: response.access_token
|
||||
});
|
||||
self.setState({
|
||||
busy: false
|
||||
});
|
||||
}, function(err) {
|
||||
if (err.message) {
|
||||
self.setState({
|
||||
|
|
|
@ -31,14 +31,22 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
onKeyDown: function(e) {
|
||||
if (e.keyCode === 27) { // escape
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.cancelPrompt();
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div>
|
||||
<div className="mx_Dialog_content">
|
||||
Sign out?
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.logOut}>Sign Out</button>
|
||||
<div className="mx_Dialog_buttons" onKeyDown={ this.onKeyDown }>
|
||||
<button autoFocus onClick={this.logOut}>Sign Out</button>
|
||||
<button onClick={this.cancelPrompt}>Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -26,9 +26,20 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
value: this.props.currentDisplayName || "Guest "+MatrixClientPeg.get().getUserIdLocalpart(),
|
||||
if (this.props.currentDisplayName) {
|
||||
return { value: this.props.currentDisplayName };
|
||||
}
|
||||
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
return { value : "Guest " + MatrixClientPeg.get().getUserIdLocalpart() };
|
||||
}
|
||||
else {
|
||||
return { value : MatrixClientPeg.get().getUserIdLocalpart() };
|
||||
}
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.refs.input_value.select();
|
||||
},
|
||||
|
||||
getValue: function() {
|
||||
|
@ -54,11 +65,12 @@ module.exports = React.createClass({
|
|||
Set a Display Name
|
||||
</div>
|
||||
<div className="mx_Dialog_content">
|
||||
Your display name is how you'll appear to others when you speak in rooms. What would you like it to be?
|
||||
Your display name is how you'll appear to others when you speak in rooms.<br/>
|
||||
What would you like it to be?
|
||||
</div>
|
||||
<form onSubmit={this.onFormSubmit}>
|
||||
<div className="mx_Dialog_content">
|
||||
<input type="text" value={this.state.value}
|
||||
<input type="text" ref="input_value" value={this.state.value}
|
||||
autoFocus={true} onChange={this.onValueChange} size="30"
|
||||
className="mx_SetDisplayNameDialog_input"
|
||||
/>
|
||||
|
|
|
@ -51,7 +51,7 @@ module.exports = React.createClass({
|
|||
if (this.props.truncateAt >= 0) {
|
||||
var overflowCount = childCount - this.props.truncateAt;
|
||||
|
||||
if (overflowCount > 0) {
|
||||
if (overflowCount > 1) {
|
||||
overflowJsx = this.props.createOverflowElement(
|
||||
overflowCount, childCount
|
||||
);
|
||||
|
|
|
@ -42,9 +42,11 @@ module.exports = React.createClass({
|
|||
// TODO: Keep this list bleeding-edge up-to-date. Practically speaking,
|
||||
// it will do for now not being updated as random new users join different
|
||||
// rooms as this list will be reloaded every room swap.
|
||||
if (this._room) {
|
||||
this._userList = MatrixClientPeg.get().getUsers().filter((u) => {
|
||||
return !this._room.hasMembershipState(u.userId, "join");
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onInvite: function(ev) {
|
||||
|
@ -87,7 +89,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
return (
|
||||
<SearchableEntityList searchPlaceholderText={"Invite / Search"}
|
||||
<SearchableEntityList searchPlaceholderText={"Invite/search by name, email, id"}
|
||||
onSubmit={this.props.onInvite}
|
||||
onQueryChanged={this.onSearchQueryChanged}
|
||||
entities={entities}
|
||||
|
|
|
@ -35,21 +35,23 @@ var invite_defer = q.defer();
|
|||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MemberList',
|
||||
|
||||
getInitialState: function() {
|
||||
if (!this.props.roomId) return { members: [] };
|
||||
var cli = MatrixClientPeg.get();
|
||||
var room = cli.getRoom(this.props.roomId);
|
||||
if (!room) return { members: [] };
|
||||
|
||||
this.memberDict = this.getMemberDict();
|
||||
|
||||
var members = this.roomMembers(INITIAL_LOAD_NUM_MEMBERS);
|
||||
return {
|
||||
members: members,
|
||||
var state = {
|
||||
members: [],
|
||||
// ideally we'd size this to the page height, but
|
||||
// in practice I find that a little constraining
|
||||
truncateAt: INITIAL_LOAD_NUM_MEMBERS,
|
||||
};
|
||||
if (!this.props.roomId) return state;
|
||||
var cli = MatrixClientPeg.get();
|
||||
var room = cli.getRoom(this.props.roomId);
|
||||
if (!room) return state;
|
||||
|
||||
this.memberDict = this.getMemberDict();
|
||||
|
||||
state.members = this.roomMembers(INITIAL_LOAD_NUM_MEMBERS);
|
||||
return state;
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
|
@ -325,7 +327,7 @@ module.exports = React.createClass({
|
|||
|
||||
var memberList = self.state.members.filter(function(userId) {
|
||||
var m = self.memberDict[userId];
|
||||
if (query && m.name.toLowerCase().indexOf(query) !== 0) {
|
||||
if (query && m.name.toLowerCase().indexOf(query) === -1) {
|
||||
return false;
|
||||
}
|
||||
return m.membership == membership;
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var sdk = require('../../../index');
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomPreviewBar',
|
||||
|
@ -27,6 +28,7 @@ module.exports = React.createClass({
|
|||
inviterName: React.PropTypes.string,
|
||||
canJoin: React.PropTypes.bool,
|
||||
canPreview: React.PropTypes.bool,
|
||||
spinner: React.PropTypes.bool,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -40,6 +42,13 @@ module.exports = React.createClass({
|
|||
render: function() {
|
||||
var joinBlock, previewBlock;
|
||||
|
||||
if (this.props.spinner) {
|
||||
var Spinner = sdk.getComponent("elements.Spinner");
|
||||
return (<div className="mx_RoomPreviewBar">
|
||||
<Spinner />
|
||||
</div>);
|
||||
}
|
||||
|
||||
if (this.props.inviterName) {
|
||||
joinBlock = (
|
||||
<div>
|
||||
|
|
|
@ -37,12 +37,14 @@ module.exports = React.createClass({
|
|||
});
|
||||
|
||||
var areNotifsMuted = false;
|
||||
if (!MatrixClientPeg.get().isGuest()) {
|
||||
var roomPushRule = MatrixClientPeg.get().getRoomPushRule("global", this.props.room.roomId);
|
||||
if (roomPushRule) {
|
||||
if (0 <= roomPushRule.actions.indexOf("dont_notify")) {
|
||||
areNotifsMuted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: this._yankValueFromEvent("m.room.name", "name"),
|
||||
|
|
|
@ -140,6 +140,7 @@ var SearchableEntityList = React.createClass({
|
|||
}
|
||||
|
||||
var list;
|
||||
if (this.state.results.length) {
|
||||
if (this.props.truncateAt) { // caller wants list truncated
|
||||
var TruncatedList = sdk.getComponent("elements.TruncatedList");
|
||||
list = (
|
||||
|
@ -161,13 +162,15 @@ var SearchableEntityList = React.createClass({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
list = <GeminiScrollbar autoshow={true} className="mx_SearchableEntityList_listWrapper">
|
||||
{ list }
|
||||
</GeminiScrollbar>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={ "mx_SearchableEntityList " + (this.state.query.length ? "mx_SearchableEntityList_expanded" : "") }>
|
||||
{inputBox}
|
||||
<GeminiScrollbar autoshow={true} className="mx_SearchableEntityList_listWrapper">
|
||||
{ inputBox }
|
||||
{ list }
|
||||
</GeminiScrollbar>
|
||||
{ this.state.query.length ? <div className="mx_SearchableEntityList_hrWrapper"><hr/></div> : '' }
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -110,19 +110,17 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
var RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
|
||||
var avatarImg;
|
||||
// Having just set an avatar we just display that since it will take a little
|
||||
// time to propagate through to the RoomAvatar.
|
||||
if (this.props.room && !this.avatarSet) {
|
||||
var RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
|
||||
avatarImg = <RoomAvatar room={this.props.room} width={ this.props.width } height={ this.props.height } resizeMethod='crop' />;
|
||||
} else {
|
||||
var style = {
|
||||
width: this.props.width,
|
||||
height: this.props.height,
|
||||
objectFit: 'cover',
|
||||
};
|
||||
avatarImg = <img className="mx_BaseAvatar_image" src={this.state.avatarUrl} style={style} />;
|
||||
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
// XXX: FIXME: once we track in the JS what our own displayname is(!) then use it here rather than ?
|
||||
avatarImg = <BaseAvatar width={this.props.width} height={this.props.height} resizeMethod='crop'
|
||||
name='?' idName={ MatrixClientPeg.get().getUserIdLocalpart() } url={this.state.avatarUrl} />
|
||||
}
|
||||
|
||||
var uploadSection;
|
||||
|
|
Loading…
Reference in a new issue