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);
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});
}
});
}

View file

@ -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);
// 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 {
safeBody = content.body;
}
var body;
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 }} />;
if (highlights && highlights.length > 0) {
var highlighter = new Highlighter(isHtml, "mx_EventTile_searchHighlight", opts.onHighlightClick);
return highlighter.applyHighlights(safeBody, highlights);
}
else {
body = safeBody;
return safeBody;
}
}
return body;
},
highlightDom: function(element) {

View file

@ -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) {

View file

@ -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;
}

View file

@ -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

View file

@ -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);

View file

@ -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}
/>
);
}
}

View file

@ -64,7 +64,10 @@ module.exports = React.createClass({
},
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) {

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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({

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() {
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>

View file

@ -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"
/>

View file

@ -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
);

View file

@ -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.
this._userList = MatrixClientPeg.get().getUsers().filter((u) => {
return !this._room.hasMembershipState(u.userId, "join");
});
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}

View file

@ -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;

View file

@ -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>

View file

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

View file

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

View file

@ -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;