From 11f3d5f9930320595ead9c42784cfd518b30b126 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 12 Apr 2016 17:18:32 +0100 Subject: [PATCH 01/51] make the UI fadable to help with decluttering --- src/components/structures/MatrixChat.js | 25 +++++++++++++------ src/components/structures/MessagePanel.js | 10 ++++++-- src/components/structures/RoomView.js | 19 ++++++++------ src/components/structures/TimelinePanel.js | 4 +++ src/components/views/rooms/MessageComposer.js | 5 +++- src/components/views/rooms/RoomSettings.js | 15 +++++++++++ 6 files changed, 60 insertions(+), 18 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 497251e5aa..7e7c6564ab 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -67,6 +67,8 @@ module.exports = React.createClass({ collapse_rhs: false, ready: false, width: 10000, + sideOpacity: 1.0, + middleOpacity: 1.0, }; if (s.logged_in) { if (MatrixClientPeg.get().getRooms().length) { @@ -369,7 +371,7 @@ module.exports = React.createClass({ onFinished: function(should_leave) { if (should_leave) { var d = MatrixClientPeg.get().leave(roomId); - + // FIXME: controller shouldn't be loading a view :( var Loader = sdk.getComponent("elements.Spinner"); var modal = Modal.createDialog(Loader); @@ -534,6 +536,12 @@ module.exports = React.createClass({ collapse_rhs: false, }); break; + case 'ui_opacity': + this.setState({ + sideOpacity: payload.sideOpacity, + middleOpacity: payload.middleOpacity, + }); + break; } }, @@ -887,7 +895,7 @@ module.exports = React.createClass({ dis.dispatch({ action: 'view_user', member: member, - }); + }); }, onLogoutClick: function(event) { @@ -1034,7 +1042,7 @@ module.exports = React.createClass({ var NewVersionBar = sdk.getComponent('globals.NewVersionBar'); var ForgotPassword = sdk.getComponent('structures.login.ForgotPassword'); - // work out the HS URL prompts we should show for + // work out the HS URL prompts we should show for // needs to be before normal PageTypes as you are logged in technically if (this.state.screen == 'post_registration') { @@ -1060,21 +1068,22 @@ module.exports = React.createClass({ highlightedEventId={this.state.highlightedEventId} eventPixelOffset={this.state.initialEventPixelOffset} key={this.state.currentRoom} + opacity={this.state.middleOpacity} ConferenceHandler={this.props.ConferenceHandler} /> ); - right_panel = + right_panel = break; case this.PageTypes.UserSettings: page_element = - right_panel = + right_panel = break; case this.PageTypes.CreateRoom: page_element = - right_panel = + right_panel = break; case this.PageTypes.RoomDirectory: page_element = - right_panel = + right_panel = break; } @@ -1098,7 +1107,7 @@ module.exports = React.createClass({
{topBar}
- +
{page_element}
diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 8f5ffd9e56..8292c0dee1 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -65,6 +65,9 @@ module.exports = React.createClass({ // callback which is called when more content is needed. onFillRequest: React.PropTypes.func, + + // opacity for dynamic UI fading effects + opacity: React.PropTypes.number, }, componentWillMount: function() { @@ -423,12 +426,15 @@ module.exports = React.createClass({ bottomSpinner =
  • ; } + var style = this.props.hidden ? { display: 'none' } : {}; + style.opacity = this.props.opacity; + return ( - {topSpinner} {this._getEventTiles()} diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 3749ee3bc6..14ce2065df 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1167,7 +1167,7 @@ module.exports = React.createClass({
    - ); + ); } else { var inviterName = undefined; @@ -1233,7 +1233,7 @@ module.exports = React.createClass({ inviterName={ inviterName } canJoin={ true } canPreview={ false } spinner={this.state.joining} - room={this.state.room} + room={this.state.room} />
    @@ -1314,7 +1314,7 @@ module.exports = React.createClass({ inviterName={inviterName} invitedEmail={invitedEmail} canPreview={this.state.canPeek} - room={this.state.room} + room={this.state.room} /> ); } @@ -1339,7 +1339,7 @@ module.exports = React.createClass({ messageComposer = + callState={this.state.callState} tabComplete={this.tabComplete} opacity={ this.props.opacity }/> } // TODO: Why aren't we storing the term/scope/count in this format @@ -1394,8 +1394,12 @@ module.exports = React.createClass({ if (this.state.searchResults) { searchResultsPanel = ( - +
  • {this.getSearchResultTiles()}
    @@ -1412,6 +1416,7 @@ module.exports = React.createClass({ eventPixelOffset={this.props.eventPixelOffset} onScroll={ this.onMessageListScroll } onReadMarkerUpdated={ this._updateTopUnreadMessagesBar } + opacity={ this.props.opacity } />); var topUnreadMessagesBar = null; @@ -1446,7 +1451,7 @@ module.exports = React.createClass({ { topUnreadMessagesBar } { messagePanel } { searchResultsPanel } -
    +
    { statusBar } diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 640ad66d72..3afa0fb77c 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -76,6 +76,9 @@ var TimelinePanel = React.createClass({ // callback which is called when the read-up-to mark is updated. onReadMarkerUpdated: React.PropTypes.func, + + // opacity for dynamic UI fading effects + opacity: React.PropTypes.number, }, statics: { @@ -861,6 +864,7 @@ var TimelinePanel = React.createClass({ stickyBottom={ stickyBottom } onScroll={ this.onMessageListScroll } onFillRequest={ this.onMessageListFillRequest } + opacity={ this.props.opacity } /> ); }, diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 6d26e7884d..8a0a91fae9 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -40,6 +40,9 @@ module.exports = React.createClass({ // callback when a file to upload is chosen uploadFile: React.PropTypes.func.isRequired, + + // opacity for dynamic UI fading effects + opacity: React.PropTypes.number, }, onUploadClick: function(ev) { @@ -182,7 +185,7 @@ module.exports = React.createClass({ } return ( -
    +
    {controls} diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index 2b5a6c7172..fd8bcbfe96 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -20,6 +20,7 @@ var MatrixClientPeg = require('../../../MatrixClientPeg'); var sdk = require('../../../index'); var Modal = require('../../../Modal'); var ObjectUtils = require("../../../ObjectUtils"); +var dis = require("../../../dispatcher"); module.exports = React.createClass({ displayName: 'RoomSettings', @@ -69,6 +70,20 @@ module.exports = React.createClass({ }, (err) => { console.error("Failed to get room visibility: " + err); }); + + dis.dispatch({ + action: 'ui_opacity', + sideOpacity: 0.3, + middleOpacity: 0.3, + }); + }, + + componentWillUnmount: function() { + dis.dispatch({ + action: 'ui_opacity', + sideOpacity: 1.0, + middleOpacity: 1.0, + }); }, setName: function(name) { From 3a2d5c4ba51fba24cac526d329842132bbfcc30e Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 12 Apr 2016 18:01:49 +0100 Subject: [PATCH 02/51] spinner on saving room settings --- src/components/structures/RoomView.js | 3 +++ src/components/views/rooms/RoomHeader.js | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 3749ee3bc6..a9c9651041 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -860,6 +860,8 @@ module.exports = React.createClass({ }, onSettingsSaveClick: function() { + if (!this.refs.room_settings) return; + this.setState({ uploadingRoomSettings: true, }); @@ -1432,6 +1434,7 @@ module.exports = React.createClass({ Cancel
    } + if (this.props.saving) { + var Spinner = sdk.getComponent("elements.Spinner"); + spinner =
    ; + } + if (can_set_room_name) { var RoomNameEditor = sdk.getComponent("rooms.RoomNameEditor"); name = @@ -280,6 +287,7 @@ module.exports = React.createClass({ { topic_el }
    + {spinner} {save_button} {cancel_button} {right_row} From 6a6739e0f36a5e362abdd0c4145f975507e6a50c Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 12 Apr 2016 18:32:46 +0100 Subject: [PATCH 03/51] fix context menu on tiles without widgets --- src/components/views/messages/MessageEvent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/messages/MessageEvent.js b/src/components/views/messages/MessageEvent.js index 1313ce6b00..6dcc8de627 100644 --- a/src/components/views/messages/MessageEvent.js +++ b/src/components/views/messages/MessageEvent.js @@ -43,7 +43,7 @@ module.exports = React.createClass({ }, getEventTileOps: function() { - return this.refs.body ? this.refs.body.getEventTileOps() : null; + return this.refs.body && this.refs.body.getEventTileOps ? this.refs.body.getEventTileOps() : null; }, render: function() { From 1361333fdc3ad2122298ce8050d043f8c2b374c1 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 12 Apr 2016 18:38:10 +0100 Subject: [PATCH 04/51] Stop the MatrixClient when the MatrixChat is unmounted The MatrixClient never gets unmounted in the real app, but I've been working on some tests which would rather like to be able to create and destroy MatrixChats and not have the clients hang around forever. --- src/components/structures/MatrixChat.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 497251e5aa..285af188d9 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -183,6 +183,7 @@ module.exports = React.createClass({ }, componentWillUnmount: function() { + this._stopMatrixClient(); dis.unregister(this.dispatcherRef); document.removeEventListener("keydown", this.onKeyDown); window.removeEventListener("focus", this.onFocus); @@ -258,12 +259,7 @@ module.exports = React.createClass({ window.localStorage.setItem("mx_hs_url", hsUrl); window.localStorage.setItem("mx_is_url", isUrl); } - Notifier.stop(); - UserActivity.stop(); - Presence.stop(); - MatrixClientPeg.get().stopClient(); - MatrixClientPeg.get().removeAllListeners(); - MatrixClientPeg.unset(); + this._stopMatrixClient(); this.notifyNewScreen('login'); this.replaceState({ logged_in: false, @@ -722,6 +718,16 @@ module.exports = React.createClass({ }); }, + // stop all the background processes related to the current client + _stopMatrixClient: function() { + Notifier.stop(); + UserActivity.stop(); + Presence.stop(); + MatrixClientPeg.get().stopClient(); + MatrixClientPeg.get().removeAllListeners(); + MatrixClientPeg.unset(); + }, + onKeyDown: function(ev) { /* // Remove this for now as ctrl+alt = alt-gr so this breaks keyboards which rely on alt-gr for numbers From bfec6d4ed9beab7709f1cc9d6e5175294c0d617e Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 13 Apr 2016 00:00:24 +0100 Subject: [PATCH 05/51] merge aviral's audio player --- src/ContentMessages.js | 5 +- src/component-index.js | 1 + src/components/views/messages/MAudioBody.js | 50 +++++++++++++++++++ src/components/views/messages/MessageEvent.js | 4 ++ 4 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 src/components/views/messages/MAudioBody.js diff --git a/src/ContentMessages.js b/src/ContentMessages.js index bbd714fa57..56e3499eae 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -74,10 +74,13 @@ class ContentMessages { var def = q.defer(); if (file.type.indexOf('image/') == 0) { content.msgtype = 'm.image'; - infoForImageFile(file).then(function(imageInfo) { + infoForImageFile(file).then(function (imageInfo) { extend(content.info, imageInfo); def.resolve(); }); + } else if (file.type.indexOf('audio/') == 0) { + content.msgtype = 'm.audio'; + def.resolve(); } else { content.msgtype = 'm.file'; def.resolve(); diff --git a/src/component-index.js b/src/component-index.js index b5f5dd0a53..0cb7e257a0 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -68,6 +68,7 @@ module.exports.components['views.messages.MFileBody'] = require('./components/vi module.exports.components['views.messages.MImageBody'] = require('./components/views/messages/MImageBody'); module.exports.components['views.messages.MVideoBody'] = require('./components/views/messages/MVideoBody'); module.exports.components['views.messages.MessageEvent'] = require('./components/views/messages/MessageEvent'); +module.exports.components['views.messages.MAudioBody'] = require('./components/views/messages/MAudioBody'); module.exports.components['views.messages.TextualBody'] = require('./components/views/messages/TextualBody'); module.exports.components['views.messages.TextualEvent'] = require('./components/views/messages/TextualEvent'); module.exports.components['views.messages.UnknownBody'] = require('./components/views/messages/UnknownBody'); diff --git a/src/components/views/messages/MAudioBody.js b/src/components/views/messages/MAudioBody.js new file mode 100644 index 0000000000..6113fa7c6c --- /dev/null +++ b/src/components/views/messages/MAudioBody.js @@ -0,0 +1,50 @@ +/* + Copyright 2016 OpenMarket Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +'use strict'; + +import React from 'react'; +import MFileBody from './MFileBody'; + +import MatrixClientPeg from '../../../MatrixClientPeg'; +import sdk from '../../../index'; + +export default class MAudioBody extends React.Component { + constructor(props) { + super(props); + this.state = { + playing: false + } + } + + onPlayToggle() { + this.setState({ + playing: !this.state.playing + }); + } + + render() { + var content = this.props.mxEvent.getContent(); + var cli = MatrixClientPeg.get(); + + return ( + + + ); + } +} diff --git a/src/components/views/messages/MessageEvent.js b/src/components/views/messages/MessageEvent.js index 6dcc8de627..35eafbff22 100644 --- a/src/components/views/messages/MessageEvent.js +++ b/src/components/views/messages/MessageEvent.js @@ -55,6 +55,7 @@ module.exports = React.createClass({ 'm.emote': sdk.getComponent('messages.TextualBody'), 'm.image': sdk.getComponent('messages.MImageBody'), 'm.file': sdk.getComponent('messages.MFileBody'), + 'm.audio': sdk.getComponent('messages.MAudioBody'), 'm.video': sdk.getComponent('messages.MVideoBody') }; @@ -63,6 +64,9 @@ module.exports = React.createClass({ var BodyType = UnknownBody; if (msgtype && bodyTypes[msgtype]) { BodyType = bodyTypes[msgtype]; + } else if (content.url) { + // Fallback to MFileBody if there's a content URL + BodyType = bodyTypes['m.file']; } return Date: Wed, 13 Apr 2016 01:46:10 +0100 Subject: [PATCH 06/51] show a spinner on MemberInfo when updating a member. and label 'disinvite' correctly --- src/components/views/rooms/MemberInfo.js | 65 ++++++++++++++---------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 43896e3e83..76e5af7612 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -57,7 +57,8 @@ module.exports = React.createClass({ var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var roomId = this.props.member.roomId; var target = this.props.member.userId; - MatrixClientPeg.get().kick(roomId, target).done(function() { + this.setState({ updating: this.state.updating + 1 }); + MatrixClientPeg.get().kick(roomId, target).then(function() { // NO-OP; rely on the m.room.member event coming down else we could // get out of sync if we force setState here! console.log("Kick success"); @@ -67,7 +68,9 @@ module.exports = React.createClass({ description: err.message }); } - ); + ).finally(()=>{ + this.setState({ updating: this.state.updating - 1 }); + }); this.props.onFinished(); }, @@ -75,7 +78,8 @@ module.exports = React.createClass({ var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var roomId = this.props.member.roomId; var target = this.props.member.userId; - MatrixClientPeg.get().ban(roomId, target).done( + this.setState({ updating: this.state.updating + 1 }); + MatrixClientPeg.get().ban(roomId, target).then( function() { // NO-OP; rely on the m.room.member event coming down else we could // get out of sync if we force setState here! @@ -86,7 +90,9 @@ module.exports = React.createClass({ description: err.message }); } - ); + ).finally(()=>{ + this.setState({ updating: this.state.updating - 1 }); + }); this.props.onFinished(); }, @@ -122,7 +128,8 @@ module.exports = React.createClass({ level = parseInt(level); if (level !== NaN) { - MatrixClientPeg.get().setPowerLevel(roomId, target, level, powerLevelEvent).done( + this.setState({ updating: this.state.updating + 1 }); + MatrixClientPeg.get().setPowerLevel(roomId, target, level, powerLevelEvent).then( function() { // NO-OP; rely on the m.room.member event coming down else we could // get out of sync if we force setState here! @@ -133,9 +140,11 @@ module.exports = React.createClass({ description: err.message }); } - ); + ).finally(()=>{ + this.setState({ updating: this.state.updating - 1 }); + }); } - this.props.onFinished(); + this.props.onFinished(); }, onModToggle: function() { @@ -164,7 +173,8 @@ module.exports = React.createClass({ if (modLevel > 50 && defaultLevel < 50) modLevel = 50; // try to stick with the vector level defaults // toggle the level var newLevel = this.state.isTargetMod ? defaultLevel : modLevel; - MatrixClientPeg.get().setPowerLevel(roomId, target, parseInt(newLevel), powerLevelEvent).done( + this.setState({ updating: this.state.updating + 1 }); + MatrixClientPeg.get().setPowerLevel(roomId, target, parseInt(newLevel), powerLevelEvent).then( function() { // NO-OP; rely on the m.room.member event coming down else we could // get out of sync if we force setState here! @@ -183,12 +193,15 @@ module.exports = React.createClass({ }); } } - ); - this.props.onFinished(); + ).finally(()=>{ + this.setState({ updating: this.state.updating - 1 }); + }); + this.props.onFinished(); }, _applyPowerChange: function(roomId, target, powerLevel, powerLevelEvent) { - MatrixClientPeg.get().setPowerLevel(roomId, target, parseInt(powerLevel), powerLevelEvent).done( + this.setState({ updating: this.state.updating + 1 }); + MatrixClientPeg.get().setPowerLevel(roomId, target, parseInt(powerLevel), powerLevelEvent).then( function() { // NO-OP; rely on the m.room.member event coming down else we could // get out of sync if we force setState here! @@ -199,7 +212,9 @@ module.exports = React.createClass({ description: err.message }); } - ); + ).finally(()=>{ + this.setState({ updating: this.state.updating - 1 }); + }); this.props.onFinished(); }, @@ -249,7 +264,7 @@ module.exports = React.createClass({ else { this._applyPowerChange(roomId, target, powerLevel, powerLevelEvent); } - }, + }, onChatClick: function() { var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -300,19 +315,17 @@ module.exports = React.createClass({ this.props.onFinished(); } else { - self.setState({ creatingRoom: true }); - if (MatrixClientPeg.get().isGuest()) { - self.setState({ creatingRoom: false }); var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog"); Modal.createDialog(NeedToRegisterDialog, { title: "Please Register", description: "Guest users can't create new rooms. Please register to create room and start a chat." }); - self.props.onFinished(); + self.props.onFinished(); return; } + self.setState({ updating: self.state.updating + 1 }); MatrixClientPeg.get().createRoom({ // XXX: FIXME: deduplicate this with "view_create_room" in MatrixChat invite: [this.props.member.userId], @@ -328,24 +341,24 @@ module.exports = React.createClass({ type: 'm.room.guest_access', state_key: '', } - ], - }).done( + ], + }).then( function(res) { - self.setState({ creatingRoom: false }); dis.dispatch({ action: 'view_room', room_id: res.room_id }); self.props.onFinished(); }, function(err) { - self.setState({ creatingRoom: false }); Modal.createDialog(ErrorDialog, { title: "Failure to start chat", description: err.message }); self.props.onFinished(); } - ); + ).finally(()=>{ + self.setState({ updating: self.state.updating - 1 }); + }); } }, @@ -367,7 +380,7 @@ module.exports = React.createClass({ }, muted: false, isTargetMod: false, - creatingRoom: false + updating: 0, } }, @@ -470,14 +483,14 @@ module.exports = React.createClass({ startChat = } - if (this.state.creatingRoom) { + if (this.state.updating) { var Loader = sdk.getComponent("elements.Spinner"); spinner = ; } if (this.state.can.kick) { kickButton =
    - Kick + { this.props.member.membership === "invite" ? "Disinvite" : "Kick" }
    ; } if (this.state.can.ban) { @@ -503,7 +516,7 @@ module.exports = React.createClass({ var adminTools; if (kickButton || banButton || muteButton || giveModButton) { - adminTools = + adminTools =

    Admin tools

    From 92f58b69273646298233bd23fdebfff3ff6ccb05 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 13 Apr 2016 01:54:06 +0100 Subject: [PATCH 07/51] don't try to do preview URLs of matrix IDs (yet :D) --- src/components/views/messages/TextualBody.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index ce33a60872..631caadac2 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -84,7 +84,10 @@ module.exports = React.createClass({ findLink: function(nodes) { for (var i = 0; i < nodes.length; i++) { var node = nodes[i]; - if (node.tagName === "A" && node.getAttribute("href")) { + if (node.tagName === "A" && node.getAttribute("href") && + (node.getAttribute("href").startsWith("http://") || + node.getAttribute("href").startsWith("https://"))) + { return node; } else if (node.children && node.children.length) { From f3793b556e08a9ca49744e5e6a05da80b8a4a279 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 13 Apr 2016 02:02:55 +0100 Subject: [PATCH 08/51] fix super-annoying key warning from react --- src/components/views/rooms/MessageComposer.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 8a0a91fae9..78319c7682 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -127,7 +127,7 @@ module.exports = React.createClass({ var controls = []; controls.push( -
    +
    ); @@ -135,17 +135,17 @@ module.exports = React.createClass({ var callButton, videoCallButton, hangupButton; if (this.props.callState && this.props.callState !== 'ended') { hangupButton = -
    +
    Hangup
    ; } else { callButton = -
    +
    videoCallButton = -
    +
    } @@ -158,7 +158,7 @@ module.exports = React.createClass({ // check separately for whether we can call, but this is slightly // complex because of conference calls. var uploadButton = ( -
    , uploadButton, hangupButton, @@ -178,7 +178,7 @@ module.exports = React.createClass({ ); } else { controls.push( -
    +
    You do not have permission to post to this room
    ); From 93a142480c1732a22a632386d25dbb924049da22 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 12 Apr 2016 19:25:07 +0100 Subject: [PATCH 09/51] RoomView: Handle joining federated rooms This hopefully fixes an issue where joining a federated room via the directory would get stuck at a spinner of doom, due to us not recognising the room in question when it came down the /sync. We now catch the room id in the response from the /join, and use it to match up the room in onRoom. props.roomAlias, props.roomId, and state.room.roomId were somewhat confusing, so I've tried to rationalise them: * props.roomAlias (named thus to stop you assuming it's a room id) is the thing that the parent component uses to identify the room of interest, and can be either an ID or an alias (ie, it replaces props.roomId and props.roomAlias) * Everything that needs a room ID now has to get it from state.room.roomId. --- src/components/structures/MatrixChat.js | 10 +- src/components/structures/RoomView.js | 178 +++++++++++++----------- 2 files changed, 106 insertions(+), 82 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 497251e5aa..b1202b1c08 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -624,10 +624,13 @@ module.exports = React.createClass({ if (!this.refs.roomView) { return; } - var roomview = this.refs.roomView; + var roomId = this.refs.roomView.getRoomId(); + if (!roomId) { + return; + } var state = roomview.getScrollState(); - this.scrollStateMap[roomview.props.roomId] = state; + this.scrollStateMap[roomId] = state; }, onLoggedIn: function(credentials) { @@ -1052,8 +1055,7 @@ module.exports = React.createClass({ page_element = ( { + MatrixClientPeg.get().peekInRoom(this.props.roomAlias).then((room) => { this.setState({ room: room, roomLoading: false, @@ -200,7 +203,6 @@ module.exports = React.createClass({ if (MatrixClientPeg.get()) { MatrixClientPeg.get().removeListener("Room", this.onRoom); MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline); - MatrixClientPeg.get().removeListener("Room.name", this.onRoomName); MatrixClientPeg.get().removeListener("Room.accountData", this.onRoomAccountData); MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember); } @@ -233,7 +235,7 @@ module.exports = React.createClass({ return; } - var call = CallHandler.getCallForRoom(payload.room_id); + var call = this._getCallForRoom(); var callState; if (call) { @@ -256,7 +258,7 @@ module.exports = React.createClass({ }, componentWillReceiveProps: function(newProps) { - if (newProps.roomId != this.props.roomId) { + if (newProps.roomAlias != this.props.roomAlias) { throw new Error("changing room on a RoomView is not supported"); } @@ -270,7 +272,7 @@ module.exports = React.createClass({ if (this.unmounted) return; // ignore events for other rooms - if (room.roomId != this.props.roomId) return; + if (!this.state.room || room.roomId != this.state.room.roomId) return; // ignore anything but real-time updates at the end of the room: // updates from pagination will happen when the paginate completes. @@ -321,30 +323,18 @@ module.exports = React.createClass({ // set it in our state and start using it (ie. init the timeline) // This will happen if we start off viewing a room we're not joined, // then join it whilst RoomView is looking at that room. - if (room.roomId == this.props.roomId && !this.state.room) { + if (!this.state.room && room.roomId == this._joiningRoomId) { + this._joiningRoomId = undefined; this.setState({ - room: room + room: room, + joining: false, }); this._onRoomLoaded(room); } }, - onRoomName: function(room) { - // NB don't set state.room here. - // - // When peeking, this event lands *before* the timeline is correctly - // synced; if we set state.room here, the TimelinePanel will be - // instantiated, and it will initialise its scroll state, with *no - // events*. In short, the scroll state will be all messed up. - // - // There's no need to set state.room here anyway. - if (room.roomId == this.props.roomId) { - this.forceUpdate(); - } - }, - updateTint: function() { - var room = MatrixClientPeg.get().getRoom(this.props.roomId); + var room = this.state.room; if (!room) return; var color_scheme_event = room.getAccountData("org.matrix.room.color_scheme"); @@ -367,28 +357,33 @@ module.exports = React.createClass({ }, onRoomStateMember: function(ev, state, member) { - if (member.roomId === this.props.roomId) { - // a member state changed in this room, refresh the tab complete list - this._updateTabCompleteList(); - - var room = MatrixClientPeg.get().getRoom(this.props.roomId); - if (!room) return; - var me = MatrixClientPeg.get().credentials.userId; - if (this.state.joining && room.hasMembershipState(me, "join")) { - this.setState({ - joining: false - }); - } - } - - if (!this.props.ConferenceHandler) { + // ignore if we don't have a room yet + if (!this.state.room) { return; } - if (member.roomId !== this.props.roomId || - member.userId !== this.props.ConferenceHandler.getConferenceUserIdForRoom(member.roomId)) { + + // ignore members in other rooms + if (member.roomId !== this.state.room.roomId) { return; } - this._updateConfCallNotification(); + + // a member state changed in this room, refresh the tab complete list + this._updateTabCompleteList(); + + // if we are now a member of the room, where we were not before, that + // means we have finished joining a room we were previously peeking + // into. + var me = MatrixClientPeg.get().credentials.userId; + if (this.state.joining && this.state.room.hasMembershipState(me, "join")) { + this.setState({ + joining: false + }); + } + + if (this.props.ConferenceHandler && + member.userId === this.props.ConferenceHandler.getConferenceUserIdForRoom(member.roomId)) { + this._updateConfCallNotification(); + } }, _hasUnsentMessages: function(room) { @@ -403,12 +398,12 @@ module.exports = React.createClass({ }, _updateConfCallNotification: function() { - var room = MatrixClientPeg.get().getRoom(this.props.roomId); + var room = this.state.room; if (!room || !this.props.ConferenceHandler) { return; } var confMember = room.getMember( - this.props.ConferenceHandler.getConferenceUserIdForRoom(this.props.roomId) + this.props.ConferenceHandler.getConferenceUserIdForRoom(room.roomId) ); if (!confMember) { @@ -427,7 +422,7 @@ module.exports = React.createClass({ }, componentDidMount: function() { - var call = CallHandler.getCallForRoom(this.props.roomId); + var call = this._getCallForRoom(); var callState = call ? call.call_state : "ended"; this.setState({ callState: callState @@ -559,25 +554,35 @@ module.exports = React.createClass({ display_name_promise.then(() => { var sign_url = this.props.thirdPartyInvite ? this.props.thirdPartyInvite.inviteSignUrl : undefined; - return MatrixClientPeg.get().joinRoom(this.props.roomAlias || this.props.roomId, + return MatrixClientPeg.get().joinRoom(this.props.roomAlias, { inviteSignUrl: sign_url } ) - }).done(function() { + }).then(function(resp) { + var roomId = resp.roomId; + // It is possible that there is no Room yet if state hasn't come down // from /sync - joinRoom will resolve when the HTTP request to join succeeds, // NOT when it comes down /sync. If there is no room, we'll keep the - // joining flag set until we see it. Likewise, if our state is not - // "join" we'll keep this flag set until it comes down /sync. + // joining flag set until we see it. // We'll need to initialise the timeline when joining, but due to // the above, we can't do it here: we do it in onRoom instead, // once we have a useable room object. - var room = MatrixClientPeg.get().getRoom(self.props.roomId); - var me = MatrixClientPeg.get().credentials.userId; - self.setState({ - joining: room ? !room.hasMembershipState(me, "join") : true, - room: room - }); - }, function(error) { + var room = MatrixClientPeg.get().getRoom(roomId); + if (!room) { + // wait for the room to turn up in onRoom. + self._joiningRoomId = roomId; + } else { + // we've got a valid room, but that might also just mean that + // it was peekable (so we had one before anyway). If we are + // not yet a member of the room, we will need to wait for that + // to happen, in onRoomStateMember. + var me = MatrixClientPeg.get().credentials.userId; + self.setState({ + joining: !room.hasMembershipState(me, "join"), + room: room + }); + } + }).catch(function(error) { self.setState({ joining: false, joinError: error @@ -606,7 +611,8 @@ module.exports = React.createClass({ description: msg }); } - }); + }).done(); + this.setState({ joining: true }); @@ -661,7 +667,7 @@ module.exports = React.createClass({ uploadFile: function(file) { var self = this; ContentMessages.sendContentToRoom( - file, this.props.roomId, MatrixClientPeg.get() + file, this.state.room.roomId, MatrixClientPeg.get() ).done(undefined, function(error) { var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { @@ -696,7 +702,7 @@ module.exports = React.createClass({ filter = { // XXX: it's unintuitive that the filter for searching doesn't have the same shape as the v2 filter API :( rooms: [ - this.props.roomId + this.state.room.roomId ] }; } @@ -902,12 +908,12 @@ module.exports = React.createClass({ onLeaveClick: function() { dis.dispatch({ action: 'leave_room', - room_id: this.props.roomId, + room_id: this.state.room.roomId, }); }, onForgetClick: function() { - MatrixClientPeg.get().forget(this.props.roomId).done(function() { + MatrixClientPeg.get().forget(this.state.room.roomId).done(function() { dis.dispatch({ action: 'view_next_room' }); }, function(err) { var errCode = err.errcode || "unknown error code"; @@ -924,7 +930,7 @@ module.exports = React.createClass({ this.setState({ rejecting: true }); - MatrixClientPeg.get().leave(this.props.roomId).done(function() { + MatrixClientPeg.get().leave(this.props.roomAlias).done(function() { dis.dispatch({ action: 'view_next_room' }); self.setState({ rejecting: false @@ -1081,7 +1087,7 @@ module.exports = React.createClass({ }, onMuteAudioClick: function() { - var call = CallHandler.getCallForRoom(this.props.roomId); + var call = this._getCallForRoom(); if (!call) { return; } @@ -1093,7 +1099,7 @@ module.exports = React.createClass({ }, onMuteVideoClick: function() { - var call = CallHandler.getCallForRoom(this.props.roomId); + var call = this._getCallForRoom(); if (!call) { return; } @@ -1133,6 +1139,29 @@ module.exports = React.createClass({ } }, + /** + * Get the ID of the displayed room + * + * Returns null if the RoomView was instantiated on a room alias and + * we haven't yet joined the room. + */ + getRoomId: function() { + if (!this.state.room) { + return null; + } + return this.state.room.roomId; + }, + + /** + * get any current call for this room + */ + _getCallForRoom: function() { + if (!this.state.room) { + return null; + } + return CallHandler.getCallForRoom(this.state.room.roomId); + }, + // this has to be a proper method rather than an unnamed function, // otherwise react calls it with null on each update. _gatherTimelinePanelRef: function(r) { @@ -1155,7 +1184,6 @@ module.exports = React.createClass({ var TimelinePanel = sdk.getComponent("structures.TimelinePanel"); if (!this.state.room) { - if (this.props.roomId) { if (this.state.roomLoading) { return (
    @@ -1192,12 +1220,6 @@ module.exports = React.createClass({
    ); } - } - else { - return ( -
    - ); - } } var myUserId = MatrixClientPeg.get().credentials.userId; @@ -1239,7 +1261,7 @@ module.exports = React.createClass({ // We have successfully loaded this room, and are not previewing. // Display the "normal" room view. - var call = CallHandler.getCallForRoom(this.props.roomId); + var call = this._getCallForRoom(); var inCall = false; if (call && (this.state.callState !== 'ended' && this.state.callState !== 'ringing')) { inCall = true; From 5706a879d063e74ccce805437ef2522d9ef33b50 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 13 Apr 2016 10:19:16 +0100 Subject: [PATCH 10/51] Hack around absence of String.codePointAt on PhantomJS I've been trying to get some tests working under PhantomJS, which appears not to support String.codePointAt (which is, to be fair, an ES6 addition). For our limited usecase, it's easier to implement the functionality from first principles than to try to polyfill support. --- src/components/views/avatars/BaseAvatar.js | 39 +++++++++++++++++----- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.js index 52f0b77387..121540a8c0 100644 --- a/src/components/views/avatars/BaseAvatar.js +++ b/src/components/views/avatars/BaseAvatar.js @@ -99,15 +99,36 @@ module.exports = React.createClass({ } }, - _getInitialLetter: function() { - var name = this.props.name; - //For large characters (exceeding 2 bytes), this function will get the correct character. - //However, this does NOT get the second character correctly if a large character is before it. - var initial = String.fromCodePoint(name.codePointAt(0)); - if ((initial === '@' || initial === '#') && name[1]) { - initial = String.fromCodePoint(name.codePointAt(1)); + /** + * returns the first (non-sigil) character of 'name', + * converted to uppercase + */ + _getInitialLetter: function(name) { + if (name.length < 1) { + return undefined; } - return initial.toUpperCase(); + + var idx = 0; + var initial = name[0]; + if ((initial === '@' || initial === '#') && name[1]) { + idx++; + } + + // string.codePointAt(0) would do this, but that isn't supported by + // some browsers (notably PhantomJS). + var chars = 1; + var first = name.charCodeAt(idx); + + // check if it’s the start of a surrogate pair + if (first >= 0xD800 && first <= 0xDBFF && name[idx+1]) { + var second = name.charCodeAt(idx+1); + if (second >= 0xDC00 && second <= 0xDFFF) { + chars++; + } + } + + var firstChar = name.substring(idx, idx+chars); + return firstChar.toUpperCase(); }, render: function() { @@ -116,7 +137,7 @@ module.exports = React.createClass({ var imageUrl = this.state.imageUrls[this.state.urlsIndex]; if (imageUrl === this.state.defaultImageUrl) { - var initialLetter = this._getInitialLetter(); + var initialLetter = this._getInitialLetter(this.props.name); return (