Merge branch 'develop' into rav/update_status_bar

This commit is contained in:
Matthew Hodgson 2016-02-17 18:38:47 +00:00
commit b087157855
21 changed files with 229 additions and 182 deletions

View file

@ -92,6 +92,7 @@ class ContentMessages {
this.inprogress.push(upload); this.inprogress.push(upload);
dis.dispatch({action: 'upload_started'}); dis.dispatch({action: 'upload_started'});
var error;
var self = this; var self = this;
return def.promise.then(function() { return def.promise.then(function() {
upload.promise = matrixClient.uploadContent(file); upload.promise = matrixClient.uploadContent(file);
@ -103,11 +104,10 @@ class ContentMessages {
dis.dispatch({action: 'upload_progress', upload: upload}); dis.dispatch({action: 'upload_progress', upload: upload});
} }
}).then(function(url) { }).then(function(url) {
dis.dispatch({action: 'upload_finished', upload: upload});
content.url = url; content.url = url;
return matrixClient.sendMessage(roomId, content); return matrixClient.sendMessage(roomId, content);
}, function(err) { }, function(err) {
dis.dispatch({action: 'upload_failed', upload: upload}); error = err;
if (!upload.canceled) { if (!upload.canceled) {
var desc = "The file '"+upload.fileName+"' failed to upload."; var desc = "The file '"+upload.fileName+"' failed to upload.";
if (err.http_status == 413) { if (err.http_status == 413) {
@ -128,6 +128,12 @@ class ContentMessages {
break; break;
} }
} }
if (error) {
dis.dispatch({action: 'upload_failed', upload: upload});
}
else {
dis.dispatch({action: 'upload_finished', upload: upload});
}
}); });
} }

View file

@ -17,13 +17,15 @@ limitations under the License.
'use strict'; 'use strict';
var React = require('react'); var React = require('react');
var ReactDOMServer = require('react-dom/server')
var sanitizeHtml = require('sanitize-html'); var sanitizeHtml = require('sanitize-html');
var highlight = require('highlight.js'); var highlight = require('highlight.js');
var sanitizeHtmlParams = { var sanitizeHtmlParams = {
allowedTags: [ 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 'del', // for markdown
// deliberately no h1/h2 to stop people shouting.
'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
'nl', 'li', 'b', 'i', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div', 'nl', 'li', 'b', 'i', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div',
'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre' 'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre'
@ -56,24 +58,17 @@ class Highlighter {
this._key = 0; this._key = 0;
} }
applyHighlights(safeSnippet, highlights) { applyHighlights(safeSnippet, safeHighlights) {
var lastOffset = 0; var lastOffset = 0;
var offset; var offset;
var nodes = []; var nodes = [];
// XXX: when highlighting HTML, synapse performs the search on the plaintext body, var safeHighlight = safeHighlights[0];
// 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];
while ((offset = safeSnippet.toLowerCase().indexOf(safeHighlight.toLowerCase(), lastOffset)) >= 0) { while ((offset = safeSnippet.toLowerCase().indexOf(safeHighlight.toLowerCase(), lastOffset)) >= 0) {
// handle preamble // handle preamble
if (offset > lastOffset) { if (offset > lastOffset) {
var subSnippet = safeSnippet.substring(lastOffset, offset); var subSnippet = safeSnippet.substring(lastOffset, offset);
nodes = nodes.concat(this._applySubHighlights(subSnippet, highlights)); nodes = nodes.concat(this._applySubHighlights(subSnippet, safeHighlights));
} }
// do highlight // do highlight
@ -85,15 +80,15 @@ class Highlighter {
// handle postamble // handle postamble
if (lastOffset != safeSnippet.length) { if (lastOffset != safeSnippet.length) {
var subSnippet = safeSnippet.substring(lastOffset, undefined); var subSnippet = safeSnippet.substring(lastOffset, undefined);
nodes = nodes.concat(this._applySubHighlights(subSnippet, highlights)); nodes = nodes.concat(this._applySubHighlights(subSnippet, safeHighlights));
} }
return nodes; return nodes;
} }
_applySubHighlights(safeSnippet, highlights) { _applySubHighlights(safeSnippet, safeHighlights) {
if (highlights[1]) { if (safeHighlights[1]) {
// recurse into this range to check for the next set of highlight matches // 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 { else {
// no more highlights to be found, just return the unhighlighted string // no more highlights to be found, just return the unhighlighted string
@ -131,7 +126,7 @@ module.exports = {
* *
* content: 'content' of the MatrixEvent * 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 * opts.onHighlightClick: optional callback function to be called when a
* highlighted word is clicked * highlighted word is clicked
@ -143,26 +138,42 @@ module.exports = {
var safeBody; var safeBody;
if (isHtml) { if (isHtml) {
safeBody = sanitizeHtml(content.formatted_body, sanitizeHtmlParams); // 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);
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 { } else {
safeBody = content.body; safeBody = content.body;
} if (highlights && highlights.length > 0) {
var highlighter = new Highlighter(isHtml, "mx_EventTile_searchHighlight", opts.onHighlightClick);
var body; return highlighter.applyHighlights(safeBody, highlights);
if (highlights && highlights.length > 0) {
var highlighter = new Highlighter(isHtml, "mx_EventTile_searchHighlight", opts.onHighlightClick);
body = highlighter.applyHighlights(safeBody, highlights);
}
else {
if (isHtml) {
body = <span className="markdown-body" dangerouslySetInnerHTML={{ __html: safeBody }} />;
} }
else { else {
body = safeBody; return safeBody;
} }
} }
return body;
}, },
highlightDom: function(element) { highlightDom: function(element) {

View file

@ -182,6 +182,9 @@ var Notifier = {
if (state === "PREPARED" || state === "SYNCING") { if (state === "PREPARED" || state === "SYNCING") {
this.isPrepared = true; this.isPrepared = true;
} }
else if (state === "STOPPED" || state === "ERROR") {
this.isPrepared = false;
}
}, },
onRoomTimeline: function(ev, room, toStartOfTimeline) { onRoomTimeline: function(ev, room, toStartOfTimeline) {

View file

@ -352,11 +352,12 @@ module.exports = {
}, },
getCommandList: function() { 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) { var cmds = Object.keys(commands).sort().map(function(cmdKey) {
return commands[cmdKey]; return commands[cmdKey];
}) })
cmds.push(new Command("me", "<action>", function(){})); cmds.push(new Command("me", "<action>", function(){}));
cmds.push(new Command("markdown", "<on|off>", function(){}));
return cmds; return cmds;
} }

View file

@ -76,6 +76,7 @@ var cached = false;
function calcCssFixups() { function calcCssFixups() {
for (var i = 0; i < document.styleSheets.length; i++) { for (var i = 0; i < document.styleSheets.length; i++) {
var ss = document.styleSheets[i]; var ss = document.styleSheets[i];
if (!ss) continue; // well done safari >:(
// Chromium apparently sometimes returns null here; unsure why. // Chromium apparently sometimes returns null here; unsure why.
// see $14534907369972FRXBx:matrix.org in HQ // see $14534907369972FRXBx:matrix.org in HQ
// ...ah, it's because there's a third party extension like // ...ah, it's because there's a third party extension like

View file

@ -30,6 +30,7 @@ class UserActivity {
* Start listening to user activity * Start listening to user activity
*/ */
start() { start() {
document.onmousedown = this._onUserActivity.bind(this);
document.onmousemove = this._onUserActivity.bind(this); document.onmousemove = this._onUserActivity.bind(this);
document.onkeypress = this._onUserActivity.bind(this); document.onkeypress = this._onUserActivity.bind(this);
// can't use document.scroll here because that's only the document // can't use document.scroll here because that's only the document
@ -46,6 +47,7 @@ class UserActivity {
* Stop tracking user activity * Stop tracking user activity
*/ */
stop() { stop() {
document.onmousedown = undefined;
document.onmousemove = undefined; document.onmousemove = undefined;
document.onkeypress = undefined; document.onkeypress = undefined;
window.removeEventListener('wheel', this._onUserActivity.bind(this), true); window.removeEventListener('wheel', this._onUserActivity.bind(this), true);

View file

@ -175,7 +175,7 @@ module.exports = React.createClass({
guest: true guest: true
}); });
}, function(err) { }, function(err) {
console.error(err.data); console.error("Failed to register as guest: " + err + " " + err.data);
self._setAutoRegisterAsGuest(false); self._setAutoRegisterAsGuest(false);
}); });
}, },
@ -316,9 +316,6 @@ module.exports = React.createClass({
}); });
break; break;
case 'view_room': 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); this._viewRoom(payload.room_id, payload.show_settings, payload.event_id);
break; break;
case 'view_prev_room': case 'view_prev_room':
@ -880,7 +877,6 @@ module.exports = React.createClass({
eventId={this.state.initialEventId} eventId={this.state.initialEventId}
highlightedEventId={this.state.highlightedEventId} highlightedEventId={this.state.highlightedEventId}
eventPixelOffset={this.state.initialEventPixelOffset} eventPixelOffset={this.state.initialEventPixelOffset}
autoPeek={this.state.autoPeek}
key={this.state.currentRoom} key={this.state.currentRoom}
ConferenceHandler={this.props.ConferenceHandler} /> ConferenceHandler={this.props.ConferenceHandler} />
); );
@ -974,7 +970,9 @@ module.exports = React.createClass({
onRegisterClick={this.onRegisterClick} onRegisterClick={this.onRegisterClick}
homeserverUrl={this.props.config.default_hs_url} homeserverUrl={this.props.config.default_hs_url}
identityServerUrl={this.props.config.default_is_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}
/>
); );
} }
} }

View file

@ -64,7 +64,10 @@ module.exports = React.createClass({
}, },
componentWillUnmount: function() { componentWillUnmount: function() {
MatrixClientPeg.get().removeListener("sync", this.onSyncStateChange); // 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) { onSyncStateChange: function(state, prevState) {

View file

@ -60,12 +60,11 @@ module.exports = React.createClass({
displayName: 'RoomView', displayName: 'RoomView',
propTypes: { propTypes: {
ConferenceHandler: React.PropTypes.any, 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, 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, eventId: React.PropTypes.string,
// where to position the event given by eventId, in pixels from the // 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. // ID of an event to highlight. If undefined, no event will be highlighted.
// Typically this will either be the same as 'eventId', or undefined. // Typically this will either be the same as 'eventId', or undefined.
highlightedEventId: React.PropTypes.string, 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: /* 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 // 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!). // succeeds then great, show the preview (but we still may be able to /join!).
if (!this.state.room) { 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); console.log("Attempting to peek into room %s", this.props.roomId);
roomProm = MatrixClientPeg.get().peekInRoom(this.props.roomId).then((room) => { roomProm = MatrixClientPeg.get().peekInRoom(this.props.roomId).then((room) => {
@ -193,11 +179,6 @@ module.exports = React.createClass({
_initTimeline: function(props) { _initTimeline: function(props) {
var initialEvent = props.eventId; 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; var pixelOffset = props.eventPixelOffset;
return this._loadTimeline(initialEvent, pixelOffset); return this._loadTimeline(initialEvent, pixelOffset);
}, },
@ -486,20 +467,6 @@ module.exports = React.createClass({
readMarkerEventId: readMarkerEventId, readMarkerEventId: readMarkerEventId,
readMarkerGhostEventId: readMarkerGhostEventId, 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); window.addEventListener('resize', this.onResize);
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(); this._updateTabCompleteList();
// XXX: EVIL HACK to autofocus inviting on empty rooms. // XXX: EVIL HACK to autofocus inviting on empty rooms.
@ -630,6 +589,16 @@ module.exports = React.createClass({
// separate component to avoid this ridiculous dance. // separate component to avoid this ridiculous dance.
if (!this.refs.messagePanel) return; 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) { if (!this.refs.messagePanel.initialised) {
this._initialiseMessagePanel(); this._initialiseMessagePanel();
} }
@ -1159,19 +1128,6 @@ module.exports = React.createClass({
// it failed, so allow retries next time the user is active // it failed, so allow retries next time the user is active
this.last_rr_sent_event_id = undefined; 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({ self.setState({
rejecting: false rejecting: false
}); });
}, function(err) { }, function(error) {
console.error("Failed to reject invite: %s", err); 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({ self.setState({
rejecting: false, rejecting: false,
rejectError: err rejectError: error
}); });
}); });
}, },
@ -1473,14 +1437,14 @@ module.exports = React.createClass({
); );
} }
else { else {
var joinErrorText = this.state.joinError ? "Failed to join room!" : "";
return ( return (
<div className="mx_RoomView"> <div className="mx_RoomView">
<RoomHeader ref="header" room={this.state.room} simpleHeader="Join room"/> <RoomHeader ref="header" room={this.state.room} simpleHeader="Join room"/>
<div className="mx_RoomView_auxPanel"> <div className="mx_RoomView_auxPanel">
<RoomPreviewBar onJoinClick={ this.onJoinButtonClicked } <RoomPreviewBar onJoinClick={ this.onJoinButtonClicked }
canJoin={ true } canPreview={ false }/> canJoin={ true } canPreview={ false }
<div className="error">{joinErrorText}</div> spinner={this.state.joining}
/>
</div> </div>
<div className="mx_RoomView_messagePanel"></div> <div className="mx_RoomView_messagePanel"></div>
</div> </div>
@ -1506,10 +1470,6 @@ module.exports = React.createClass({
} else { } else {
var inviteEvent = myMember.events.member; var inviteEvent = myMember.events.member;
var inviterName = inviteEvent.sender ? inviteEvent.sender.name : inviteEvent.getSender(); 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 // We deliberately don't try to peek into invites, even if we have permission to peek
// as they could be a spam vector. // as they could be a spam vector.
@ -1522,9 +1482,9 @@ module.exports = React.createClass({
<RoomPreviewBar onJoinClick={ this.onJoinButtonClicked } <RoomPreviewBar onJoinClick={ this.onJoinButtonClicked }
onRejectClick={ this.onRejectButtonClicked } onRejectClick={ this.onRejectButtonClicked }
inviterName={ inviterName } inviterName={ inviterName }
canJoin={ true } canPreview={ false }/> canJoin={ true } canPreview={ false }
<div className="error">{joinErrorText}</div> spinner={this.state.joining}
<div className="error">{rejectErrorText}</div> />
</div> </div>
<div className="mx_RoomView_messagePanel"></div> <div className="mx_RoomView_messagePanel"></div>
</div> </div>
@ -1588,13 +1548,17 @@ module.exports = React.createClass({
else if (this.state.guestsCanJoin && MatrixClientPeg.get().isGuest() && else if (this.state.guestsCanJoin && MatrixClientPeg.get().isGuest() &&
(!myMember || myMember.membership !== "join")) { (!myMember || myMember.membership !== "join")) {
aux = ( aux = (
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked} canJoin={true} /> <RoomPreviewBar onJoinClick={this.onJoinButtonClicked} canJoin={true}
spinner={this.state.joining}
/>
); );
} }
else if (this.state.canPeek && else if (this.state.canPeek &&
(!myMember || myMember.membership !== "join")) { (!myMember || myMember.membership !== "join")) {
aux = ( 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> </div>
); );
} else { } else {
// it's important that stickyBottom = false on this, otherwise if somebody hits the // give the messagepanel a stickybottom if we're at the end of the
// bottom of the loaded events when viewing historical messages, we get stuck in a // live timeline, so that the arrival of new events triggers a
// loop of paginating our way through the entire history of the room. // 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 = ( messagePanel = (
<ScrollPanel ref="messagePanel" className="mx_RoomView_messagePanel" <ScrollPanel ref="messagePanel" className="mx_RoomView_messagePanel"
onScroll={ this.onMessageListScroll } onScroll={ this.onMessageListScroll }
onFillRequest={ this.onMessageListFillRequest } onFillRequest={ this.onMessageListFillRequest }
style={ hideMessagePanel ? { display: 'none' } : {} } style={ hideMessagePanel ? { display: 'none' } : {} }
stickyBottom={ false }> stickyBottom={ stickyBottom }>
<li className={scrollheader_classes}></li> <li className={scrollheader_classes}></li>
{this.getEventTiles()} {this.getEventTiles()}
</ScrollPanel> </ScrollPanel>

View file

@ -315,6 +315,16 @@ module.exports = React.createClass({
onFinished={this.onPasswordChanged} /> 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 ( return (
<div className="mx_UserSettings"> <div className="mx_UserSettings">
@ -364,11 +374,7 @@ module.exports = React.createClass({
{accountJsx} {accountJsx}
</div> </div>
<h2>Notifications</h2> {notification_area}
<div className="mx_UserSettings_section">
<Notifications/>
</div>
<h2>Advanced</h2> <h2>Advanced</h2>

View file

@ -35,7 +35,8 @@ module.exports = React.createClass({displayName: 'Login',
// login shouldn't know or care how registration is done. // login shouldn't know or care how registration is done.
onRegisterClick: React.PropTypes.func.isRequired, onRegisterClick: React.PropTypes.func.isRequired,
// login shouldn't care how password recovery is done. // login shouldn't care how password recovery is done.
onForgotPasswordClick: React.PropTypes.func onForgotPasswordClick: React.PropTypes.func,
onLoginAsGuestClick: React.PropTypes.func,
}, },
getDefaultProps: function() { getDefaultProps: function() {
@ -167,6 +168,13 @@ module.exports = React.createClass({displayName: 'Login',
var LoginFooter = sdk.getComponent("login.LoginFooter"); var LoginFooter = sdk.getComponent("login.LoginFooter");
var loader = this.state.busy ? <div className="mx_Login_loader"><Loader /></div> : null; 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 ( return (
<div className="mx_Login"> <div className="mx_Login">
<div className="mx_Login_box"> <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="#"> <a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
Create a new account Create a new account
</a> </a>
{ loginAsGuestJsx }
<br/> <br/>
<LoginFooter /> <LoginFooter />
</div> </div>

View file

@ -115,6 +115,9 @@ module.exports = React.createClass({
onProcessingRegistration: function(promise) { onProcessingRegistration: function(promise) {
var self = this; var self = this;
promise.done(function(response) { promise.done(function(response) {
self.setState({
busy: false
});
if (!response || !response.access_token) { if (!response || !response.access_token) {
console.warn( console.warn(
"FIXME: Register fulfilled without a final response, " + "FIXME: Register fulfilled without a final response, " +
@ -126,7 +129,7 @@ module.exports = React.createClass({
if (!response || !response.user_id || !response.access_token) { if (!response || !response.user_id || !response.access_token) {
console.error("Final response is missing keys."); console.error("Final response is missing keys.");
self.setState({ self.setState({
errorText: "There was a problem processing the response." errorText: "Registration failed on server"
}); });
return; return;
} }
@ -136,9 +139,6 @@ module.exports = React.createClass({
identityServerUrl: self.registerLogic.getIdentityServerUrl(), identityServerUrl: self.registerLogic.getIdentityServerUrl(),
accessToken: response.access_token accessToken: response.access_token
}); });
self.setState({
busy: false
});
}, function(err) { }, function(err) {
if (err.message) { if (err.message) {
self.setState({ self.setState({

View file

@ -31,14 +31,22 @@ module.exports = React.createClass({
} }
}, },
onKeyDown: function(e) {
if (e.keyCode === 27) { // escape
e.stopPropagation();
e.preventDefault();
this.cancelPrompt();
}
},
render: function() { render: function() {
return ( return (
<div> <div>
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">
Sign out? Sign out?
</div> </div>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons" onKeyDown={ this.onKeyDown }>
<button onClick={this.logOut}>Sign Out</button> <button autoFocus onClick={this.logOut}>Sign Out</button>
<button onClick={this.cancelPrompt}>Cancel</button> <button onClick={this.cancelPrompt}>Cancel</button>
</div> </div>
</div> </div>

View file

@ -26,9 +26,20 @@ module.exports = React.createClass({
}, },
getInitialState: function() { getInitialState: function() {
return { if (this.props.currentDisplayName) {
value: this.props.currentDisplayName || "Guest "+MatrixClientPeg.get().getUserIdLocalpart(), 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() { getValue: function() {
@ -54,11 +65,12 @@ module.exports = React.createClass({
Set a Display Name Set a Display Name
</div> </div>
<div className="mx_Dialog_content"> <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> </div>
<form onSubmit={this.onFormSubmit}> <form onSubmit={this.onFormSubmit}>
<div className="mx_Dialog_content"> <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" autoFocus={true} onChange={this.onValueChange} size="30"
className="mx_SetDisplayNameDialog_input" className="mx_SetDisplayNameDialog_input"
/> />

View file

@ -51,7 +51,7 @@ module.exports = React.createClass({
if (this.props.truncateAt >= 0) { if (this.props.truncateAt >= 0) {
var overflowCount = childCount - this.props.truncateAt; var overflowCount = childCount - this.props.truncateAt;
if (overflowCount > 0) { if (overflowCount > 1) {
overflowJsx = this.props.createOverflowElement( overflowJsx = this.props.createOverflowElement(
overflowCount, childCount overflowCount, childCount
); );

View file

@ -42,9 +42,11 @@ module.exports = React.createClass({
// TODO: Keep this list bleeding-edge up-to-date. Practically speaking, // 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 // it will do for now not being updated as random new users join different
// rooms as this list will be reloaded every room swap. // rooms as this list will be reloaded every room swap.
this._userList = MatrixClientPeg.get().getUsers().filter((u) => { if (this._room) {
return !this._room.hasMembershipState(u.userId, "join"); this._userList = MatrixClientPeg.get().getUsers().filter((u) => {
}); return !this._room.hasMembershipState(u.userId, "join");
});
}
}, },
onInvite: function(ev) { onInvite: function(ev) {
@ -87,7 +89,7 @@ module.exports = React.createClass({
} }
return ( return (
<SearchableEntityList searchPlaceholderText={"Invite / Search"} <SearchableEntityList searchPlaceholderText={"Invite/search by name, email, id"}
onSubmit={this.props.onInvite} onSubmit={this.props.onInvite}
onQueryChanged={this.onSearchQueryChanged} onQueryChanged={this.onSearchQueryChanged}
entities={entities} entities={entities}

View file

@ -35,21 +35,23 @@ var invite_defer = q.defer();
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'MemberList', displayName: 'MemberList',
getInitialState: function() { getInitialState: function() {
if (!this.props.roomId) return { members: [] }; var state = {
var cli = MatrixClientPeg.get(); members: [],
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,
// ideally we'd size this to the page height, but // ideally we'd size this to the page height, but
// in practice I find that a little constraining // in practice I find that a little constraining
truncateAt: INITIAL_LOAD_NUM_MEMBERS, 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() { componentWillMount: function() {
@ -325,7 +327,7 @@ module.exports = React.createClass({
var memberList = self.state.members.filter(function(userId) { var memberList = self.state.members.filter(function(userId) {
var m = self.memberDict[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 false;
} }
return m.membership == membership; return m.membership == membership;

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict'; 'use strict';
var React = require('react'); var React = require('react');
var sdk = require('../../../index');
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'RoomPreviewBar', displayName: 'RoomPreviewBar',
@ -27,6 +28,7 @@ module.exports = React.createClass({
inviterName: React.PropTypes.string, inviterName: React.PropTypes.string,
canJoin: React.PropTypes.bool, canJoin: React.PropTypes.bool,
canPreview: React.PropTypes.bool, canPreview: React.PropTypes.bool,
spinner: React.PropTypes.bool,
}, },
getDefaultProps: function() { getDefaultProps: function() {
@ -40,6 +42,13 @@ module.exports = React.createClass({
render: function() { render: function() {
var joinBlock, previewBlock; var joinBlock, previewBlock;
if (this.props.spinner) {
var Spinner = sdk.getComponent("elements.Spinner");
return (<div className="mx_RoomPreviewBar">
<Spinner />
</div>);
}
if (this.props.inviterName) { if (this.props.inviterName) {
joinBlock = ( joinBlock = (
<div> <div>

View file

@ -37,10 +37,12 @@ module.exports = React.createClass({
}); });
var areNotifsMuted = false; var areNotifsMuted = false;
var roomPushRule = MatrixClientPeg.get().getRoomPushRule("global", this.props.room.roomId); if (!MatrixClientPeg.get().isGuest()) {
if (roomPushRule) { var roomPushRule = MatrixClientPeg.get().getRoomPushRule("global", this.props.room.roomId);
if (0 <= roomPushRule.actions.indexOf("dont_notify")) { if (roomPushRule) {
areNotifsMuted = true; if (0 <= roomPushRule.actions.indexOf("dont_notify")) {
areNotifsMuted = true;
}
} }
} }

View file

@ -140,34 +140,37 @@ var SearchableEntityList = React.createClass({
} }
var list; var list;
if (this.props.truncateAt) { // caller wants list truncated if (this.state.results.length) {
var TruncatedList = sdk.getComponent("elements.TruncatedList"); if (this.props.truncateAt) { // caller wants list truncated
list = ( var TruncatedList = sdk.getComponent("elements.TruncatedList");
<TruncatedList className="mx_SearchableEntityList_list" list = (
truncateAt={this.state.truncateAt} // use state truncation as it may be expanded <TruncatedList className="mx_SearchableEntityList_list"
createOverflowElement={this._createOverflowEntity}> truncateAt={this.state.truncateAt} // use state truncation as it may be expanded
{this.state.results.map((entity) => { createOverflowElement={this._createOverflowEntity}>
return entity.getJsx(); {this.state.results.map((entity) => {
})} return entity.getJsx();
</TruncatedList> })}
); </TruncatedList>
} );
else { }
list = ( else {
<div className="mx_SearchableEntityList_list"> list = (
{this.state.results.map((entity) => { <div className="mx_SearchableEntityList_list">
return entity.getJsx(); {this.state.results.map((entity) => {
})} return entity.getJsx();
</div> })}
); </div>
);
}
list = <GeminiScrollbar autoshow={true} className="mx_SearchableEntityList_listWrapper">
{ list }
</GeminiScrollbar>;
} }
return ( return (
<div className={ "mx_SearchableEntityList " + (this.state.query.length ? "mx_SearchableEntityList_expanded" : "") }> <div className={ "mx_SearchableEntityList " + (this.state.query.length ? "mx_SearchableEntityList_expanded" : "") }>
{inputBox} { inputBox }
<GeminiScrollbar autoshow={true} className="mx_SearchableEntityList_listWrapper"> { list }
{ list }
</GeminiScrollbar>
{ this.state.query.length ? <div className="mx_SearchableEntityList_hrWrapper"><hr/></div> : '' } { this.state.query.length ? <div className="mx_SearchableEntityList_hrWrapper"><hr/></div> : '' }
</div> </div>
); );

View file

@ -110,19 +110,17 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
var RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
var avatarImg; var avatarImg;
// Having just set an avatar we just display that since it will take a little // Having just set an avatar we just display that since it will take a little
// time to propagate through to the RoomAvatar. // time to propagate through to the RoomAvatar.
if (this.props.room && !this.avatarSet) { 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' />; avatarImg = <RoomAvatar room={this.props.room} width={ this.props.width } height={ this.props.height } resizeMethod='crop' />;
} else { } else {
var style = { var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
width: this.props.width, // XXX: FIXME: once we track in the JS what our own displayname is(!) then use it here rather than ?
height: this.props.height, avatarImg = <BaseAvatar width={this.props.width} height={this.props.height} resizeMethod='crop'
objectFit: 'cover', name='?' idName={ MatrixClientPeg.get().getUserIdLocalpart() } url={this.state.avatarUrl} />
};
avatarImg = <img className="mx_BaseAvatar_image" src={this.state.avatarUrl} style={style} />;
} }
var uploadSection; var uploadSection;