From ef88e02931f8de61960e362a636e47bc7fb102fc Mon Sep 17 00:00:00 2001 From: Sijmen Schoon Date: Sun, 8 Jan 2017 02:20:59 +0100 Subject: [PATCH 001/284] Add support for pasting into the text box Only supports the new rich-text-supporting text editor --- src/ContentMessages.js | 4 ++-- src/components/views/rooms/MessageComposer.js | 10 ++++++---- src/components/views/rooms/MessageComposerInput.js | 8 ++++++++ 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/ContentMessages.js b/src/ContentMessages.js index c169ce64b5..765c7ed976 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -276,7 +276,7 @@ class ContentMessages { sendContentToRoom(file, roomId, matrixClient) { const content = { - body: file.name, + body: file.name || 'Attachment', info: { size: file.size, } @@ -316,7 +316,7 @@ class ContentMessages { } const upload = { - fileName: file.name, + fileName: file.name || 'Attachment', roomId: roomId, total: 0, loaded: 0, diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index ee9c49d52a..6810e75f53 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -91,8 +91,9 @@ export default class MessageComposer extends React.Component { this.refs.uploadInput.click(); } - onUploadFileSelected(ev) { - let files = ev.target.files; + onUploadFileSelected(files, isPasted) { + if (!isPasted) + files = files.target.files; let QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); let TintableSvg = sdk.getComponent("elements.TintableSvg"); @@ -100,7 +101,7 @@ export default class MessageComposer extends React.Component { let fileList = []; for (let i=0; i - {files[i].name} + {files[i].name || 'Attachment'} ); } @@ -171,7 +172,7 @@ export default class MessageComposer extends React.Component { } onUpArrow() { - return this.refs.autocomplete.onUpArrow(); + return this.refs.autocomplete.onUpArrow(); } onDownArrow() { @@ -293,6 +294,7 @@ export default class MessageComposer extends React.Component { tryComplete={this._tryComplete} onUpArrow={this.onUpArrow} onDownArrow={this.onDownArrow} + onUploadFileSelected={this.onUploadFileSelected} tabComplete={this.props.tabComplete} // used for old messagecomposerinput/tabcomplete onContentChanged={this.onInputContentChanged} onInputStateChanged={this.onInputStateChanged} />, diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 37d937d6f5..f0658ab543 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -83,6 +83,7 @@ export default class MessageComposerInput extends React.Component { this.onAction = this.onAction.bind(this); this.handleReturn = this.handleReturn.bind(this); this.handleKeyCommand = this.handleKeyCommand.bind(this); + this.handlePastedFiles = this.handlePastedFiles.bind(this); this.onEditorContentChanged = this.onEditorContentChanged.bind(this); this.setEditorState = this.setEditorState.bind(this); this.onUpArrow = this.onUpArrow.bind(this); @@ -473,6 +474,10 @@ export default class MessageComposerInput extends React.Component { return false; } + handlePastedFiles(files) { + this.props.onUploadFileSelected(files, true); + } + handleReturn(ev) { if (ev.shiftKey) { this.onEditorContentChanged(RichUtils.insertSoftNewline(this.state.editorState)); @@ -728,6 +733,7 @@ export default class MessageComposerInput extends React.Component { keyBindingFn={MessageComposerInput.getKeyBinding} handleKeyCommand={this.handleKeyCommand} handleReturn={this.handleReturn} + handlePastedFiles={this.handlePastedFiles} stripPastedStyles={!this.state.isRichtextEnabled} onTab={this.onTab} onUpArrow={this.onUpArrow} @@ -757,6 +763,8 @@ MessageComposerInput.propTypes = { onDownArrow: React.PropTypes.func, + onUploadFileSelected: React.PropTypes.func, + // attempts to confirm currently selected completion, returns whether actually confirmed tryComplete: React.PropTypes.func, From ca5c2fb82e22c1c71c4f9474d6afa2f8f11be3a9 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 3 Mar 2017 13:48:37 +0000 Subject: [PATCH 002/284] Allow user to choose from existing DMs on new chat When creating a new chat with one person, show a dialog that asks the user whether they'd like to use an existing chat or actually create a new room. Fixes https://github.com/vector-im/riot-web/issues/2760 --- src/component-index.js | 2 + .../views/dialogs/ChatCreateOrReuseDialog.js | 102 ++++++++++++++++++ .../views/dialogs/ChatInviteDialog.js | 58 +++++----- src/components/views/rooms/RoomTile.js | 4 + 4 files changed, 142 insertions(+), 24 deletions(-) create mode 100644 src/components/views/dialogs/ChatCreateOrReuseDialog.js diff --git a/src/component-index.js b/src/component-index.js index c705150e12..2644f1a379 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -75,6 +75,8 @@ import views$create_room$RoomAlias from './components/views/create_room/RoomAlia views$create_room$RoomAlias && (module.exports.components['views.create_room.RoomAlias'] = views$create_room$RoomAlias); import views$dialogs$BaseDialog from './components/views/dialogs/BaseDialog'; views$dialogs$BaseDialog && (module.exports.components['views.dialogs.BaseDialog'] = views$dialogs$BaseDialog); +import views$dialogs$ChatCreateOrReuseDialog from './components/views/dialogs/ChatCreateOrReuseDialog'; +views$dialogs$ChatCreateOrReuseDialog && (module.exports.components['views.dialogs.ChatCreateOrReuseDialog'] = views$dialogs$ChatCreateOrReuseDialog); import views$dialogs$ChatInviteDialog from './components/views/dialogs/ChatInviteDialog'; views$dialogs$ChatInviteDialog && (module.exports.components['views.dialogs.ChatInviteDialog'] = views$dialogs$ChatInviteDialog); import views$dialogs$ConfirmUserActionDialog from './components/views/dialogs/ConfirmUserActionDialog'; diff --git a/src/components/views/dialogs/ChatCreateOrReuseDialog.js b/src/components/views/dialogs/ChatCreateOrReuseDialog.js new file mode 100644 index 0000000000..241c7755d2 --- /dev/null +++ b/src/components/views/dialogs/ChatCreateOrReuseDialog.js @@ -0,0 +1,102 @@ +/* +Copyright 2017 Vector Creations 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. +*/ + +import React from 'react'; +import sdk from '../../../index'; +import dis from '../../../dispatcher'; +import MatrixClientPeg from '../../../MatrixClientPeg'; +import DMRoomMap from '../../../utils/DMRoomMap'; +import AccessibleButton from '../elements/AccessibleButton'; +import Unread from '../../../Unread'; +import classNames from 'classnames'; +import createRoom from '../../../createRoom'; + +export default class CreateOrReuseChatDialog extends React.Component { + + constructor (props) { + super(props); + this._onNewDMClick = this._onNewDMClick.bind(this); + } + + _onNewDMClick () { + createRoom({dmUserId: this.props.userId}); + this.props.onFinished(true); + } + + render () { + const client = MatrixClientPeg.get(); + + const dmRoomMap = new DMRoomMap(client); + const dmRooms = dmRoomMap.getDMRoomsForUserId(this.props.userId); + + const RoomTile = sdk.getComponent("rooms.RoomTile"); + + const tiles = []; + for (const roomId of dmRooms) { + const room = client.getRoom(roomId); + if (room) { + const me = room.getMember(client.credentials.userId); + const highlight = ( + room.getUnreadNotificationCount('highlight') > 0 || + me.membership == "invite" + ); + tiles.push( + this.props.onFinished(true)} + /> + ); + } + } + + const labelClasses = classNames({ + mx_MemberInfo_createRoom_label: true, + mx_RoomTile_name: true, + }); + const startNewChat = +
+ +
+
Start new chat
+
; + + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + return ( + { + this.props.onFinished(false) + }} + title='Create a new chat or reuse an existing one' + > +

Direct chats

+ {tiles} + {startNewChat} +
+ ); + } +} + +CreateOrReuseChatDialog.propTyps = { + userId: React.PropTypes.string.isRequired, + onFinished: React.PropTypes.func.isRequired, +}; diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index ca3b07aa00..db50a12904 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -97,18 +97,27 @@ module.exports = React.createClass({ if (inviteList === null) return; } + const addrTexts = inviteList.map(addr => addr.address); if (inviteList.length > 0) { - if (this._isDmChat(inviteList)) { + if (this._isDmChat(addrTexts)) { + const userId = inviteList[0].address; // Direct Message chat - var room = this._getDirectMessageRoom(inviteList[0]); - if (room) { - // A Direct Message room already exists for this user and you - // so go straight to that room - dis.dispatch({ - action: 'view_room', - room_id: room.roomId, + const rooms = this._getDirectMessageRooms(userId); + if (rooms.length > 0) { + // A Direct Message room already exists for this user, so select a + // room from a list that is similar to the one in MemberInfo panel + const ChatCreateOrReuseDialog = sdk.getComponent( + "views.dialogs.ChatCreateOrReuseDialog" + ); + Modal.createDialog(ChatCreateOrReuseDialog, { + userId: userId, + onFinished: (success) => { + if (success) { + this.props.onFinished(true, inviteList[0]); + } + // else show this ChatInviteDialog again + } }); - this.props.onFinished(true, inviteList[0]); } else { this._startChat(inviteList); } @@ -238,22 +247,20 @@ module.exports = React.createClass({ if (this._cancelThreepidLookup) this._cancelThreepidLookup(); }, - _getDirectMessageRoom: function(addr) { + _getDirectMessageRooms: function(addr) { const dmRoomMap = new DMRoomMap(MatrixClientPeg.get()); - var dmRooms = dmRoomMap.getDMRoomsForUserId(addr); - if (dmRooms.length > 0) { - // Cycle through all the DM rooms and find the first non forgotten or parted room - for (let i = 0; i < dmRooms.length; i++) { - let room = MatrixClientPeg.get().getRoom(dmRooms[i]); - if (room) { - const me = room.getMember(MatrixClientPeg.get().credentials.userId); - if (me.membership == 'join') { - return room; - } + const dmRooms = dmRoomMap.getDMRoomsForUserId(addr); + const rooms = []; + dmRooms.forEach(dmRoom => { + let room = MatrixClientPeg.get().getRoom(dmRoom); + if (room) { + const me = room.getMember(MatrixClientPeg.get().credentials.userId); + if (me.membership == 'join') { + rooms.push(room); } } - } - return null; + }); + return rooms; }, _startChat: function(addrs) { @@ -386,8 +393,11 @@ module.exports = React.createClass({ return false; }, - _isDmChat: function(addrs) { - if (addrs.length === 1 && getAddressType(addrs[0]) === "mx" && !this.props.roomId) { + _isDmChat: function(addrTexts) { + if (addrTexts.length === 1 && + getAddressType(addrTexts[0]) === "mx" && + !this.props.roomId + ) { return true; } else { return false; diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index f6c0f7034e..485b567ddc 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -35,6 +35,7 @@ module.exports = React.createClass({ propTypes: { connectDragSource: React.PropTypes.func, connectDropTarget: React.PropTypes.func, + onClick: React.PropTypes.func, isDragging: React.PropTypes.bool, room: React.PropTypes.object.isRequired, @@ -104,6 +105,9 @@ module.exports = React.createClass({ action: 'view_room', room_id: this.props.room.roomId, }); + if (this.props.onClick) { + this.props.onClick(); + } }, onMouseEnter: function() { From 2bd9885288c09e4fe0d56c5154ca1d816f5f9efc Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 3 Mar 2017 15:42:24 +0000 Subject: [PATCH 003/284] Start to show redacted events --- src/TextForEvent.js | 1 - src/components/structures/MessagePanel.js | 11 +++++++++-- src/components/views/messages/TextualBody.js | 4 ++++ src/components/views/rooms/EventTile.js | 11 +++++++---- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/TextForEvent.js b/src/TextForEvent.js index 3f772e9cfb..3e1659f392 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -116,7 +116,6 @@ function textForRoomNameEvent(ev) { function textForMessageEvent(ev) { var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); - var message = senderDisplayName + ': ' + ev.getContent().body; if (ev.getContent().msgtype === "m.emote") { message = "* " + senderDisplayName + " " + message; diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 0981b7b706..21665bb421 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -295,7 +295,10 @@ module.exports = React.createClass({ var last = (i == lastShownEventIndex); // Wrap consecutive member events in a ListSummary, ignore if redacted - if (isMembershipChange(mxEv) && EventTile.haveTileForEvent(mxEv)) { + if (isMembershipChange(mxEv) && + EventTile.haveTileForEvent(mxEv) && + !mxEv.isRedacted() + ) { let ts1 = mxEv.getTs(); // Ensure that the key of the MemberEventListSummary does not change with new // member events. This will prevent it from being re-created unnecessarily, and @@ -481,13 +484,17 @@ module.exports = React.createClass({ // here. return !this.props.suppressFirstDateSeparator; } + const prevEventDate = prevEvent.getDate(); + if (!nextEventDate || !prevEventDate) { + return false; + } // Return early for events that are > 24h apart if (Math.abs(prevEvent.getTs() - nextEventDate.getTime()) > MILLIS_IN_DAY) { return true; } // Compare weekdays - return prevEvent.getDate().getDay() !== nextEventDate.getDay(); + return prevEventDate.getDay() !== nextEventDate.getDay(); }, // get a list of read receipts that should be shown next to this event diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index a625e63062..0030fe6575 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -246,6 +246,10 @@ module.exports = React.createClass({ var mxEvent = this.props.mxEvent; var content = mxEvent.getContent(); + if (mxEvent.isRedacted()) { + content = {body: "Message redacted by " + mxEvent.event.redacted_because.sender}; + } + var body = HtmlUtils.bodyToHtml(content, this.props.highlights, {}); if (this.props.highlightLink) { diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index c9508428ba..f011b5517a 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -396,6 +396,7 @@ module.exports = WithMatrixClient(React.createClass({ var e2eEnabled = this.props.matrixClient.isRoomEncrypted(this.props.mxEvent.getRoomId()); var isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1); + const isRedacted = this.props.mxEvent.isRedacted(); var classes = classNames({ mx_EventTile: true, @@ -412,6 +413,7 @@ module.exports = WithMatrixClient(React.createClass({ mx_EventTile_verified: this.state.verified == true, mx_EventTile_unverified: this.state.verified == false, mx_EventTile_bad: this.props.mxEvent.getContent().msgtype === 'm.bad.encrypted', + mx_EventTile_redacted: isRedacted, }); var permalink = "#/room/" + this.props.mxEvent.getRoomId() +"/"+ this.props.mxEvent.getId(); @@ -486,6 +488,8 @@ module.exports = WithMatrixClient(React.createClass({ else if (e2eEnabled) { e2e = ; } + const timestamp = this.props.mxEvent.isRedacted() ? + null : ; if (this.props.tileShape === "notif") { var room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId()); @@ -501,7 +505,7 @@ module.exports = WithMatrixClient(React.createClass({ { avatar } { sender } - + { timestamp }
@@ -530,7 +534,7 @@ module.exports = WithMatrixClient(React.createClass({
{ sender } - + { timestamp }
@@ -546,7 +550,7 @@ module.exports = WithMatrixClient(React.createClass({ { sender }
- + { timestamp } { e2e } Date: Fri, 3 Mar 2017 15:51:14 +0000 Subject: [PATCH 004/284] Remove seemingly unused "bounce" --- src/components/views/rooms/EventTile.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index f011b5517a..c262fea15f 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -29,14 +29,6 @@ var dispatcher = require("../../../dispatcher"); var ObjectUtils = require('../../../ObjectUtils'); -var bounce = false; -try { - if (global.localStorage) { - bounce = global.localStorage.getItem('avatar_bounce') == 'true'; - } -} catch (e) { -} - var eventTileTypes = { 'm.room.message': 'messages.MessageEvent', 'm.room.member' : 'messages.TextualEvent', From 5ef61b7c35f0ca55695b09e5a5f1892bbcd22af8 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 3 Mar 2017 16:45:29 +0000 Subject: [PATCH 005/284] Only show a redaction tile for messages --- src/components/views/messages/TextualBody.js | 4 ---- src/components/views/messages/UnknownBody.js | 7 +++++-- src/components/views/rooms/EventTile.js | 9 +++++++-- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index 0030fe6575..a625e63062 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -246,10 +246,6 @@ module.exports = React.createClass({ var mxEvent = this.props.mxEvent; var content = mxEvent.getContent(); - if (mxEvent.isRedacted()) { - content = {body: "Message redacted by " + mxEvent.event.redacted_because.sender}; - } - var body = HtmlUtils.bodyToHtml(content, this.props.highlights, {}); if (this.props.highlightLink) { diff --git a/src/components/views/messages/UnknownBody.js b/src/components/views/messages/UnknownBody.js index 00784b18b0..5504c0b1fe 100644 --- a/src/components/views/messages/UnknownBody.js +++ b/src/components/views/messages/UnknownBody.js @@ -22,10 +22,13 @@ module.exports = React.createClass({ displayName: 'UnknownBody', render: function() { - var content = this.props.mxEvent.getContent(); + var text = this.props.mxEvent.getContent().body; + if (this.props.mxEvent.isRedacted()) { + text = "This event was redacted"; + } return ( - {content.body} + {text} ); }, diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index c262fea15f..087cef7689 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -388,7 +388,7 @@ module.exports = WithMatrixClient(React.createClass({ var e2eEnabled = this.props.matrixClient.isRoomEncrypted(this.props.mxEvent.getRoomId()); var isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1); - const isRedacted = this.props.mxEvent.isRedacted(); + const isRedacted = (eventType === 'm.room.message') && this.props.mxEvent.isRedacted(); var classes = classNames({ mx_EventTile: true, @@ -415,7 +415,10 @@ module.exports = WithMatrixClient(React.createClass({ let avatarSize; let needsSenderProfile; - if (this.props.tileShape === "notif") { + if (isRedacted) { + avatarSize = 0; + needsSenderProfile = false; + } else if (this.props.tileShape === "notif") { avatarSize = 24; needsSenderProfile = true; } else if (isInfoMessage) { @@ -560,6 +563,8 @@ module.exports = WithMatrixClient(React.createClass({ })); module.exports.haveTileForEvent = function(e) { + // Only messages have a tile (black-rectangle) if redacted + if (e.isRedacted() && e.getType() !== 'm.room.message') return false; if (eventTileTypes[e.getType()] == undefined) return false; if (eventTileTypes[e.getType()] == 'messages.TextualEvent') { return TextForEvent.textForEvent(e) !== ''; From 9bae9368165e76b8622df6cb574b4c866ba9cbf5 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 3 Mar 2017 17:35:42 +0000 Subject: [PATCH 006/284] Add the redacter display name to the redaction text --- src/components/views/messages/UnknownBody.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/views/messages/UnknownBody.js b/src/components/views/messages/UnknownBody.js index 5504c0b1fe..95b3a1b54a 100644 --- a/src/components/views/messages/UnknownBody.js +++ b/src/components/views/messages/UnknownBody.js @@ -17,14 +17,19 @@ limitations under the License. 'use strict'; var React = require('react'); +var MatrixClientPeg = require('../../../MatrixClientPeg'); module.exports = React.createClass({ displayName: 'UnknownBody', render: function() { - var text = this.props.mxEvent.getContent().body; - if (this.props.mxEvent.isRedacted()) { - text = "This event was redacted"; + const ev = this.props.mxEvent; + var text = ev.getContent().body; + if (ev.isRedacted()) { + const room = MatrixClientPeg.get().getRoom(ev.getRoomId()); + const because = ev.getUnsigned().redacted_because; + const name = room.getMember(because.sender).name || because.sender; + text = "This event was redacted by " + name; } return ( From abd71cd2ac19bd7ba12a4c683cff05908daee1d7 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 3 Mar 2017 17:57:13 +0000 Subject: [PATCH 007/284] No need for "redactor" as we dont currently show it --- src/components/views/messages/UnknownBody.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/views/messages/UnknownBody.js b/src/components/views/messages/UnknownBody.js index 95b3a1b54a..374a4b9396 100644 --- a/src/components/views/messages/UnknownBody.js +++ b/src/components/views/messages/UnknownBody.js @@ -17,7 +17,6 @@ limitations under the License. 'use strict'; var React = require('react'); -var MatrixClientPeg = require('../../../MatrixClientPeg'); module.exports = React.createClass({ displayName: 'UnknownBody', @@ -26,10 +25,7 @@ module.exports = React.createClass({ const ev = this.props.mxEvent; var text = ev.getContent().body; if (ev.isRedacted()) { - const room = MatrixClientPeg.get().getRoom(ev.getRoomId()); - const because = ev.getUnsigned().redacted_because; - const name = room.getMember(because.sender).name || because.sender; - text = "This event was redacted by " + name; + text = "This event was redacted"; } return ( From edccfeb20b28e0306e1fca1bffbf1b36d99bc821 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 6 Mar 2017 10:26:26 +0000 Subject: [PATCH 008/284] No text required, do not continuate after redacted even It's curious, however, that a continuation occured after a redacted event, given that the event shouldn't have a sender --- src/components/structures/MessagePanel.js | 4 +++- src/components/views/messages/UnknownBody.js | 6 +----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 21665bb421..0b16a41590 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -411,7 +411,9 @@ module.exports = React.createClass({ // is this a continuation of the previous message? var continuation = false; - if (prevEvent !== null && prevEvent.sender && mxEv.sender + + if (prevEvent !== null + && !prevEvent.isRedacted() && prevEvent.sender && mxEv.sender && mxEv.sender.userId === prevEvent.sender.userId && mxEv.getType() == prevEvent.getType()) { continuation = true; diff --git a/src/components/views/messages/UnknownBody.js b/src/components/views/messages/UnknownBody.js index 374a4b9396..a0fe8fdf74 100644 --- a/src/components/views/messages/UnknownBody.js +++ b/src/components/views/messages/UnknownBody.js @@ -22,11 +22,7 @@ module.exports = React.createClass({ displayName: 'UnknownBody', render: function() { - const ev = this.props.mxEvent; - var text = ev.getContent().body; - if (ev.isRedacted()) { - text = "This event was redacted"; - } + const text = this.props.mxEvent.getContent().body; return ( {text} From c0fc3ba3fe6418aeae9df721a0ddbe9f5a916565 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 6 Mar 2017 14:20:24 +0000 Subject: [PATCH 009/284] Make redactions appear when the event has been redacted (on Room.redaction) --- src/components/structures/MessagePanel.js | 1 + src/components/views/rooms/EventTile.js | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 0b16a41590..ff507b6f90 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -466,6 +466,7 @@ module.exports = React.createClass({ ref={this._collectEventNode.bind(this, eventId)} data-scroll-token={scrollToken}> Date: Mon, 6 Mar 2017 14:22:12 +0000 Subject: [PATCH 010/284] Style --- src/components/views/dialogs/ChatCreateOrReuseDialog.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/dialogs/ChatCreateOrReuseDialog.js b/src/components/views/dialogs/ChatCreateOrReuseDialog.js index 241c7755d2..129dc24aac 100644 --- a/src/components/views/dialogs/ChatCreateOrReuseDialog.js +++ b/src/components/views/dialogs/ChatCreateOrReuseDialog.js @@ -26,17 +26,17 @@ import createRoom from '../../../createRoom'; export default class CreateOrReuseChatDialog extends React.Component { - constructor (props) { + constructor(props) { super(props); this._onNewDMClick = this._onNewDMClick.bind(this); } - _onNewDMClick () { + _onNewDMClick() { createRoom({dmUserId: this.props.userId}); this.props.onFinished(true); } - render () { + render() { const client = MatrixClientPeg.get(); const dmRoomMap = new DMRoomMap(client); From 4548d1b824dc869dcbe6acde201466dbb0eb4530 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 6 Mar 2017 14:51:01 +0000 Subject: [PATCH 011/284] Use dispatch instead of passing `onClick`, adjust dialog wording. --- .../views/dialogs/ChatCreateOrReuseDialog.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/components/views/dialogs/ChatCreateOrReuseDialog.js b/src/components/views/dialogs/ChatCreateOrReuseDialog.js index 129dc24aac..676ed0ce0a 100644 --- a/src/components/views/dialogs/ChatCreateOrReuseDialog.js +++ b/src/components/views/dialogs/ChatCreateOrReuseDialog.js @@ -29,6 +29,16 @@ export default class CreateOrReuseChatDialog extends React.Component { constructor(props) { super(props); this._onNewDMClick = this._onNewDMClick.bind(this); + dis.register(this._onAction.bind(this)); + } + + _onAction(payload) { + switch(payload.action) { + case 'view_room': + this.props.onFinished(true); + break; + default: + } } _onNewDMClick() { @@ -60,7 +70,6 @@ export default class CreateOrReuseChatDialog extends React.Component { unread={Unread.doesRoomHaveUnreadMessages(room)} highlight={highlight} isInvite={me.membership == "invite"} - onClick={() => this.props.onFinished(true)} /> ); } @@ -88,7 +97,7 @@ export default class CreateOrReuseChatDialog extends React.Component { }} title='Create a new chat or reuse an existing one' > -

Direct chats

+ You already have existing direct chats with this user: {tiles} {startNewChat} From 20fa36325f9ad25b6bbf4e7874411c710d092aff Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 6 Mar 2017 15:01:46 +0000 Subject: [PATCH 012/284] Remember to unregister on unmoun --- src/components/views/dialogs/ChatCreateOrReuseDialog.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/views/dialogs/ChatCreateOrReuseDialog.js b/src/components/views/dialogs/ChatCreateOrReuseDialog.js index 676ed0ce0a..53c5113b5d 100644 --- a/src/components/views/dialogs/ChatCreateOrReuseDialog.js +++ b/src/components/views/dialogs/ChatCreateOrReuseDialog.js @@ -29,7 +29,11 @@ export default class CreateOrReuseChatDialog extends React.Component { constructor(props) { super(props); this._onNewDMClick = this._onNewDMClick.bind(this); - dis.register(this._onAction.bind(this)); + this.dispatcherRef = dis.register(this._onAction.bind(this)); + } + + componentWillUnmount() { + dis.unregister(this.dispatcherRef); } _onAction(payload) { From bf348a0f7879dd65100cdc5fe3e06f800fd36f48 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 6 Mar 2017 17:44:29 +0000 Subject: [PATCH 013/284] Instead of listening for view_room, use a callback But make sure that nothing other than the callback is done when RoomTile is clicked. --- .../views/dialogs/ChatCreateOrReuseDialog.js | 30 ++++++++----------- src/components/views/rooms/MemberInfo.js | 8 +++++ src/components/views/rooms/RoomTile.js | 6 +--- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/components/views/dialogs/ChatCreateOrReuseDialog.js b/src/components/views/dialogs/ChatCreateOrReuseDialog.js index 53c5113b5d..7761e25010 100644 --- a/src/components/views/dialogs/ChatCreateOrReuseDialog.js +++ b/src/components/views/dialogs/ChatCreateOrReuseDialog.js @@ -28,28 +28,23 @@ export default class CreateOrReuseChatDialog extends React.Component { constructor(props) { super(props); - this._onNewDMClick = this._onNewDMClick.bind(this); - this.dispatcherRef = dis.register(this._onAction.bind(this)); + this.onNewDMClick = this.onNewDMClick.bind(this); + this.onRoomTileClick = this.onRoomTileClick.bind(this); } - componentWillUnmount() { - dis.unregister(this.dispatcherRef); - } - - _onAction(payload) { - switch(payload.action) { - case 'view_room': - this.props.onFinished(true); - break; - default: - } - } - - _onNewDMClick() { + onNewDMClick() { createRoom({dmUserId: this.props.userId}); this.props.onFinished(true); } + onRoomTileClick(roomId) { + dis.dispatch({ + action: 'view_room', + room_id: roomId, + }); + this.props.onFinished(true); + } + render() { const client = MatrixClientPeg.get(); @@ -74,6 +69,7 @@ export default class CreateOrReuseChatDialog extends React.Component { unread={Unread.doesRoomHaveUnreadMessages(room)} highlight={highlight} isInvite={me.membership == "invite"} + onClick={this.onRoomTileClick} /> ); } @@ -85,7 +81,7 @@ export default class CreateOrReuseChatDialog extends React.Component { }); const startNewChat =
diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 0c54565b9d..995453e9c1 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -553,6 +553,13 @@ module.exports = WithMatrixClient(React.createClass({ Modal.createDialog(ImageView, params, "mx_Dialog_lightbox"); }, + onRoomTileClick(roomId) { + dis.dispatch({ + action: 'view_room', + room_id: roomId, + }); + }, + _renderDevices: function() { if (!this._enableDevices) { return null; @@ -613,6 +620,7 @@ module.exports = WithMatrixClient(React.createClass({ unread={Unread.doesRoomHaveUnreadMessages(room)} highlight={highlight} isInvite={me.membership == "invite"} + onClick={this.onRoomTileClick} /> ); } diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 485b567ddc..1fd293cae8 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -101,12 +101,8 @@ module.exports = React.createClass({ }, onClick: function() { - dis.dispatch({ - action: 'view_room', - room_id: this.props.room.roomId, - }); if (this.props.onClick) { - this.props.onClick(); + this.props.onClick(this.props.room.roomId); } }, From 391886cac46dc6ae50188b1c6c63e6bfb76a0f1f Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 6 Mar 2017 17:45:25 +0000 Subject: [PATCH 014/284] Remove dis as a dep in RoomTile --- src/components/views/rooms/RoomTile.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 1fd293cae8..7d9034edd2 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -19,7 +19,6 @@ limitations under the License. var React = require('react'); var ReactDOM = require("react-dom"); var classNames = require('classnames'); -var dis = require("../../../dispatcher"); var MatrixClientPeg = require('../../../MatrixClientPeg'); import DMRoomMap from '../../../utils/DMRoomMap'; var sdk = require('../../../index'); From 06a05c351d95fbb90ee2db65cb04bb902eb23228 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 8 Mar 2017 10:25:54 +0000 Subject: [PATCH 015/284] Decide on which screen to show after login in one place This follows from a small amount of refactoring done when RTS was introduced. Instead of setting the screen after sync, do it only after login. This requires as-yet-to-be-PRd riot-web changes. This includes: - initialScreenAfterLogin, which can be used to set the screen after login, and represents the screen that would be viewed if the window.location at the time of initialising Riot were routed. - guestCreds are now part of state, because otherwise they don't cause the login/registration views to update when set. - instead of worrying about races and using this._setPage, use a dispatch. --- src/components/structures/MatrixChat.js | 79 +++++++++++-------- .../structures/login/Registration.js | 1 - 2 files changed, 44 insertions(+), 36 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 44fdfcf23e..7c398b39f9 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -63,6 +63,13 @@ module.exports = React.createClass({ // called when the session load completes onLoadCompleted: React.PropTypes.func, + // Represents the screen to display as a result of parsing the initial + // window.location + initialScreenAfterLogin: React.PropTypes.shape({ + screen: React.PropTypes.string.isRequired, + params: React.PropTypes.object, + }), + // displayname, if any, to set on the device when logging // in/registering. defaultDeviceDisplayName: React.PropTypes.string, @@ -89,6 +96,12 @@ module.exports = React.createClass({ var s = { loading: true, screen: undefined, + screenAfterLogin: this.props.initialScreenAfterLogin, + + // Stashed guest credentials if the user logs out + // whilst logged in as a guest user (so they can change + // their mind & log back in) + guestCreds: null, // What the LoggedInView would be showing if visible page_type: null, @@ -184,11 +197,6 @@ module.exports = React.createClass({ componentWillMount: function() { SdkConfig.put(this.props.config); - // Stashed guest credentials if the user logs out - // whilst logged in as a guest user (so they can change - // their mind & log back in) - this.guestCreds = null; - // if the automatic session load failed, the error this.sessionLoadError = null; @@ -322,9 +330,6 @@ module.exports = React.createClass({ var self = this; switch (payload.action) { case 'logout': - if (MatrixClientPeg.get().isGuest()) { - this.guestCreds = MatrixClientPeg.getCredentials(); - } Lifecycle.logout(); break; case 'start_registration': @@ -344,7 +349,11 @@ module.exports = React.createClass({ this.notifyNewScreen('register'); break; case 'start_login': - if (this.state.logged_in) return; + if (MatrixClientPeg.get().isGuest()) { + this.setState({ + guestCreds: MatrixClientPeg.getCredentials(), + }); + } this.setStateForNewScreen({ screen: 'login', }); @@ -359,8 +368,8 @@ module.exports = React.createClass({ // also stash our credentials, then if we restore the session, // we can just do it the same way whether we started upgrade // registration or explicitly logged out - this.guestCreds = MatrixClientPeg.getCredentials(); this.setStateForNewScreen({ + guestCreds: MatrixClientPeg.getCredentials(), screen: "register", upgradeUsername: MatrixClientPeg.get().getUserIdLocalpart(), guestAccessToken: MatrixClientPeg.get().getAccessToken(), @@ -708,19 +717,34 @@ module.exports = React.createClass({ * Called when a new logged in session has started */ _onLoggedIn: function(teamToken) { - this.guestCreds = null; - this.notifyNewScreen(''); this.setState({ - screen: undefined, + guestCreds: null, logged_in: true, }); + // If screenAfterLogin is set, use that, then null it so that a second login will + // result in view_home_page, _user_settings or _room_directory + if (this.state.screenAfterLogin) { + this.showScreen( + this.state.screenAfterLogin.screen, + this.state.screenAfterLogin.params + ); + this.setState({screenAfterLogin: null}); + return; + } else { + this.setState({screen: undefined}); + } + if (teamToken) { this._teamToken = teamToken; - this._setPage(PageTypes.HomePage); + dis.dispatch({action: 'view_home_page'}); + return; } else if (this._is_registered) { - this._setPage(PageTypes.UserSettings); + dis.dispatch({action: 'view_user_settings'}); + return; } + + dis.dispatch({action: 'view_room_directory'}); }, /** @@ -768,12 +792,6 @@ module.exports = React.createClass({ cli.getRooms() )[0].roomId; self.setState({ready: true, currentRoomId: firstRoom, page_type: PageTypes.RoomView}); - } else { - if (self._teamToken) { - self.setState({ready: true, page_type: PageTypes.HomePage}); - } else { - self.setState({ready: true, page_type: PageTypes.RoomDirectory}); - } } } else { self.setState({ready: true, page_type: PageTypes.RoomView}); @@ -790,16 +808,7 @@ module.exports = React.createClass({ if (presentedId != undefined) { self.notifyNewScreen('room/'+presentedId); - } else { - // There is no information on presentedId - // so point user to fallback like /directory - if (self._teamToken) { - self.notifyNewScreen('home'); - } else { - self.notifyNewScreen('directory'); - } } - dis.dispatch({action: 'focus_composer'}); } else { self.setState({ready: true}); @@ -1002,9 +1011,9 @@ module.exports = React.createClass({ onReturnToGuestClick: function() { // reanimate our guest login - if (this.guestCreds) { - Lifecycle.setLoggedIn(this.guestCreds); - this.guestCreds = null; + if (this.state.guestCreds) { + Lifecycle.setLoggedIn(this.state.guestCreds); + this.setState({guestCreds: null}); } }, @@ -1153,7 +1162,7 @@ module.exports = React.createClass({ onLoggedIn={this.onRegistered} onLoginClick={this.onLoginClick} onRegisterClick={this.onRegisterClick} - onCancelClick={this.guestCreds ? this.onReturnToGuestClick : null} + onCancelClick={this.state.guestCreds ? this.onReturnToGuestClick : null} /> ); } else if (this.state.screen == 'forgot_password') { @@ -1180,7 +1189,7 @@ module.exports = React.createClass({ defaultDeviceDisplayName={this.props.defaultDeviceDisplayName} onForgotPasswordClick={this.onForgotPasswordClick} enableGuest={this.props.enableGuest} - onCancelClick={this.guestCreds ? this.onReturnToGuestClick : null} + onCancelClick={this.state.guestCreds ? this.onReturnToGuestClick : null} initialErrorText={this.sessionLoadError} /> ); diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index 92f64eb6ab..57a7d6e19d 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -192,7 +192,6 @@ module.exports = React.createClass({ const teamToken = data.team_token; // Store for use /w welcome pages window.localStorage.setItem('mx_team_token', teamToken); - this.props.onTeamMemberRegistered(teamToken); this._rtsClient.getTeam(teamToken).then((team) => { console.log( From eca82bdb42fd03e889d4618bedf2695212dc8e51 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 8 Mar 2017 10:45:07 +0000 Subject: [PATCH 016/284] Make sure the screen is set, otherwise ignore screenAfterLogin --- src/components/structures/MatrixChat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 7c398b39f9..bae1f0849a 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -724,7 +724,7 @@ module.exports = React.createClass({ // If screenAfterLogin is set, use that, then null it so that a second login will // result in view_home_page, _user_settings or _room_directory - if (this.state.screenAfterLogin) { + if (this.state.screenAfterLogin && this.state.screenAfterLogin.screen) { this.showScreen( this.state.screenAfterLogin.screen, this.state.screenAfterLogin.params From c4001b5c5d43c78101ca51b48f4cc63a2c5667f2 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 8 Mar 2017 15:11:38 +0000 Subject: [PATCH 017/284] Use else instead of two returns --- src/components/structures/MatrixChat.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index bae1f0849a..fbb585924e 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -738,13 +738,11 @@ module.exports = React.createClass({ if (teamToken) { this._teamToken = teamToken; dis.dispatch({action: 'view_home_page'}); - return; } else if (this._is_registered) { dis.dispatch({action: 'view_user_settings'}); - return; + } else { + dis.dispatch({action: 'view_room_directory'}); } - - dis.dispatch({action: 'view_room_directory'}); }, /** From 2513bfa612a79d61b342f43a61975543deca1975 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 8 Mar 2017 16:55:44 +0000 Subject: [PATCH 018/284] Add onClick to permalinks to route within Riot --- src/components/views/rooms/EventTile.js | 32 +++++++++++++++++++------ 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 5fb65096a5..52bc856c31 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -25,7 +25,7 @@ var TextForEvent = require('../../../TextForEvent'); import WithMatrixClient from '../../../wrappers/WithMatrixClient'; var ContextualMenu = require('../../structures/ContextualMenu'); -var dispatcher = require("../../../dispatcher"); +import dis from '../../../dispatcher'; var ObjectUtils = require('../../../ObjectUtils'); @@ -356,7 +356,7 @@ module.exports = WithMatrixClient(React.createClass({ onSenderProfileClick: function(event) { var mxEvent = this.props.mxEvent; - dispatcher.dispatch({ + dis.dispatch({ action: 'insert_displayname', displayname: (mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender()).replace(' (IRC)', ''), }); @@ -372,6 +372,17 @@ module.exports = WithMatrixClient(React.createClass({ }); }, + onPermalinkClicked: function(e) { + // This allows the permalink to be open in a new tab/window or copied as + // matrix.to, but also for it to enable routing within Riot when clicked. + e.preventDefault(); + dis.dispatch({ + action: 'view_room', + event_id: this.props.mxEvent.getId(), + room_id: this.props.mxEvent.getRoomId(), + }); + }, + render: function() { var MessageTimestamp = sdk.getComponent('messages.MessageTimestamp'); var SenderProfile = sdk.getComponent('messages.SenderProfile'); @@ -413,7 +424,10 @@ module.exports = WithMatrixClient(React.createClass({ mx_EventTile_unverified: this.state.verified == false, mx_EventTile_bad: this.props.mxEvent.getContent().msgtype === 'm.bad.encrypted', }); - var permalink = "https://matrix.to/#/" + this.props.mxEvent.getRoomId() +"/"+ this.props.mxEvent.getId(); + + const permalink = "https://matrix.to/#/" + + this.props.mxEvent.getRoomId() + "/" + + this.props.mxEvent.getId(); var readAvatars = this.getReadAvatars(); @@ -493,13 +507,13 @@ module.exports = WithMatrixClient(React.createClass({ return (
{ avatar } - + { sender } @@ -527,7 +541,11 @@ module.exports = WithMatrixClient(React.createClass({ tileShape={this.props.tileShape} onWidgetLoad={this.props.onWidgetLoad} />
- +
{ sender } @@ -545,7 +563,7 @@ module.exports = WithMatrixClient(React.createClass({ { avatar } { sender }
- + { e2e } From 173daddb04f7be1466d9ce81a772dd44f7b9b1b6 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 9 Mar 2017 09:56:52 +0000 Subject: [PATCH 019/284] Comment typo --- src/components/views/rooms/EventTile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 52bc856c31..74fc4af400 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -373,7 +373,7 @@ module.exports = WithMatrixClient(React.createClass({ }, onPermalinkClicked: function(e) { - // This allows the permalink to be open in a new tab/window or copied as + // This allows the permalink to be opened in a new tab/window or copied as // matrix.to, but also for it to enable routing within Riot when clicked. e.preventDefault(); dis.dispatch({ From 4f7914813dc07df330364b20509703163ce17968 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 9 Mar 2017 10:44:09 +0000 Subject: [PATCH 020/284] Make UDD appear when UDE on uploading a file This has highlighted the fact that an unsent image looks very much like a sent image (https://github.com/vector-im/riot-web/issues/3391). Also, the "Resend" status bar doesn't appear when an image is unsent. --- src/components/structures/RoomView.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 696d15f84a..47b572b6c3 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -915,8 +915,6 @@ module.exports = React.createClass({ }, uploadFile: function(file) { - var self = this; - if (MatrixClientPeg.get().isGuest()) { var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog"); Modal.createDialog(NeedToRegisterDialog, { @@ -928,8 +926,16 @@ module.exports = React.createClass({ ContentMessages.sendContentToRoom( file, this.state.room.roomId, MatrixClientPeg.get() - ).done(undefined, function(error) { - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + ).done(undefined, (error) => { + if (error.name === "UnknownDeviceError") { + dis.dispatch({ + action: 'unknown_device_error', + err: error, + room: this.state.room, + }); + return; + } + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { title: "Failed to upload file", description: error.toString() From 02695623834215634244ce733e079149e98673bb Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 9 Mar 2017 10:59:22 +0000 Subject: [PATCH 021/284] Support registration & login with phone number (#742) * WIP msisdn sign in * A mostly working country picker * Fix bug where you'dbe logged out after registering Stop the guest sync, otherwise it gets 401ed for using a guest access token for a non-guest, causing us to beliebe we've been logged out. * Use InteractiveAuth component for registration * Fix tests * Remove old signup code * Signup -> Login Now that Signup contains no code whatsoever related to signing up, rename it to Login. Get rid of the Signup class. * Stray newline * Fix more merge failing * Get phone country & number to the right place * More-or-less working msisdn auth component * Send the bind_msisdn param on registration * Refinements to country dropdown Rendering the whole lot when the component was rendered just makes the page load really slow, so just show 2 at a time and rely on type-to-search. Make type-to-search always display an exact iso2 match first * Propagate initial inputs to the phone input * Support msisdn login * semicolon * Fix PropTypes * Oops, use the 1qst element of the array Not the array of object keys which has no particular order * Make dropdown/countrydropdown controlled * Unused line * Add note on DOM layout * onOptionChange is required * More docs * Add missing propTypes * Don't resume promise on error * Use React.Children to manipulate children * Make catch less weird * Fix null dereference Assuming [0] of an empty list == undefined doesn't work if you're then taking a property of it. --- src/HtmlUtils.js | 16 + src/Login.js | 41 +- src/component-index.js | 4 + src/components/structures/login/Login.js | 36 +- .../structures/login/Registration.js | 18 +- .../views/elements/AccessibleButton.js | 4 +- src/components/views/elements/Dropdown.js | 324 +++++ src/components/views/login/CountryDropdown.js | 123 ++ .../login/InteractiveAuthEntryComponents.js | 134 ++ src/components/views/login/PasswordLogin.js | 46 +- .../views/login/RegistrationForm.js | 43 + src/phonenumber.js | 1273 +++++++++++++++++ 12 files changed, 2032 insertions(+), 30 deletions(-) create mode 100644 src/components/views/elements/Dropdown.js create mode 100644 src/components/views/login/CountryDropdown.js create mode 100644 src/phonenumber.js diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index c500076783..f1420d0a22 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -58,6 +58,22 @@ export function unicodeToImage(str) { return str; } +/** + * Given one or more unicode characters (represented by unicode + * character number), return an image node with the corresponding + * emoji. + * + * @param alt {string} String to use for the image alt text + * @param unicode {integer} One or more integers representing unicode characters + * @returns A img node with the corresponding emoji + */ +export function charactersToImageNode(alt, ...unicode) { + const fileName = unicode.map((u) => { + return u.toString(16); + }).join('-'); + return {alt}; +} + export function stripParagraphs(html: string): string { const contentDiv = document.createElement('div'); contentDiv.innerHTML = html; diff --git a/src/Login.js b/src/Login.js index 96f953c130..053f88ce93 100644 --- a/src/Login.js +++ b/src/Login.js @@ -105,21 +105,38 @@ export default class Login { }); } - loginViaPassword(username, pass) { - var self = this; - var isEmail = username.indexOf("@") > 0; - var loginParams = { - password: pass, - initial_device_display_name: this._defaultDeviceDisplayName, - }; - if (isEmail) { - loginParams.medium = 'email'; - loginParams.address = username; + loginViaPassword(username, phoneCountry, phoneNumber, pass) { + const self = this; + + const isEmail = username.indexOf("@") > 0; + + let identifier; + if (phoneCountry && phoneNumber) { + identifier = { + type: 'm.id.phone', + country: phoneCountry, + number: phoneNumber, + }; + } else if (isEmail) { + identifier = { + type: 'm.id.thirdparty', + medium: 'email', + address: username, + }; } else { - loginParams.user = username; + identifier = { + type: 'm.id.user', + user: username, + }; } - var client = this._createTemporaryClient(); + const loginParams = { + password: pass, + identifier: identifier, + initial_device_display_name: this._defaultDeviceDisplayName, + }; + + const client = this._createTemporaryClient(); return client.login('m.login.password', loginParams).then(function(data) { return q({ homeserverUrl: self._hsUrl, diff --git a/src/component-index.js b/src/component-index.js index 2644f1a379..59d3ad53e4 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -109,6 +109,8 @@ import views$elements$DeviceVerifyButtons from './components/views/elements/Devi views$elements$DeviceVerifyButtons && (module.exports.components['views.elements.DeviceVerifyButtons'] = views$elements$DeviceVerifyButtons); import views$elements$DirectorySearchBox from './components/views/elements/DirectorySearchBox'; views$elements$DirectorySearchBox && (module.exports.components['views.elements.DirectorySearchBox'] = views$elements$DirectorySearchBox); +import views$elements$Dropdown from './components/views/elements/Dropdown'; +views$elements$Dropdown && (module.exports.components['views.elements.Dropdown'] = views$elements$Dropdown); import views$elements$EditableText from './components/views/elements/EditableText'; views$elements$EditableText && (module.exports.components['views.elements.EditableText'] = views$elements$EditableText); import views$elements$EditableTextContainer from './components/views/elements/EditableTextContainer'; @@ -131,6 +133,8 @@ import views$login$CaptchaForm from './components/views/login/CaptchaForm'; views$login$CaptchaForm && (module.exports.components['views.login.CaptchaForm'] = views$login$CaptchaForm); import views$login$CasLogin from './components/views/login/CasLogin'; views$login$CasLogin && (module.exports.components['views.login.CasLogin'] = views$login$CasLogin); +import views$login$CountryDropdown from './components/views/login/CountryDropdown'; +views$login$CountryDropdown && (module.exports.components['views.login.CountryDropdown'] = views$login$CountryDropdown); import views$login$CustomServerDialog from './components/views/login/CustomServerDialog'; views$login$CustomServerDialog && (module.exports.components['views.login.CustomServerDialog'] = views$login$CustomServerDialog); import views$login$InteractiveAuthEntryComponents from './components/views/login/InteractiveAuthEntryComponents'; diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index 69195fc715..0a1549f75b 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -64,8 +65,10 @@ module.exports = React.createClass({ enteredHomeserverUrl: this.props.customHsUrl || this.props.defaultHsUrl, enteredIdentityServerUrl: this.props.customIsUrl || this.props.defaultIsUrl, - // used for preserving username when changing homeserver + // used for preserving form values when changing homeserver username: "", + phoneCountry: null, + phoneNumber: "", }; }, @@ -73,20 +76,21 @@ module.exports = React.createClass({ this._initLoginLogic(); }, - onPasswordLogin: function(username, password) { - var self = this; - self.setState({ + onPasswordLogin: function(username, phoneCountry, phoneNumber, password) { + this.setState({ busy: true, errorText: null, loginIncorrect: false, }); - this._loginLogic.loginViaPassword(username, password).then(function(data) { - self.props.onLoggedIn(data); - }, function(error) { - self._setStateFromError(error, true); - }).finally(function() { - self.setState({ + this._loginLogic.loginViaPassword( + username, phoneCountry, phoneNumber, password, + ).then((data) => { + this.props.onLoggedIn(data); + }, (error) => { + this._setStateFromError(error, true); + }).finally(() => { + this.setState({ busy: false }); }).done(); @@ -119,6 +123,14 @@ module.exports = React.createClass({ this.setState({ username: username }); }, + onPhoneCountryChanged: function(phoneCountry) { + this.setState({ phoneCountry: phoneCountry }); + }, + + onPhoneNumberChanged: function(phoneNumber) { + this.setState({ phoneNumber: phoneNumber }); + }, + onHsUrlChanged: function(newHsUrl) { var self = this; this.setState({ @@ -225,7 +237,11 @@ module.exports = React.createClass({ diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index cbc8929158..f4805ef044 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -262,6 +262,9 @@ module.exports = React.createClass({ case "RegistrationForm.ERR_EMAIL_INVALID": errMsg = "This doesn't look like a valid email address"; break; + case "RegistrationForm.ERR_PHONE_NUMBER_INVALID": + errMsg = "This doesn't look like a valid phone number"; + break; case "RegistrationForm.ERR_USERNAME_INVALID": errMsg = "User names may only contain letters, numbers, dots, hyphens and underscores."; break; @@ -296,15 +299,20 @@ module.exports = React.createClass({ guestAccessToken = null; } + // Only send the bind params if we're sending username / pw params + // (Since we need to send no params at all to use the ones saved in the + // session). + const bindThreepids = this.state.formVals.password ? { + email: true, + msisdn: true, + } : {}; + return this._matrixClient.register( this.state.formVals.username, this.state.formVals.password, undefined, // session id: included in the auth dict already auth, - // Only send the bind_email param if we're sending username / pw params - // (Since we need to send no params at all to use the ones saved in the - // session). - Boolean(this.state.formVals.username) || undefined, + bindThreepids, guestAccessToken, ); }, @@ -355,6 +363,8 @@ module.exports = React.createClass({ + {this.props.children} +
+ } +}; + +MenuOption.propTypes = { + children: React.PropTypes.oneOfType([ + React.PropTypes.arrayOf(React.PropTypes.node), + React.PropTypes.node + ]), + highlighted: React.PropTypes.bool, + dropdownKey: React.PropTypes.string, + onClick: React.PropTypes.func.isRequired, + onMouseEnter: React.PropTypes.func.isRequired, +}; + +/* + * Reusable dropdown select control, akin to react-select, + * but somewhat simpler as react-select is 79KB of minified + * javascript. + * + * TODO: Port NetworkDropdown to use this. + */ +export default class Dropdown extends React.Component { + constructor(props) { + super(props); + + this.dropdownRootElement = null; + this.ignoreEvent = null; + + this._onInputClick = this._onInputClick.bind(this); + this._onRootClick = this._onRootClick.bind(this); + this._onDocumentClick = this._onDocumentClick.bind(this); + this._onMenuOptionClick = this._onMenuOptionClick.bind(this); + this._onInputKeyPress = this._onInputKeyPress.bind(this); + this._onInputKeyUp = this._onInputKeyUp.bind(this); + this._onInputChange = this._onInputChange.bind(this); + this._collectRoot = this._collectRoot.bind(this); + this._collectInputTextBox = this._collectInputTextBox.bind(this); + this._setHighlightedOption = this._setHighlightedOption.bind(this); + + this.inputTextBox = null; + + this._reindexChildren(this.props.children); + + const firstChild = React.Children.toArray(props.children)[0]; + + this.state = { + // True if the menu is dropped-down + expanded: false, + // The key of the highlighted option + // (the option that would become selected if you pressed enter) + highlightedOption: firstChild ? firstChild.key : null, + // the current search query + searchQuery: '', + }; + } + + componentWillMount() { + // Listen for all clicks on the document so we can close the + // menu when the user clicks somewhere else + document.addEventListener('click', this._onDocumentClick, false); + } + + componentWillUnmount() { + document.removeEventListener('click', this._onDocumentClick, false); + } + + componentWillReceiveProps(nextProps) { + this._reindexChildren(nextProps.children); + const firstChild = React.Children.toArray(nextProps.children)[0]; + this.setState({ + highlightedOption: firstChild ? firstChild.key : null, + }); + } + + _reindexChildren(children) { + this.childrenByKey = {}; + React.Children.forEach(children, (child) => { + this.childrenByKey[child.key] = child; + }); + } + + _onDocumentClick(ev) { + // Close the dropdown if the user clicks anywhere that isn't + // within our root element + if (ev !== this.ignoreEvent) { + this.setState({ + expanded: false, + }); + } + } + + _onRootClick(ev) { + // This captures any clicks that happen within our elements, + // such that we can then ignore them when they're seen by the + // click listener on the document handler, ie. not close the + // dropdown immediately after opening it. + // NB. We can't just stopPropagation() because then the event + // doesn't reach the React onClick(). + this.ignoreEvent = ev; + } + + _onInputClick(ev) { + this.setState({ + expanded: !this.state.expanded, + }); + ev.preventDefault(); + } + + _onMenuOptionClick(dropdownKey) { + this.setState({ + expanded: false, + }); + this.props.onOptionChange(dropdownKey); + } + + _onInputKeyPress(e) { + // This needs to be on the keypress event because otherwise + // it can't cancel the form submission + if (e.key == 'Enter') { + this.setState({ + expanded: false, + }); + this.props.onOptionChange(this.state.highlightedOption); + e.preventDefault(); + } + } + + _onInputKeyUp(e) { + // These keys don't generate keypress events and so needs to + // be on keyup + if (e.key == 'Escape') { + this.setState({ + expanded: false, + }); + } else if (e.key == 'ArrowDown') { + this.setState({ + highlightedOption: this._nextOption(this.state.highlightedOption), + }); + } else if (e.key == 'ArrowUp') { + this.setState({ + highlightedOption: this._prevOption(this.state.highlightedOption), + }); + } + } + + _onInputChange(e) { + this.setState({ + searchQuery: e.target.value, + }); + if (this.props.onSearchChange) { + this.props.onSearchChange(e.target.value); + } + } + + _collectRoot(e) { + if (this.dropdownRootElement) { + this.dropdownRootElement.removeEventListener( + 'click', this._onRootClick, false, + ); + } + if (e) { + e.addEventListener('click', this._onRootClick, false); + } + this.dropdownRootElement = e; + } + + _collectInputTextBox(e) { + this.inputTextBox = e; + if (e) e.focus(); + } + + _setHighlightedOption(optionKey) { + this.setState({ + highlightedOption: optionKey, + }); + } + + _nextOption(optionKey) { + const keys = Object.keys(this.childrenByKey); + const index = keys.indexOf(optionKey); + return keys[(index + 1) % keys.length]; + } + + _prevOption(optionKey) { + const keys = Object.keys(this.childrenByKey); + const index = keys.indexOf(optionKey); + return keys[(index - 1) % keys.length]; + } + + _getMenuOptions() { + const options = React.Children.map(this.props.children, (child) => { + return ( + + {child} + + ); + }); + + if (!this.state.searchQuery) { + options.push( +
+ Type to search... +
+ ); + } + return options; + } + + render() { + let currentValue; + + const menuStyle = {}; + if (this.props.menuWidth) menuStyle.width = this.props.menuWidth; + + let menu; + if (this.state.expanded) { + currentValue = ; + menu =
+ {this._getMenuOptions()} +
; + } else { + const selectedChild = this.props.getShortOption ? + this.props.getShortOption(this.props.value) : + this.childrenByKey[this.props.value]; + currentValue =
+ {selectedChild} +
+ } + + const dropdownClasses = { + mx_Dropdown: true, + }; + if (this.props.className) { + dropdownClasses[this.props.className] = true; + } + + // Note the menu sits inside the AccessibleButton div so it's anchored + // to the input, but overflows below it. The root contains both. + return
+ + {currentValue} + + {menu} + +
; + } +} + +Dropdown.propTypes = { + // The width that the dropdown should be. If specified, + // the dropped-down part of the menu will be set to this + // width. + menuWidth: React.PropTypes.number, + // Called when the selected option changes + onOptionChange: React.PropTypes.func.isRequired, + // Called when the value of the search field changes + onSearchChange: React.PropTypes.func, + // Function that, given the key of an option, returns + // a node representing that option to be displayed in the + // box itself as the currently-selected option (ie. as + // opposed to in the actual dropped-down part). If + // unspecified, the appropriate child element is used as + // in the dropped-down menu. + getShortOption: React.PropTypes.func, + value: React.PropTypes.string, +} diff --git a/src/components/views/login/CountryDropdown.js b/src/components/views/login/CountryDropdown.js new file mode 100644 index 0000000000..fc1e89661b --- /dev/null +++ b/src/components/views/login/CountryDropdown.js @@ -0,0 +1,123 @@ +/* +Copyright 2017 Vector Creations 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. +*/ + +import React from 'react'; + +import sdk from '../../../index'; + +import { COUNTRIES } from '../../../phonenumber'; +import { charactersToImageNode } from '../../../HtmlUtils'; + +const COUNTRIES_BY_ISO2 = new Object(null); +for (const c of COUNTRIES) { + COUNTRIES_BY_ISO2[c.iso2] = c; +} + +function countryMatchesSearchQuery(query, country) { + if (country.name.toUpperCase().indexOf(query.toUpperCase()) == 0) return true; + if (country.iso2 == query.toUpperCase()) return true; + if (country.prefix == query) return true; + return false; +} + +const MAX_DISPLAYED_ROWS = 2; + +export default class CountryDropdown extends React.Component { + constructor(props) { + super(props); + this._onSearchChange = this._onSearchChange.bind(this); + + this.state = { + searchQuery: '', + } + + if (!props.value) { + // If no value is given, we start with the first + // country selected, but our parent component + // doesn't know this, therefore we do this. + this.props.onOptionChange(COUNTRIES[0].iso2); + } + } + + _onSearchChange(search) { + this.setState({ + searchQuery: search, + }); + } + + _flagImgForIso2(iso2) { + // Unicode Regional Indicator Symbol letter 'A' + const RIS_A = 0x1F1E6; + const ASCII_A = 65; + return charactersToImageNode(iso2, + RIS_A + (iso2.charCodeAt(0) - ASCII_A), + RIS_A + (iso2.charCodeAt(1) - ASCII_A), + ); + } + + render() { + const Dropdown = sdk.getComponent('elements.Dropdown'); + + let displayedCountries; + if (this.state.searchQuery) { + displayedCountries = COUNTRIES.filter( + countryMatchesSearchQuery.bind(this, this.state.searchQuery), + ); + if ( + this.state.searchQuery.length == 2 && + COUNTRIES_BY_ISO2[this.state.searchQuery.toUpperCase()] + ) { + // exact ISO2 country name match: make the first result the matches ISO2 + const matched = COUNTRIES_BY_ISO2[this.state.searchQuery.toUpperCase()]; + displayedCountries = displayedCountries.filter((c) => { + return c.iso2 != matched.iso2; + }); + displayedCountries.unshift(matched); + } + } else { + displayedCountries = COUNTRIES; + } + + if (displayedCountries.length > MAX_DISPLAYED_ROWS) { + displayedCountries = displayedCountries.slice(0, MAX_DISPLAYED_ROWS); + } + + const options = displayedCountries.map((country) => { + return
+ {this._flagImgForIso2(country.iso2)} + {country.name} +
; + }); + + // default value here too, otherwise we need to handle null / undefined + // values between mounting and the initial value propgating + const value = this.props.value || COUNTRIES[0].iso2; + + return + {options} + + } +} + +CountryDropdown.propTypes = { + className: React.PropTypes.string, + onOptionChange: React.PropTypes.func.isRequired, + value: React.PropTypes.string, +}; diff --git a/src/components/views/login/InteractiveAuthEntryComponents.js b/src/components/views/login/InteractiveAuthEntryComponents.js index e75cb082d4..2d8abf9216 100644 --- a/src/components/views/login/InteractiveAuthEntryComponents.js +++ b/src/components/views/login/InteractiveAuthEntryComponents.js @@ -16,6 +16,8 @@ limitations under the License. */ import React from 'react'; +import url from 'url'; +import classnames from 'classnames'; import sdk from '../../../index'; @@ -255,6 +257,137 @@ export const EmailIdentityAuthEntry = React.createClass({ }, }); +export const MsisdnAuthEntry = React.createClass({ + displayName: 'MsisdnAuthEntry', + + statics: { + LOGIN_TYPE: "m.login.msisdn", + }, + + propTypes: { + inputs: React.PropTypes.shape({ + phoneCountry: React.PropTypes.string, + phoneNumber: React.PropTypes.string, + }), + fail: React.PropTypes.func, + clientSecret: React.PropTypes.func, + submitAuthDict: React.PropTypes.func.isRequired, + matrixClient: React.PropTypes.object, + submitAuthDict: React.PropTypes.func, + }, + + getInitialState: function() { + return { + token: '', + requestingToken: false, + }; + }, + + componentWillMount: function() { + this._sid = null; + this._msisdn = null; + this._tokenBox = null; + + this.setState({requestingToken: true}); + this._requestMsisdnToken().catch((e) => { + this.props.fail(e); + }).finally(() => { + this.setState({requestingToken: false}); + }).done(); + }, + + /* + * Requests a verification token by SMS. + */ + _requestMsisdnToken: function() { + return this.props.matrixClient.requestRegisterMsisdnToken( + this.props.inputs.phoneCountry, + this.props.inputs.phoneNumber, + this.props.clientSecret, + 1, // TODO: Multiple send attempts? + ).then((result) => { + this._sid = result.sid; + this._msisdn = result.msisdn; + }); + }, + + _onTokenChange: function(e) { + this.setState({ + token: e.target.value, + }); + }, + + _onFormSubmit: function(e) { + e.preventDefault(); + if (this.state.token == '') return; + + this.setState({ + errorText: null, + }); + + this.props.matrixClient.submitMsisdnToken( + this._sid, this.props.clientSecret, this.state.token + ).then((result) => { + if (result.success) { + const idServerParsedUrl = url.parse( + this.props.matrixClient.getIdentityServerUrl(), + ) + this.props.submitAuthDict({ + type: MsisdnAuthEntry.LOGIN_TYPE, + threepid_creds: { + sid: this._sid, + client_secret: this.props.clientSecret, + id_server: idServerParsedUrl.host, + }, + }); + } else { + this.setState({ + errorText: "Token incorrect", + }); + } + }).catch((e) => { + this.props.fail(e); + console.log("Failed to submit msisdn token"); + }).done(); + }, + + render: function() { + if (this.state.requestingToken) { + const Loader = sdk.getComponent("elements.Spinner"); + return ; + } else { + const enableSubmit = Boolean(this.state.token); + const submitClasses = classnames({ + mx_InteractiveAuthEntryComponents_msisdnSubmit: true, + mx_UserSettings_button: true, // XXX button classes + }); + return ( +
+

A text message has been sent to +{this._msisdn}

+

Please enter the code it contains:

+
+
+ +
+ +
+
+ {this.state.errorText} +
+
+
+ ); + } + }, +}); + export const FallbackAuthEntry = React.createClass({ displayName: 'FallbackAuthEntry', @@ -313,6 +446,7 @@ const AuthEntryComponents = [ PasswordAuthEntry, RecaptchaAuthEntry, EmailIdentityAuthEntry, + MsisdnAuthEntry, ]; export function getEntryComponentForLoginType(loginType) { diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index 6f6081858b..61cb3da652 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,6 +18,7 @@ limitations under the License. import React from 'react'; import ReactDOM from 'react-dom'; import classNames from 'classnames'; +import sdk from '../../../index'; import {field_input_incorrect} from '../../../UiEffects'; @@ -28,8 +30,12 @@ module.exports = React.createClass({displayName: 'PasswordLogin', onSubmit: React.PropTypes.func.isRequired, // fn(username, password) onForgotPasswordClick: React.PropTypes.func, // fn() initialUsername: React.PropTypes.string, + initialPhoneCountry: React.PropTypes.string, + initialPhoneNumber: React.PropTypes.string, initialPassword: React.PropTypes.string, onUsernameChanged: React.PropTypes.func, + onPhoneCountryChanged: React.PropTypes.func, + onPhoneNumberChanged: React.PropTypes.func, onPasswordChanged: React.PropTypes.func, loginIncorrect: React.PropTypes.bool, }, @@ -38,7 +44,11 @@ module.exports = React.createClass({displayName: 'PasswordLogin', return { onUsernameChanged: function() {}, onPasswordChanged: function() {}, + onPhoneCountryChanged: function() {}, + onPhoneNumberChanged: function() {}, initialUsername: "", + initialPhoneCountry: "", + initialPhoneNumber: "", initialPassword: "", loginIncorrect: false, }; @@ -48,6 +58,8 @@ module.exports = React.createClass({displayName: 'PasswordLogin', return { username: this.props.initialUsername, password: this.props.initialPassword, + phoneCountry: this.props.initialPhoneCountry, + phoneNumber: this.props.initialPhoneNumber, }; }, @@ -63,7 +75,12 @@ module.exports = React.createClass({displayName: 'PasswordLogin', onSubmitForm: function(ev) { ev.preventDefault(); - this.props.onSubmit(this.state.username, this.state.password); + this.props.onSubmit( + this.state.username, + this.state.phoneCountry, + this.state.phoneNumber, + this.state.password, + ); }, onUsernameChanged: function(ev) { @@ -71,6 +88,16 @@ module.exports = React.createClass({displayName: 'PasswordLogin', this.props.onUsernameChanged(ev.target.value); }, + onPhoneCountryChanged: function(country) { + this.setState({phoneCountry: country}); + this.props.onPhoneCountryChanged(country); + }, + + onPhoneNumberChanged: function(ev) { + this.setState({phoneNumber: ev.target.value}); + this.props.onPhoneNumberChanged(ev.target.value); + }, + onPasswordChanged: function(ev) { this.setState({password: ev.target.value}); this.props.onPasswordChanged(ev.target.value); @@ -92,13 +119,28 @@ module.exports = React.createClass({displayName: 'PasswordLogin', error: this.props.loginIncorrect, }); + const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); return (
- + or +
+ + +

{this._passwordField = e;}} type="password" name="password" diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js index 93e3976834..4868c9de63 100644 --- a/src/components/views/login/RegistrationForm.js +++ b/src/components/views/login/RegistrationForm.js @@ -19,9 +19,12 @@ import React from 'react'; import { field_input_incorrect } from '../../../UiEffects'; import sdk from '../../../index'; import Email from '../../../email'; +import { looksValid as phoneNumberLooksValid } from '../../../phonenumber'; import Modal from '../../../Modal'; const FIELD_EMAIL = 'field_email'; +const FIELD_PHONE_COUNTRY = 'field_phone_country'; +const FIELD_PHONE_NUMBER = 'field_phone_number'; const FIELD_USERNAME = 'field_username'; const FIELD_PASSWORD = 'field_password'; const FIELD_PASSWORD_CONFIRM = 'field_password_confirm'; @@ -35,6 +38,8 @@ module.exports = React.createClass({ propTypes: { // Values pre-filled in the input boxes when the component loads defaultEmail: React.PropTypes.string, + defaultPhoneCountry: React.PropTypes.string, + defaultPhoneNumber: React.PropTypes.string, defaultUsername: React.PropTypes.string, defaultPassword: React.PropTypes.string, teamsConfig: React.PropTypes.shape({ @@ -71,6 +76,8 @@ module.exports = React.createClass({ return { fieldValid: {}, selectedTeam: null, + // The ISO2 country code selected in the phone number entry + phoneCountry: this.props.defaultPhoneCountry, }; }, @@ -85,6 +92,7 @@ module.exports = React.createClass({ this.validateField(FIELD_PASSWORD_CONFIRM); this.validateField(FIELD_PASSWORD); this.validateField(FIELD_USERNAME); + this.validateField(FIELD_PHONE_NUMBER); this.validateField(FIELD_EMAIL); var self = this; @@ -118,6 +126,8 @@ module.exports = React.createClass({ username: this.refs.username.value.trim() || this.props.guestUsername, password: this.refs.password.value.trim(), email: email, + phoneCountry: this.state.phoneCountry, + phoneNumber: this.refs.phoneNumber.value.trim(), }); if (promise) { @@ -174,6 +184,11 @@ module.exports = React.createClass({ const emailValid = email === '' || Email.looksValid(email); this.markFieldValid(field_id, emailValid, "RegistrationForm.ERR_EMAIL_INVALID"); break; + case FIELD_PHONE_NUMBER: + const phoneNumber = this.refs.phoneNumber.value; + const phoneNumberValid = phoneNumber === '' || phoneNumberLooksValid(phoneNumber); + this.markFieldValid(field_id, phoneNumberValid, "RegistrationForm.ERR_PHONE_NUMBER_INVALID"); + break; case FIELD_USERNAME: // XXX: SPEC-1 var username = this.refs.username.value.trim() || this.props.guestUsername; @@ -233,6 +248,8 @@ module.exports = React.createClass({ switch (field_id) { case FIELD_EMAIL: return this.refs.email; + case FIELD_PHONE_NUMBER: + return this.refs.phoneNumber; case FIELD_USERNAME: return this.refs.username; case FIELD_PASSWORD: @@ -251,6 +268,12 @@ module.exports = React.createClass({ return cls; }, + _onPhoneCountryChange(newVal) { + this.setState({ + phoneCountry: newVal, + }); + }, + render: function() { var self = this; @@ -286,6 +309,25 @@ module.exports = React.createClass({ } } + const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); + const phoneSection = ( +
+ + +
+ ); + const registerButton = ( ); @@ -300,6 +342,7 @@ module.exports = React.createClass({ {emailSection} {belowEmailSection} + {phoneSection} Date: Thu, 9 Mar 2017 17:03:57 +0000 Subject: [PATCH 022/284] Merge the two RoomTile context menus into one This will require riot-web changes https://github.com/vector-im/riot-web/pull/3395 --- src/components/structures/MatrixChat.js | 48 ++++++++++++---- src/components/views/rooms/RoomTile.js | 76 +++++++------------------ 2 files changed, 56 insertions(+), 68 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 44fdfcf23e..fd0c69c662 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -317,6 +317,8 @@ module.exports = React.createClass({ }, onAction: function(payload) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); var roomIndexDelta = 1; var self = this; @@ -382,25 +384,23 @@ module.exports = React.createClass({ this.notifyNewScreen('forgot_password'); break; case 'leave_room': - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - - var roomId = payload.room_id; Modal.createDialog(QuestionDialog, { title: "Leave room", description: "Are you sure you want to leave the room?", - onFinished: function(should_leave) { + onFinished: (should_leave) => { if (should_leave) { - var d = MatrixClientPeg.get().leave(roomId); + const d = MatrixClientPeg.get().leave(payload.room_id); // FIXME: controller shouldn't be loading a view :( - var Loader = sdk.getComponent("elements.Spinner"); - var modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner'); + const Loader = sdk.getComponent("elements.Spinner"); + const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner'); - d.then(function() { + d.then(() => { modal.close(); - dis.dispatch({action: 'view_next_room'}); - }, function(err) { + if (this.currentRoomId === payload.room_id) { + dis.dispatch({action: 'view_next_room'}); + } + }, (err) => { modal.close(); Modal.createDialog(ErrorDialog, { title: "Failed to leave room", @@ -411,6 +411,32 @@ module.exports = React.createClass({ } }); break; + case 'reject_invite': + Modal.createDialog(QuestionDialog, { + title: "Reject invitation", + description: "Are you sure you want to reject the invitation?", + onFinished: (confirm) => { + if (confirm) { + // FIXME: controller shouldn't be loading a view :( + const Loader = sdk.getComponent("elements.Spinner"); + const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner'); + + MatrixClientPeg.get().leave(payload.room_id).done(() => { + modal.close(); + if (this.currentRoomId === payload.room_id) { + dis.dispatch({action: 'view_next_room'}); + } + }, (err) => { + modal.close(); + Modal.createDialog(ErrorDialog, { + title: "Failed to reject invitation", + description: err.toString() + }); + }); + } + } + }); + break; case 'view_user': // FIXME: ugly hack to expand the RightPanel and then re-dispatch. if (this.state.collapse_rhs) { diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 7d9034edd2..06b05e9299 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -56,8 +56,7 @@ module.exports = React.createClass({ return({ hover : false, badgeHover : false, - notificationTagMenu: false, - roomTagMenu: false, + menuDisplayed: false, notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId), }); }, @@ -136,62 +135,32 @@ module.exports = React.createClass({ this.setState({ hover: false }); } - var NotificationStateMenu = sdk.getComponent('context_menus.NotificationStateContextMenu'); + var RoomTileContextMenu = sdk.getComponent('context_menus.RoomTileContextMenu'); var elementRect = e.target.getBoundingClientRect(); + // The window X and Y offsets are to adjust position when zoomed in to page - var x = elementRect.right + window.pageXOffset + 3; - var y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset) - 53; + const x = elementRect.right + window.pageXOffset + 3; + const chevronOffset = 12; + let y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset); + y = y - (chevronOffset + 8); // where 8 is half the height of the chevron + var self = this; - ContextualMenu.createMenu(NotificationStateMenu, { - menuWidth: 188, - menuHeight: 126, - chevronOffset: 45, + ContextualMenu.createMenu(RoomTileContextMenu, { + chevronOffset: chevronOffset, left: x, top: y, room: this.props.room, onFinished: function() { - self.setState({ notificationTagMenu: false }); + self.setState({ menuDisplayed: false }); self.props.refreshSubList(); } }); - this.setState({ notificationTagMenu: true }); + this.setState({ menuDisplayed: true }); } // Prevent the RoomTile onClick event firing as well e.stopPropagation(); }, - onAvatarClicked: function(e) { - // Only allow none guests to access the context menu - if (!MatrixClientPeg.get().isGuest() && !this.props.collapsed) { - - // If the badge is clicked, then no longer show tooltip - if (this.props.collapsed) { - this.setState({ hover: false }); - } - - var RoomTagMenu = sdk.getComponent('context_menus.RoomTagContextMenu'); - var elementRect = e.target.getBoundingClientRect(); - // The window X and Y offsets are to adjust position when zoomed in to page - var x = elementRect.right + window.pageXOffset + 3; - var y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset) - 19; - var self = this; - ContextualMenu.createMenu(RoomTagMenu, { - chevronOffset: 10, - // XXX: fix horrid hardcoding - menuColour: UserSettingsStore.getSyncedSettings().theme === 'dark' ? "#2d2d2d" : "#FFFFFF", - left: x, - top: y, - room: this.props.room, - onFinished: function() { - self.setState({ roomTagMenu: false }); - } - }); - this.setState({ roomTagMenu: true }); - // Prevent the RoomTile onClick event firing as well - e.stopPropagation(); - } - }, - render: function() { var myUserId = MatrixClientPeg.get().credentials.userId; var me = this.props.room.currentState.members[myUserId]; @@ -210,7 +179,7 @@ module.exports = React.createClass({ 'mx_RoomTile_unreadNotify': notifBadges, 'mx_RoomTile_highlight': mentionBadges, 'mx_RoomTile_invited': (me && me.membership == 'invite'), - 'mx_RoomTile_notificationTagMenu': this.state.notificationTagMenu, + 'mx_RoomTile_menuDisplayed': this.state.menuDisplayed, 'mx_RoomTile_noBadges': !badges, }); @@ -218,14 +187,9 @@ module.exports = React.createClass({ 'mx_RoomTile_avatar': true, }); - var avatarContainerClasses = classNames({ - 'mx_RoomTile_avatar_container': true, - 'mx_RoomTile_avatar_roomTagMenu': this.state.roomTagMenu, - }); - var badgeClasses = classNames({ 'mx_RoomTile_badge': true, - 'mx_RoomTile_badgeButton': this.state.badgeHover || this.state.notificationTagMenu, + 'mx_RoomTile_badgeButton': this.state.badgeHover || this.state.menuDisplayed, }); // XXX: We should never display raw room IDs, but sometimes the @@ -236,7 +200,7 @@ module.exports = React.createClass({ var badge; var badgeContent; - if (this.state.badgeHover || this.state.notificationTagMenu) { + if (this.state.badgeHover || this.state.menuDisplayed) { badgeContent = "\u00B7\u00B7\u00B7"; } else if (badges) { var limitedCount = FormattingUtils.formatCount(notificationCount); @@ -254,7 +218,7 @@ module.exports = React.createClass({ var nameClasses = classNames({ 'mx_RoomTile_name': true, 'mx_RoomTile_invite': this.props.isInvite, - 'mx_RoomTile_badgeShown': badges || this.state.badgeHover || this.state.notificationTagMenu, + 'mx_RoomTile_badgeShown': badges || this.state.badgeHover || this.state.menuDisplayed, }); if (this.props.selected) { @@ -293,11 +257,9 @@ module.exports = React.createClass({
{ /* Only native elements can be wrapped in a DnD object. */}
-
-
- - {directMessageIndicator} -
+
+ + {directMessageIndicator}
From 2786fb0f467153c62463b906cc500e5fb169b1d7 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 9 Mar 2017 18:32:44 +0000 Subject: [PATCH 023/284] Revert "Support registration & login with phone number (#742)" This reverts commit 02695623834215634244ce733e079149e98673bb. This breaks against the current synapse release. We need to think more carefully about backwards compatibility. --- src/HtmlUtils.js | 16 - src/Login.js | 39 +- src/component-index.js | 4 - src/components/structures/login/Login.js | 36 +- .../structures/login/Registration.js | 18 +- .../views/elements/AccessibleButton.js | 4 +- src/components/views/elements/Dropdown.js | 324 ----- src/components/views/login/CountryDropdown.js | 123 -- .../login/InteractiveAuthEntryComponents.js | 134 -- src/components/views/login/PasswordLogin.js | 46 +- .../views/login/RegistrationForm.js | 43 - src/phonenumber.js | 1273 ----------------- 12 files changed, 29 insertions(+), 2031 deletions(-) delete mode 100644 src/components/views/elements/Dropdown.js delete mode 100644 src/components/views/login/CountryDropdown.js delete mode 100644 src/phonenumber.js diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index f1420d0a22..c500076783 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -58,22 +58,6 @@ export function unicodeToImage(str) { return str; } -/** - * Given one or more unicode characters (represented by unicode - * character number), return an image node with the corresponding - * emoji. - * - * @param alt {string} String to use for the image alt text - * @param unicode {integer} One or more integers representing unicode characters - * @returns A img node with the corresponding emoji - */ -export function charactersToImageNode(alt, ...unicode) { - const fileName = unicode.map((u) => { - return u.toString(16); - }).join('-'); - return {alt}; -} - export function stripParagraphs(html: string): string { const contentDiv = document.createElement('div'); contentDiv.innerHTML = html; diff --git a/src/Login.js b/src/Login.js index 053f88ce93..96f953c130 100644 --- a/src/Login.js +++ b/src/Login.js @@ -105,38 +105,21 @@ export default class Login { }); } - loginViaPassword(username, phoneCountry, phoneNumber, pass) { - const self = this; - - const isEmail = username.indexOf("@") > 0; - - let identifier; - if (phoneCountry && phoneNumber) { - identifier = { - type: 'm.id.phone', - country: phoneCountry, - number: phoneNumber, - }; - } else if (isEmail) { - identifier = { - type: 'm.id.thirdparty', - medium: 'email', - address: username, - }; - } else { - identifier = { - type: 'm.id.user', - user: username, - }; - } - - const loginParams = { + loginViaPassword(username, pass) { + var self = this; + var isEmail = username.indexOf("@") > 0; + var loginParams = { password: pass, - identifier: identifier, initial_device_display_name: this._defaultDeviceDisplayName, }; + if (isEmail) { + loginParams.medium = 'email'; + loginParams.address = username; + } else { + loginParams.user = username; + } - const client = this._createTemporaryClient(); + var client = this._createTemporaryClient(); return client.login('m.login.password', loginParams).then(function(data) { return q({ homeserverUrl: self._hsUrl, diff --git a/src/component-index.js b/src/component-index.js index 59d3ad53e4..2644f1a379 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -109,8 +109,6 @@ import views$elements$DeviceVerifyButtons from './components/views/elements/Devi views$elements$DeviceVerifyButtons && (module.exports.components['views.elements.DeviceVerifyButtons'] = views$elements$DeviceVerifyButtons); import views$elements$DirectorySearchBox from './components/views/elements/DirectorySearchBox'; views$elements$DirectorySearchBox && (module.exports.components['views.elements.DirectorySearchBox'] = views$elements$DirectorySearchBox); -import views$elements$Dropdown from './components/views/elements/Dropdown'; -views$elements$Dropdown && (module.exports.components['views.elements.Dropdown'] = views$elements$Dropdown); import views$elements$EditableText from './components/views/elements/EditableText'; views$elements$EditableText && (module.exports.components['views.elements.EditableText'] = views$elements$EditableText); import views$elements$EditableTextContainer from './components/views/elements/EditableTextContainer'; @@ -133,8 +131,6 @@ import views$login$CaptchaForm from './components/views/login/CaptchaForm'; views$login$CaptchaForm && (module.exports.components['views.login.CaptchaForm'] = views$login$CaptchaForm); import views$login$CasLogin from './components/views/login/CasLogin'; views$login$CasLogin && (module.exports.components['views.login.CasLogin'] = views$login$CasLogin); -import views$login$CountryDropdown from './components/views/login/CountryDropdown'; -views$login$CountryDropdown && (module.exports.components['views.login.CountryDropdown'] = views$login$CountryDropdown); import views$login$CustomServerDialog from './components/views/login/CustomServerDialog'; views$login$CustomServerDialog && (module.exports.components['views.login.CustomServerDialog'] = views$login$CustomServerDialog); import views$login$InteractiveAuthEntryComponents from './components/views/login/InteractiveAuthEntryComponents'; diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index 0a1549f75b..69195fc715 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -1,6 +1,5 @@ /* Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -65,10 +64,8 @@ module.exports = React.createClass({ enteredHomeserverUrl: this.props.customHsUrl || this.props.defaultHsUrl, enteredIdentityServerUrl: this.props.customIsUrl || this.props.defaultIsUrl, - // used for preserving form values when changing homeserver + // used for preserving username when changing homeserver username: "", - phoneCountry: null, - phoneNumber: "", }; }, @@ -76,21 +73,20 @@ module.exports = React.createClass({ this._initLoginLogic(); }, - onPasswordLogin: function(username, phoneCountry, phoneNumber, password) { - this.setState({ + onPasswordLogin: function(username, password) { + var self = this; + self.setState({ busy: true, errorText: null, loginIncorrect: false, }); - this._loginLogic.loginViaPassword( - username, phoneCountry, phoneNumber, password, - ).then((data) => { - this.props.onLoggedIn(data); - }, (error) => { - this._setStateFromError(error, true); - }).finally(() => { - this.setState({ + this._loginLogic.loginViaPassword(username, password).then(function(data) { + self.props.onLoggedIn(data); + }, function(error) { + self._setStateFromError(error, true); + }).finally(function() { + self.setState({ busy: false }); }).done(); @@ -123,14 +119,6 @@ module.exports = React.createClass({ this.setState({ username: username }); }, - onPhoneCountryChanged: function(phoneCountry) { - this.setState({ phoneCountry: phoneCountry }); - }, - - onPhoneNumberChanged: function(phoneNumber) { - this.setState({ phoneNumber: phoneNumber }); - }, - onHsUrlChanged: function(newHsUrl) { var self = this; this.setState({ @@ -237,11 +225,7 @@ module.exports = React.createClass({ diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index f4805ef044..cbc8929158 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -262,9 +262,6 @@ module.exports = React.createClass({ case "RegistrationForm.ERR_EMAIL_INVALID": errMsg = "This doesn't look like a valid email address"; break; - case "RegistrationForm.ERR_PHONE_NUMBER_INVALID": - errMsg = "This doesn't look like a valid phone number"; - break; case "RegistrationForm.ERR_USERNAME_INVALID": errMsg = "User names may only contain letters, numbers, dots, hyphens and underscores."; break; @@ -299,20 +296,15 @@ module.exports = React.createClass({ guestAccessToken = null; } - // Only send the bind params if we're sending username / pw params - // (Since we need to send no params at all to use the ones saved in the - // session). - const bindThreepids = this.state.formVals.password ? { - email: true, - msisdn: true, - } : {}; - return this._matrixClient.register( this.state.formVals.username, this.state.formVals.password, undefined, // session id: included in the auth dict already auth, - bindThreepids, + // Only send the bind_email param if we're sending username / pw params + // (Since we need to send no params at all to use the ones saved in the + // session). + Boolean(this.state.formVals.username) || undefined, guestAccessToken, ); }, @@ -363,8 +355,6 @@ module.exports = React.createClass({ - {this.props.children} -
- } -}; - -MenuOption.propTypes = { - children: React.PropTypes.oneOfType([ - React.PropTypes.arrayOf(React.PropTypes.node), - React.PropTypes.node - ]), - highlighted: React.PropTypes.bool, - dropdownKey: React.PropTypes.string, - onClick: React.PropTypes.func.isRequired, - onMouseEnter: React.PropTypes.func.isRequired, -}; - -/* - * Reusable dropdown select control, akin to react-select, - * but somewhat simpler as react-select is 79KB of minified - * javascript. - * - * TODO: Port NetworkDropdown to use this. - */ -export default class Dropdown extends React.Component { - constructor(props) { - super(props); - - this.dropdownRootElement = null; - this.ignoreEvent = null; - - this._onInputClick = this._onInputClick.bind(this); - this._onRootClick = this._onRootClick.bind(this); - this._onDocumentClick = this._onDocumentClick.bind(this); - this._onMenuOptionClick = this._onMenuOptionClick.bind(this); - this._onInputKeyPress = this._onInputKeyPress.bind(this); - this._onInputKeyUp = this._onInputKeyUp.bind(this); - this._onInputChange = this._onInputChange.bind(this); - this._collectRoot = this._collectRoot.bind(this); - this._collectInputTextBox = this._collectInputTextBox.bind(this); - this._setHighlightedOption = this._setHighlightedOption.bind(this); - - this.inputTextBox = null; - - this._reindexChildren(this.props.children); - - const firstChild = React.Children.toArray(props.children)[0]; - - this.state = { - // True if the menu is dropped-down - expanded: false, - // The key of the highlighted option - // (the option that would become selected if you pressed enter) - highlightedOption: firstChild ? firstChild.key : null, - // the current search query - searchQuery: '', - }; - } - - componentWillMount() { - // Listen for all clicks on the document so we can close the - // menu when the user clicks somewhere else - document.addEventListener('click', this._onDocumentClick, false); - } - - componentWillUnmount() { - document.removeEventListener('click', this._onDocumentClick, false); - } - - componentWillReceiveProps(nextProps) { - this._reindexChildren(nextProps.children); - const firstChild = React.Children.toArray(nextProps.children)[0]; - this.setState({ - highlightedOption: firstChild ? firstChild.key : null, - }); - } - - _reindexChildren(children) { - this.childrenByKey = {}; - React.Children.forEach(children, (child) => { - this.childrenByKey[child.key] = child; - }); - } - - _onDocumentClick(ev) { - // Close the dropdown if the user clicks anywhere that isn't - // within our root element - if (ev !== this.ignoreEvent) { - this.setState({ - expanded: false, - }); - } - } - - _onRootClick(ev) { - // This captures any clicks that happen within our elements, - // such that we can then ignore them when they're seen by the - // click listener on the document handler, ie. not close the - // dropdown immediately after opening it. - // NB. We can't just stopPropagation() because then the event - // doesn't reach the React onClick(). - this.ignoreEvent = ev; - } - - _onInputClick(ev) { - this.setState({ - expanded: !this.state.expanded, - }); - ev.preventDefault(); - } - - _onMenuOptionClick(dropdownKey) { - this.setState({ - expanded: false, - }); - this.props.onOptionChange(dropdownKey); - } - - _onInputKeyPress(e) { - // This needs to be on the keypress event because otherwise - // it can't cancel the form submission - if (e.key == 'Enter') { - this.setState({ - expanded: false, - }); - this.props.onOptionChange(this.state.highlightedOption); - e.preventDefault(); - } - } - - _onInputKeyUp(e) { - // These keys don't generate keypress events and so needs to - // be on keyup - if (e.key == 'Escape') { - this.setState({ - expanded: false, - }); - } else if (e.key == 'ArrowDown') { - this.setState({ - highlightedOption: this._nextOption(this.state.highlightedOption), - }); - } else if (e.key == 'ArrowUp') { - this.setState({ - highlightedOption: this._prevOption(this.state.highlightedOption), - }); - } - } - - _onInputChange(e) { - this.setState({ - searchQuery: e.target.value, - }); - if (this.props.onSearchChange) { - this.props.onSearchChange(e.target.value); - } - } - - _collectRoot(e) { - if (this.dropdownRootElement) { - this.dropdownRootElement.removeEventListener( - 'click', this._onRootClick, false, - ); - } - if (e) { - e.addEventListener('click', this._onRootClick, false); - } - this.dropdownRootElement = e; - } - - _collectInputTextBox(e) { - this.inputTextBox = e; - if (e) e.focus(); - } - - _setHighlightedOption(optionKey) { - this.setState({ - highlightedOption: optionKey, - }); - } - - _nextOption(optionKey) { - const keys = Object.keys(this.childrenByKey); - const index = keys.indexOf(optionKey); - return keys[(index + 1) % keys.length]; - } - - _prevOption(optionKey) { - const keys = Object.keys(this.childrenByKey); - const index = keys.indexOf(optionKey); - return keys[(index - 1) % keys.length]; - } - - _getMenuOptions() { - const options = React.Children.map(this.props.children, (child) => { - return ( - - {child} - - ); - }); - - if (!this.state.searchQuery) { - options.push( -
- Type to search... -
- ); - } - return options; - } - - render() { - let currentValue; - - const menuStyle = {}; - if (this.props.menuWidth) menuStyle.width = this.props.menuWidth; - - let menu; - if (this.state.expanded) { - currentValue = ; - menu =
- {this._getMenuOptions()} -
; - } else { - const selectedChild = this.props.getShortOption ? - this.props.getShortOption(this.props.value) : - this.childrenByKey[this.props.value]; - currentValue =
- {selectedChild} -
- } - - const dropdownClasses = { - mx_Dropdown: true, - }; - if (this.props.className) { - dropdownClasses[this.props.className] = true; - } - - // Note the menu sits inside the AccessibleButton div so it's anchored - // to the input, but overflows below it. The root contains both. - return
- - {currentValue} - - {menu} - -
; - } -} - -Dropdown.propTypes = { - // The width that the dropdown should be. If specified, - // the dropped-down part of the menu will be set to this - // width. - menuWidth: React.PropTypes.number, - // Called when the selected option changes - onOptionChange: React.PropTypes.func.isRequired, - // Called when the value of the search field changes - onSearchChange: React.PropTypes.func, - // Function that, given the key of an option, returns - // a node representing that option to be displayed in the - // box itself as the currently-selected option (ie. as - // opposed to in the actual dropped-down part). If - // unspecified, the appropriate child element is used as - // in the dropped-down menu. - getShortOption: React.PropTypes.func, - value: React.PropTypes.string, -} diff --git a/src/components/views/login/CountryDropdown.js b/src/components/views/login/CountryDropdown.js deleted file mode 100644 index fc1e89661b..0000000000 --- a/src/components/views/login/CountryDropdown.js +++ /dev/null @@ -1,123 +0,0 @@ -/* -Copyright 2017 Vector Creations 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. -*/ - -import React from 'react'; - -import sdk from '../../../index'; - -import { COUNTRIES } from '../../../phonenumber'; -import { charactersToImageNode } from '../../../HtmlUtils'; - -const COUNTRIES_BY_ISO2 = new Object(null); -for (const c of COUNTRIES) { - COUNTRIES_BY_ISO2[c.iso2] = c; -} - -function countryMatchesSearchQuery(query, country) { - if (country.name.toUpperCase().indexOf(query.toUpperCase()) == 0) return true; - if (country.iso2 == query.toUpperCase()) return true; - if (country.prefix == query) return true; - return false; -} - -const MAX_DISPLAYED_ROWS = 2; - -export default class CountryDropdown extends React.Component { - constructor(props) { - super(props); - this._onSearchChange = this._onSearchChange.bind(this); - - this.state = { - searchQuery: '', - } - - if (!props.value) { - // If no value is given, we start with the first - // country selected, but our parent component - // doesn't know this, therefore we do this. - this.props.onOptionChange(COUNTRIES[0].iso2); - } - } - - _onSearchChange(search) { - this.setState({ - searchQuery: search, - }); - } - - _flagImgForIso2(iso2) { - // Unicode Regional Indicator Symbol letter 'A' - const RIS_A = 0x1F1E6; - const ASCII_A = 65; - return charactersToImageNode(iso2, - RIS_A + (iso2.charCodeAt(0) - ASCII_A), - RIS_A + (iso2.charCodeAt(1) - ASCII_A), - ); - } - - render() { - const Dropdown = sdk.getComponent('elements.Dropdown'); - - let displayedCountries; - if (this.state.searchQuery) { - displayedCountries = COUNTRIES.filter( - countryMatchesSearchQuery.bind(this, this.state.searchQuery), - ); - if ( - this.state.searchQuery.length == 2 && - COUNTRIES_BY_ISO2[this.state.searchQuery.toUpperCase()] - ) { - // exact ISO2 country name match: make the first result the matches ISO2 - const matched = COUNTRIES_BY_ISO2[this.state.searchQuery.toUpperCase()]; - displayedCountries = displayedCountries.filter((c) => { - return c.iso2 != matched.iso2; - }); - displayedCountries.unshift(matched); - } - } else { - displayedCountries = COUNTRIES; - } - - if (displayedCountries.length > MAX_DISPLAYED_ROWS) { - displayedCountries = displayedCountries.slice(0, MAX_DISPLAYED_ROWS); - } - - const options = displayedCountries.map((country) => { - return
- {this._flagImgForIso2(country.iso2)} - {country.name} -
; - }); - - // default value here too, otherwise we need to handle null / undefined - // values between mounting and the initial value propgating - const value = this.props.value || COUNTRIES[0].iso2; - - return - {options} - - } -} - -CountryDropdown.propTypes = { - className: React.PropTypes.string, - onOptionChange: React.PropTypes.func.isRequired, - value: React.PropTypes.string, -}; diff --git a/src/components/views/login/InteractiveAuthEntryComponents.js b/src/components/views/login/InteractiveAuthEntryComponents.js index 2d8abf9216..e75cb082d4 100644 --- a/src/components/views/login/InteractiveAuthEntryComponents.js +++ b/src/components/views/login/InteractiveAuthEntryComponents.js @@ -16,8 +16,6 @@ limitations under the License. */ import React from 'react'; -import url from 'url'; -import classnames from 'classnames'; import sdk from '../../../index'; @@ -257,137 +255,6 @@ export const EmailIdentityAuthEntry = React.createClass({ }, }); -export const MsisdnAuthEntry = React.createClass({ - displayName: 'MsisdnAuthEntry', - - statics: { - LOGIN_TYPE: "m.login.msisdn", - }, - - propTypes: { - inputs: React.PropTypes.shape({ - phoneCountry: React.PropTypes.string, - phoneNumber: React.PropTypes.string, - }), - fail: React.PropTypes.func, - clientSecret: React.PropTypes.func, - submitAuthDict: React.PropTypes.func.isRequired, - matrixClient: React.PropTypes.object, - submitAuthDict: React.PropTypes.func, - }, - - getInitialState: function() { - return { - token: '', - requestingToken: false, - }; - }, - - componentWillMount: function() { - this._sid = null; - this._msisdn = null; - this._tokenBox = null; - - this.setState({requestingToken: true}); - this._requestMsisdnToken().catch((e) => { - this.props.fail(e); - }).finally(() => { - this.setState({requestingToken: false}); - }).done(); - }, - - /* - * Requests a verification token by SMS. - */ - _requestMsisdnToken: function() { - return this.props.matrixClient.requestRegisterMsisdnToken( - this.props.inputs.phoneCountry, - this.props.inputs.phoneNumber, - this.props.clientSecret, - 1, // TODO: Multiple send attempts? - ).then((result) => { - this._sid = result.sid; - this._msisdn = result.msisdn; - }); - }, - - _onTokenChange: function(e) { - this.setState({ - token: e.target.value, - }); - }, - - _onFormSubmit: function(e) { - e.preventDefault(); - if (this.state.token == '') return; - - this.setState({ - errorText: null, - }); - - this.props.matrixClient.submitMsisdnToken( - this._sid, this.props.clientSecret, this.state.token - ).then((result) => { - if (result.success) { - const idServerParsedUrl = url.parse( - this.props.matrixClient.getIdentityServerUrl(), - ) - this.props.submitAuthDict({ - type: MsisdnAuthEntry.LOGIN_TYPE, - threepid_creds: { - sid: this._sid, - client_secret: this.props.clientSecret, - id_server: idServerParsedUrl.host, - }, - }); - } else { - this.setState({ - errorText: "Token incorrect", - }); - } - }).catch((e) => { - this.props.fail(e); - console.log("Failed to submit msisdn token"); - }).done(); - }, - - render: function() { - if (this.state.requestingToken) { - const Loader = sdk.getComponent("elements.Spinner"); - return ; - } else { - const enableSubmit = Boolean(this.state.token); - const submitClasses = classnames({ - mx_InteractiveAuthEntryComponents_msisdnSubmit: true, - mx_UserSettings_button: true, // XXX button classes - }); - return ( -
-

A text message has been sent to +{this._msisdn}

-

Please enter the code it contains:

-
- - -
- - -
- {this.state.errorText} -
-
-
- ); - } - }, -}); - export const FallbackAuthEntry = React.createClass({ displayName: 'FallbackAuthEntry', @@ -446,7 +313,6 @@ const AuthEntryComponents = [ PasswordAuthEntry, RecaptchaAuthEntry, EmailIdentityAuthEntry, - MsisdnAuthEntry, ]; export function getEntryComponentForLoginType(loginType) { diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index 61cb3da652..6f6081858b 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.js @@ -1,6 +1,5 @@ /* Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,7 +17,6 @@ limitations under the License. import React from 'react'; import ReactDOM from 'react-dom'; import classNames from 'classnames'; -import sdk from '../../../index'; import {field_input_incorrect} from '../../../UiEffects'; @@ -30,12 +28,8 @@ module.exports = React.createClass({displayName: 'PasswordLogin', onSubmit: React.PropTypes.func.isRequired, // fn(username, password) onForgotPasswordClick: React.PropTypes.func, // fn() initialUsername: React.PropTypes.string, - initialPhoneCountry: React.PropTypes.string, - initialPhoneNumber: React.PropTypes.string, initialPassword: React.PropTypes.string, onUsernameChanged: React.PropTypes.func, - onPhoneCountryChanged: React.PropTypes.func, - onPhoneNumberChanged: React.PropTypes.func, onPasswordChanged: React.PropTypes.func, loginIncorrect: React.PropTypes.bool, }, @@ -44,11 +38,7 @@ module.exports = React.createClass({displayName: 'PasswordLogin', return { onUsernameChanged: function() {}, onPasswordChanged: function() {}, - onPhoneCountryChanged: function() {}, - onPhoneNumberChanged: function() {}, initialUsername: "", - initialPhoneCountry: "", - initialPhoneNumber: "", initialPassword: "", loginIncorrect: false, }; @@ -58,8 +48,6 @@ module.exports = React.createClass({displayName: 'PasswordLogin', return { username: this.props.initialUsername, password: this.props.initialPassword, - phoneCountry: this.props.initialPhoneCountry, - phoneNumber: this.props.initialPhoneNumber, }; }, @@ -75,12 +63,7 @@ module.exports = React.createClass({displayName: 'PasswordLogin', onSubmitForm: function(ev) { ev.preventDefault(); - this.props.onSubmit( - this.state.username, - this.state.phoneCountry, - this.state.phoneNumber, - this.state.password, - ); + this.props.onSubmit(this.state.username, this.state.password); }, onUsernameChanged: function(ev) { @@ -88,16 +71,6 @@ module.exports = React.createClass({displayName: 'PasswordLogin', this.props.onUsernameChanged(ev.target.value); }, - onPhoneCountryChanged: function(country) { - this.setState({phoneCountry: country}); - this.props.onPhoneCountryChanged(country); - }, - - onPhoneNumberChanged: function(ev) { - this.setState({phoneNumber: ev.target.value}); - this.props.onPhoneNumberChanged(ev.target.value); - }, - onPasswordChanged: function(ev) { this.setState({password: ev.target.value}); this.props.onPasswordChanged(ev.target.value); @@ -119,28 +92,13 @@ module.exports = React.createClass({displayName: 'PasswordLogin', error: this.props.loginIncorrect, }); - const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); return (
- - or -
- - -

{this._passwordField = e;}} type="password" name="password" diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js index 4868c9de63..93e3976834 100644 --- a/src/components/views/login/RegistrationForm.js +++ b/src/components/views/login/RegistrationForm.js @@ -19,12 +19,9 @@ import React from 'react'; import { field_input_incorrect } from '../../../UiEffects'; import sdk from '../../../index'; import Email from '../../../email'; -import { looksValid as phoneNumberLooksValid } from '../../../phonenumber'; import Modal from '../../../Modal'; const FIELD_EMAIL = 'field_email'; -const FIELD_PHONE_COUNTRY = 'field_phone_country'; -const FIELD_PHONE_NUMBER = 'field_phone_number'; const FIELD_USERNAME = 'field_username'; const FIELD_PASSWORD = 'field_password'; const FIELD_PASSWORD_CONFIRM = 'field_password_confirm'; @@ -38,8 +35,6 @@ module.exports = React.createClass({ propTypes: { // Values pre-filled in the input boxes when the component loads defaultEmail: React.PropTypes.string, - defaultPhoneCountry: React.PropTypes.string, - defaultPhoneNumber: React.PropTypes.string, defaultUsername: React.PropTypes.string, defaultPassword: React.PropTypes.string, teamsConfig: React.PropTypes.shape({ @@ -76,8 +71,6 @@ module.exports = React.createClass({ return { fieldValid: {}, selectedTeam: null, - // The ISO2 country code selected in the phone number entry - phoneCountry: this.props.defaultPhoneCountry, }; }, @@ -92,7 +85,6 @@ module.exports = React.createClass({ this.validateField(FIELD_PASSWORD_CONFIRM); this.validateField(FIELD_PASSWORD); this.validateField(FIELD_USERNAME); - this.validateField(FIELD_PHONE_NUMBER); this.validateField(FIELD_EMAIL); var self = this; @@ -126,8 +118,6 @@ module.exports = React.createClass({ username: this.refs.username.value.trim() || this.props.guestUsername, password: this.refs.password.value.trim(), email: email, - phoneCountry: this.state.phoneCountry, - phoneNumber: this.refs.phoneNumber.value.trim(), }); if (promise) { @@ -184,11 +174,6 @@ module.exports = React.createClass({ const emailValid = email === '' || Email.looksValid(email); this.markFieldValid(field_id, emailValid, "RegistrationForm.ERR_EMAIL_INVALID"); break; - case FIELD_PHONE_NUMBER: - const phoneNumber = this.refs.phoneNumber.value; - const phoneNumberValid = phoneNumber === '' || phoneNumberLooksValid(phoneNumber); - this.markFieldValid(field_id, phoneNumberValid, "RegistrationForm.ERR_PHONE_NUMBER_INVALID"); - break; case FIELD_USERNAME: // XXX: SPEC-1 var username = this.refs.username.value.trim() || this.props.guestUsername; @@ -248,8 +233,6 @@ module.exports = React.createClass({ switch (field_id) { case FIELD_EMAIL: return this.refs.email; - case FIELD_PHONE_NUMBER: - return this.refs.phoneNumber; case FIELD_USERNAME: return this.refs.username; case FIELD_PASSWORD: @@ -268,12 +251,6 @@ module.exports = React.createClass({ return cls; }, - _onPhoneCountryChange(newVal) { - this.setState({ - phoneCountry: newVal, - }); - }, - render: function() { var self = this; @@ -309,25 +286,6 @@ module.exports = React.createClass({ } } - const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); - const phoneSection = ( -
- - -
- ); - const registerButton = ( ); @@ -342,7 +300,6 @@ module.exports = React.createClass({ {emailSection} {belowEmailSection} - {phoneSection} Date: Sun, 12 Mar 2017 20:03:05 +0000 Subject: [PATCH 024/284] beautify UserSettings error msg fix up default dialog cancel button --- src/components/structures/UserSettings.js | 2 +- src/components/views/dialogs/BaseDialog.js | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 10ffbca0d3..1e99a12e4d 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -208,7 +208,7 @@ module.exports = React.createClass({ var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { title: "Can't load user settings", - description: error.toString() + description: "Server may be unavailable or overloaded", }); }); }, diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js index e83403ef7c..f404bdd33d 100644 --- a/src/components/views/dialogs/BaseDialog.js +++ b/src/components/views/dialogs/BaseDialog.js @@ -65,15 +65,14 @@ export default React.createClass({ }, render: function() { + const TintableSvg = sdk.getComponent("elements.TintableSvg"); + return (
- Cancel +
{ this.props.title } From e5a5b5cd08e6acb7449ac44cca1472752f6aa5ea Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 12 Mar 2017 20:13:39 +0000 Subject: [PATCH 025/284] oops --- src/components/views/dialogs/BaseDialog.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js index f404bdd33d..0b2ca5225d 100644 --- a/src/components/views/dialogs/BaseDialog.js +++ b/src/components/views/dialogs/BaseDialog.js @@ -18,6 +18,7 @@ import React from 'react'; import * as KeyCode from '../../../KeyCode'; import AccessibleButton from '../elements/AccessibleButton'; +import sdk from '../../../index'; /** * Basic container for modal dialogs. From 71e0780eeecbe3db8b00722f9128ee717eaee31c Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 12 Mar 2017 22:24:16 +0000 Subject: [PATCH 026/284] beautify search fail error --- src/components/structures/RoomView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 696d15f84a..fe7dad3a69 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1018,7 +1018,7 @@ module.exports = React.createClass({ var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { title: "Search failed", - description: error.toString() + description: "Server may be unavailable, overloaded, or search timed out :(" }); }).finally(function() { self.setState({ From 3aaf37df1a02846f7122f964a6090ba581979b5c Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 12 Mar 2017 22:59:41 +0000 Subject: [PATCH 027/284] beautify a tonne more errors --- src/CallHandler.js | 3 ++- src/components/structures/MatrixChat.js | 3 ++- src/components/structures/RoomView.js | 4 ++- src/components/structures/UserSettings.js | 20 +++++++++------ .../views/dialogs/ChatInviteDialog.js | 12 ++++----- src/components/views/rooms/MemberInfo.js | 25 +++++++++++-------- .../views/rooms/MessageComposerInput.js | 2 +- .../views/rooms/MessageComposerInputOld.js | 2 +- src/components/views/rooms/RoomHeader.js | 3 ++- src/components/views/rooms/RoomSettings.js | 5 ++-- src/createRoom.js | 3 ++- 11 files changed, 50 insertions(+), 32 deletions(-) diff --git a/src/CallHandler.js b/src/CallHandler.js index bb46056d19..42cc681d08 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -310,9 +310,10 @@ function _onAction(payload) { placeCall(call); }, function(err) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Conference call failed: " + err); Modal.createDialog(ErrorDialog, { title: "Failed to set up conference call", - description: "Conference call failed: " + err, + description: "Conference call failed.", }); }); } diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 44fdfcf23e..2fa5e92608 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -402,9 +402,10 @@ module.exports = React.createClass({ dis.dispatch({action: 'view_next_room'}); }, function(err) { modal.close(); + console.error("Failed to leave room " + payload.room_id + " " + err); Modal.createDialog(ErrorDialog, { title: "Failed to leave room", - description: err.toString() + description: "Server may be unavailable, overloaded, or you hit a bug." }); }); } diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index fe7dad3a69..52161012aa 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -930,9 +930,10 @@ module.exports = React.createClass({ file, this.state.room.roomId, MatrixClientPeg.get() ).done(undefined, function(error) { var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Failed to upload file " + file + " " + error); Modal.createDialog(ErrorDialog, { title: "Failed to upload file", - description: error.toString() + description: "Server may be unavailable, overloaded, or the file too big", }); }); }, @@ -1016,6 +1017,7 @@ module.exports = React.createClass({ }); }, function(error) { var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Search failed: " + error); Modal.createDialog(ErrorDialog, { title: "Search failed", description: "Server may be unavailable, overloaded, or search timed out :(" diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 1e99a12e4d..febdccd9c3 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -206,6 +206,7 @@ module.exports = React.createClass({ }); }, function(error) { var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Failed to load user settings: " + error); Modal.createDialog(ErrorDialog, { title: "Can't load user settings", description: "Server may be unavailable or overloaded", @@ -246,10 +247,11 @@ module.exports = React.createClass({ self._refreshFromServer(); }, function(err) { var errMsg = (typeof err === "string") ? err : (err.error || ""); + console.error("Failed to set avatar: " + err); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { title: "Error", - description: "Failed to set avatar. " + errMsg + description: "Failed to set avatar." }); }); }, @@ -286,6 +288,7 @@ module.exports = React.createClass({ errMsg += ` (HTTP status ${err.httpStatus})`; } var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Failed to change password: " + errMsg); Modal.createDialog(ErrorDialog, { title: "Error", description: errMsg @@ -337,9 +340,10 @@ module.exports = React.createClass({ }); }, (err) => { this.setState({email_add_pending: false}); + console.error("Unable to add email address " + email_address + " " + err); Modal.createDialog(ErrorDialog, { - title: "Unable to add email address", - description: err.message + title: "Error", + description: "Unable to add email address" }); }); ReactDOM.findDOMNode(this.refs.add_threepid_input).blur(); @@ -361,9 +365,10 @@ module.exports = React.createClass({ return this._refreshFromServer(); }).catch((err) => { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Unable to remove contact information: " + err); Modal.createDialog(ErrorDialog, { - title: "Unable to remove contact information", - description: err.toString(), + title: "Error", + description: "Unable to remove contact information", }); }).done(); } @@ -401,9 +406,10 @@ module.exports = React.createClass({ }); } else { var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Unable to verify email address: " + err); Modal.createDialog(ErrorDialog, { - title: "Unable to verify email address", - description: err.toString(), + title: "Error", + description: "Unable to verify email address", }); } }); diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index 0e6a2b62e6..f958b8887c 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -318,8 +318,8 @@ module.exports = React.createClass({ console.error(err.stack); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - title: "Failure to invite", - description: err.toString() + title: "Error", + description: "Failed to invite", }); return null; }) @@ -331,8 +331,8 @@ module.exports = React.createClass({ console.error(err.stack); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - title: "Failure to invite user", - description: err.toString() + title: "Error", + description: "Failed to invite user", }); return null; }) @@ -352,8 +352,8 @@ module.exports = React.createClass({ console.error(err.stack); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - title: "Failure to invite", - description: err.toString() + title: "Error", + description: "Failed to invite", }); return null; }) diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 467c31eb2a..39a6c052f8 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -237,9 +237,10 @@ module.exports = WithMatrixClient(React.createClass({ console.log("Kick success"); }, function(err) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Kick error: " + err); Modal.createDialog(ErrorDialog, { - title: "Kick error", - description: err.message + title: "Error", + description: "Failed to kick user", }); } ).finally(()=>{ @@ -278,9 +279,10 @@ module.exports = WithMatrixClient(React.createClass({ console.log("Ban success"); }, function(err) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Ban error: " + err); Modal.createDialog(ErrorDialog, { - title: "Ban error", - description: err.message, + title: "Error", + description: "Failed to ban user", }); } ).finally(()=>{ @@ -327,9 +329,10 @@ module.exports = WithMatrixClient(React.createClass({ // get out of sync if we force setState here! console.log("Mute toggle success"); }, function(err) { + console.error("Mute error: " + err); Modal.createDialog(ErrorDialog, { - title: "Mute error", - description: err.message + title: "Error", + description: "Failed to mute user", }); } ).finally(()=>{ @@ -375,9 +378,10 @@ module.exports = WithMatrixClient(React.createClass({ description: "This action cannot be performed by a guest user. Please register to be able to do this." }); } else { + console.error("Toggle moderator error:" + err); Modal.createDialog(ErrorDialog, { - title: "Moderator toggle error", - description: err.message + title: "Error", + description: "Failed to toggle moderator status", }); } } @@ -395,9 +399,10 @@ module.exports = WithMatrixClient(React.createClass({ console.log("Power change success"); }, function(err) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Failed to change power level " + err); Modal.createDialog(ErrorDialog, { - title: "Failure to change power level", - description: err.message + title: "Error", + description: "Failed to change power level", }); } ).finally(()=>{ diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index ef66942637..d702b7558d 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -509,7 +509,7 @@ export default class MessageComposerInput extends React.Component { var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { title: "Server error", - description: err.message + description: "Server unavailable, overloaded, or something else went wrong.", }); }); } diff --git a/src/components/views/rooms/MessageComposerInputOld.js b/src/components/views/rooms/MessageComposerInputOld.js index 9f6464b69b..f0b650eb04 100644 --- a/src/components/views/rooms/MessageComposerInputOld.js +++ b/src/components/views/rooms/MessageComposerInputOld.js @@ -311,7 +311,7 @@ export default React.createClass({ var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { title: "Server error", - description: err.message + description: "Server unavailable, overloaded, or something else went wrong.", }); }); } diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 1a8776cd96..94f2691f2c 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -115,9 +115,10 @@ module.exports = React.createClass({ changeAvatar.onFileSelected(ev).catch(function(err) { var errMsg = (typeof err === "string") ? err : (err.error || ""); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Failed to set avatar: " + errMsg); Modal.createDialog(ErrorDialog, { title: "Error", - description: "Failed to set avatar. " + errMsg + description: "Failed to set avatar.", }); }).done(); }, diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index 3247f5a90b..2c7e1d7140 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -54,9 +54,10 @@ const BannedUser = React.createClass({ this.props.member.roomId, this.props.member.userId, ).catch((err) => { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Failed to unban: " + err); Modal.createDialog(ErrorDialog, { - title: "Failed to unban", - description: err.message, + title: "Error", + description: "Failed to unban", }); }).done(); }, diff --git a/src/createRoom.js b/src/createRoom.js index 2a23fb0787..674fe23d28 100644 --- a/src/createRoom.js +++ b/src/createRoom.js @@ -102,9 +102,10 @@ function createRoom(opts) { }); return roomId; }, function(err) { + console.error("Failed to create room " + roomId + " " + err); Modal.createDialog(ErrorDialog, { title: "Failure to create room", - description: err.toString() + description: "Server may be unavailable, overloaded, or you hit a bug.", }); return null; }); From 185473b8982d0ea5575241f21f74cd5fc513cd1b Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 12 Mar 2017 23:48:49 +0000 Subject: [PATCH 028/284] copyright... --- src/UnknownDeviceErrorHandler.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/UnknownDeviceErrorHandler.js b/src/UnknownDeviceErrorHandler.js index 88f4f57fe4..d842cc3a6e 100644 --- a/src/UnknownDeviceErrorHandler.js +++ b/src/UnknownDeviceErrorHandler.js @@ -1,3 +1,19 @@ +/* +Copyright 2017 Vector Creations 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. +*/ + import dis from './dispatcher'; import sdk from './index'; import Modal from './Modal'; From 3a849bce603542bcc5e1c9e2607ba164c10f6fa9 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 12 Mar 2017 23:48:57 +0000 Subject: [PATCH 029/284] name class to match file --- src/components/views/dialogs/ChatCreateOrReuseDialog.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/dialogs/ChatCreateOrReuseDialog.js b/src/components/views/dialogs/ChatCreateOrReuseDialog.js index 7761e25010..8f57bf9ae3 100644 --- a/src/components/views/dialogs/ChatCreateOrReuseDialog.js +++ b/src/components/views/dialogs/ChatCreateOrReuseDialog.js @@ -24,7 +24,7 @@ import Unread from '../../../Unread'; import classNames from 'classnames'; import createRoom from '../../../createRoom'; -export default class CreateOrReuseChatDialog extends React.Component { +export default class ChatCreateOrReuseChatDialog extends React.Component { constructor(props) { super(props); @@ -91,7 +91,7 @@ export default class CreateOrReuseChatDialog extends React.Component { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); return ( - { this.props.onFinished(false) }} @@ -105,7 +105,7 @@ export default class CreateOrReuseChatDialog extends React.Component { } } -CreateOrReuseChatDialog.propTyps = { +ChatCreateOrReuseChatDialog.propTyps = { userId: React.PropTypes.string.isRequired, onFinished: React.PropTypes.func.isRequired, }; From bf64f387ced295501a8e05d6e5f8dc04be73f1db Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 12 Mar 2017 23:50:12 +0000 Subject: [PATCH 030/284] name class to match file --- src/components/views/dialogs/ChatCreateOrReuseDialog.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/dialogs/ChatCreateOrReuseDialog.js b/src/components/views/dialogs/ChatCreateOrReuseDialog.js index 8f57bf9ae3..559a6f39a9 100644 --- a/src/components/views/dialogs/ChatCreateOrReuseDialog.js +++ b/src/components/views/dialogs/ChatCreateOrReuseDialog.js @@ -24,7 +24,7 @@ import Unread from '../../../Unread'; import classNames from 'classnames'; import createRoom from '../../../createRoom'; -export default class ChatCreateOrReuseChatDialog extends React.Component { +export default class ChatCreateOrReuseDialog extends React.Component { constructor(props) { super(props); @@ -91,7 +91,7 @@ export default class ChatCreateOrReuseChatDialog extends React.Component { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); return ( - { this.props.onFinished(false) }} @@ -105,7 +105,7 @@ export default class ChatCreateOrReuseChatDialog extends React.Component { } } -ChatCreateOrReuseChatDialog.propTyps = { +ChatCreateOrReuseDialog.propTyps = { userId: React.PropTypes.string.isRequired, onFinished: React.PropTypes.func.isRequired, }; From 8a0b08e7f620f0ea9b7cc3dcf68f4d5f50c980b5 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 13 Mar 2017 00:03:33 +0000 Subject: [PATCH 031/284] fix CSS for ChatCreateOrReuseDialog.js --- .../views/dialogs/ChatCreateOrReuseDialog.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/views/dialogs/ChatCreateOrReuseDialog.js b/src/components/views/dialogs/ChatCreateOrReuseDialog.js index 559a6f39a9..1a6ddf0456 100644 --- a/src/components/views/dialogs/ChatCreateOrReuseDialog.js +++ b/src/components/views/dialogs/ChatCreateOrReuseDialog.js @@ -97,9 +97,13 @@ export default class ChatCreateOrReuseDialog extends React.Component { }} title='Create a new chat or reuse an existing one' > - You already have existing direct chats with this user: - {tiles} - {startNewChat} +
+ You already have existing direct chats with this user: +
+ {tiles} + {startNewChat} +
+
); } From 925bbb79ad370d7ccf9c18852a85fc4080779706 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 13 Mar 2017 00:47:33 +0000 Subject: [PATCH 032/284] fix kick dialog CSS --- src/components/views/dialogs/ConfirmUserActionDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/ConfirmUserActionDialog.js b/src/components/views/dialogs/ConfirmUserActionDialog.js index 4bd9cb669c..6cfaac65d4 100644 --- a/src/components/views/dialogs/ConfirmUserActionDialog.js +++ b/src/components/views/dialogs/ConfirmUserActionDialog.js @@ -97,7 +97,7 @@ export default React.createClass({ >
- +
{this.props.member.name}
{this.props.member.userId}
From d8a30aa848eab30138a9de102106b014230a3a5d Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 13 Mar 2017 13:48:15 +0000 Subject: [PATCH 033/284] Double UNPAGINATION_PADDING again --- src/components/structures/ScrollPanel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index 4a0faae9db..44176f73af 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -25,7 +25,7 @@ var DEBUG_SCROLL = false; // The amount of extra scroll distance to allow prior to unfilling. // See _getExcessHeight. -const UNPAGINATION_PADDING = 3000; +const UNPAGINATION_PADDING = 6000; // The number of milliseconds to debounce calls to onUnfillRequest, to prevent // many scroll events causing many unfilling requests. const UNFILL_REQUEST_DEBOUNCE_MS = 200; From ba0715ba7c3818d9b7656d66bab4de534bd33bfb Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 13 Mar 2017 14:10:14 +0000 Subject: [PATCH 034/284] Fix import for Lifecycle This fixes https://github.com/vector-im/riot-web/issues/2991 although we might need to give more feedback than just showing the login screen. Maybe a dialog that says "your account has been deactivated". --- src/components/views/dialogs/DeactivateAccountDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/DeactivateAccountDialog.js b/src/components/views/dialogs/DeactivateAccountDialog.js index 54a4e99424..b4879982bf 100644 --- a/src/components/views/dialogs/DeactivateAccountDialog.js +++ b/src/components/views/dialogs/DeactivateAccountDialog.js @@ -18,7 +18,7 @@ import React from 'react'; import sdk from '../../../index'; import MatrixClientPeg from '../../../MatrixClientPeg'; -import Lifecycle from '../../../Lifecycle'; +import * as Lifecycle from '../../../Lifecycle'; import Velocity from 'velocity-vector'; export default class DeactivateAccountDialog extends React.Component { From 878413f6a48a4dcaa7e14873fefc41661236fb0a Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 14 Mar 2017 11:50:13 +0000 Subject: [PATCH 035/284] Support msisdn signin Changes from https://github.com/matrix-org/matrix-react-sdk/pull/742 --- src/HtmlUtils.js | 16 + src/Login.js | 41 +- src/component-index.js | 4 + src/components/structures/login/Login.js | 36 +- .../structures/login/Registration.js | 18 +- .../views/elements/AccessibleButton.js | 4 +- src/components/views/elements/Dropdown.js | 324 +++++ src/components/views/login/CountryDropdown.js | 123 ++ .../login/InteractiveAuthEntryComponents.js | 134 ++ src/components/views/login/PasswordLogin.js | 46 +- .../views/login/RegistrationForm.js | 43 + src/phonenumber.js | 1273 +++++++++++++++++ 12 files changed, 2032 insertions(+), 30 deletions(-) create mode 100644 src/components/views/elements/Dropdown.js create mode 100644 src/components/views/login/CountryDropdown.js create mode 100644 src/phonenumber.js diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index c500076783..f1420d0a22 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -58,6 +58,22 @@ export function unicodeToImage(str) { return str; } +/** + * Given one or more unicode characters (represented by unicode + * character number), return an image node with the corresponding + * emoji. + * + * @param alt {string} String to use for the image alt text + * @param unicode {integer} One or more integers representing unicode characters + * @returns A img node with the corresponding emoji + */ +export function charactersToImageNode(alt, ...unicode) { + const fileName = unicode.map((u) => { + return u.toString(16); + }).join('-'); + return {alt}; +} + export function stripParagraphs(html: string): string { const contentDiv = document.createElement('div'); contentDiv.innerHTML = html; diff --git a/src/Login.js b/src/Login.js index 96f953c130..053f88ce93 100644 --- a/src/Login.js +++ b/src/Login.js @@ -105,21 +105,38 @@ export default class Login { }); } - loginViaPassword(username, pass) { - var self = this; - var isEmail = username.indexOf("@") > 0; - var loginParams = { - password: pass, - initial_device_display_name: this._defaultDeviceDisplayName, - }; - if (isEmail) { - loginParams.medium = 'email'; - loginParams.address = username; + loginViaPassword(username, phoneCountry, phoneNumber, pass) { + const self = this; + + const isEmail = username.indexOf("@") > 0; + + let identifier; + if (phoneCountry && phoneNumber) { + identifier = { + type: 'm.id.phone', + country: phoneCountry, + number: phoneNumber, + }; + } else if (isEmail) { + identifier = { + type: 'm.id.thirdparty', + medium: 'email', + address: username, + }; } else { - loginParams.user = username; + identifier = { + type: 'm.id.user', + user: username, + }; } - var client = this._createTemporaryClient(); + const loginParams = { + password: pass, + identifier: identifier, + initial_device_display_name: this._defaultDeviceDisplayName, + }; + + const client = this._createTemporaryClient(); return client.login('m.login.password', loginParams).then(function(data) { return q({ homeserverUrl: self._hsUrl, diff --git a/src/component-index.js b/src/component-index.js index 2644f1a379..59d3ad53e4 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -109,6 +109,8 @@ import views$elements$DeviceVerifyButtons from './components/views/elements/Devi views$elements$DeviceVerifyButtons && (module.exports.components['views.elements.DeviceVerifyButtons'] = views$elements$DeviceVerifyButtons); import views$elements$DirectorySearchBox from './components/views/elements/DirectorySearchBox'; views$elements$DirectorySearchBox && (module.exports.components['views.elements.DirectorySearchBox'] = views$elements$DirectorySearchBox); +import views$elements$Dropdown from './components/views/elements/Dropdown'; +views$elements$Dropdown && (module.exports.components['views.elements.Dropdown'] = views$elements$Dropdown); import views$elements$EditableText from './components/views/elements/EditableText'; views$elements$EditableText && (module.exports.components['views.elements.EditableText'] = views$elements$EditableText); import views$elements$EditableTextContainer from './components/views/elements/EditableTextContainer'; @@ -131,6 +133,8 @@ import views$login$CaptchaForm from './components/views/login/CaptchaForm'; views$login$CaptchaForm && (module.exports.components['views.login.CaptchaForm'] = views$login$CaptchaForm); import views$login$CasLogin from './components/views/login/CasLogin'; views$login$CasLogin && (module.exports.components['views.login.CasLogin'] = views$login$CasLogin); +import views$login$CountryDropdown from './components/views/login/CountryDropdown'; +views$login$CountryDropdown && (module.exports.components['views.login.CountryDropdown'] = views$login$CountryDropdown); import views$login$CustomServerDialog from './components/views/login/CustomServerDialog'; views$login$CustomServerDialog && (module.exports.components['views.login.CustomServerDialog'] = views$login$CustomServerDialog); import views$login$InteractiveAuthEntryComponents from './components/views/login/InteractiveAuthEntryComponents'; diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index 69195fc715..0a1549f75b 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -64,8 +65,10 @@ module.exports = React.createClass({ enteredHomeserverUrl: this.props.customHsUrl || this.props.defaultHsUrl, enteredIdentityServerUrl: this.props.customIsUrl || this.props.defaultIsUrl, - // used for preserving username when changing homeserver + // used for preserving form values when changing homeserver username: "", + phoneCountry: null, + phoneNumber: "", }; }, @@ -73,20 +76,21 @@ module.exports = React.createClass({ this._initLoginLogic(); }, - onPasswordLogin: function(username, password) { - var self = this; - self.setState({ + onPasswordLogin: function(username, phoneCountry, phoneNumber, password) { + this.setState({ busy: true, errorText: null, loginIncorrect: false, }); - this._loginLogic.loginViaPassword(username, password).then(function(data) { - self.props.onLoggedIn(data); - }, function(error) { - self._setStateFromError(error, true); - }).finally(function() { - self.setState({ + this._loginLogic.loginViaPassword( + username, phoneCountry, phoneNumber, password, + ).then((data) => { + this.props.onLoggedIn(data); + }, (error) => { + this._setStateFromError(error, true); + }).finally(() => { + this.setState({ busy: false }); }).done(); @@ -119,6 +123,14 @@ module.exports = React.createClass({ this.setState({ username: username }); }, + onPhoneCountryChanged: function(phoneCountry) { + this.setState({ phoneCountry: phoneCountry }); + }, + + onPhoneNumberChanged: function(phoneNumber) { + this.setState({ phoneNumber: phoneNumber }); + }, + onHsUrlChanged: function(newHsUrl) { var self = this; this.setState({ @@ -225,7 +237,11 @@ module.exports = React.createClass({ diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index cbc8929158..f4805ef044 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -262,6 +262,9 @@ module.exports = React.createClass({ case "RegistrationForm.ERR_EMAIL_INVALID": errMsg = "This doesn't look like a valid email address"; break; + case "RegistrationForm.ERR_PHONE_NUMBER_INVALID": + errMsg = "This doesn't look like a valid phone number"; + break; case "RegistrationForm.ERR_USERNAME_INVALID": errMsg = "User names may only contain letters, numbers, dots, hyphens and underscores."; break; @@ -296,15 +299,20 @@ module.exports = React.createClass({ guestAccessToken = null; } + // Only send the bind params if we're sending username / pw params + // (Since we need to send no params at all to use the ones saved in the + // session). + const bindThreepids = this.state.formVals.password ? { + email: true, + msisdn: true, + } : {}; + return this._matrixClient.register( this.state.formVals.username, this.state.formVals.password, undefined, // session id: included in the auth dict already auth, - // Only send the bind_email param if we're sending username / pw params - // (Since we need to send no params at all to use the ones saved in the - // session). - Boolean(this.state.formVals.username) || undefined, + bindThreepids, guestAccessToken, ); }, @@ -355,6 +363,8 @@ module.exports = React.createClass({ + {this.props.children} +
+ } +}; + +MenuOption.propTypes = { + children: React.PropTypes.oneOfType([ + React.PropTypes.arrayOf(React.PropTypes.node), + React.PropTypes.node + ]), + highlighted: React.PropTypes.bool, + dropdownKey: React.PropTypes.string, + onClick: React.PropTypes.func.isRequired, + onMouseEnter: React.PropTypes.func.isRequired, +}; + +/* + * Reusable dropdown select control, akin to react-select, + * but somewhat simpler as react-select is 79KB of minified + * javascript. + * + * TODO: Port NetworkDropdown to use this. + */ +export default class Dropdown extends React.Component { + constructor(props) { + super(props); + + this.dropdownRootElement = null; + this.ignoreEvent = null; + + this._onInputClick = this._onInputClick.bind(this); + this._onRootClick = this._onRootClick.bind(this); + this._onDocumentClick = this._onDocumentClick.bind(this); + this._onMenuOptionClick = this._onMenuOptionClick.bind(this); + this._onInputKeyPress = this._onInputKeyPress.bind(this); + this._onInputKeyUp = this._onInputKeyUp.bind(this); + this._onInputChange = this._onInputChange.bind(this); + this._collectRoot = this._collectRoot.bind(this); + this._collectInputTextBox = this._collectInputTextBox.bind(this); + this._setHighlightedOption = this._setHighlightedOption.bind(this); + + this.inputTextBox = null; + + this._reindexChildren(this.props.children); + + const firstChild = React.Children.toArray(props.children)[0]; + + this.state = { + // True if the menu is dropped-down + expanded: false, + // The key of the highlighted option + // (the option that would become selected if you pressed enter) + highlightedOption: firstChild ? firstChild.key : null, + // the current search query + searchQuery: '', + }; + } + + componentWillMount() { + // Listen for all clicks on the document so we can close the + // menu when the user clicks somewhere else + document.addEventListener('click', this._onDocumentClick, false); + } + + componentWillUnmount() { + document.removeEventListener('click', this._onDocumentClick, false); + } + + componentWillReceiveProps(nextProps) { + this._reindexChildren(nextProps.children); + const firstChild = React.Children.toArray(nextProps.children)[0]; + this.setState({ + highlightedOption: firstChild ? firstChild.key : null, + }); + } + + _reindexChildren(children) { + this.childrenByKey = {}; + React.Children.forEach(children, (child) => { + this.childrenByKey[child.key] = child; + }); + } + + _onDocumentClick(ev) { + // Close the dropdown if the user clicks anywhere that isn't + // within our root element + if (ev !== this.ignoreEvent) { + this.setState({ + expanded: false, + }); + } + } + + _onRootClick(ev) { + // This captures any clicks that happen within our elements, + // such that we can then ignore them when they're seen by the + // click listener on the document handler, ie. not close the + // dropdown immediately after opening it. + // NB. We can't just stopPropagation() because then the event + // doesn't reach the React onClick(). + this.ignoreEvent = ev; + } + + _onInputClick(ev) { + this.setState({ + expanded: !this.state.expanded, + }); + ev.preventDefault(); + } + + _onMenuOptionClick(dropdownKey) { + this.setState({ + expanded: false, + }); + this.props.onOptionChange(dropdownKey); + } + + _onInputKeyPress(e) { + // This needs to be on the keypress event because otherwise + // it can't cancel the form submission + if (e.key == 'Enter') { + this.setState({ + expanded: false, + }); + this.props.onOptionChange(this.state.highlightedOption); + e.preventDefault(); + } + } + + _onInputKeyUp(e) { + // These keys don't generate keypress events and so needs to + // be on keyup + if (e.key == 'Escape') { + this.setState({ + expanded: false, + }); + } else if (e.key == 'ArrowDown') { + this.setState({ + highlightedOption: this._nextOption(this.state.highlightedOption), + }); + } else if (e.key == 'ArrowUp') { + this.setState({ + highlightedOption: this._prevOption(this.state.highlightedOption), + }); + } + } + + _onInputChange(e) { + this.setState({ + searchQuery: e.target.value, + }); + if (this.props.onSearchChange) { + this.props.onSearchChange(e.target.value); + } + } + + _collectRoot(e) { + if (this.dropdownRootElement) { + this.dropdownRootElement.removeEventListener( + 'click', this._onRootClick, false, + ); + } + if (e) { + e.addEventListener('click', this._onRootClick, false); + } + this.dropdownRootElement = e; + } + + _collectInputTextBox(e) { + this.inputTextBox = e; + if (e) e.focus(); + } + + _setHighlightedOption(optionKey) { + this.setState({ + highlightedOption: optionKey, + }); + } + + _nextOption(optionKey) { + const keys = Object.keys(this.childrenByKey); + const index = keys.indexOf(optionKey); + return keys[(index + 1) % keys.length]; + } + + _prevOption(optionKey) { + const keys = Object.keys(this.childrenByKey); + const index = keys.indexOf(optionKey); + return keys[(index - 1) % keys.length]; + } + + _getMenuOptions() { + const options = React.Children.map(this.props.children, (child) => { + return ( + + {child} + + ); + }); + + if (!this.state.searchQuery) { + options.push( +
+ Type to search... +
+ ); + } + return options; + } + + render() { + let currentValue; + + const menuStyle = {}; + if (this.props.menuWidth) menuStyle.width = this.props.menuWidth; + + let menu; + if (this.state.expanded) { + currentValue = ; + menu =
+ {this._getMenuOptions()} +
; + } else { + const selectedChild = this.props.getShortOption ? + this.props.getShortOption(this.props.value) : + this.childrenByKey[this.props.value]; + currentValue =
+ {selectedChild} +
+ } + + const dropdownClasses = { + mx_Dropdown: true, + }; + if (this.props.className) { + dropdownClasses[this.props.className] = true; + } + + // Note the menu sits inside the AccessibleButton div so it's anchored + // to the input, but overflows below it. The root contains both. + return
+ + {currentValue} + + {menu} + +
; + } +} + +Dropdown.propTypes = { + // The width that the dropdown should be. If specified, + // the dropped-down part of the menu will be set to this + // width. + menuWidth: React.PropTypes.number, + // Called when the selected option changes + onOptionChange: React.PropTypes.func.isRequired, + // Called when the value of the search field changes + onSearchChange: React.PropTypes.func, + // Function that, given the key of an option, returns + // a node representing that option to be displayed in the + // box itself as the currently-selected option (ie. as + // opposed to in the actual dropped-down part). If + // unspecified, the appropriate child element is used as + // in the dropped-down menu. + getShortOption: React.PropTypes.func, + value: React.PropTypes.string, +} diff --git a/src/components/views/login/CountryDropdown.js b/src/components/views/login/CountryDropdown.js new file mode 100644 index 0000000000..fc1e89661b --- /dev/null +++ b/src/components/views/login/CountryDropdown.js @@ -0,0 +1,123 @@ +/* +Copyright 2017 Vector Creations 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. +*/ + +import React from 'react'; + +import sdk from '../../../index'; + +import { COUNTRIES } from '../../../phonenumber'; +import { charactersToImageNode } from '../../../HtmlUtils'; + +const COUNTRIES_BY_ISO2 = new Object(null); +for (const c of COUNTRIES) { + COUNTRIES_BY_ISO2[c.iso2] = c; +} + +function countryMatchesSearchQuery(query, country) { + if (country.name.toUpperCase().indexOf(query.toUpperCase()) == 0) return true; + if (country.iso2 == query.toUpperCase()) return true; + if (country.prefix == query) return true; + return false; +} + +const MAX_DISPLAYED_ROWS = 2; + +export default class CountryDropdown extends React.Component { + constructor(props) { + super(props); + this._onSearchChange = this._onSearchChange.bind(this); + + this.state = { + searchQuery: '', + } + + if (!props.value) { + // If no value is given, we start with the first + // country selected, but our parent component + // doesn't know this, therefore we do this. + this.props.onOptionChange(COUNTRIES[0].iso2); + } + } + + _onSearchChange(search) { + this.setState({ + searchQuery: search, + }); + } + + _flagImgForIso2(iso2) { + // Unicode Regional Indicator Symbol letter 'A' + const RIS_A = 0x1F1E6; + const ASCII_A = 65; + return charactersToImageNode(iso2, + RIS_A + (iso2.charCodeAt(0) - ASCII_A), + RIS_A + (iso2.charCodeAt(1) - ASCII_A), + ); + } + + render() { + const Dropdown = sdk.getComponent('elements.Dropdown'); + + let displayedCountries; + if (this.state.searchQuery) { + displayedCountries = COUNTRIES.filter( + countryMatchesSearchQuery.bind(this, this.state.searchQuery), + ); + if ( + this.state.searchQuery.length == 2 && + COUNTRIES_BY_ISO2[this.state.searchQuery.toUpperCase()] + ) { + // exact ISO2 country name match: make the first result the matches ISO2 + const matched = COUNTRIES_BY_ISO2[this.state.searchQuery.toUpperCase()]; + displayedCountries = displayedCountries.filter((c) => { + return c.iso2 != matched.iso2; + }); + displayedCountries.unshift(matched); + } + } else { + displayedCountries = COUNTRIES; + } + + if (displayedCountries.length > MAX_DISPLAYED_ROWS) { + displayedCountries = displayedCountries.slice(0, MAX_DISPLAYED_ROWS); + } + + const options = displayedCountries.map((country) => { + return
+ {this._flagImgForIso2(country.iso2)} + {country.name} +
; + }); + + // default value here too, otherwise we need to handle null / undefined + // values between mounting and the initial value propgating + const value = this.props.value || COUNTRIES[0].iso2; + + return + {options} + + } +} + +CountryDropdown.propTypes = { + className: React.PropTypes.string, + onOptionChange: React.PropTypes.func.isRequired, + value: React.PropTypes.string, +}; diff --git a/src/components/views/login/InteractiveAuthEntryComponents.js b/src/components/views/login/InteractiveAuthEntryComponents.js index e75cb082d4..2d8abf9216 100644 --- a/src/components/views/login/InteractiveAuthEntryComponents.js +++ b/src/components/views/login/InteractiveAuthEntryComponents.js @@ -16,6 +16,8 @@ limitations under the License. */ import React from 'react'; +import url from 'url'; +import classnames from 'classnames'; import sdk from '../../../index'; @@ -255,6 +257,137 @@ export const EmailIdentityAuthEntry = React.createClass({ }, }); +export const MsisdnAuthEntry = React.createClass({ + displayName: 'MsisdnAuthEntry', + + statics: { + LOGIN_TYPE: "m.login.msisdn", + }, + + propTypes: { + inputs: React.PropTypes.shape({ + phoneCountry: React.PropTypes.string, + phoneNumber: React.PropTypes.string, + }), + fail: React.PropTypes.func, + clientSecret: React.PropTypes.func, + submitAuthDict: React.PropTypes.func.isRequired, + matrixClient: React.PropTypes.object, + submitAuthDict: React.PropTypes.func, + }, + + getInitialState: function() { + return { + token: '', + requestingToken: false, + }; + }, + + componentWillMount: function() { + this._sid = null; + this._msisdn = null; + this._tokenBox = null; + + this.setState({requestingToken: true}); + this._requestMsisdnToken().catch((e) => { + this.props.fail(e); + }).finally(() => { + this.setState({requestingToken: false}); + }).done(); + }, + + /* + * Requests a verification token by SMS. + */ + _requestMsisdnToken: function() { + return this.props.matrixClient.requestRegisterMsisdnToken( + this.props.inputs.phoneCountry, + this.props.inputs.phoneNumber, + this.props.clientSecret, + 1, // TODO: Multiple send attempts? + ).then((result) => { + this._sid = result.sid; + this._msisdn = result.msisdn; + }); + }, + + _onTokenChange: function(e) { + this.setState({ + token: e.target.value, + }); + }, + + _onFormSubmit: function(e) { + e.preventDefault(); + if (this.state.token == '') return; + + this.setState({ + errorText: null, + }); + + this.props.matrixClient.submitMsisdnToken( + this._sid, this.props.clientSecret, this.state.token + ).then((result) => { + if (result.success) { + const idServerParsedUrl = url.parse( + this.props.matrixClient.getIdentityServerUrl(), + ) + this.props.submitAuthDict({ + type: MsisdnAuthEntry.LOGIN_TYPE, + threepid_creds: { + sid: this._sid, + client_secret: this.props.clientSecret, + id_server: idServerParsedUrl.host, + }, + }); + } else { + this.setState({ + errorText: "Token incorrect", + }); + } + }).catch((e) => { + this.props.fail(e); + console.log("Failed to submit msisdn token"); + }).done(); + }, + + render: function() { + if (this.state.requestingToken) { + const Loader = sdk.getComponent("elements.Spinner"); + return ; + } else { + const enableSubmit = Boolean(this.state.token); + const submitClasses = classnames({ + mx_InteractiveAuthEntryComponents_msisdnSubmit: true, + mx_UserSettings_button: true, // XXX button classes + }); + return ( +
+

A text message has been sent to +{this._msisdn}

+

Please enter the code it contains:

+
+ + +
+ + +
+ {this.state.errorText} +
+
+
+ ); + } + }, +}); + export const FallbackAuthEntry = React.createClass({ displayName: 'FallbackAuthEntry', @@ -313,6 +446,7 @@ const AuthEntryComponents = [ PasswordAuthEntry, RecaptchaAuthEntry, EmailIdentityAuthEntry, + MsisdnAuthEntry, ]; export function getEntryComponentForLoginType(loginType) { diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index 6f6081858b..61cb3da652 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,6 +18,7 @@ limitations under the License. import React from 'react'; import ReactDOM from 'react-dom'; import classNames from 'classnames'; +import sdk from '../../../index'; import {field_input_incorrect} from '../../../UiEffects'; @@ -28,8 +30,12 @@ module.exports = React.createClass({displayName: 'PasswordLogin', onSubmit: React.PropTypes.func.isRequired, // fn(username, password) onForgotPasswordClick: React.PropTypes.func, // fn() initialUsername: React.PropTypes.string, + initialPhoneCountry: React.PropTypes.string, + initialPhoneNumber: React.PropTypes.string, initialPassword: React.PropTypes.string, onUsernameChanged: React.PropTypes.func, + onPhoneCountryChanged: React.PropTypes.func, + onPhoneNumberChanged: React.PropTypes.func, onPasswordChanged: React.PropTypes.func, loginIncorrect: React.PropTypes.bool, }, @@ -38,7 +44,11 @@ module.exports = React.createClass({displayName: 'PasswordLogin', return { onUsernameChanged: function() {}, onPasswordChanged: function() {}, + onPhoneCountryChanged: function() {}, + onPhoneNumberChanged: function() {}, initialUsername: "", + initialPhoneCountry: "", + initialPhoneNumber: "", initialPassword: "", loginIncorrect: false, }; @@ -48,6 +58,8 @@ module.exports = React.createClass({displayName: 'PasswordLogin', return { username: this.props.initialUsername, password: this.props.initialPassword, + phoneCountry: this.props.initialPhoneCountry, + phoneNumber: this.props.initialPhoneNumber, }; }, @@ -63,7 +75,12 @@ module.exports = React.createClass({displayName: 'PasswordLogin', onSubmitForm: function(ev) { ev.preventDefault(); - this.props.onSubmit(this.state.username, this.state.password); + this.props.onSubmit( + this.state.username, + this.state.phoneCountry, + this.state.phoneNumber, + this.state.password, + ); }, onUsernameChanged: function(ev) { @@ -71,6 +88,16 @@ module.exports = React.createClass({displayName: 'PasswordLogin', this.props.onUsernameChanged(ev.target.value); }, + onPhoneCountryChanged: function(country) { + this.setState({phoneCountry: country}); + this.props.onPhoneCountryChanged(country); + }, + + onPhoneNumberChanged: function(ev) { + this.setState({phoneNumber: ev.target.value}); + this.props.onPhoneNumberChanged(ev.target.value); + }, + onPasswordChanged: function(ev) { this.setState({password: ev.target.value}); this.props.onPasswordChanged(ev.target.value); @@ -92,13 +119,28 @@ module.exports = React.createClass({displayName: 'PasswordLogin', error: this.props.loginIncorrect, }); + const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); return (
- + or +
+ + +

{this._passwordField = e;}} type="password" name="password" diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js index 93e3976834..4868c9de63 100644 --- a/src/components/views/login/RegistrationForm.js +++ b/src/components/views/login/RegistrationForm.js @@ -19,9 +19,12 @@ import React from 'react'; import { field_input_incorrect } from '../../../UiEffects'; import sdk from '../../../index'; import Email from '../../../email'; +import { looksValid as phoneNumberLooksValid } from '../../../phonenumber'; import Modal from '../../../Modal'; const FIELD_EMAIL = 'field_email'; +const FIELD_PHONE_COUNTRY = 'field_phone_country'; +const FIELD_PHONE_NUMBER = 'field_phone_number'; const FIELD_USERNAME = 'field_username'; const FIELD_PASSWORD = 'field_password'; const FIELD_PASSWORD_CONFIRM = 'field_password_confirm'; @@ -35,6 +38,8 @@ module.exports = React.createClass({ propTypes: { // Values pre-filled in the input boxes when the component loads defaultEmail: React.PropTypes.string, + defaultPhoneCountry: React.PropTypes.string, + defaultPhoneNumber: React.PropTypes.string, defaultUsername: React.PropTypes.string, defaultPassword: React.PropTypes.string, teamsConfig: React.PropTypes.shape({ @@ -71,6 +76,8 @@ module.exports = React.createClass({ return { fieldValid: {}, selectedTeam: null, + // The ISO2 country code selected in the phone number entry + phoneCountry: this.props.defaultPhoneCountry, }; }, @@ -85,6 +92,7 @@ module.exports = React.createClass({ this.validateField(FIELD_PASSWORD_CONFIRM); this.validateField(FIELD_PASSWORD); this.validateField(FIELD_USERNAME); + this.validateField(FIELD_PHONE_NUMBER); this.validateField(FIELD_EMAIL); var self = this; @@ -118,6 +126,8 @@ module.exports = React.createClass({ username: this.refs.username.value.trim() || this.props.guestUsername, password: this.refs.password.value.trim(), email: email, + phoneCountry: this.state.phoneCountry, + phoneNumber: this.refs.phoneNumber.value.trim(), }); if (promise) { @@ -174,6 +184,11 @@ module.exports = React.createClass({ const emailValid = email === '' || Email.looksValid(email); this.markFieldValid(field_id, emailValid, "RegistrationForm.ERR_EMAIL_INVALID"); break; + case FIELD_PHONE_NUMBER: + const phoneNumber = this.refs.phoneNumber.value; + const phoneNumberValid = phoneNumber === '' || phoneNumberLooksValid(phoneNumber); + this.markFieldValid(field_id, phoneNumberValid, "RegistrationForm.ERR_PHONE_NUMBER_INVALID"); + break; case FIELD_USERNAME: // XXX: SPEC-1 var username = this.refs.username.value.trim() || this.props.guestUsername; @@ -233,6 +248,8 @@ module.exports = React.createClass({ switch (field_id) { case FIELD_EMAIL: return this.refs.email; + case FIELD_PHONE_NUMBER: + return this.refs.phoneNumber; case FIELD_USERNAME: return this.refs.username; case FIELD_PASSWORD: @@ -251,6 +268,12 @@ module.exports = React.createClass({ return cls; }, + _onPhoneCountryChange(newVal) { + this.setState({ + phoneCountry: newVal, + }); + }, + render: function() { var self = this; @@ -286,6 +309,25 @@ module.exports = React.createClass({ } } + const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); + const phoneSection = ( +
+ + +
+ ); + const registerButton = ( ); @@ -300,6 +342,7 @@ module.exports = React.createClass({ {emailSection} {belowEmailSection} + {phoneSection} Date: Tue, 14 Mar 2017 14:37:18 +0000 Subject: [PATCH 036/284] Send legacy parameters on login call To support login on old HSes --- src/Login.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Login.js b/src/Login.js index 053f88ce93..107a8825e9 100644 --- a/src/Login.js +++ b/src/Login.js @@ -111,23 +111,32 @@ export default class Login { const isEmail = username.indexOf("@") > 0; let identifier; + let legacyParams; // parameters added to support old HSes if (phoneCountry && phoneNumber) { identifier = { type: 'm.id.phone', country: phoneCountry, number: phoneNumber, }; + // No legacy support for phone number login } else if (isEmail) { identifier = { type: 'm.id.thirdparty', medium: 'email', address: username, }; + legacyParams = { + medium: 'email', + address: username, + }; } else { identifier = { type: 'm.id.user', user: username, }; + legacyParams = { + user: username, + }; } const loginParams = { @@ -135,6 +144,7 @@ export default class Login { identifier: identifier, initial_device_display_name: this._defaultDeviceDisplayName, }; + Object.assign(loginParams, legacyParams); const client = this._createTemporaryClient(); return client.login('m.login.password', loginParams).then(function(data) { From 47958180a6cbf352fa5ac1a293374c5021b9ba39 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 14 Mar 2017 15:13:36 +0000 Subject: [PATCH 037/284] Add null check to start_login --- src/components/structures/MatrixChat.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 7229c71437..4f3c8fe7e5 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -351,7 +351,9 @@ module.exports = React.createClass({ this.notifyNewScreen('register'); break; case 'start_login': - if (MatrixClientPeg.get().isGuest()) { + if (MatrixClientPeg.get() && + MatrixClientPeg.get().isGuest() + ) { this.setState({ guestCreds: MatrixClientPeg.getCredentials(), }); From 238e48e4afb46a387d5632c552163b0b58ce7078 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 15 Mar 2017 12:02:08 +0000 Subject: [PATCH 038/284] Do routing to /register _onLoadCompleted _onLoadCompleted happens straight away because Lifecycle finishes loading the session instantly when registration parameters (client_secret etc.) are set. --- src/components/structures/MatrixChat.js | 29 ++++++++++++++++--------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 4f3c8fe7e5..48e7f40dca 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -696,6 +696,13 @@ module.exports = React.createClass({ _onLoadCompleted: function() { this.props.onLoadCompleted(); this.setState({loading: false}); + + // Show screens (like 'register') that need to be shown without onLoggedIn + // being called. 'register' needs to be routed here when the email confirmation + // link is clicked on. + if (['register'].indexOf(this.state.screenAfterLogin.screen) !== -1) { + this._showScreenAfterLogin(); + } }, /** @@ -751,6 +758,17 @@ module.exports = React.createClass({ logged_in: true, }); + if (teamToken) { + this._teamToken = teamToken; + dis.dispatch({action: 'view_home_page'}); + } else if (this._is_registered) { + dis.dispatch({action: 'view_user_settings'}); + } else { + this._showScreenAfterLogin(); + } + }, + + _showScreenAfterLogin: function() { // If screenAfterLogin is set, use that, then null it so that a second login will // result in view_home_page, _user_settings or _room_directory if (this.state.screenAfterLogin && this.state.screenAfterLogin.screen) { @@ -758,17 +776,8 @@ module.exports = React.createClass({ this.state.screenAfterLogin.screen, this.state.screenAfterLogin.params ); + this.notifyNewScreen(this.state.screenAfterLogin.screen); this.setState({screenAfterLogin: null}); - return; - } else { - this.setState({screen: undefined}); - } - - if (teamToken) { - this._teamToken = teamToken; - dis.dispatch({action: 'view_home_page'}); - } else if (this._is_registered) { - dis.dispatch({action: 'view_user_settings'}); } else { dis.dispatch({action: 'view_room_directory'}); } From 5330e47b3fd91df7833f1cc22bd96c7cea4d0130 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 15 Mar 2017 13:05:03 +0000 Subject: [PATCH 039/284] Add null check --- src/components/structures/MatrixChat.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 48e7f40dca..2337d62fd8 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -700,7 +700,8 @@ module.exports = React.createClass({ // Show screens (like 'register') that need to be shown without onLoggedIn // being called. 'register' needs to be routed here when the email confirmation // link is clicked on. - if (['register'].indexOf(this.state.screenAfterLogin.screen) !== -1) { + if (this.state.screenAfterLogin && + ['register'].indexOf(this.state.screenAfterLogin.screen) !== -1) { this._showScreenAfterLogin(); } }, From d292a627d8e1f4cb7b8aee4d3488e0b06da4d096 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 15 Mar 2017 16:44:56 +0000 Subject: [PATCH 040/284] Handle no-auth-flow error from js-sdk --- src/components/structures/InteractiveAuth.js | 1 - src/components/structures/login/Registration.js | 13 ++++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/components/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js index 71fee883be..a58ad9aaa4 100644 --- a/src/components/structures/InteractiveAuth.js +++ b/src/components/structures/InteractiveAuth.js @@ -107,7 +107,6 @@ export default React.createClass({ return; } - const msg = error.message || error.toString(); this.setState({ errorText: msg }); diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index f4805ef044..a878657de9 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -155,10 +155,21 @@ module.exports = React.createClass({ _onUIAuthFinished: function(success, response, extra) { if (!success) { + let msg = response.message || response.toString(); + // can we give a better error message? + if (response.required_stages && response.required_stages.indexOf('m.login.msisdn') > -1) { + let msisdn_available = false; + for (const flow of response.available_flows) { + msisdn_available |= flow.stages.indexOf('m.login.msisdn') > -1; + } + if (!msisdn_available) { + msg = "This server does not support authentication with a phone number"; + } + } this.setState({ busy: false, doingUIAuth: false, - errorText: response.message || response.toString(), + errorText: msg, }); return; } From ed22a74eafc63ba8206cae3dcc31c2dee2162906 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 16 Mar 2017 11:31:47 +0000 Subject: [PATCH 041/284] Fix People section a bit This does two things: - Sets `editable` to true for the "People" `RoomSubList` so that people can be dragged to favourites. This has the downside that you can drag a "People" to "Rooms", but it won't set the direct chat flag. This is because im.vector.fake.direct != m.direct, sadly. - Sets `alwaysShowHeader` to `true` so that the `IncomingCallDialog` can be showneven when there are no people in `sortedList`. Fixes https://github.com/vector-im/riot-web/issues/2956. --- src/components/views/rooms/RoomList.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index c3ee5f1730..e84c56e693 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -485,11 +485,12 @@ module.exports = React.createClass({ From 67757a16f368d5ca5ba60d1fbd4b6ac240229c54 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 16 Mar 2017 12:54:18 +0000 Subject: [PATCH 042/284] Don't remove the line that gets the error message --- src/components/structures/InteractiveAuth.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js index a58ad9aaa4..71fee883be 100644 --- a/src/components/structures/InteractiveAuth.js +++ b/src/components/structures/InteractiveAuth.js @@ -107,6 +107,7 @@ export default React.createClass({ return; } + const msg = error.message || error.toString(); this.setState({ errorText: msg }); From b21f016d37badbe94be6689675eaf15f4c64fb9b Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 16 Mar 2017 14:18:18 +0000 Subject: [PATCH 043/284] Add "Export E2E keys" option to logout dialog Fixes https://github.com/vector-im/riot-web/issues/3184 --- src/components/structures/UserSettings.js | 6 ++++++ src/components/views/dialogs/QuestionDialog.js | 8 ++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index febdccd9c3..9e6d454fe9 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -268,6 +268,12 @@ module.exports = React.createClass({ but for now be warned.
, button: "Sign out", + extraButtons: [ + + ], onFinished: (confirmed) => { if (confirmed) { dis.dispatch({action: 'logout'}); diff --git a/src/components/views/dialogs/QuestionDialog.js b/src/components/views/dialogs/QuestionDialog.js index 0260fc29e2..6012541b94 100644 --- a/src/components/views/dialogs/QuestionDialog.js +++ b/src/components/views/dialogs/QuestionDialog.js @@ -21,10 +21,8 @@ export default React.createClass({ displayName: 'QuestionDialog', propTypes: { title: React.PropTypes.string, - description: React.PropTypes.oneOfType([ - React.PropTypes.element, - React.PropTypes.string, - ]), + description: React.PropTypes.node, + extraButtons: React.PropTypes.node, button: React.PropTypes.string, focus: React.PropTypes.bool, onFinished: React.PropTypes.func.isRequired, @@ -34,6 +32,7 @@ export default React.createClass({ return { title: "", description: "", + extraButtons: null, button: "OK", focus: true, hasCancelButton: true, @@ -67,6 +66,7 @@ export default React.createClass({ + {this.props.extraButtons} {cancelButton}
From 544a6593e1ab91e60efd30a0085a4c3b25c2beae Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 16 Mar 2017 14:19:17 +0000 Subject: [PATCH 044/284] Unregister the UploadBar event listener on unmount --- src/components/structures/UploadBar.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/structures/UploadBar.js b/src/components/structures/UploadBar.js index 8266a11bc8..01a879fd1b 100644 --- a/src/components/structures/UploadBar.js +++ b/src/components/structures/UploadBar.js @@ -25,12 +25,13 @@ module.exports = React.createClass({displayName: 'UploadBar', }, componentDidMount: function() { - dis.register(this.onAction); + this.dispatcherRef = dis.register(this.onAction); this.mounted = true; }, componentWillUnmount: function() { this.mounted = false; + dis.unregister(this.dispatcherRef); }, onAction: function(payload) { From af8c3edba6d7cdd3bb0acb1f741f7eeaf27020a6 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 16 Mar 2017 14:56:26 +0000 Subject: [PATCH 045/284] Support adding phone numbers in UserSettings --- src/AddThreepid.js | 52 +++++++- src/components/structures/UserSettings.js | 146 +++++++++++++++++++--- 2 files changed, 181 insertions(+), 17 deletions(-) diff --git a/src/AddThreepid.js b/src/AddThreepid.js index d6a1d58aa0..44d709371b 100644 --- a/src/AddThreepid.js +++ b/src/AddThreepid.js @@ -1,5 +1,6 @@ /* Copyright 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -51,11 +52,35 @@ class AddThreepid { }); } + /** + * Attempt to add a msisdn threepid. This will trigger a side-effect of + * sending a test message to the provided phone number. + * @param {string} emailAddress The email address to add + * @param {boolean} bind If True, bind this phone number to this mxid on the Identity Server + * @return {Promise} Resolves when the text message has been sent. Then call haveMsisdnToken(). + */ + addMsisdn(phoneCountry, phoneNumber, bind) { + this.bind = bind; + return MatrixClientPeg.get().requestAdd3pidMsisdnToken( + phoneCountry, phoneNumber, this.clientSecret, 1, + ).then((res) => { + this.sessionId = res.sid; + return res; + }, function(err) { + if (err.errcode == 'M_THREEPID_IN_USE') { + err.message = "This phone number is already in use"; + } else if (err.httpStatus) { + err.message = err.message + ` (Status ${err.httpStatus})`; + } + throw err; + }); + } + /** * Checks if the email link has been clicked by attempting to add the threepid - * @return {Promise} Resolves if the password was reset. Rejects with an object + * @return {Promise} Resolves if the email address was added. Rejects with an object * with a "message" property which contains a human-readable message detailing why - * the reset failed, e.g. "There is no mapped matrix user ID for the given email address". + * the request failed. */ checkEmailLinkClicked() { var identityServerDomain = MatrixClientPeg.get().idBaseUrl.split("://")[1]; @@ -73,6 +98,29 @@ class AddThreepid { throw err; }); } + + /** + * Takes a phone number verification code as entered by the user and validates + * it with the ID server, then if successful, adds the phone number. + * @return {Promise} Resolves if the email address was added. Rejects with an object + * with a "message" property which contains a human-readable message detailing why + * the request failed. + */ + haveMsisdnToken(token) { + return MatrixClientPeg.get().submitMsisdnToken( + this.sessionId, this.clientSecret, token, + ).then((result) => { + if (result.errcode) { + throw result; + } + const identityServerDomain = MatrixClientPeg.get().idBaseUrl.split("://")[1]; + return MatrixClientPeg.get().addThreePid({ + sid: this.sessionId, + client_secret: this.clientSecret, + id_server: identityServerDomain + }, this.bind); + }); + } } module.exports = AddThreepid; diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index febdccd9c3..ed8a271241 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -132,13 +132,17 @@ module.exports = React.createClass({ threePids: [], phase: "UserSettings.LOADING", // LOADING, DISPLAY email_add_pending: false, + msisdn_add_pending: false, vectorVersion: null, rejectingInvites: false, + phoneCountry: null, + phoneNumber: "", }; }, componentWillMount: function() { this._unmounted = false; + this._addThreepid = null; if (PlatformPeg.get()) { q().then(() => { @@ -214,6 +218,14 @@ module.exports = React.createClass({ }); }, + _onPhoneCountryChange: function(phoneCountry) { + this.setState({ phoneCountry: phoneCountry }); + }, + + _onPhoneNumberChange: function(ev) { + this.setState({ phoneNumber: ev.target.value }); + }, + onAction: function(payload) { if (payload.action === "notifier_enabled") { this.forceUpdate(); @@ -315,12 +327,26 @@ module.exports = React.createClass({ UserSettingsStore.setEnableNotifications(event.target.checked); }, - onAddThreepidClicked: function(value, shouldSubmit) { + _onAddEmailEditFinished: function(value, shouldSubmit) { if (!shouldSubmit) return; + this._addEmail(); + }, + + _onAddMsisdnEditFinished: function(value, shouldSubmit) { + if (!shouldSubmit) return; + this._addMsisdn(); + }, + + _onAddMsisdnSubmit: function(ev) { + ev.preventDefault(); + this._addMsisdn(); + }, + + _addEmail: function() { var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - var email_address = this.refs.add_threepid_input.value; + var email_address = this.refs.add_email_input.value; if (!Email.looksValid(email_address)) { Modal.createDialog(ErrorDialog, { title: "Invalid Email Address", @@ -328,10 +354,10 @@ module.exports = React.createClass({ }); return; } - this.add_threepid = new AddThreepid(); + this._addThreepid = new AddThreepid(); // we always bind emails when registering, so let's do the // same here. - this.add_threepid.addEmailAddress(email_address, true).done(() => { + this._addThreepid.addEmailAddress(email_address, true).done(() => { Modal.createDialog(QuestionDialog, { title: "Verification Pending", description: "Please check your email and click on the link it contains. Once this is done, click continue.", @@ -346,10 +372,69 @@ module.exports = React.createClass({ description: "Unable to add email address" }); }); - ReactDOM.findDOMNode(this.refs.add_threepid_input).blur(); + ReactDOM.findDOMNode(this.refs.add_email_input).blur(); this.setState({email_add_pending: true}); }, + _addMsisdn: function() { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + + this._addThreepid = new AddThreepid(); + // we always phone numbers when registering, so let's do the + // same here. + this._addThreepid.addMsisdn(this.state.phoneCountry, this.state.phoneNumber, true).then((resp) => { + this._promptForMsisdnVerificationCode(resp.msisdn); + }).catch((err) => { + console.error("Unable to add phone number: " + err); + let msg = err.message; + Modal.createDialog(ErrorDialog, { + title: "Error", + description: msg, + }); + }).finally(() => { + this.setState({msisdn_add_pending: false}); + }).done();; + ReactDOM.findDOMNode(this.refs.add_msisdn_input).blur(); + this.setState({msisdn_add_pending: true}); + }, + + _promptForMsisdnVerificationCode(msisdn, err) { + const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog"); + let msgElements = [ +
A text message has been sent to +{msisdn}. + Please enter the verification code it contains
+ ]; + if (err) { + let msg = err.error; + if (err.errcode == 'M_THREEPID_AUTH_FAILED') { + msg = "Incorrect verification code"; + } + msgElements.push(
{msg}
); + } + Modal.createDialog(TextInputDialog, { + title: "Enter Code", + description:
{msgElements}
, + button: "Submit", + onFinished: (should_verify, token) => { + if (!should_verify) { + this._addThreepid = null; + return; + } + this.setState({msisdn_add_pending: true}); + this._addThreepid.haveMsisdnToken(token).then(() => { + this._addThreepid = null; + this.setState({phoneNumber: ''}); + return this._refreshFromServer(); + }).catch((err) => { + this._promptForMsisdnVerificationCode(msisdn, err); + }).finally(() => { + this.setState({msisdn_add_pending: false}); + }).done(); + } + }); + }, + onRemoveThreepidClicked: function(threepid) { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); Modal.createDialog(QuestionDialog, { @@ -385,8 +470,8 @@ module.exports = React.createClass({ }, verifyEmailAddress: function() { - this.add_threepid.checkEmailLinkClicked().done(() => { - this.add_threepid = undefined; + this._addThreepid.checkEmailLinkClicked().done(() => { + this._addThreepid = null; this.setState({ phase: "UserSettings.LOADING", }); @@ -795,30 +880,61 @@ module.exports = React.createClass({
); }); - var addThreepidSection; + let addEmailSection; + let addMsisdnSection; if (this.state.email_add_pending) { - addThreepidSection = ; + addEmailSection = ; } else if (!MatrixClientPeg.get().isGuest()) { - addThreepidSection = ( -
+ addEmailSection = ( +
+ onValueChanged={ this._onAddEmailEditFinished } />
- Add + Add
); } - threepidsSection.push(addThreepidSection); + if (this.state.msisdn_add_pending) { + addMsisdnSection = ; + } else if (!MatrixClientPeg.get().isGuest()) { + const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); + addMsisdnSection = ( +
+
+
+
+ + + + +
+
+ Add +
+
+ ); + } + threepidsSection.push(addEmailSection); + threepidsSection.push(addMsisdnSection); var accountJsx; From 3ce0da452849fbb3eedfd63392c2f8f580b9ea8e Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 16 Mar 2017 15:05:54 +0000 Subject: [PATCH 046/284] Fix UDD for voip in e2e rooms When starting a call, several events are sent and if some devices are unverified, all three will trigger their own UnknownDeviceError. This causes three overlapping, identical UnknownDeviceDialogs. This change effectively dedupes the dialogs so that only one is shown. This is safe to do because the UDD allows resending of _all_ events that were left unsent. Fixes https://github.com/vector-im/riot-web/issues/3285 --- src/UnknownDeviceErrorHandler.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/UnknownDeviceErrorHandler.js b/src/UnknownDeviceErrorHandler.js index d842cc3a6e..2aa0573e22 100644 --- a/src/UnknownDeviceErrorHandler.js +++ b/src/UnknownDeviceErrorHandler.js @@ -18,13 +18,17 @@ import dis from './dispatcher'; import sdk from './index'; import Modal from './Modal'; +let isDialogOpen = false; + const onAction = function(payload) { - if (payload.action === 'unknown_device_error') { + if (payload.action === 'unknown_device_error' && !isDialogOpen) { var UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog"); + isDialogOpen = true; Modal.createDialog(UnknownDeviceDialog, { devices: payload.err.devices, room: payload.room, onFinished: (r) => { + isDialogOpen = false; // XXX: temporary logging to try to diagnose // https://github.com/vector-im/riot-web/issues/3148 console.log('UnknownDeviceDialog closed with '+r); From b06111202da17763b3b807b3bf04412540217968 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 16 Mar 2017 15:16:24 +0000 Subject: [PATCH 047/284] Display threepids slightly prettier ie. Put a + on the front of msisdns. --- src/components/structures/UserSettings.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index febdccd9c3..8b30d0f497 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -755,6 +755,14 @@ module.exports = React.createClass({ return medium[0].toUpperCase() + medium.slice(1); }, + presentableTextForThreepid: function(threepid) { + if (threepid.medium == 'msisdn') { + return '+' + threepid.address; + } else { + return threepid.address; + } + }, + render: function() { var Loader = sdk.getComponent("elements.Spinner"); switch (this.state.phase) { @@ -787,7 +795,9 @@ module.exports = React.createClass({
- +
Remove From d4ed9e816ba6fa0820f005276744c5996cefad07 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 16 Mar 2017 17:00:10 +0000 Subject: [PATCH 048/284] Display timestamps and profiles for redacted events This is part of fixing https://github.com/vector-im/riot-web/issues/3390 --- src/components/structures/MessagePanel.js | 2 +- src/components/views/rooms/EventTile.js | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index ff507b6f90..0f8d35f525 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -413,7 +413,7 @@ module.exports = React.createClass({ var continuation = false; if (prevEvent !== null - && !prevEvent.isRedacted() && prevEvent.sender && mxEv.sender + && prevEvent.sender && mxEv.sender && mxEv.sender.userId === prevEvent.sender.userId && mxEv.getType() == prevEvent.getType()) { continuation = true; diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 48f0f282c1..b451d1c046 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -435,10 +435,7 @@ module.exports = WithMatrixClient(React.createClass({ let avatarSize; let needsSenderProfile; - if (isRedacted) { - avatarSize = 0; - needsSenderProfile = false; - } else if (this.props.tileShape === "notif") { + if (this.props.tileShape === "notif") { avatarSize = 24; needsSenderProfile = true; } else if (isInfoMessage) { @@ -503,8 +500,8 @@ module.exports = WithMatrixClient(React.createClass({ else if (e2eEnabled) { e2e = ; } - const timestamp = this.props.mxEvent.isRedacted() ? - null : ; + const timestamp = this.props.mxEvent.getTs() ? + : null; if (this.props.tileShape === "notif") { var room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId()); From 375ae8fb04d8775951569d9a75027553bcbf82e8 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 16 Mar 2017 17:26:42 +0000 Subject: [PATCH 049/284] Fix password UI auth test By adding a way to wait a short time for a component to appear in the DOM, so we don't get flakey failures like this when we change something to returning a promise that needs to resolve before the component actually appears. --- .../dialogs/InteractiveAuthDialog-test.js | 75 ++++++++++--------- test/test-utils.js | 50 +++++++++++-- 2 files changed, 83 insertions(+), 42 deletions(-) diff --git a/test/components/views/dialogs/InteractiveAuthDialog-test.js b/test/components/views/dialogs/InteractiveAuthDialog-test.js index da8fc17001..50500ba6e3 100644 --- a/test/components/views/dialogs/InteractiveAuthDialog-test.js +++ b/test/components/views/dialogs/InteractiveAuthDialog-test.js @@ -68,50 +68,51 @@ describe('InteractiveAuthDialog', function () { onFinished={onFinished} />, parentDiv); - // at this point there should be a password box and a submit button - const formNode = ReactTestUtils.findRenderedDOMComponentWithTag(dlg, "form"); - const inputNodes = ReactTestUtils.scryRenderedDOMComponentsWithTag( - dlg, "input" - ); - let passwordNode; - let submitNode; - for (const node of inputNodes) { - if (node.type == 'password') { - passwordNode = node; - } else if (node.type == 'submit') { - submitNode = node; + // wait for a password box and a submit button + test_utils.waitForRenderedDOMComponentWithTag(dlg, "form").then((formNode) => { + const inputNodes = ReactTestUtils.scryRenderedDOMComponentsWithTag( + dlg, "input" + ); + let passwordNode; + let submitNode; + for (const node of inputNodes) { + if (node.type == 'password') { + passwordNode = node; + } else if (node.type == 'submit') { + submitNode = node; + } } - } - expect(passwordNode).toExist(); - expect(submitNode).toExist(); + expect(passwordNode).toExist(); + expect(submitNode).toExist(); - // submit should be disabled - expect(submitNode.disabled).toBe(true); + // submit should be disabled + expect(submitNode.disabled).toBe(true); - // put something in the password box, and hit enter; that should - // trigger a request - passwordNode.value = "s3kr3t"; - ReactTestUtils.Simulate.change(passwordNode); - expect(submitNode.disabled).toBe(false); - ReactTestUtils.Simulate.submit(formNode, {}); + // put something in the password box, and hit enter; that should + // trigger a request + passwordNode.value = "s3kr3t"; + ReactTestUtils.Simulate.change(passwordNode); + expect(submitNode.disabled).toBe(false); + ReactTestUtils.Simulate.submit(formNode, {}); - expect(doRequest.callCount).toEqual(1); - expect(doRequest.calledWithExactly({ - session: "sess", - type: "m.login.password", - password: "s3kr3t", - user: "@user:id", - })).toBe(true); + expect(doRequest.callCount).toEqual(1); + expect(doRequest.calledWithExactly({ + session: "sess", + type: "m.login.password", + password: "s3kr3t", + user: "@user:id", + })).toBe(true); - // there should now be a spinner - ReactTestUtils.findRenderedComponentWithType( - dlg, sdk.getComponent('elements.Spinner'), - ); + // there should now be a spinner + ReactTestUtils.findRenderedComponentWithType( + dlg, sdk.getComponent('elements.Spinner'), + ); - // let the request complete - q.delay(1).then(() => { + // let the request complete + return q.delay(1); + }).then(() => { expect(onFinished.callCount).toEqual(1); expect(onFinished.calledWithExactly(true, {a:1})).toBe(true); - }).done(done, done); + }).done(done); }); }); diff --git a/test/test-utils.js b/test/test-utils.js index aca91ad399..5209465362 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -1,11 +1,51 @@ "use strict"; -var sinon = require('sinon'); -var q = require('q'); +import sinon from 'sinon'; +import q from 'q'; +import ReactTestUtils from 'react-addons-test-utils'; -var peg = require('../src/MatrixClientPeg.js'); -var jssdk = require('matrix-js-sdk'); -var MatrixEvent = jssdk.MatrixEvent; +import peg from '../src/MatrixClientPeg.js'; +import jssdk from 'matrix-js-sdk'; +const MatrixEvent = jssdk.MatrixEvent; + +/** + * Wrapper around window.requestAnimationFrame that returns a promise + * @private + */ +function _waitForFrame() { + const def = q.defer(); + window.requestAnimationFrame(() => { + def.resolve(); + }); + return def.promise; +} + +/** + * Waits a small number of animation frames for a component to appear + * in the DOM. Like findRenderedDOMComponentWithTag(), but allows + * for the element to appear a short time later, eg. if a promise needs + * to resolve first. + * @return a promise that resolves once the component appears, or rejects + * if it doesn't appear after a nominal number of animation frames. + */ +export function waitForRenderedDOMComponentWithTag(tree, tag, attempts) { + if (attempts === undefined) { + // Let's start by assuming we'll only need to wait a single frame, and + // we can try increasing this if necessary. + attempts = 1; + } else if (attempts == 0) { + return q.reject("Gave up waiting for component with tag: " + tag); + } + + return _waitForFrame().then(() => { + const result = ReactTestUtils.scryRenderedDOMComponentsWithTag(tree, tag); + if (result.length > 0) { + return result[0]; + } else { + return waitForRenderedDOMComponentWithTag(tree, tag, attempts - 1); + } + }); +} /** * Perform common actions before each test case, e.g. printing the test case From 23c38bd8a3370304d8678b8ea11aade4fff8dc91 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 16 Mar 2017 17:47:15 +0000 Subject: [PATCH 050/284] Put back both done's mocha takes the exception arg and does the right thing --- test/components/views/dialogs/InteractiveAuthDialog-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/components/views/dialogs/InteractiveAuthDialog-test.js b/test/components/views/dialogs/InteractiveAuthDialog-test.js index 50500ba6e3..b8a8e49769 100644 --- a/test/components/views/dialogs/InteractiveAuthDialog-test.js +++ b/test/components/views/dialogs/InteractiveAuthDialog-test.js @@ -113,6 +113,6 @@ describe('InteractiveAuthDialog', function () { }).then(() => { expect(onFinished.callCount).toEqual(1); expect(onFinished.calledWithExactly(true, {a:1})).toBe(true); - }).done(done); + }).done(done, done); }); }); From 7ecabe49d2130884cb71b03236344f0259787d17 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 17 Mar 2017 11:59:22 +0000 Subject: [PATCH 051/284] Fix people section DropTarget and refactor Rooms - Set the verb for the people section to "tag as direct chat". This requires some CSS modifications to Riot because it's a long bit of text relative to, say, "demote". - Because it's quite useful to be able to set the DM status of a room with just a boolean, add a convenience function for guessing a DM member and setting the DM flag on that room with the resulting member. --- src/Rooms.js | 18 ++++++++++++++++++ src/components/views/rooms/RoomList.js | 2 ++ 2 files changed, 20 insertions(+) diff --git a/src/Rooms.js b/src/Rooms.js index fbcc843ad2..0f81bf5391 100644 --- a/src/Rooms.js +++ b/src/Rooms.js @@ -79,6 +79,24 @@ export function looksLikeDirectMessageRoom(room, me) { return false; } +export function guessAndSetDMRoom(room, isDirect) { + let newTarget; + if (isDirect) { + const guessedTarget = guessDMRoomTarget( + room, room.getMember(MatrixClientPeg.get().credentials.userId), + ); + newTarget = guessedTarget.userId; + } else { + newTarget = null; + } + + // give some time for the user to see the icon change first, since + // this will hide the context menu once it completes + return q.delay(500).then(() => { + return setDMRoom(room.roomId, newTarget); + }); +} + /** * Marks or unmarks the given room as being as a DM room. * @param {string} roomId The ID of the room to modify diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index e84c56e693..51811f672a 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -485,6 +485,8 @@ module.exports = React.createClass({ Date: Sat, 18 Mar 2017 11:43:35 +0100 Subject: [PATCH 052/284] don't show link preview when link is inside of a quote Signed-off-by: Lieuwe Rooijakkers --- src/components/views/messages/TextualBody.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index a625e63062..c493094cbe 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -131,7 +131,8 @@ module.exports = React.createClass({ links.push(node); } } - else if (node.tagName === "PRE" || node.tagName === "CODE") { + else if (node.tagName === "PRE" || node.tagName === "CODE" || + node.tagName === "BLOCKQUOTE") { continue; } else if (node.children && node.children.length) { From 25a4f4e3b6d16c4b661e3cfb4d19fb8134e3e858 Mon Sep 17 00:00:00 2001 From: Keyvan Fatehi Date: Sat, 18 Mar 2017 18:58:28 -0700 Subject: [PATCH 053/284] Add ConfirmRedactDialog component Signed-off-by: Keyvan Fatehi --- src/component-index.js | 2 + .../views/dialogs/ConfirmRedactDialog.js | 73 +++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 src/components/views/dialogs/ConfirmRedactDialog.js diff --git a/src/component-index.js b/src/component-index.js index 2644f1a379..04cb746163 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -79,6 +79,8 @@ import views$dialogs$ChatCreateOrReuseDialog from './components/views/dialogs/Ch views$dialogs$ChatCreateOrReuseDialog && (module.exports.components['views.dialogs.ChatCreateOrReuseDialog'] = views$dialogs$ChatCreateOrReuseDialog); import views$dialogs$ChatInviteDialog from './components/views/dialogs/ChatInviteDialog'; views$dialogs$ChatInviteDialog && (module.exports.components['views.dialogs.ChatInviteDialog'] = views$dialogs$ChatInviteDialog); +import views$dialogs$ConfirmRedactDialog from './components/views/dialogs/ConfirmRedactDialog'; +views$dialogs$ConfirmRedactDialog && (module.exports.components['views.dialogs.ConfirmRedactDialog'] = views$dialogs$ConfirmRedactDialog); import views$dialogs$ConfirmUserActionDialog from './components/views/dialogs/ConfirmUserActionDialog'; views$dialogs$ConfirmUserActionDialog && (module.exports.components['views.dialogs.ConfirmUserActionDialog'] = views$dialogs$ConfirmUserActionDialog); import views$dialogs$DeactivateAccountDialog from './components/views/dialogs/DeactivateAccountDialog'; diff --git a/src/components/views/dialogs/ConfirmRedactDialog.js b/src/components/views/dialogs/ConfirmRedactDialog.js new file mode 100644 index 0000000000..ad1c73eb96 --- /dev/null +++ b/src/components/views/dialogs/ConfirmRedactDialog.js @@ -0,0 +1,73 @@ +/* +Copyright 2017 Vector Creations 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. +*/ + +import React from 'react'; +import sdk from '../../../index'; +import classnames from 'classnames'; + +/* + * A dialog for confirming a redaction. + */ +export default React.createClass({ + displayName: 'ConfirmRedactDialog', + propTypes: { + onFinished: React.PropTypes.func.isRequired, + }, + + defaultProps: { + danger: false, + }, + + onOk: function() { + this.props.onFinished(true); + }, + + onCancel: function() { + this.props.onFinished(false); + }, + + render: function() { + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + + const title = "Confirm Redaction"; + + const confirmButtonClass = classnames({ + 'mx_Dialog_primary': true, + 'danger': false, + }); + + return ( + +
+ Are you sure you wish to redact this event? + Note that if you redact a room name or topic change, it could undo the change. +
+
+ + + +
+
+ ); + }, +}); From df63c779dd71793487e6c3b0e6a6c146b5dc2eee Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 19 Mar 2017 02:34:25 +0000 Subject: [PATCH 054/284] clarify that redact === delete --- src/components/views/dialogs/ConfirmRedactDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/ConfirmRedactDialog.js b/src/components/views/dialogs/ConfirmRedactDialog.js index ad1c73eb96..fc9e55f666 100644 --- a/src/components/views/dialogs/ConfirmRedactDialog.js +++ b/src/components/views/dialogs/ConfirmRedactDialog.js @@ -55,7 +55,7 @@ export default React.createClass({ title={title} >
- Are you sure you wish to redact this event? + Are you sure you wish to redact (delete) this event? Note that if you redact a room name or topic change, it could undo the change.
From 7891f9b246756415103c056e82cf8fea2b6eeeab Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Sun, 19 Mar 2017 15:18:19 +0530 Subject: [PATCH 055/284] UnknownBody: add explanatory title --- src/components/views/messages/UnknownBody.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/messages/UnknownBody.js b/src/components/views/messages/UnknownBody.js index a0fe8fdf74..9b1bd74087 100644 --- a/src/components/views/messages/UnknownBody.js +++ b/src/components/views/messages/UnknownBody.js @@ -24,7 +24,7 @@ module.exports = React.createClass({ render: function() { const text = this.props.mxEvent.getContent().body; return ( - + {text} ); From c697b48f99f21f1da8427d38ff961becffb73d54 Mon Sep 17 00:00:00 2001 From: Lieuwe Rooijakkers Date: Sun, 19 Mar 2017 21:52:24 +0100 Subject: [PATCH 056/284] fix leading extraneous space in emotes --- src/components/views/rooms/MessageComposerInput.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index d702b7558d..60088ddd6f 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -541,7 +541,7 @@ export default class MessageComposerInput extends React.Component { let sendTextFn = this.client.sendTextMessage; if (contentText.startsWith('/me')) { - contentText = contentText.replace('/me', ''); + contentText = contentText.replace('/me ', ''); // bit of a hack, but the alternative would be quite complicated if (contentHTML) contentHTML = contentHTML.replace('/me', ''); sendHtmlFn = this.client.sendHtmlEmote; From bf8973ad33b62b9ef88007d6a1d13a1408130e37 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 19 Mar 2017 21:33:18 +0000 Subject: [PATCH 057/284] avoid leading space in HTML /me too --- src/components/views/rooms/MessageComposerInput.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 60088ddd6f..51c9ba881b 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -543,7 +543,7 @@ export default class MessageComposerInput extends React.Component { if (contentText.startsWith('/me')) { contentText = contentText.replace('/me ', ''); // bit of a hack, but the alternative would be quite complicated - if (contentHTML) contentHTML = contentHTML.replace('/me', ''); + if (contentHTML) contentHTML = contentHTML.replace('/me ', ''); sendHtmlFn = this.client.sendHtmlEmote; sendTextFn = this.client.sendEmoteMessage; } From 69c3bd7f80ddc987cd45977ae26c66e3c0b9f1f1 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 20 Mar 2017 12:13:21 +0000 Subject: [PATCH 058/284] Escape closes UserSettings Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/LoggedInView.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index c2243820cd..a8e75c0cdd 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -99,6 +99,17 @@ export default React.createClass({ var handled = false; switch (ev.keyCode) { + case KeyCode.ESCAPE: + + // Implemented this way so possible handling for other pages is neater + switch (this.props.page_type) { + case PageTypes.UserSettings: + this.props.onUserSettingsClose(); + handled = true; + break; + } + + break; case KeyCode.UP: case KeyCode.DOWN: if (ev.altKey) { From ec63e18b42f3b5b195150c99060ae257ff84ed30 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 21 Mar 2017 18:40:41 +0000 Subject: [PATCH 059/284] Show spinner whilst processing recaptcha response The fact that we showed no feedback whilst submitting the captcha response was causing confusion on slower connections where this took a nontrivial amount of time. Takes a new flag from the js-sdk that indicates whether the request being made is a background request, presenting a spinner appropriately. Requires https://github.com/matrix-org/matrix-js-sdk/pull/396 --- src/components/structures/InteractiveAuth.js | 12 +++++++----- .../views/login/InteractiveAuthEntryComponents.js | 6 ++++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/components/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js index 71fee883be..3dd34f51b4 100644 --- a/src/components/structures/InteractiveAuth.js +++ b/src/components/structures/InteractiveAuth.js @@ -140,9 +140,9 @@ export default React.createClass({ }); }, - _requestCallback: function(auth) { + _requestCallback: function(auth, background) { this.setState({ - busy: true, + busy: !background, errorText: null, stageErrorText: null, }); @@ -150,9 +150,11 @@ export default React.createClass({ if (this._unmounted) { return; } - this.setState({ - busy: false, - }); + if (background) { + this.setState({ + busy: false, + }); + } }); }, diff --git a/src/components/views/login/InteractiveAuthEntryComponents.js b/src/components/views/login/InteractiveAuthEntryComponents.js index 2d8abf9216..c4084facb2 100644 --- a/src/components/views/login/InteractiveAuthEntryComponents.js +++ b/src/components/views/login/InteractiveAuthEntryComponents.js @@ -160,6 +160,7 @@ export const RecaptchaAuthEntry = React.createClass({ submitAuthDict: React.PropTypes.func.isRequired, stageParams: React.PropTypes.object.isRequired, errorText: React.PropTypes.string, + busy: React.PropTypes.bool, }, _onCaptchaResponse: function(response) { @@ -170,6 +171,11 @@ export const RecaptchaAuthEntry = React.createClass({ }, render: function() { + if (this.props.busy) { + const Loader = sdk.getComponent("elements.Spinner"); + return ; + } + const CaptchaForm = sdk.getComponent("views.login.CaptchaForm"); var sitePublicKey = this.props.stageParams.public_key; return ( From e5a5ca9efcd8de1513fe927516c4e8eaaad69d2e Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 22 Mar 2017 10:53:15 +0000 Subject: [PATCH 060/284] Don't set busy state at all for background request --- src/components/structures/InteractiveAuth.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/components/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js index 3dd34f51b4..d520f4dff9 100644 --- a/src/components/structures/InteractiveAuth.js +++ b/src/components/structures/InteractiveAuth.js @@ -141,16 +141,20 @@ export default React.createClass({ }, _requestCallback: function(auth, background) { - this.setState({ - busy: !background, - errorText: null, - stageErrorText: null, - }); + // only set the busy flag if this is a non-background request + if (!background) { + this.setState({ + busy: true, + errorText: null, + stageErrorText: null, + }); + } return this.props.makeRequest(auth).finally(() => { if (this._unmounted) { return; } - if (background) { + // only unset the busy flag if this is a non-background request + if (!background) { this.setState({ busy: false, }); From 5ae7d5e4b218c633664ce093cdbdc803b0649b67 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 22 Mar 2017 11:13:00 +0000 Subject: [PATCH 061/284] More comments --- src/components/structures/InteractiveAuth.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js index d520f4dff9..fe7552d20f 100644 --- a/src/components/structures/InteractiveAuth.js +++ b/src/components/structures/InteractiveAuth.js @@ -141,7 +141,9 @@ export default React.createClass({ }, _requestCallback: function(auth, background) { - // only set the busy flag if this is a non-background request + // only set the busy flag if this is a non-background request, + // otherwise, the user initiated a request, so make the busy + // spinner appear and clear and existing error messages. if (!background) { this.setState({ busy: true, From 6a5682897446e362ce9d86867cb2f2010d5d965f Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 22 Mar 2017 11:25:33 +0000 Subject: [PATCH 062/284] Just return the promise if it's a bg request This makes the code a bit neater. --- src/components/structures/InteractiveAuth.js | 33 ++++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/src/components/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js index fe7552d20f..7c8a5b8065 100644 --- a/src/components/structures/InteractiveAuth.js +++ b/src/components/structures/InteractiveAuth.js @@ -141,26 +141,25 @@ export default React.createClass({ }, _requestCallback: function(auth, background) { - // only set the busy flag if this is a non-background request, - // otherwise, the user initiated a request, so make the busy - // spinner appear and clear and existing error messages. - if (!background) { - this.setState({ - busy: true, - errorText: null, - stageErrorText: null, - }); - } - return this.props.makeRequest(auth).finally(() => { + const makeRequestPromise = this.props.makeRequest(auth); + + // if it's a background request, just do it: we don't want + // it to affect the state of our UI. + if (background) return makeRequestPromise; + + // otherwise, manage the state of the spinner and error messages + this.setState({ + busy: true, + errorText: null, + stageErrorText: null, + }); + return makeRequestPromise.finally(() => { if (this._unmounted) { return; } - // only unset the busy flag if this is a non-background request - if (!background) { - this.setState({ - busy: false, - }); - } + this.setState({ + busy: false, + }); }); }, From 6a37fc432544c3680a03a9f08834103e13268341 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 22 Mar 2017 12:00:16 +0000 Subject: [PATCH 063/284] Comment typos --- src/AddThreepid.js | 5 +++-- src/components/structures/UserSettings.js | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/AddThreepid.js b/src/AddThreepid.js index 44d709371b..c89de4f5fa 100644 --- a/src/AddThreepid.js +++ b/src/AddThreepid.js @@ -55,7 +55,8 @@ class AddThreepid { /** * Attempt to add a msisdn threepid. This will trigger a side-effect of * sending a test message to the provided phone number. - * @param {string} emailAddress The email address to add + * @param {string} phoneCountry The ISO 2 letter code of the country to resolve phoneNumber in + * @param {string} phoneNumber The national or international formatted phone number to add * @param {boolean} bind If True, bind this phone number to this mxid on the Identity Server * @return {Promise} Resolves when the text message has been sent. Then call haveMsisdnToken(). */ @@ -102,7 +103,7 @@ class AddThreepid { /** * Takes a phone number verification code as entered by the user and validates * it with the ID server, then if successful, adds the phone number. - * @return {Promise} Resolves if the email address was added. Rejects with an object + * @return {Promise} Resolves if the phone number was added. Rejects with an object * with a "message" property which contains a human-readable message detailing why * the request failed. */ diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 6626d2c400..b50c3318ce 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -387,7 +387,7 @@ module.exports = React.createClass({ const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); this._addThreepid = new AddThreepid(); - // we always phone numbers when registering, so let's do the + // we always bind phone numbers when registering, so let's do the // same here. this._addThreepid.addMsisdn(this.state.phoneCountry, this.state.phoneNumber, true).then((resp) => { this._promptForMsisdnVerificationCode(resp.msisdn); From 4cebded04f159c154d526a81cdf70b30593f3b95 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 22 Mar 2017 15:06:52 +0000 Subject: [PATCH 064/284] Add canResetTimeline callback and thread it through to TimelinePanel --- src/components/structures/LoggedInView.js | 7 +++++++ src/components/structures/MatrixChat.js | 24 +++++++++++++++++++++- src/components/structures/RoomView.js | 7 +++++++ src/components/structures/TimelinePanel.js | 4 ++++ 4 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index c2243820cd..6e2f0a3a5b 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -81,6 +81,13 @@ export default React.createClass({ return this._scrollStateMap[roomId]; }, + canResetTimelineInRoom: function(roomId) { + if (!this.refs.roomView) { + return true; + } + return this.refs.roomView.canResetTimeline(); + }, + _onKeyDown: function(ev) { /* // Remove this for now as ctrl+alt = alt-gr so this breaks keyboards which rely on alt-gr for numbers diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 2337d62fd8..9b51e7f3fb 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -806,9 +806,31 @@ module.exports = React.createClass({ * (useful for setting listeners) */ _onWillStartClient() { + var self = this; var cli = MatrixClientPeg.get(); - var self = this; + // Allow the JS SDK to reap timeline events. This reduces the amount of + // memory consumed as the JS SDK stores multiple distinct copies of room + // state (each of which can be 10s of MBs) for each DISJOINT timeline. This is + // particularly noticeable when there are lots of 'limited' /sync responses + // such as when laptops unsleep. + // https://github.com/vector-im/riot-web/issues/3307#issuecomment-282895568 + cli.setCanResetTimelineCallback(function(roomId) { + console.log("Request to reset timeline in room ", roomId, " viewing:", self.state.currentRoomId); + if (roomId !== self.state.currentRoomId) { + // It is safe to remove events from rooms we are not viewing. + return true; + } + // We are viewing the room which we want to reset. It is only safe to do + // this if we are not scrolled up in the view. To find out, delegate to + // the timeline panel. If the timeline panel doesn't exist, then we assume + // it is safe to reset the timeline. + if (!self.refs.loggedInView) { + return true; + } + return self.refs.loggedInView.canResetTimelineInRoom(roomId); + }); + cli.on('sync', function(state, prevState) { self.updateStatusIndicator(state, prevState); if (state === "SYNCING" && prevState === "SYNCING") { diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 345d0f6b80..b22d867acf 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -490,6 +490,13 @@ module.exports = React.createClass({ } }, + canResetTimeline: function() { + if (!this.refs.messagePanel) { + return true; + } + return this.refs.messagePanel.canResetTimeline(); + }, + // called when state.room is first initialised (either at initial load, // after a successful peek, or after we join the room). _onRoomLoaded: function(room) { diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index cb42f701a3..8ef0e7631f 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -431,6 +431,10 @@ var TimelinePanel = React.createClass({ } }, + canResetTimeline: function() { + return this.refs.messagePanel && this.refs.messagePanel.isAtBottom(); + }, + onRoomRedaction: function(ev, room) { if (this.unmounted) return; From 4cd24d15d453b749ebcc967e30d15207fd10ebc3 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 22 Mar 2017 15:18:27 +0000 Subject: [PATCH 065/284] Factor out AddPhoneNumber to a separate component --- src/component-index.js | 2 + src/components/structures/UserSettings.js | 114 +----------- .../views/settings/AddPhoneNumber.js | 170 ++++++++++++++++++ 3 files changed, 177 insertions(+), 109 deletions(-) create mode 100644 src/components/views/settings/AddPhoneNumber.js diff --git a/src/component-index.js b/src/component-index.js index c83c0dbb11..d6873c6dfd 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -229,6 +229,8 @@ import views$rooms$TopUnreadMessagesBar from './components/views/rooms/TopUnread views$rooms$TopUnreadMessagesBar && (module.exports.components['views.rooms.TopUnreadMessagesBar'] = views$rooms$TopUnreadMessagesBar); import views$rooms$UserTile from './components/views/rooms/UserTile'; views$rooms$UserTile && (module.exports.components['views.rooms.UserTile'] = views$rooms$UserTile); +import views$settings$AddPhoneNumber from './components/views/settings/AddPhoneNumber'; +views$settings$AddPhoneNumber && (module.exports.components['views.settings.AddPhoneNumber'] = views$settings$AddPhoneNumber); import views$settings$ChangeAvatar from './components/views/settings/ChangeAvatar'; views$settings$ChangeAvatar && (module.exports.components['views.settings.ChangeAvatar'] = views$settings$ChangeAvatar); import views$settings$ChangeDisplayName from './components/views/settings/ChangeDisplayName'; diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index b50c3318ce..5633bd0bc7 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -135,8 +136,6 @@ module.exports = React.createClass({ msisdn_add_pending: false, vectorVersion: null, rejectingInvites: false, - phoneCountry: null, - phoneNumber: "", }; }, @@ -218,14 +217,6 @@ module.exports = React.createClass({ }); }, - _onPhoneCountryChange: function(phoneCountry) { - this.setState({ phoneCountry: phoneCountry }); - }, - - _onPhoneNumberChange: function(ev) { - this.setState({ phoneNumber: ev.target.value }); - }, - onAction: function(payload) { if (payload.action === "notifier_enabled") { this.forceUpdate(); @@ -338,16 +329,6 @@ module.exports = React.createClass({ this._addEmail(); }, - _onAddMsisdnEditFinished: function(value, shouldSubmit) { - if (!shouldSubmit) return; - this._addMsisdn(); - }, - - _onAddMsisdnSubmit: function(ev) { - ev.preventDefault(); - this._addMsisdn(); - }, - _addEmail: function() { var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); @@ -382,65 +363,6 @@ module.exports = React.createClass({ this.setState({email_add_pending: true}); }, - _addMsisdn: function() { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - - this._addThreepid = new AddThreepid(); - // we always bind phone numbers when registering, so let's do the - // same here. - this._addThreepid.addMsisdn(this.state.phoneCountry, this.state.phoneNumber, true).then((resp) => { - this._promptForMsisdnVerificationCode(resp.msisdn); - }).catch((err) => { - console.error("Unable to add phone number: " + err); - let msg = err.message; - Modal.createDialog(ErrorDialog, { - title: "Error", - description: msg, - }); - }).finally(() => { - this.setState({msisdn_add_pending: false}); - }).done();; - ReactDOM.findDOMNode(this.refs.add_msisdn_input).blur(); - this.setState({msisdn_add_pending: true}); - }, - - _promptForMsisdnVerificationCode(msisdn, err) { - const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog"); - let msgElements = [ -
A text message has been sent to +{msisdn}. - Please enter the verification code it contains
- ]; - if (err) { - let msg = err.error; - if (err.errcode == 'M_THREEPID_AUTH_FAILED') { - msg = "Incorrect verification code"; - } - msgElements.push(
{msg}
); - } - Modal.createDialog(TextInputDialog, { - title: "Enter Code", - description:
{msgElements}
, - button: "Submit", - onFinished: (should_verify, token) => { - if (!should_verify) { - this._addThreepid = null; - return; - } - this.setState({msisdn_add_pending: true}); - this._addThreepid.haveMsisdnToken(token).then(() => { - this._addThreepid = null; - this.setState({phoneNumber: ''}); - return this._refreshFromServer(); - }).catch((err) => { - this._promptForMsisdnVerificationCode(msisdn, err); - }).finally(() => { - this.setState({msisdn_add_pending: false}); - }).done(); - } - }); - }, - onRemoveThreepidClicked: function(threepid) { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); Modal.createDialog(QuestionDialog, { @@ -897,7 +819,6 @@ module.exports = React.createClass({ ); }); let addEmailSection; - let addMsisdnSection; if (this.state.email_add_pending) { addEmailSection = ; } else if (!MatrixClientPeg.get().isGuest()) { @@ -920,35 +841,10 @@ module.exports = React.createClass({
); } - if (this.state.msisdn_add_pending) { - addMsisdnSection = ; - } else if (!MatrixClientPeg.get().isGuest()) { - const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); - addMsisdnSection = ( -
-
-
-
-
- - - -
-
- Add -
-
- ); - } + const AddPhoneNumber = sdk.getComponent('views.settings.AddPhoneNumber'); + const addMsisdnSection = ( + + ); threepidsSection.push(addEmailSection); threepidsSection.push(addMsisdnSection); diff --git a/src/components/views/settings/AddPhoneNumber.js b/src/components/views/settings/AddPhoneNumber.js new file mode 100644 index 0000000000..905a21f61d --- /dev/null +++ b/src/components/views/settings/AddPhoneNumber.js @@ -0,0 +1,170 @@ +/* +Copyright 2017 Vector Creations 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. +*/ + +import React from 'react'; + +import sdk from '../../../index'; +import AddThreepid from '../../../AddThreepid'; +import WithMatrixClient from '../../../wrappers/WithMatrixClient'; +import Modal from '../../../Modal'; + + +class AddPhoneNumber extends React.Component { + constructor(props, context) { + super(props, context); + + this._addThreepid = null; + this._addMsisdnInput = null; + + this.state = { + busy: false, + phoneCountry: null, + phoneNumber: "", + }; + + this._onPhoneCountryChange = this._onPhoneCountryChange.bind(this); + this._onPhoneNumberChange = this._onPhoneNumberChange.bind(this); + this._onAddMsisdnEditFinished = this._onAddMsisdnEditFinished.bind(this); + this._onAddMsisdnSubmit = this._onAddMsisdnSubmit.bind(this); + this._collectAddMsisdnInput = this._collectAddMsisdnInput.bind(this); + this._addMsisdn = this._addMsisdn.bind(this); + this._promptForMsisdnVerificationCode = this._promptForMsisdnVerificationCode.bind(this); + } + + _onPhoneCountryChange(phoneCountry) { + this.setState({ phoneCountry: phoneCountry }); + } + + _onPhoneNumberChange(ev) { + this.setState({ phoneNumber: ev.target.value }); + } + + _onAddMsisdnEditFinished(value, shouldSubmit) { + if (!shouldSubmit) return; + this._addMsisdn(); + } + + _onAddMsisdnSubmit(ev) { + ev.preventDefault(); + this._addMsisdn(); + } + + _collectAddMsisdnInput(e) { + this._addMsisdnInput = e; + } + + _addMsisdn() { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + + this._addThreepid = new AddThreepid(); + // we always bind phone numbers when registering, so let's do the + // same here. + this._addThreepid.addMsisdn(this.state.phoneCountry, this.state.phoneNumber, true).then((resp) => { + this._promptForMsisdnVerificationCode(resp.msisdn); + }).catch((err) => { + console.error("Unable to add phone number: " + err); + let msg = err.message; + Modal.createDialog(ErrorDialog, { + title: "Error", + description: msg, + }); + }).finally(() => { + this.setState({msisdn_add_pending: false}); + }).done();; + this._addMsisdnInput.blur(); + this.setState({msisdn_add_pending: true}); + } + + _promptForMsisdnVerificationCode(msisdn, err) { + const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog"); + let msgElements = [ +
A text message has been sent to +{msisdn}. + Please enter the verification code it contains
+ ]; + if (err) { + let msg = err.error; + if (err.errcode == 'M_THREEPID_AUTH_FAILED') { + msg = "Incorrect verification code"; + } + msgElements.push(
{msg}
); + } + Modal.createDialog(TextInputDialog, { + title: "Enter Code", + description:
{msgElements}
, + button: "Submit", + onFinished: (should_verify, token) => { + if (!should_verify) { + this._addThreepid = null; + return; + } + this.setState({msisdn_add_pending: true}); + this._addThreepid.haveMsisdnToken(token).then(() => { + this._addThreepid = null; + this.setState({phoneNumber: ''}); + if (this.props.onThreepidAdded) this.props.onThreepidAdded(); + }).catch((err) => { + this._promptForMsisdnVerificationCode(msisdn, err); + }).finally(() => { + this.setState({msisdn_add_pending: false}); + }).done(); + } + }); + } + + render() { + const Loader = sdk.getComponent("elements.Spinner"); + if (this.state.msisdn_add_pending) { + return ; + } else if (!this.props.matrixClient.isGuest()) { + const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); + // XXX: This CSS relies on the CSS surrounding it in UserSettings as its in + // a tabular format to align the submit buttons + return ( +
+
+
+
+
+ + + +
+
+ Add +
+
+ ); + } + } +} + +AddPhoneNumber.propTypes = { + matrixClient: React.PropTypes.object.isRequired, + onThreepidAdded: React.PropTypes.func, +}; + +AddPhoneNumber = WithMatrixClient(AddPhoneNumber); +export default AddPhoneNumber; From cca607d4694256fc0a0f701e563db7456acfb6fe Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 22 Mar 2017 15:39:09 +0000 Subject: [PATCH 066/284] Make phone number form a bit more semantic --- src/components/views/settings/AddPhoneNumber.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/views/settings/AddPhoneNumber.js b/src/components/views/settings/AddPhoneNumber.js index 905a21f61d..e058fce0f2 100644 --- a/src/components/views/settings/AddPhoneNumber.js +++ b/src/components/views/settings/AddPhoneNumber.js @@ -134,11 +134,11 @@ class AddPhoneNumber extends React.Component { // XXX: This CSS relies on the CSS surrounding it in UserSettings as its in // a tabular format to align the submit buttons return ( -
+
- +
- +
- Add +
-
+ ); } } From e39979a61f284aa873f38f659ab3a57d7f58f8d0 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 22 Mar 2017 16:15:45 +0000 Subject: [PATCH 067/284] Convert to old style react class --- .../views/settings/AddPhoneNumber.js | 67 ++++++++----------- 1 file changed, 29 insertions(+), 38 deletions(-) diff --git a/src/components/views/settings/AddPhoneNumber.js b/src/components/views/settings/AddPhoneNumber.js index e058fce0f2..c64ed4b545 100644 --- a/src/components/views/settings/AddPhoneNumber.js +++ b/src/components/views/settings/AddPhoneNumber.js @@ -22,51 +22,50 @@ import WithMatrixClient from '../../../wrappers/WithMatrixClient'; import Modal from '../../../Modal'; -class AddPhoneNumber extends React.Component { - constructor(props, context) { - super(props, context); +export default WithMatrixClient(React.createClass({ + displayName: 'AddPhoneNumber', - this._addThreepid = null; - this._addMsisdnInput = null; + propTypes: { + matrixClient: React.PropTypes.object.isRequired, + onThreepidAdded: React.PropTypes.func, + }, - this.state = { + getInitialState: function() { + return { busy: false, phoneCountry: null, phoneNumber: "", }; + }, - this._onPhoneCountryChange = this._onPhoneCountryChange.bind(this); - this._onPhoneNumberChange = this._onPhoneNumberChange.bind(this); - this._onAddMsisdnEditFinished = this._onAddMsisdnEditFinished.bind(this); - this._onAddMsisdnSubmit = this._onAddMsisdnSubmit.bind(this); - this._collectAddMsisdnInput = this._collectAddMsisdnInput.bind(this); - this._addMsisdn = this._addMsisdn.bind(this); - this._promptForMsisdnVerificationCode = this._promptForMsisdnVerificationCode.bind(this); - } + componentWillMount: function() { + this._addThreepid = null; + this._addMsisdnInput = null; + }, - _onPhoneCountryChange(phoneCountry) { + _onPhoneCountryChange: function(phoneCountry) { this.setState({ phoneCountry: phoneCountry }); - } + }, - _onPhoneNumberChange(ev) { + _onPhoneNumberChange: function(ev) { this.setState({ phoneNumber: ev.target.value }); - } + }, - _onAddMsisdnEditFinished(value, shouldSubmit) { + _onAddMsisdnEditFinished: function(value, shouldSubmit) { if (!shouldSubmit) return; this._addMsisdn(); - } + }, - _onAddMsisdnSubmit(ev) { + _onAddMsisdnSubmit: function(ev) { ev.preventDefault(); this._addMsisdn(); - } + }, - _collectAddMsisdnInput(e) { + _collectAddMsisdnInput: function(e) { this._addMsisdnInput = e; - } + }, - _addMsisdn() { + _addMsisdn: function() { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); @@ -87,9 +86,9 @@ class AddPhoneNumber extends React.Component { }).done();; this._addMsisdnInput.blur(); this.setState({msisdn_add_pending: true}); - } + }, - _promptForMsisdnVerificationCode(msisdn, err) { + _promptForMsisdnVerificationCode:function (msisdn, err) { const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog"); let msgElements = [
A text message has been sent to +{msisdn}. @@ -123,9 +122,9 @@ class AddPhoneNumber extends React.Component { }).done(); } }); - } + }, - render() { + render: function() { const Loader = sdk.getComponent("elements.Spinner"); if (this.state.msisdn_add_pending) { return ; @@ -159,12 +158,4 @@ class AddPhoneNumber extends React.Component { ); } } -} - -AddPhoneNumber.propTypes = { - matrixClient: React.PropTypes.object.isRequired, - onThreepidAdded: React.PropTypes.func, -}; - -AddPhoneNumber = WithMatrixClient(AddPhoneNumber); -export default AddPhoneNumber; +})) From 6b78440466234c6cc378e6baecd7c986333cc2e5 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 22 Mar 2017 16:36:42 +0000 Subject: [PATCH 068/284] Unmounted guard --- src/components/views/settings/AddPhoneNumber.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/views/settings/AddPhoneNumber.js b/src/components/views/settings/AddPhoneNumber.js index c64ed4b545..83c331dd33 100644 --- a/src/components/views/settings/AddPhoneNumber.js +++ b/src/components/views/settings/AddPhoneNumber.js @@ -41,6 +41,11 @@ export default WithMatrixClient(React.createClass({ componentWillMount: function() { this._addThreepid = null; this._addMsisdnInput = null; + this._unmounted = false; + }, + + componentWillUnmount: function() { + this._unmounted = true; }, _onPhoneCountryChange: function(phoneCountry) { @@ -67,7 +72,6 @@ export default WithMatrixClient(React.createClass({ _addMsisdn: function() { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); this._addThreepid = new AddThreepid(); // we always bind phone numbers when registering, so let's do the @@ -89,6 +93,7 @@ export default WithMatrixClient(React.createClass({ }, _promptForMsisdnVerificationCode:function (msisdn, err) { + if (this._unmounted) return; const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog"); let msgElements = [
A text message has been sent to +{msisdn}. From b58d8bffe1c784485f529525020a2582fc5db6df Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 22 Mar 2017 16:41:08 +0000 Subject: [PATCH 069/284] More PR feedback Unmounted guards, extra semicolon, return early to lose indent level, add keys. --- .../views/settings/AddPhoneNumber.js | 66 ++++++++++--------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/src/components/views/settings/AddPhoneNumber.js b/src/components/views/settings/AddPhoneNumber.js index 83c331dd33..9680bdd12d 100644 --- a/src/components/views/settings/AddPhoneNumber.js +++ b/src/components/views/settings/AddPhoneNumber.js @@ -86,8 +86,9 @@ export default WithMatrixClient(React.createClass({ description: msg, }); }).finally(() => { + if (this._unmounted) return; this.setState({msisdn_add_pending: false}); - }).done();; + }).done(); this._addMsisdnInput.blur(); this.setState({msisdn_add_pending: true}); }, @@ -96,7 +97,7 @@ export default WithMatrixClient(React.createClass({ if (this._unmounted) return; const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog"); let msgElements = [ -
A text message has been sent to +{msisdn}. +
A text message has been sent to +{msisdn}. Please enter the verification code it contains
]; if (err) { @@ -104,7 +105,7 @@ export default WithMatrixClient(React.createClass({ if (err.errcode == 'M_THREEPID_AUTH_FAILED') { msg = "Incorrect verification code"; } - msgElements.push(
{msg}
); + msgElements.push(
{msg}
); } Modal.createDialog(TextInputDialog, { title: "Enter Code", @@ -123,6 +124,7 @@ export default WithMatrixClient(React.createClass({ }).catch((err) => { this._promptForMsisdnVerificationCode(msisdn, err); }).finally(() => { + if (this._unmounted) return; this.setState({msisdn_add_pending: false}); }).done(); } @@ -133,34 +135,36 @@ export default WithMatrixClient(React.createClass({ const Loader = sdk.getComponent("elements.Spinner"); if (this.state.msisdn_add_pending) { return ; - } else if (!this.props.matrixClient.isGuest()) { - const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); - // XXX: This CSS relies on the CSS surrounding it in UserSettings as its in - // a tabular format to align the submit buttons - return ( -
-
-
-
-
- - -
-
-
- -
-
- ); + } else if (this.props.matrixClient.isGuest()) { + return null; } + + const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); + // XXX: This CSS relies on the CSS surrounding it in UserSettings as its in + // a tabular format to align the submit buttons + return ( +
+
+
+
+
+ + +
+
+
+ +
+
+ ); } })) From d5272149f6ab734da3b683fffe3e513b987787be Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 22 Mar 2017 16:42:44 +0000 Subject: [PATCH 070/284] Another unmounted guard --- src/components/views/settings/AddPhoneNumber.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/settings/AddPhoneNumber.js b/src/components/views/settings/AddPhoneNumber.js index 9680bdd12d..bb5ecd2694 100644 --- a/src/components/views/settings/AddPhoneNumber.js +++ b/src/components/views/settings/AddPhoneNumber.js @@ -116,6 +116,7 @@ export default WithMatrixClient(React.createClass({ this._addThreepid = null; return; } + if (this._unmounted) return; this.setState({msisdn_add_pending: true}); this._addThreepid.haveMsisdnToken(token).then(() => { this._addThreepid = null; From 707fd6062446d86bcbf2ed7b8452e3e8996d8c95 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 23 Mar 2017 10:38:00 +0000 Subject: [PATCH 071/284] Prevent crash on login of no guest session The bound functions are only set when the Notifier is started, so if stop() was called without start() having been called, the listener function would be null which would throw an exception. --- src/Notifier.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Notifier.js b/src/Notifier.js index 67642e734a..7fc7d3e338 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,8 +15,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - var MatrixClientPeg = require("./MatrixClientPeg"); var PlatformPeg = require("./PlatformPeg"); var TextForEvent = require('./TextForEvent'); @@ -103,7 +102,7 @@ var Notifier = { }, stop: function() { - if (MatrixClientPeg.get()) { + if (MatrixClientPeg.get() && this.boundOnRoomTimeline) { MatrixClientPeg.get().removeListener('Room.timeline', this.boundOnRoomTimeline); MatrixClientPeg.get().removeListener("Room.receipt", this.boundOnRoomReceipt); MatrixClientPeg.get().removeListener('sync', this.boundOnSyncStateChange); From 5e3b991ec23deabb10bd8e78f6280ef46b3ed237 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 24 Mar 2017 10:45:38 +0000 Subject: [PATCH 072/284] PR feedback fixes --- src/components/structures/UserSettings.js | 1 - src/components/views/settings/AddPhoneNumber.js | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 5633bd0bc7..0cb120019e 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -133,7 +133,6 @@ module.exports = React.createClass({ threePids: [], phase: "UserSettings.LOADING", // LOADING, DISPLAY email_add_pending: false, - msisdn_add_pending: false, vectorVersion: null, rejectingInvites: false, }; diff --git a/src/components/views/settings/AddPhoneNumber.js b/src/components/views/settings/AddPhoneNumber.js index bb5ecd2694..3a348393aa 100644 --- a/src/components/views/settings/AddPhoneNumber.js +++ b/src/components/views/settings/AddPhoneNumber.js @@ -35,6 +35,7 @@ export default WithMatrixClient(React.createClass({ busy: false, phoneCountry: null, phoneNumber: "", + msisdn_add_pending: false, }; }, @@ -137,7 +138,7 @@ export default WithMatrixClient(React.createClass({ if (this.state.msisdn_add_pending) { return ; } else if (this.props.matrixClient.isGuest()) { - return null; + return
; } const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); From 975a2db3e4354c840202263c9853f1887cac8029 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 27 Mar 2017 09:42:04 +0100 Subject: [PATCH 073/284] UI delay done in UI --- src/Rooms.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Rooms.js b/src/Rooms.js index 0f81bf5391..08fa7f797f 100644 --- a/src/Rooms.js +++ b/src/Rooms.js @@ -90,11 +90,7 @@ export function guessAndSetDMRoom(room, isDirect) { newTarget = null; } - // give some time for the user to see the icon change first, since - // this will hide the context menu once it completes - return q.delay(500).then(() => { - return setDMRoom(room.roomId, newTarget); - }); + return setDMRoom(room.roomId, newTarget); } /** From a4b4c3feb822382987a64d80cbcc40e675712bf1 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 27 Mar 2017 14:34:05 +0100 Subject: [PATCH 074/284] Read Receipt offset Read receipts were always one read receipt to the left further than they should have been. This fixes that and simplifies the logic. --- src/components/views/rooms/EventTile.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index b451d1c046..8b8e52ae83 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -283,9 +283,10 @@ module.exports = WithMatrixClient(React.createClass({ }, getReadAvatars: function() { - var ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker'); - var avatars = []; - var left = 0; + const ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker'); + const avatars = []; + const receiptOffset = 15; + let left = 0; // It's possible that the receipt was sent several days AFTER the event. // If it is, we want to display the complete date along with the HH:MM:SS, @@ -305,6 +306,12 @@ module.exports = WithMatrixClient(React.createClass({ if ((i < MAX_READ_AVATARS) || this.state.allReadAvatars) { hidden = false; } + // TODO: we keep the extra read avatars in the dom to make animation simpler + // we could optimise this to reduce the dom size. + + // If hidden, set offset equal to the offset of the final visible avatar or + // else set it proportional to index + left = (hidden ? MAX_READ_AVATARS - 1 : i) * -receiptOffset; var userId = receipt.roomMember.userId; var readReceiptInfo; @@ -316,11 +323,6 @@ module.exports = WithMatrixClient(React.createClass({ this.props.readReceiptMap[userId] = readReceiptInfo; } } - // TODO: we keep the extra read avatars in the dom to make animation simpler - // we could optimise this to reduce the dom size. - if (!hidden) { - left -= 15; - } // add to the start so the most recent is on the end (ie. ends up rightmost) avatars.unshift( @@ -341,7 +343,7 @@ module.exports = WithMatrixClient(React.createClass({ if (remainder > 0) { remText = { remainder }+ + style={{ right: -(left - receiptOffset) }}>{ remainder }+ ; } } From 30c5af35e572fea179beefc0ba9c4b3e8e342dba Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 27 Mar 2017 16:39:04 +0100 Subject: [PATCH 075/284] Add state loggingIn to MatrixChat to fix flashing login To prevent the login screen from flashing when refreshing the app, use some state to indicate that a login is in progress, and OR that with the existing `loading` boolean to show the `` instead of the default ``. This might be too invasive, and a default spinner may be better. --- src/Lifecycle.js | 5 +++++ src/components/structures/MatrixChat.js | 10 +++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index fc8087e12d..f20716cae6 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -276,6 +276,11 @@ export function setLoggedIn(credentials) { console.log("setLoggedIn => %s (guest=%s) hs=%s", credentials.userId, credentials.guest, credentials.homeserverUrl); + // This is dispatched to indicate that the user is still in the process of logging in + // because `teamPromise` may take some time to resolve, breaking the assumption that + // `setLoggedIn` takes an "instant" to complete, and dispatch `on_logged_in` a few ms + // later than MatrixChat might assume. + dis.dispatch({action: 'on_logging_in'}); // Resolves by default let teamPromise = Promise.resolve(null); diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 9b51e7f3fb..c264fbec63 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -568,6 +568,9 @@ module.exports = React.createClass({ case 'set_theme': this._onSetTheme(payload.value); break; + case 'on_logging_in': + this.setState({loggingIn: true}); + break; case 'on_logged_in': this._onLoggedIn(payload.teamToken); break; @@ -757,6 +760,7 @@ module.exports = React.createClass({ this.setState({ guestCreds: null, logged_in: true, + loggingIn: false, }); if (teamToken) { @@ -1160,7 +1164,11 @@ module.exports = React.createClass({ // console.log("rendering; loading="+this.state.loading+"; screen="+this.state.screen + // "; logged_in="+this.state.logged_in+"; ready="+this.state.ready); - if (this.state.loading) { + // `loading` might be set to false before `logged_in = true`, causing the default + // (``) to be visible for a few MS (say, whilst a request is in-flight to + // the RTS). So in the meantime, use `loggingIn`, which is true between + // actions `on_logging_in` and `on_logged_in`. + if (this.state.loading || this.state.loggingIn) { var Spinner = sdk.getComponent('elements.Spinner'); return (
From a230354dbec8b3b1d22210c80d9e6de670faa7f9 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 27 Mar 2017 16:40:01 +0100 Subject: [PATCH 076/284] Fix bug where you can't invite a valid address Always show the entered text as an option if it's a valid address, otherwise there's no way to invite an address that is valid and also returns other mxids in the search results. Fixes https://github.com/vector-im/riot-web/issues/3496 --- .../views/dialogs/ChatInviteDialog.js | 36 +++++++------------ 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index f958b8887c..f78752955b 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -30,15 +30,6 @@ import Fuse from 'fuse.js'; const TRUNCATE_QUERY_LIST = 40; -/* - * Escapes a string so it can be used in a RegExp - * Basically just replaces: \ ^ $ * + ? . ( ) | { } [ ] - * From http://stackoverflow.com/a/6969486 - */ -function escapeRegExp(str) { - return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); -} - module.exports = React.createClass({ displayName: "ChatInviteDialog", propTypes: { @@ -220,20 +211,19 @@ module.exports = React.createClass({ } }); - // If the query isn't a user we know about, but is a - // valid address, add an entry for that - if (queryList.length == 0) { - const addrType = getAddressType(query); - if (addrType !== null) { - queryList[0] = { - addressType: addrType, - address: query, - isKnown: false, - }; - if (this._cancelThreepidLookup) this._cancelThreepidLookup(); - if (addrType == 'email') { - this._lookupThreepid(addrType, query).done(); - } + // If the query is a valid address, add an entry for that + // This i simportant, otherwise there's no way to invite + // a perfectly valid address if there are close matches. + const addrType = getAddressType(query); + if (addrType !== null) { + queryList.unshift({ + addressType: addrType, + address: query, + isKnown: false, + }); + if (this._cancelThreepidLookup) this._cancelThreepidLookup(); + if (addrType == 'email') { + this._lookupThreepid(addrType, query).done(); } } } From a3efa45795c27cacbcff015e23b3e7e29d933d4c Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 27 Mar 2017 16:53:00 +0100 Subject: [PATCH 077/284] Correct confirm prompt for disinvite It should be 'disinvite' not 'kick', and probably doesn't really need a reason. --- src/components/views/rooms/MemberInfo.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 39a6c052f8..1459ad3eb7 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -218,11 +218,13 @@ module.exports = WithMatrixClient(React.createClass({ }, onKick: function() { + const membership = this.props.member.membership; + const kickLabel = membership === "invite" ? "Disinvite" : "Kick"; const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); Modal.createDialog(ConfirmUserActionDialog, { member: this.props.member, - action: 'Kick', - askReason: true, + action: kickLabel, + askReason: membership == "join", danger: true, onFinished: (proceed, reason) => { if (!proceed) return; From 9125c1b2ccee1854309e6a7859b9f83cfe8c4662 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 27 Mar 2017 17:00:05 +0100 Subject: [PATCH 078/284] I can't type --- src/components/views/dialogs/ChatInviteDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index f78752955b..ad20ebaa18 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -212,7 +212,7 @@ module.exports = React.createClass({ }); // If the query is a valid address, add an entry for that - // This i simportant, otherwise there's no way to invite + // This is simportant, otherwise there's no way to invite // a perfectly valid address if there are close matches. const addrType = getAddressType(query); if (addrType !== null) { From db3339de33eaaae2cd8a49e6d0979c3c332e2975 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 27 Mar 2017 17:03:59 +0100 Subject: [PATCH 079/284] I still can't type --- src/components/views/dialogs/ChatInviteDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index ad20ebaa18..16f756a773 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -212,7 +212,7 @@ module.exports = React.createClass({ }); // If the query is a valid address, add an entry for that - // This is simportant, otherwise there's no way to invite + // This is important, otherwise there's no way to invite // a perfectly valid address if there are close matches. const addrType = getAddressType(query); if (addrType !== null) { From c650cfffaca9ff80d704d1faedaf3ec9ac341881 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 27 Mar 2017 17:14:39 +0100 Subject: [PATCH 080/284] logged_in -> loggedIn Also added `loggingIn` to `initialState` and removed some commented code. --- src/components/structures/MatrixChat.js | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index c264fbec63..8ae38b9f3c 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -117,7 +117,8 @@ module.exports = React.createClass({ // If we're trying to just view a user ID (i.e. /user URL), this is it viewUserId: null, - logged_in: false, + loggedIn: false, + loggingIn: false, collapse_lhs: false, collapse_rhs: false, ready: false, @@ -315,7 +316,7 @@ module.exports = React.createClass({ const newState = { screen: undefined, viewUserId: null, - logged_in: false, + loggedIn: false, ready: false, upgradeUsername: null, guestAccessToken: null, @@ -364,7 +365,7 @@ module.exports = React.createClass({ this.notifyNewScreen('login'); break; case 'start_post_registration': - this.setState({ // don't clobber logged_in status + this.setState({ // don't clobber loggedIn status screen: 'post_registration' }); break; @@ -388,7 +389,7 @@ module.exports = React.createClass({ this.notifyNewScreen('register'); break; case 'start_password_recovery': - if (this.state.logged_in) return; + if (this.state.loggedIn) return; this.setStateForNewScreen({ screen: 'forgot_password', }); @@ -759,7 +760,7 @@ module.exports = React.createClass({ _onLoggedIn: function(teamToken) { this.setState({ guestCreds: null, - logged_in: true, + loggedIn: true, loggingIn: false, }); @@ -794,7 +795,7 @@ module.exports = React.createClass({ _onLoggedOut: function() { this.notifyNewScreen('login'); this.setStateForNewScreen({ - logged_in: false, + loggedIn: false, ready: false, collapse_lhs: false, collapse_rhs: false, @@ -975,7 +976,7 @@ module.exports = React.createClass({ // we can't view a room unless we're logged in // (a guest account is fine) - if (!this.state.logged_in) { + if (!this.state.loggedIn) { // we may still be loading (ie, trying to register a guest // session); otherwise we're (probably) already showing a login // screen. Either way, we'll show the room once the client starts. @@ -1161,10 +1162,7 @@ module.exports = React.createClass({ var ForgotPassword = sdk.getComponent('structures.login.ForgotPassword'); var LoggedInView = sdk.getComponent('structures.LoggedInView'); - // console.log("rendering; loading="+this.state.loading+"; screen="+this.state.screen + - // "; logged_in="+this.state.logged_in+"; ready="+this.state.ready); - - // `loading` might be set to false before `logged_in = true`, causing the default + // `loading` might be set to false before `loggedIn = true`, causing the default // (``) to be visible for a few MS (say, whilst a request is in-flight to // the RTS). So in the meantime, use `loggingIn`, which is true between // actions `on_logging_in` and `on_logged_in`. @@ -1182,7 +1180,7 @@ module.exports = React.createClass({ ); - } else if (this.state.logged_in && this.state.ready) { + } else if (this.state.loggedIn && this.state.ready) { /* for now, we stuff the entirety of our props and state into the LoggedInView. * we should go through and figure out what we actually need to pass down, as well * as using something like redux to avoid having a billion bits of state kicking around. @@ -1197,7 +1195,7 @@ module.exports = React.createClass({ {...this.state} /> ); - } else if (this.state.logged_in) { + } else if (this.state.loggedIn) { // we think we are logged in, but are still waiting for the /sync to complete var Spinner = sdk.getComponent('elements.Spinner'); return ( From ffd8ef84d6a73b0f1b7660dc59e5317ab7ffb2ac Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 28 Mar 2017 09:30:41 +0100 Subject: [PATCH 081/284] Fix VOIP bar hidden on first render of RoomStatusBar componentDidUpdate is not called for the initial render of a React component (https://facebook.github.io/react/docs/react-component.html#componentdidupdate) componentWillMount is used so that the initial state and props of RoomStatusBar can also trigger props.isVisible. This fixes https://github.com/vector-im/riot-web/issues/3181 --- src/components/structures/RoomStatusBar.js | 54 ++++++++-------------- 1 file changed, 20 insertions(+), 34 deletions(-) diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index 626c376d9f..0389b606aa 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -96,26 +96,12 @@ module.exports = React.createClass({ componentWillMount: function() { MatrixClientPeg.get().on("sync", this.onSyncStateChange); MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping); + + this._checkSize(); }, - componentDidUpdate: function(prevProps, prevState) { - if(this.props.onResize && this._checkForResize(prevProps, prevState)) { - this.props.onResize(); - } - - const size = this._getSize(this.props, this.state); - if (size > 0) { - this.props.onVisible(); - } else { - if (this.hideDebouncer) { - clearTimeout(this.hideDebouncer); - } - this.hideDebouncer = setTimeout(() => { - // temporarily stop hiding the statusbar as per - // https://github.com/vector-im/riot-web/issues/1991#issuecomment-276953915 - // this.props.onHidden(); - }, HIDE_DEBOUNCE_MS); - } + componentDidUpdate: function() { + this._checkSize(); }, componentWillUnmount: function() { @@ -142,33 +128,33 @@ module.exports = React.createClass({ }); }, + // Check whether current size is greater than 0, if yes call props.onVisible + _checkSize: function () { + if (this.props.onVisible && this._getSize()) { + this.props.onVisible(); + } + }, + // We don't need the actual height - just whether it is likely to have // changed - so we use '0' to indicate normal size, and other values to // indicate other sizes. - _getSize: function(props, state) { - if (state.syncState === "ERROR" || - (state.usersTyping.length > 0) || - props.numUnreadMessages || - !props.atEndOfLiveTimeline || - props.hasActiveCall || - props.tabComplete.isTabCompleting() + _getSize: function() { + if (this.state.syncState === "ERROR" || + (this.state.usersTyping.length > 0) || + this.props.numUnreadMessages || + !this.props.atEndOfLiveTimeline || + this.props.hasActiveCall || + this.props.tabComplete.isTabCompleting() ) { return STATUS_BAR_EXPANDED; - } else if (props.tabCompleteEntries) { + } else if (this.props.tabCompleteEntries) { return STATUS_BAR_HIDDEN; - } else if (props.unsentMessageError) { + } else if (this.props.unsentMessageError) { return STATUS_BAR_EXPANDED_LARGE; } return STATUS_BAR_HIDDEN; }, - // determine if we need to call onResize - _checkForResize: function(prevProps, prevState) { - // figure out the old height and the new height of the status bar. - return this._getSize(prevProps, prevState) - !== this._getSize(this.props, this.state); - }, - // return suitable content for the image on the left of the status bar. // // if wantPlaceholder is true, we include a "..." placeholder if From f4dc7ae8b18c0106a9dac1672c9dc393977ae2db Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 28 Mar 2017 10:38:57 +0100 Subject: [PATCH 082/284] Improve zeroing of RoomList notification badges Use an action and force an update when zeroing the number of notifications in a room. This is better than waiting for a `render` to happen at some point. This will hopefully fix https://github.com/vector-im/riot-web/issues/3257 --- src/components/structures/TimelinePanel.js | 7 ++++--- src/components/views/rooms/RoomList.js | 6 ++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 8ef0e7631f..cc14bd7c02 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -476,9 +476,10 @@ var TimelinePanel = React.createClass({ // if we are scrolled to the bottom, do a quick-reset of our unreadNotificationCount // to avoid having to wait from the remote echo from the homeserver. if (this.isAtEndOfLiveTimeline()) { - this.props.timelineSet.room.setUnreadNotificationCount('total', 0); - this.props.timelineSet.room.setUnreadNotificationCount('highlight', 0); - // XXX: i'm a bit surprised we don't have to emit an event or dispatch to get this picked up + dis.dispatch({ + action: 'on_room_read', + room: this.props.timelineSet.room, + }); } var currentReadUpToEventId = this._getCurrentReadReceipt(true); diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 51811f672a..1ec3c152e4 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -96,6 +96,12 @@ module.exports = React.createClass({ }); } break; + case 'on_room_read': + payload.room.setUnreadNotificationCount('total', 0); + payload.room.setUnreadNotificationCount('highlight', 0); + // Force an update because this state is too deep to cause an update + this.forceUpdate(); + break; } }, From 4a0988f83e4ae4f76dea12ac4e6555dd867bb164 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 28 Mar 2017 11:26:40 +0100 Subject: [PATCH 083/284] Do not send the room with action By not sending the room with the action, we prevent its state from being updated by registered views listening for on_room_read --- src/components/structures/TimelinePanel.js | 3 ++- src/components/views/rooms/RoomList.js | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index cc14bd7c02..7fe515b958 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -476,9 +476,10 @@ var TimelinePanel = React.createClass({ // if we are scrolled to the bottom, do a quick-reset of our unreadNotificationCount // to avoid having to wait from the remote echo from the homeserver. if (this.isAtEndOfLiveTimeline()) { + this.props.timelineSet.room.setUnreadNotificationCount('total', 0); + this.props.timelineSet.room.setUnreadNotificationCount('highlight', 0); dis.dispatch({ action: 'on_room_read', - room: this.props.timelineSet.room, }); } diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 1ec3c152e4..59346d5f4d 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -97,9 +97,9 @@ module.exports = React.createClass({ } break; case 'on_room_read': - payload.room.setUnreadNotificationCount('total', 0); - payload.room.setUnreadNotificationCount('highlight', 0); - // Force an update because this state is too deep to cause an update + // Force an update because the notif count state is too deep to cause + // an update. This forces the local echo of reading notifs to be + // reflected by the RoomTiles. this.forceUpdate(); break; } From 98a0b804c7b9bb0eac68fb5d9587a74b818a2737 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 28 Mar 2017 13:02:13 +0100 Subject: [PATCH 084/284] Only join a room when enter is hit when the join button is shown --- src/components/views/elements/DirectorySearchBox.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/DirectorySearchBox.js b/src/components/views/elements/DirectorySearchBox.js index 3ea0d16336..467878caad 100644 --- a/src/components/views/elements/DirectorySearchBox.js +++ b/src/components/views/elements/DirectorySearchBox.js @@ -59,7 +59,7 @@ export default class DirectorySearchBox extends React.Component { } _onKeyUp(ev) { - if (ev.key == 'Enter') { + if (ev.key == 'Enter' && this.props.showJoinButton) { if (this.props.onJoinClick) { this.props.onJoinClick(this.state.value); } From 4587b5f995de63ed2a89aa109650a37ba08920aa Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 28 Mar 2017 15:02:08 +0100 Subject: [PATCH 085/284] Don't re-notify about messages on browser refresh Don't notify unless the js-sdk state is SYNCING, otherwise we'll display notifications for messages that come in during the incremental between the js-sdk cached data and the present. Requires https://github.com/matrix-org/matrix-js-sdk/pull/405 Fixes https://github.com/vector-im/riot-web/issues/3251 --- src/Notifier.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Notifier.js b/src/Notifier.js index 7fc7d3e338..92770877b7 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -98,7 +98,7 @@ var Notifier = { MatrixClientPeg.get().on("Room.receipt", this.boundOnRoomReceipt); MatrixClientPeg.get().on("sync", this.boundOnSyncStateChange); this.toolbarHidden = false; - this.isPrepared = false; + this.isSyncing = false; }, stop: function() { @@ -107,7 +107,7 @@ var Notifier = { MatrixClientPeg.get().removeListener("Room.receipt", this.boundOnRoomReceipt); MatrixClientPeg.get().removeListener('sync', this.boundOnSyncStateChange); } - this.isPrepared = false; + this.isSyncing = false; }, supportsDesktopNotifications: function() { @@ -213,18 +213,18 @@ var Notifier = { }, onSyncStateChange: function(state) { - if (state === "PREPARED" || state === "SYNCING") { - this.isPrepared = true; + if (state === "SYNCING") { + this.isSyncing = true; } else if (state === "STOPPED" || state === "ERROR") { - this.isPrepared = false; + this.isSyncing = false; } }, onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) { if (toStartOfTimeline) return; if (!room) return; - if (!this.isPrepared) return; // don't alert for any messages initially + if (!this.isSyncing) return; // don't alert for any messages initially if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) return; if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return; From 3373f00d90337e0ad83941c13dd70d49be3c1455 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 29 Mar 2017 14:08:31 +0100 Subject: [PATCH 086/284] Only clear the local notification count if needed Only zero the local notification count when we actually send a read receipt, otherwise we cause a re-render of the RoomList every time the user moves the cursor in the window, basically. --- src/components/structures/TimelinePanel.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 7fe515b958..ac4a7cbffc 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -473,16 +473,6 @@ var TimelinePanel = React.createClass({ // we still have a client. if (!MatrixClientPeg.get()) return; - // if we are scrolled to the bottom, do a quick-reset of our unreadNotificationCount - // to avoid having to wait from the remote echo from the homeserver. - if (this.isAtEndOfLiveTimeline()) { - this.props.timelineSet.room.setUnreadNotificationCount('total', 0); - this.props.timelineSet.room.setUnreadNotificationCount('highlight', 0); - dis.dispatch({ - action: 'on_room_read', - }); - } - var currentReadUpToEventId = this._getCurrentReadReceipt(true); var currentReadUpToEventIndex = this._indexForEventId(currentReadUpToEventId); @@ -520,6 +510,14 @@ var TimelinePanel = React.createClass({ // it failed, so allow retries next time the user is active this.last_rr_sent_event_id = undefined; }); + + // do a quick-reset of our unreadNotificationCount to avoid having + // to wait from the remote echo from the homeserver. + this.props.timelineSet.room.setUnreadNotificationCount('total', 0); + this.props.timelineSet.room.setUnreadNotificationCount('highlight', 0); + dis.dispatch({ + action: 'on_room_read', + }); } }, From 90242c2c851454a31ace58d7f0c077aaafba9539 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 29 Mar 2017 14:12:50 +0100 Subject: [PATCH 087/284] Only send local echo RR if we're at the end --- src/components/structures/TimelinePanel.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index ac4a7cbffc..07e534a359 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -513,11 +513,16 @@ var TimelinePanel = React.createClass({ // do a quick-reset of our unreadNotificationCount to avoid having // to wait from the remote echo from the homeserver. - this.props.timelineSet.room.setUnreadNotificationCount('total', 0); - this.props.timelineSet.room.setUnreadNotificationCount('highlight', 0); - dis.dispatch({ - action: 'on_room_read', - }); + // we only do this if we're right at the end, because we're just assuming + // that sending an RR for the latest message will set our notif counter + // to zero: it may not do this if we send an RR for somewhere before the end. + if (this.isAtEndOfLiveTimeline()) { + this.props.timelineSet.room.setUnreadNotificationCount('total', 0); + this.props.timelineSet.room.setUnreadNotificationCount('highlight', 0); + dis.dispatch({ + action: 'on_room_read', + }); + } } }, From 2146e89c0900d9752b58c3b3fb833b8bbcc0b4dc Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 29 Mar 2017 15:02:28 +0100 Subject: [PATCH 088/284] Attempt to fix the flakyness seen with tests Specifically: ``` JS 2.1.1 (Linux 0.0.0) joining a room over federation should not get stuck at a spinner FAILED Did not find exactly one match (found: 0) for componentType:function (props, context, updater) { ``` actually meant that the room directory wasn't displayed - probably because the dispatch `view_room_directory` ended up on another tick of the event loop, meaning that the directory wasn't displayedi. The fix attempted in ths commit is to use `this._setPage` instead to view the directory. This uses `setState` to set the screen to the directory, so I'm not entirely convinced this will solve the problem (as `setState` may also end up doing things on another tick. and ``` JS 2.1.1 (Linux 0.0.0) loading: MatrixClient rehydrated from stored credentials: shows a room view if we followed a room link FAILED MatrixChat still not ready after 5 tries awaitRoomView@/home/travis/build/vector-im/riot-web/test/all-tests.js:201363:90 ``` was happening probably because in the handler for the `sync` event in `MatrixChat` (around line 840), there was one case in which the `ready` state may not be true (causing all 5 attempts to fail), and this case relied on `starting_room_alias_payload`. This `starting_room_alias_payload` is now redundant because of `initialScreenAfterLogin`, which was added recently. --- src/components/structures/MatrixChat.js | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 8ae38b9f3c..be842c6598 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -845,10 +845,7 @@ module.exports = React.createClass({ if (state !== "PREPARED") { return; } self.sdkReady = true; - if (self.starting_room_alias_payload) { - dis.dispatch(self.starting_room_alias_payload); - delete self.starting_room_alias_payload; - } else if (!self.state.page_type) { + if (!self.state.page_type) { if (!self.state.currentRoomId) { var firstRoom = null; if (cli.getRooms() && cli.getRooms().length) { @@ -976,12 +973,7 @@ module.exports = React.createClass({ // we can't view a room unless we're logged in // (a guest account is fine) - if (!this.state.loggedIn) { - // we may still be loading (ie, trying to register a guest - // session); otherwise we're (probably) already showing a login - // screen. Either way, we'll show the room once the client starts. - this.starting_room_alias_payload = payload; - } else { + if (this.state.loggedIn) { dis.dispatch(payload); } } else if (screen.indexOf('user/') == 0) { From e139f5212a98be8e2a318da12e75c32042f10af9 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 29 Mar 2017 15:05:49 +0100 Subject: [PATCH 089/284] Remove React warning Fire onOptionChange in componentWillMount, otherwise end up trying to update state effectively in a render method, which is bad. --- src/components/views/login/CountryDropdown.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/login/CountryDropdown.js b/src/components/views/login/CountryDropdown.js index fc1e89661b..9729c9e23f 100644 --- a/src/components/views/login/CountryDropdown.js +++ b/src/components/views/login/CountryDropdown.js @@ -43,8 +43,10 @@ export default class CountryDropdown extends React.Component { this.state = { searchQuery: '', } + } - if (!props.value) { + componentWillMount() { + if (!this.props.value) { // If no value is given, we start with the first // country selected, but our parent component // doesn't know this, therefore we do this. From cee2628b4139205ea983e9f4921b06570ff36c61 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 29 Mar 2017 15:24:47 +0100 Subject: [PATCH 090/284] Use Login & Register via component interface Login & Register were being imprted directly for some reason, rather than going via the normal component interface. Should be functionally identical. --- src/components/structures/MatrixChat.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 8ae38b9f3c..d336fcf50d 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -29,10 +29,6 @@ var UserActivity = require("../../UserActivity"); var Presence = require("../../Presence"); var dis = require("../../dispatcher"); -var Login = require("./login/Login"); -var Registration = require("./login/Registration"); -var PostRegistration = require("./login/PostRegistration"); - var Modal = require("../../Modal"); var Tinter = require("../../Tinter"); var sdk = require('../../index'); @@ -1159,15 +1155,12 @@ module.exports = React.createClass({ }, render: function() { - var ForgotPassword = sdk.getComponent('structures.login.ForgotPassword'); - var LoggedInView = sdk.getComponent('structures.LoggedInView'); - // `loading` might be set to false before `loggedIn = true`, causing the default // (``) to be visible for a few MS (say, whilst a request is in-flight to // the RTS). So in the meantime, use `loggingIn`, which is true between // actions `on_logging_in` and `on_logged_in`. if (this.state.loading || this.state.loggingIn) { - var Spinner = sdk.getComponent('elements.Spinner'); + const Spinner = sdk.getComponent('elements.Spinner'); return (
@@ -1176,6 +1169,7 @@ module.exports = React.createClass({ } // needs to be before normal PageTypes as you are logged in technically else if (this.state.screen == 'post_registration') { + const PostRegistration = sdk.getComponent('structures.login.PostRegistration'); return ( @@ -1185,6 +1179,7 @@ module.exports = React.createClass({ * we should go through and figure out what we actually need to pass down, as well * as using something like redux to avoid having a billion bits of state kicking around. */ + const LoggedInView = sdk.getComponent('structures.LoggedInView'); return ( @@ -1207,6 +1202,7 @@ module.exports = React.createClass({
); } else if (this.state.screen == 'register') { + const Registration = sdk.getComponent('structures.login.Registration'); return ( ); } else if (this.state.screen == 'forgot_password') { + const ForgotPassword = sdk.getComponent('structures.login.ForgotPassword'); return ( ); } else { + const Login = sdk.getComponent('structures.login.Login'); var r = ( Date: Wed, 29 Mar 2017 16:23:18 +0100 Subject: [PATCH 091/284] Remove non-functional session load error MatrixChat was trying to display an error if the session failed to restore, but it was never actually being shown because it was just set as a member variable and therefore never actually caused a re-render for the error to be displayed. Almost all errors are caught by _restoreFromLocalStorage which displays the fancy dialog if your session can't be restored, so I'm not convinced this ever even tried to do anything anyway. Remove it. --- src/components/structures/MatrixChat.js | 14 +------------- src/components/structures/login/Login.js | 4 +--- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index d336fcf50d..df3497291e 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -194,9 +194,6 @@ module.exports = React.createClass({ componentWillMount: function() { SdkConfig.put(this.props.config); - // if the automatic session load failed, the error - this.sessionLoadError = null; - if (this.props.config.sync_timeline_limit) { MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit; } @@ -285,7 +282,6 @@ module.exports = React.createClass({ }); }).catch((e) => { console.error("Unable to load session", e); - this.sessionLoadError = e.message; }).done(()=>{ // stuff this through the dispatcher so that it happens // after the on_logged_in action. @@ -1240,7 +1236,7 @@ module.exports = React.createClass({ ); } else { const Login = sdk.getComponent('structures.login.Login'); - var r = ( + return ( ); - - // we only want to show the session load error the first time the - // Login component is rendered. This is pretty hacky but I can't - // think of another way to achieve it. - this.sessionLoadError = null; - - return r; } } }); diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index 0a1549f75b..7e1a5f9d35 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -53,14 +53,12 @@ module.exports = React.createClass({ // login shouldn't care how password recovery is done. onForgotPasswordClick: React.PropTypes.func, onCancelClick: React.PropTypes.func, - - initialErrorText: React.PropTypes.string, }, getInitialState: function() { return { busy: false, - errorText: this.props.initialErrorText, + errorText: null, loginIncorrect: false, enteredHomeserverUrl: this.props.customHsUrl || this.props.defaultHsUrl, enteredIdentityServerUrl: this.props.customIsUrl || this.props.defaultIsUrl, From fff83ba23456e7c72b2d0c8b2ba5e3465802da6a Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 30 Mar 2017 17:18:22 +0100 Subject: [PATCH 092/284] Fix the onFinished for timeline pos dialog This was causing a blank RoomView because it was trying to work with `room_id = undefined`. --- src/components/structures/MatrixChat.js | 10 ---------- src/components/structures/TimelinePanel.js | 2 +- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index a4cbb1ac0d..50e93a89d9 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -648,16 +648,6 @@ module.exports = React.createClass({ if (room) { var theAlias = Rooms.getDisplayAliasForRoom(room); if (theAlias) presentedId = theAlias; - - // No need to do this given RoomView triggers it itself... - // var color_scheme_event = room.getAccountData("org.matrix.room.color_scheme"); - // var color_scheme = {}; - // if (color_scheme_event) { - // color_scheme = color_scheme_event.getContent(); - // // XXX: we should validate the event - // } - // console.log("Tinter.tint from _viewRoom"); - // Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color); } if (room_info.event_id) { diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 07e534a359..ffeb5b9a1f 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -819,7 +819,7 @@ var TimelinePanel = React.createClass({ // go via the dispatcher so that the URL is updated dis.dispatch({ action: 'view_room', - room_id: this.props.timelineSet.roomId, + room_id: this.props.timelineSet.room.roomId, }); }; } From 8e5a83a0568316020450d6f738801918cb2e55e3 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 30 Mar 2017 18:02:33 +0100 Subject: [PATCH 093/284] Reduce number of unpaginated events by 1 When unpaginating in the backwards direction --- src/components/structures/TimelinePanel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index ffeb5b9a1f..0a7a2c90b7 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -263,7 +263,7 @@ var TimelinePanel = React.createClass({ } ); - let count = backwards ? marker + 1 : this.state.events.length - marker; + let count = backwards ? marker : this.state.events.length - marker; if (count > 0) { debuglog("TimelinePanel: Unpaginating", count, "in direction", dir); From 9c4614e7ffd849ae989142ea5619426251e8b57b Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 31 Mar 2017 14:55:33 +0100 Subject: [PATCH 094/284] Update for new IndexedDBStore interface --- src/MatrixClientPeg.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index baa3293073..d92973ba7a 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -125,12 +125,11 @@ class MatrixClientPeg { // FIXME: bodge to remove old database. Remove this after a few weeks. window.indexedDB.deleteDatabase("matrix-js-sdk:default"); - opts.store = new Matrix.IndexedDBStore( - new Matrix.IndexedDBStoreBackend(window.indexedDB, "riot-web-sync"), - new Matrix.SyncAccumulator(), { - localStorage: localStorage, - } - ); + opts.store = new Matrix.IndexedDBStore({ + indexedDB: window.indexedDB, + dbName: "riot-web-sync", + localStorage: localStorage, + }); } this.matrixClient = Matrix.createClient(opts); From 3ff54b8e4bf8922a656c57b2f7c4fd4e2cd1b506 Mon Sep 17 00:00:00 2001 From: Shell Turner Date: Sun, 2 Apr 2017 11:19:50 +0100 Subject: [PATCH 095/284] Add
    to allowed attributes list Fixes vector-im/riot-web#3273 Signed-off-by: Shell Turner --- src/HtmlUtils.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index c7b13bc071..ad91d2f61f 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -96,6 +96,7 @@ var sanitizeHtmlParams = { // We don't currently allow img itself by default, but this // would make sense if we did img: ['src'], + ol: ['start'], }, // Lots of these won't come up by default because we don't allow them selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta'], From 94fe9999db265589cc5dfeb0e7e7723e220c7abc Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 4 Apr 2017 11:55:53 +0100 Subject: [PATCH 096/284] Reimplement _saveScrollState The actual fix to https://github.com/vector-im/riot-web/issues/3175 is this change to `_saveScrollState`, which is to pick the trackedScrollToken based on which node is intersected by the bottom of the scroll panel. This is opposed to the previous logic that picked based on which node was the first from the bottom to be above the bottom of the viewport. In the case where the viewport bottom does not intersect any events, the topmost event is used. --- src/components/structures/ScrollPanel.js | 31 +++++++++++++++------- src/components/structures/TimelinePanel.js | 2 +- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index 44176f73af..7460d6dac8 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -589,24 +589,35 @@ module.exports = React.createClass({ var itemlist = this.refs.itemlist; var wrapperRect = ReactDOM.findDOMNode(this).getBoundingClientRect(); var messages = itemlist.children; + let newScrollState = null; for (var i = messages.length-1; i >= 0; --i) { var node = messages[i]; if (!node.dataset.scrollToken) continue; var boundingRect = node.getBoundingClientRect(); - if (boundingRect.bottom < wrapperRect.bottom) { - this.scrollState = { - stuckAtBottom: false, - trackedScrollToken: node.dataset.scrollToken, - pixelOffset: wrapperRect.bottom - boundingRect.bottom, - }; - debuglog("ScrollPanel: saved scroll state", this.scrollState); - return; + newScrollState = { + stuckAtBottom: false, + trackedScrollToken: node.dataset.scrollToken, + pixelOffset: wrapperRect.bottom - boundingRect.bottom, + }; + // If the bottom of the panel intersects the ClientRect of node, use this node + // as the scrollToken. + // If this is false for the entire for-loop, we default to the last node + // (which is why newScrollState is set on every iteration). + if (boundingRect.top < wrapperRect.bottom && + wrapperRect.bottom < boundingRect.bottom) { + // Use this node as the scrollToken + break; } } - - debuglog("ScrollPanel: unable to save scroll state: found no children in the viewport"); + // This is only false if there were no nodes with `node.dataset.scrollToken` set. + if (newScrollState) { + this.scrollState = newScrollState; + debuglog("ScrollPanel: saved scroll state", this.scrollState); + } else { + debuglog("ScrollPanel: unable to save scroll state: found no children in the viewport"); + } }, _restoreSavedScrollState: function() { diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 0a7a2c90b7..ffeb5b9a1f 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -263,7 +263,7 @@ var TimelinePanel = React.createClass({ } ); - let count = backwards ? marker : this.state.events.length - marker; + let count = backwards ? marker + 1 : this.state.events.length - marker; if (count > 0) { debuglog("TimelinePanel: Unpaginating", count, "in direction", dir); From 573799495765c50a64bc69dd74e1911e67d5e059 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 4 Apr 2017 13:28:26 +0100 Subject: [PATCH 097/284] Clarify and simplfiy unpagination logic --- src/components/structures/ScrollPanel.js | 40 +++++++++------------- src/components/structures/TimelinePanel.js | 4 ++- 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index 7460d6dac8..cff6a86b2c 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -333,34 +333,28 @@ module.exports = React.createClass({ if (excessHeight <= 0) { return; } - var itemlist = this.refs.itemlist; - var tiles = itemlist.children; + const tiles = this.refs.itemlist.children; // The scroll token of the first/last tile to be unpaginated let markerScrollToken = null; - // Subtract clientHeights to simulate the events being unpaginated whilst counting - // the events to be unpaginated. - if (backwards) { - // Iterate forwards from start of tiles, subtracting event tile height - let i = 0; - while (i < tiles.length && excessHeight > tiles[i].clientHeight) { - excessHeight -= tiles[i].clientHeight; - if (tiles[i].dataset.scrollToken) { - markerScrollToken = tiles[i].dataset.scrollToken; - } - i++; - } - } else { - // Iterate backwards from end of tiles, subtracting event tile height - let i = tiles.length - 1; - while (i > 0 && excessHeight > tiles[i].clientHeight) { - excessHeight -= tiles[i].clientHeight; - if (tiles[i].dataset.scrollToken) { - markerScrollToken = tiles[i].dataset.scrollToken; - } - i--; + // Subtract heights of tiles to simulate the tiles being unpaginated until the + // excess height is less than the height of the next tile to subtract. This + // prevents excessHeight becoming negative, which could lead to future + // pagination. + // + // If backwards is true, we unpaginate (remove) tiles from the back (top). + let i = backwards ? 0 : tiles.length - 1; + while ( + (backwards ? i < tiles.length : i > 0) && excessHeight > tiles[i].clientHeight + ) { + // Subtract height of tile as if it were unpaginated + excessHeight -= tiles[i].clientHeight; + // The tile may not have a scroll token, so guard it + if (tiles[i].dataset.scrollToken) { + markerScrollToken = tiles[i].dataset.scrollToken; } + i += backwards ? 1 : -1; } if (markerScrollToken) { diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index ffeb5b9a1f..fe4a2f46fa 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -251,10 +251,12 @@ var TimelinePanel = React.createClass({ }, onMessageListUnfillRequest: function(backwards, scrollToken) { + // If backwards, unpaginate from the back let dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS; debuglog("TimelinePanel: unpaginating events in direction", dir); - // All tiles are inserted by MessagePanel to have a scrollToken === eventId + // All tiles are inserted by MessagePanel to have a scrollToken === eventId, and + // this particular event should be the first or last to be unpaginated. let eventId = scrollToken; let marker = this.state.events.findIndex( From 106ce90916e8bd4c74af6bef0b179898043cac0b Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 4 Apr 2017 16:31:28 +0100 Subject: [PATCH 098/284] Change "Unread messages." to "Jump to first unread message." Also get rid of the "up" arrow so as not to indiciate direction. This is important because in future the RM will not be based on what has been paginated into the client (but instead RM will be handled server-side) and thus we cannot assert any kind of direction on it relative to the events in the viewport. --- src/components/views/rooms/TopUnreadMessagesBar.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/components/views/rooms/TopUnreadMessagesBar.js b/src/components/views/rooms/TopUnreadMessagesBar.js index 82149c150b..5bef8c0b0a 100644 --- a/src/components/views/rooms/TopUnreadMessagesBar.js +++ b/src/components/views/rooms/TopUnreadMessagesBar.js @@ -32,10 +32,7 @@ module.exports = React.createClass({
    - Scroll to unread messages - Unread messages. Mark all read + Jump to first unread message. Mark all read
    Date: Wed, 5 Apr 2017 17:48:24 +0100 Subject: [PATCH 099/284] Simplify simulated unfill --- src/components/structures/ScrollPanel.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index cff6a86b2c..3575d69b3f 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -344,17 +344,17 @@ module.exports = React.createClass({ // pagination. // // If backwards is true, we unpaginate (remove) tiles from the back (top). - let i = backwards ? 0 : tiles.length - 1; - while ( - (backwards ? i < tiles.length : i > 0) && excessHeight > tiles[i].clientHeight - ) { + for (let i = 0; i < tiles.length; i++) { + const tile = tiles[backwards ? tiles.length - 1 - i : i]; // Subtract height of tile as if it were unpaginated - excessHeight -= tiles[i].clientHeight; + excessHeight -= tile.clientHeight; // The tile may not have a scroll token, so guard it - if (tiles[i].dataset.scrollToken) { - markerScrollToken = tiles[i].dataset.scrollToken; + if (tile.dataset.scrollToken) { + markerScrollToken = tile.dataset.scrollToken; + } + if (tile.clientHeight > excessHeight) { + break; } - i += backwards ? 1 : -1; } if (markerScrollToken) { From 423babdb176d953989d485e89604c6da79303c12 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 5 Apr 2017 17:51:07 +0100 Subject: [PATCH 100/284] Remove fairly redundant condition Making sure that a node is intersected by the bottom of the wrapper is a bit overkill, given that we iterate from the bottom. This also prevents the scenario of having no nodes that are not precisely intersected, but possibly straddling the bottom of the wrapper. --- src/components/structures/ScrollPanel.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index 3575d69b3f..63725b8b86 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -599,8 +599,7 @@ module.exports = React.createClass({ // as the scrollToken. // If this is false for the entire for-loop, we default to the last node // (which is why newScrollState is set on every iteration). - if (boundingRect.top < wrapperRect.bottom && - wrapperRect.bottom < boundingRect.bottom) { + if (boundingRect.top < wrapperRect.bottom) { // Use this node as the scrollToken break; } From b0a04e6f007f516168eec33093acb29c947a3408 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 5 Apr 2017 17:52:05 +0100 Subject: [PATCH 101/284] Clarify comment --- src/components/structures/TimelinePanel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index fe4a2f46fa..8cd820c284 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -251,7 +251,7 @@ var TimelinePanel = React.createClass({ }, onMessageListUnfillRequest: function(backwards, scrollToken) { - // If backwards, unpaginate from the back + // If backwards, unpaginate from the back (i.e. the start of the timeline) let dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS; debuglog("TimelinePanel: unpaginating events in direction", dir); From b78f6544761d632be58862356beedccfd89266e9 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 6 Apr 2017 11:13:39 +0100 Subject: [PATCH 102/284] Add support for using indexeddb in a webworker --- src/MatrixClientPeg.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index d92973ba7a..452b67c4ee 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -50,6 +50,18 @@ class MatrixClientPeg { this.opts = { initialSyncLimit: 20, }; + this.indexedDbWorkerScript = null; + } + + /** + * Sets the script href passed to the IndexedDB web worker + * If set, a separate web worker will be started to run the IndexedDB + * queries on. + * + * @param {string} script href to the script to be passed to the web worker + */ + setIndexedDbWorkerScript(script) { + this.indexedDbWorkerScript = script; } get(): MatrixClient { @@ -129,6 +141,7 @@ class MatrixClientPeg { indexedDB: window.indexedDB, dbName: "riot-web-sync", localStorage: localStorage, + workerScript: this.indexedDbWorkerScript, }); } From 95b40a976c255b94af63e576b124382b114398f6 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 6 Apr 2017 11:30:47 +0100 Subject: [PATCH 103/284] Replace sdkReady with firstSyncPromise, add mx_last_room_id - Create a promise that will serve as a lock to be blocked on by things that need to wait for the first sync before accessing state. - Use this promise to block `view_room` calls until a sync has occured instead of just dropping them silently if the sync hasn't happened yet. - Store the current room ID in a localStorage item `mx_last_room_id` when `view_room` fires. This persists the last viewed room ID so that it can be restored on refresh, browser quit. This replaces the previous logic which set the room following a sync based on the most recent unread room. --- src/components/structures/MatrixChat.js | 80 +++++++++++++------------ 1 file changed, 41 insertions(+), 39 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 50e93a89d9..056024f24b 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -194,6 +194,10 @@ module.exports = React.createClass({ componentWillMount: function() { SdkConfig.put(this.props.config); + // Used by _viewRoom before getting state from sync + this.firstSyncComplete = false; + this.firstSyncPromise = q.defer(); + if (this.props.config.sync_timeline_limit) { MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit; } @@ -637,26 +641,38 @@ module.exports = React.createClass({ } } - if (this.sdkReady) { - // if the SDK is not ready yet, remember what room - // we're supposed to be on but don't notify about - // the new screen yet (we won't be showing it yet) - // The normal case where this happens is navigating - // to the room in the URL bar on page load. - var presentedId = room_info.room_alias || room_info.room_id; - var room = MatrixClientPeg.get().getRoom(room_info.room_id); + // Wait for the first sync to complete so that if a room does have an alias, + // it would have been retrieved. + let waitFor = q(null); + if (!firstSyncComplete) { + if (!this.firstSyncPromise) { + console.warn('Cannot view a room before first sync. room_id:', room_info.room_id); + return; + } + waitFor = this.firstSyncPromise.promise; + } + + waitFor.done(() => { + let presentedId = room_info.room_alias || room_info.room_id; + const room = MatrixClientPeg.get().getRoom(room_info.room_id); if (room) { - var theAlias = Rooms.getDisplayAliasForRoom(room); + const theAlias = Rooms.getDisplayAliasForRoom(room); if (theAlias) presentedId = theAlias; + + // Store this as the ID of the last room accessed. This is so that we can + // persist which room is being stored across refreshes and browser quits. + if (localStorage) { + localStorage.setItem('mx_last_room_id', room.roomId); + } } if (room_info.event_id) { - presentedId += "/"+room_info.event_id; + presentedId += "/" + room_info.event_id; } - this.notifyNewScreen('room/'+presentedId); + this.notifyNewScreen('room/' + presentedId); newState.ready = true; - } - this.setState(newState); + this.setState(newState); + }); }, _createChat: function() { @@ -683,7 +699,7 @@ module.exports = React.createClass({ this.props.onLoadCompleted(); this.setState({loading: false}); - // Show screens (like 'register') that need to be shown without onLoggedIn + // Show screens (like 'register') that need to be shown without _onLoggedIn // being called. 'register' needs to be routed here when the email confirmation // link is clicked on. if (this.state.screenAfterLogin && @@ -766,6 +782,12 @@ module.exports = React.createClass({ ); this.notifyNewScreen(this.state.screenAfterLogin.screen); this.setState({screenAfterLogin: null}); + } else if (localStorage && localStorage.getItem('mx_last_room_id')) { + // Before defaulting to directory, show the last viewed room + dis.dispatch({ + action: 'view_room', + room_id: localStorage.getItem('mx_last_room_id'), + }); } else { dis.dispatch({action: 'view_room_directory'}); } @@ -825,33 +847,13 @@ module.exports = React.createClass({ } console.log("MatrixClient sync state => %s", state); if (state !== "PREPARED") { return; } - self.sdkReady = true; + + self.firstSyncComplete = true; + self.firstSyncPromise.resolve(); if (!self.state.page_type) { - if (!self.state.currentRoomId) { - var firstRoom = null; - if (cli.getRooms() && cli.getRooms().length) { - firstRoom = RoomListSorter.mostRecentActivityFirst( - cli.getRooms() - )[0].roomId; - self.setState({ready: true, currentRoomId: firstRoom, page_type: PageTypes.RoomView}); - } - } else { - self.setState({ready: true, page_type: PageTypes.RoomView}); - } - - // we notifyNewScreen now because now the room will actually be displayed, - // and (mostly) now we can get the correct alias. - var presentedId = self.state.currentRoomId; - var room = MatrixClientPeg.get().getRoom(self.state.currentRoomId); - if (room) { - var theAlias = Rooms.getDisplayAliasForRoom(room); - if (theAlias) presentedId = theAlias; - } - - if (presentedId != undefined) { - self.notifyNewScreen('room/'+presentedId); - } + // Switch to room view but allow _onLoggedIn to specify a room (if any) + self.setState({ready: true}); dis.dispatch({action: 'focus_composer'}); } else { self.setState({ready: true}); From 5f8a7b46a818a089d124ae67cb0a95c8302eeb46 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 6 Apr 2017 11:44:15 +0100 Subject: [PATCH 104/284] Remove redundant setState call, always focus composer after sync --- src/components/structures/MatrixChat.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 056024f24b..7fe3b1ee38 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -644,7 +644,7 @@ module.exports = React.createClass({ // Wait for the first sync to complete so that if a room does have an alias, // it would have been retrieved. let waitFor = q(null); - if (!firstSyncComplete) { + if (!this.firstSyncComplete) { if (!this.firstSyncPromise) { console.warn('Cannot view a room before first sync. room_id:', room_info.room_id); return; @@ -851,13 +851,8 @@ module.exports = React.createClass({ self.firstSyncComplete = true; self.firstSyncPromise.resolve(); - if (!self.state.page_type) { - // Switch to room view but allow _onLoggedIn to specify a room (if any) - self.setState({ready: true}); - dis.dispatch({action: 'focus_composer'}); - } else { - self.setState({ready: true}); - } + dis.dispatch({action: 'focus_composer'}); + self.setState({ready: true}); }); cli.on('Call.incoming', function(call) { dis.dispatch({ From 6c09a08a7d26f2bff4adecd1ea844f7f89570865 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 6 Apr 2017 14:08:59 +0100 Subject: [PATCH 105/284] Click emote sender -> insert display name into composer Also, fix the imports TextualBody. --- src/components/views/messages/TextualBody.js | 42 ++++++++++++++------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index a625e63062..9aebb30f54 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -16,17 +16,18 @@ limitations under the License. 'use strict'; -var React = require('react'); -var ReactDOM = require('react-dom'); -var highlight = require('highlight.js'); -var HtmlUtils = require('../../../HtmlUtils'); -var linkify = require('linkifyjs'); -var linkifyElement = require('linkifyjs/element'); -var linkifyMatrix = require('../../../linkify-matrix'); -var sdk = require('../../../index'); -var ScalarAuthClient = require("../../../ScalarAuthClient"); -var Modal = require("../../../Modal"); -var SdkConfig = require('../../../SdkConfig'); +import React from 'react'; +import ReactDOM from 'react-dom'; +import highlight from 'highlight.js'; +import * as HtmlUtils from '../../../HtmlUtils'; +import * as linkify from 'linkifyjs'; +import linkifyElement from 'linkifyjs/element'; +import linkifyMatrix from '../../../linkify-matrix'; +import sdk from '../../../index'; +import ScalarAuthClient from '../../../ScalarAuthClient'; +import Modal from '../../../Modal'; +import SdkConfig from '../../../SdkConfig'; +import dis from '../../../dispatcher'; linkifyMatrix(linkify); @@ -187,6 +188,15 @@ module.exports = React.createClass({ this.forceUpdate(); }, + onEmoteSenderClick: function(event) { + const mxEvent = this.props.mxEvent; + const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender(); + dis.dispatch({ + action: 'insert_displayname', + displayname: name.replace(' (IRC)', ''), + }); + }, + getEventTileOps: function() { var self = this; return { @@ -273,7 +283,15 @@ module.exports = React.createClass({ const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender(); return ( - * {name} { body } + *  + + {name} + +   + { body } { widgets } ); From 6010350ce5d52b946217c6314a69a87fddef2d4b Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 6 Apr 2017 17:02:35 +0100 Subject: [PATCH 106/284] Implement power-level changes in timeline Fixes https://github.com/vector-im/riot-web/issues/266 --- src/TextForEvent.js | 55 +++++++++++++++++++++++++ src/components/views/rooms/EventTile.js | 1 + 2 files changed, 56 insertions(+) diff --git a/src/TextForEvent.js b/src/TextForEvent.js index 3e1659f392..2560264346 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -17,6 +17,13 @@ limitations under the License. var MatrixClientPeg = require("./MatrixClientPeg"); var CallHandler = require("./CallHandler"); +const roles = { + undefined: 'Default', + 0: 'User', + 50: 'Moderator', + 100: 'Admin', +}; + function textForMemberEvent(ev) { // XXX: SYJS-16 "sender is sometimes null for join messages" var senderName = ev.sender ? ev.sender.name : ev.getSender(); @@ -182,6 +189,53 @@ function textForEncryptionEvent(event) { return senderName + " turned on end-to-end encryption (algorithm " + event.getContent().algorithm + ")"; } +function formatPowerLevel(level, roles, userDefault) { + if (roles[level]) { + return roles[level] + (level !== undefined ? ` (${level})` : ` (${userDefault})`); + } else { + return level; + } +} + +// Currently will only display a change if a user's power level is changed +function textForPowerEvent(event) { + const senderName = event.sender ? event.sender.name : event.getSender(); + if (!event.getPrevContent() || !event.getPrevContent().users) { + return ''; + } + const userDefault = event.getContent().users_default || 0; + // Construct set of userIds + let users = []; + Object.keys(event.getContent().users).forEach( + (userId) => { + if (users.indexOf(userId) === -1) users.push(userId); + } + ); + Object.keys(event.getPrevContent().users).forEach( + (userId) => { + if (users.indexOf(userId) === -1) users.push(userId); + } + ); + let diff = []; + users.forEach((userId) => { + // Previous power level + const from = event.getPrevContent().users[userId]; + // Current power level + const to = event.getContent().users[userId]; + if (to !== from) { + diff.push( + userId + + ' from ' + formatPowerLevel(from, roles, userDefault) + + ' to ' + formatPowerLevel(to, roles, userDefault) + ); + } + }); + if (!diff.length) { + return ''; + } + return senderName + ' changed the power level of ' + diff.join(', '); +} + var handlers = { 'm.room.message': textForMessageEvent, 'm.room.name': textForRoomNameEvent, @@ -193,6 +247,7 @@ var handlers = { 'm.room.third_party_invite': textForThreePidInviteEvent, 'm.room.history_visibility': textForHistoryVisibilityEvent, 'm.room.encryption': textForEncryptionEvent, + 'm.room.power_levels': textForPowerEvent, }; module.exports = { diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 8b8e52ae83..9df0499eb2 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -40,6 +40,7 @@ var eventTileTypes = { 'm.room.third_party_invite' : 'messages.TextualEvent', 'm.room.history_visibility' : 'messages.TextualEvent', 'm.room.encryption' : 'messages.TextualEvent', + 'm.room.power_levels' : 'messages.TextualEvent', }; var MAX_READ_AVATARS = 5; From a815788af88bdfc2d4aa0770fc4ca2787506be28 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 6 Apr 2017 17:10:32 +0100 Subject: [PATCH 107/284] Fix issue where teamTokenMap was ignored for guests This was an issue because guests do not log in with a teamToken, it is implicitly set by MatrixChat when it mounts. The fix is to view_home_page when a login occurs and MatrixChat has this._teamToken set. --- src/components/structures/MatrixChat.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 7fe3b1ee38..b449ff3094 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -763,9 +763,11 @@ module.exports = React.createClass({ }); if (teamToken) { + // A team member has logged in, not a guest this._teamToken = teamToken; dis.dispatch({action: 'view_home_page'}); } else if (this._is_registered) { + // The user has just logged in after registering dis.dispatch({action: 'view_user_settings'}); } else { this._showScreenAfterLogin(); @@ -788,6 +790,10 @@ module.exports = React.createClass({ action: 'view_room', room_id: localStorage.getItem('mx_last_room_id'), }); + } else if (this._teamToken) { + // Team token might be set if we're a guest. + // Guests do not call _onLoggedIn with a teamToken + dis.dispatch({action: 'view_home_page'}); } else { dis.dispatch({action: 'view_room_directory'}); } From d218f90cde33a32e778e4cd87b74afc8a6e03916 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 7 Apr 2017 11:31:52 +0100 Subject: [PATCH 108/284] Fix infinite pagination/glitches with pagination I think this was being caused by a bug introduced in 47f29b that meant that `backwards` was actually being used as `forwards`. --- src/components/structures/ScrollPanel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index 63725b8b86..83bec03e9e 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -345,7 +345,7 @@ module.exports = React.createClass({ // // If backwards is true, we unpaginate (remove) tiles from the back (top). for (let i = 0; i < tiles.length; i++) { - const tile = tiles[backwards ? tiles.length - 1 - i : i]; + const tile = tiles[backwards ? i : tiles.length - 1 - i]; // Subtract height of tile as if it were unpaginated excessHeight -= tile.clientHeight; // The tile may not have a scroll token, so guard it From 2285239428a224188a3dd9fd556b52e51011edfb Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 7 Apr 2017 17:02:47 +0100 Subject: [PATCH 109/284] js-sdk prerelease --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a07e2236aa..0457fc4764 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "isomorphic-fetch": "^2.2.1", "linkifyjs": "^2.1.3", "lodash": "^4.13.1", - "matrix-js-sdk": "matrix-org/matrix-js-sdk#develop", + "matrix-js-sdk": "0.7.6-rc.1", "optimist": "^0.6.1", "q": "^1.4.1", "react": "^15.4.0", From 76ec3edb8368dc2e1f7f73d69b25df157ef2cbae Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 7 Apr 2017 17:04:03 +0100 Subject: [PATCH 110/284] Prepare changelog for v0.8.7-rc.1 --- CHANGELOG.md | 217 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 488a9814e6..20ee43ff2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,220 @@ +Changes in [0.8.7-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.7-rc.1) (2017-04-07) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.6...v0.8.7-rc.1) + + * Add support for using indexeddb in a webworker + [\#792](https://github.com/matrix-org/matrix-react-sdk/pull/792) + * Fix infinite pagination/glitches with pagination + [\#795](https://github.com/matrix-org/matrix-react-sdk/pull/795) + * Fix issue where teamTokenMap was ignored for guests + [\#793](https://github.com/matrix-org/matrix-react-sdk/pull/793) + * Click emote sender -> insert display name into composer + [\#791](https://github.com/matrix-org/matrix-react-sdk/pull/791) + * Fix scroll token selection logic + [\#785](https://github.com/matrix-org/matrix-react-sdk/pull/785) + * Replace sdkReady with firstSyncPromise, add mx_last_room_id + [\#790](https://github.com/matrix-org/matrix-react-sdk/pull/790) + * Change "Unread messages." to "Jump to first unread message." + [\#789](https://github.com/matrix-org/matrix-react-sdk/pull/789) + * Update for new IndexedDBStore interface + [\#786](https://github.com/matrix-org/matrix-react-sdk/pull/786) + * Add
      to allowed attributes list + [\#787](https://github.com/matrix-org/matrix-react-sdk/pull/787) + * Fix the onFinished for timeline pos dialog + [\#784](https://github.com/matrix-org/matrix-react-sdk/pull/784) + * Only join a room when enter is hit if the join button is shown + [\#776](https://github.com/matrix-org/matrix-react-sdk/pull/776) + * Remove non-functional session load error + [\#783](https://github.com/matrix-org/matrix-react-sdk/pull/783) + * Use Login & Register via component interface + [\#782](https://github.com/matrix-org/matrix-react-sdk/pull/782) + * Attempt to fix the flakyness seen with tests + [\#781](https://github.com/matrix-org/matrix-react-sdk/pull/781) + * Remove React warning + [\#780](https://github.com/matrix-org/matrix-react-sdk/pull/780) + * Only clear the local notification count if needed + [\#779](https://github.com/matrix-org/matrix-react-sdk/pull/779) + * Don't re-notify about messages on browser refresh + [\#777](https://github.com/matrix-org/matrix-react-sdk/pull/777) + * Improve zeroing of RoomList notification badges + [\#775](https://github.com/matrix-org/matrix-react-sdk/pull/775) + * Fix VOIP bar hidden on first render of RoomStatusBar + [\#774](https://github.com/matrix-org/matrix-react-sdk/pull/774) + * Correct confirm prompt for disinvite + [\#772](https://github.com/matrix-org/matrix-react-sdk/pull/772) + * Add state loggingIn to MatrixChat to fix flashing login + [\#773](https://github.com/matrix-org/matrix-react-sdk/pull/773) + * Fix bug where you can't invite a valid address + [\#771](https://github.com/matrix-org/matrix-react-sdk/pull/771) + * Fix people section DropTarget and refactor Rooms + [\#761](https://github.com/matrix-org/matrix-react-sdk/pull/761) + * Read Receipt offset + [\#770](https://github.com/matrix-org/matrix-react-sdk/pull/770) + * Support adding phone numbers in UserSettings + [\#756](https://github.com/matrix-org/matrix-react-sdk/pull/756) + * Prevent crash on login of no guest session + [\#769](https://github.com/matrix-org/matrix-react-sdk/pull/769) + * Add canResetTimeline callback and thread it through to TimelinePanel + [\#768](https://github.com/matrix-org/matrix-react-sdk/pull/768) + * Show spinner whilst processing recaptcha response + [\#767](https://github.com/matrix-org/matrix-react-sdk/pull/767) + * Login / registration with phone number, mark 2 + [\#750](https://github.com/matrix-org/matrix-react-sdk/pull/750) + * Display threepids slightly prettier + [\#758](https://github.com/matrix-org/matrix-react-sdk/pull/758) + * Fix extraneous leading space in sent emotes + [\#764](https://github.com/matrix-org/matrix-react-sdk/pull/764) + * Add ConfirmRedactDialog component + [\#763](https://github.com/matrix-org/matrix-react-sdk/pull/763) + * Fix password UI auth test + [\#760](https://github.com/matrix-org/matrix-react-sdk/pull/760) + * Display timestamps and profiles for redacted events + [\#759](https://github.com/matrix-org/matrix-react-sdk/pull/759) + * Fix UDD for voip in e2e rooms + [\#757](https://github.com/matrix-org/matrix-react-sdk/pull/757) + * Add "Export E2E keys" option to logout dialog + [\#755](https://github.com/matrix-org/matrix-react-sdk/pull/755) + * Fix People section a bit + [\#754](https://github.com/matrix-org/matrix-react-sdk/pull/754) + * Do routing to /register _onLoadCompleted + [\#753](https://github.com/matrix-org/matrix-react-sdk/pull/753) + * Double UNPAGINATION_PADDING again + [\#747](https://github.com/matrix-org/matrix-react-sdk/pull/747) + * Add null check to start_login + [\#751](https://github.com/matrix-org/matrix-react-sdk/pull/751) + * Merge the two RoomTile context menus into one + [\#746](https://github.com/matrix-org/matrix-react-sdk/pull/746) + * Fix import for Lifecycle + [\#748](https://github.com/matrix-org/matrix-react-sdk/pull/748) + * Make UDD appear when UDE on uploading a file + [\#745](https://github.com/matrix-org/matrix-react-sdk/pull/745) + * Decide on which screen to show after login in one place + [\#743](https://github.com/matrix-org/matrix-react-sdk/pull/743) + * Add onClick to permalinks to route within Riot + [\#744](https://github.com/matrix-org/matrix-react-sdk/pull/744) + * Add support for pasting files into the text box + [\#605](https://github.com/matrix-org/matrix-react-sdk/pull/605) + * Show message redactions as black event tiles + [\#739](https://github.com/matrix-org/matrix-react-sdk/pull/739) + * Allow user to choose from existing DMs on new chat + [\#736](https://github.com/matrix-org/matrix-react-sdk/pull/736) + * Fix the team server registration + [\#741](https://github.com/matrix-org/matrix-react-sdk/pull/741) + * Clarify "No devices" message + [\#740](https://github.com/matrix-org/matrix-react-sdk/pull/740) + * Change timestamp permalinks to matrix.to + [\#735](https://github.com/matrix-org/matrix-react-sdk/pull/735) + * Fix resend bar and "send anyway" in UDD + [\#734](https://github.com/matrix-org/matrix-react-sdk/pull/734) + * Make COLOR_REGEX stricter + [\#737](https://github.com/matrix-org/matrix-react-sdk/pull/737) + * Port registration over to use InteractiveAuth + [\#729](https://github.com/matrix-org/matrix-react-sdk/pull/729) + * Test to see how fuse feels + [\#732](https://github.com/matrix-org/matrix-react-sdk/pull/732) + * Submit a new display name on blur of input field + [\#733](https://github.com/matrix-org/matrix-react-sdk/pull/733) + * Allow [bf]g colors for style attrib + [\#610](https://github.com/matrix-org/matrix-react-sdk/pull/610) + * MELS: either expanded or summary, not both + [\#683](https://github.com/matrix-org/matrix-react-sdk/pull/683) + * Autoplay videos and GIFs if enabled by the user. + [\#730](https://github.com/matrix-org/matrix-react-sdk/pull/730) + * Warn users about using e2e for the first time + [\#731](https://github.com/matrix-org/matrix-react-sdk/pull/731) + * Show UDDialog on UDE during VoIP calls + [\#721](https://github.com/matrix-org/matrix-react-sdk/pull/721) + * Notify MatrixChat of teamToken after login + [\#726](https://github.com/matrix-org/matrix-react-sdk/pull/726) + * Fix a couple of issues with RRs + [\#727](https://github.com/matrix-org/matrix-react-sdk/pull/727) + * Do not push a dummy element with a scroll token for invisible events + [\#718](https://github.com/matrix-org/matrix-react-sdk/pull/718) + * MELS: check scroll on load + use mels-1,-2,... key + [\#715](https://github.com/matrix-org/matrix-react-sdk/pull/715) + * Fix message composer placeholders + [\#723](https://github.com/matrix-org/matrix-react-sdk/pull/723) + * Clarify non-e2e vs. e2e /w composers placeholder + [\#720](https://github.com/matrix-org/matrix-react-sdk/pull/720) + * Fix status bar expanded on tab-complete + [\#722](https://github.com/matrix-org/matrix-react-sdk/pull/722) + * add .editorconfig + [\#713](https://github.com/matrix-org/matrix-react-sdk/pull/713) + * Change the name of the database + [\#719](https://github.com/matrix-org/matrix-react-sdk/pull/719) + * Allow setting the default HS from the query parameter + [\#716](https://github.com/matrix-org/matrix-react-sdk/pull/716) + * first cut of improving UX for deleting devices. + [\#717](https://github.com/matrix-org/matrix-react-sdk/pull/717) + * Fix block quotes all being on a single line + [\#711](https://github.com/matrix-org/matrix-react-sdk/pull/711) + * Support reasons for kick / ban + [\#710](https://github.com/matrix-org/matrix-react-sdk/pull/710) + * Show when you've been kicked or banned + [\#709](https://github.com/matrix-org/matrix-react-sdk/pull/709) + * Add a 'Clear Cache' button + [\#708](https://github.com/matrix-org/matrix-react-sdk/pull/708) + * Update the room view on room name change + [\#707](https://github.com/matrix-org/matrix-react-sdk/pull/707) + * Add a button to un-ban users in RoomSettings + [\#698](https://github.com/matrix-org/matrix-react-sdk/pull/698) + * Use IndexedDBStore from the JS-SDK + [\#687](https://github.com/matrix-org/matrix-react-sdk/pull/687) + * Make UserSettings use the right teamToken + [\#706](https://github.com/matrix-org/matrix-react-sdk/pull/706) + * If the home page is somehow accessed, goto directory + [\#705](https://github.com/matrix-org/matrix-react-sdk/pull/705) + * Display avatar initials in typing notifications + [\#699](https://github.com/matrix-org/matrix-react-sdk/pull/699) + * fix eslint's no-invalid-this rule for class properties + [\#703](https://github.com/matrix-org/matrix-react-sdk/pull/703) + * If a referrer hasn't been specified, use empty string + [\#701](https://github.com/matrix-org/matrix-react-sdk/pull/701) + * Don't force-logout the user if reading localstorage fails + [\#700](https://github.com/matrix-org/matrix-react-sdk/pull/700) + * Convert some missed buttons to AccessibleButton + [\#697](https://github.com/matrix-org/matrix-react-sdk/pull/697) + * Make ban either ban or unban + [\#696](https://github.com/matrix-org/matrix-react-sdk/pull/696) + * Add confirmation dialog to kick/ban buttons + [\#694](https://github.com/matrix-org/matrix-react-sdk/pull/694) + * Fix typo with Scalar popup + [\#695](https://github.com/matrix-org/matrix-react-sdk/pull/695) + * Treat the literal team token string "undefined" as undefined + [\#693](https://github.com/matrix-org/matrix-react-sdk/pull/693) + * Store retrieved sid in the signupInstance of EmailIdentityStage + [\#692](https://github.com/matrix-org/matrix-react-sdk/pull/692) + * Split out InterActiveAuthDialog + [\#691](https://github.com/matrix-org/matrix-react-sdk/pull/691) + * View /home on registered /w team + [\#689](https://github.com/matrix-org/matrix-react-sdk/pull/689) + * Instead of sending userId, userEmail, send sid, client_secret + [\#688](https://github.com/matrix-org/matrix-react-sdk/pull/688) + * Enable branded URLs again by parsing the path client-side + [\#686](https://github.com/matrix-org/matrix-react-sdk/pull/686) + * Use new method of getting team icon + [\#680](https://github.com/matrix-org/matrix-react-sdk/pull/680) + * Persist query parameter team token across refreshes + [\#685](https://github.com/matrix-org/matrix-react-sdk/pull/685) + * Thread teamToken through to LeftPanel for "Home" button + [\#684](https://github.com/matrix-org/matrix-react-sdk/pull/684) + * Fix typing notif and status bar + [\#682](https://github.com/matrix-org/matrix-react-sdk/pull/682) + * Consider emails ending in matrix.org as a uni email + [\#681](https://github.com/matrix-org/matrix-react-sdk/pull/681) + * Set referrer qp in nextLink + [\#679](https://github.com/matrix-org/matrix-react-sdk/pull/679) + * Do not set team_token if not returned by RTS on login + [\#678](https://github.com/matrix-org/matrix-react-sdk/pull/678) + * Get team_token from the RTS on login + [\#676](https://github.com/matrix-org/matrix-react-sdk/pull/676) + * Quick and dirty support for custom welcome pages + [\#550](https://github.com/matrix-org/matrix-react-sdk/pull/550) + * RTS Welcome Pages + [\#666](https://github.com/matrix-org/matrix-react-sdk/pull/666) + * Logging to try to track down riot-web#3148 + [\#677](https://github.com/matrix-org/matrix-react-sdk/pull/677) + Changes in [0.8.6](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.6) (2017-02-04) =================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.6-rc.3...v0.8.6) From 440307bd39c0eb8a6bfa61e7c98f7a49adcd1bbe Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 7 Apr 2017 17:04:03 +0100 Subject: [PATCH 111/284] v0.8.7-rc.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0457fc4764..4da45e6f8a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "0.8.6", + "version": "0.8.7-rc.1", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From 6dbb4e9002d2f7842c1a5ea77e86b892134dca39 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 7 Apr 2017 23:34:11 +0100 Subject: [PATCH 112/284] fix the warning shown to users about needing to export e2e keys apparently when we added the buttons to export e2e keys to the Logout button, we didn't change the text warning the user that e2e export was coming soon. likewise when changing password and forgetting password (where we didn't even have a button to export keys) --- src/components/structures/UserSettings.js | 8 +++---- .../structures/login/ForgotPassword.js | 24 ++++++++++++++++--- .../views/settings/ChangePassword.js | 24 ++++++++++++++++--- 3 files changed, 46 insertions(+), 10 deletions(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 0cb120019e..b2c2251455 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -264,10 +264,10 @@ module.exports = React.createClass({ title: "Sign out?", description:
      - For security, logging out will delete any end-to-end encryption keys from this browser, - making previous encrypted chat history unreadable if you log back in. - In future this will be improved, - but for now be warned. + For security, logging out will delete any end-to-end encryption keys from this browser. + + If you want to be able to decrypt your conversation history from future Riot sessions, + please export your room keys for safe-keeping.
      , button: "Sign out", extraButtons: [ diff --git a/src/components/structures/login/ForgotPassword.js b/src/components/structures/login/ForgotPassword.js index 2c10052b98..d75c7b7584 100644 --- a/src/components/structures/login/ForgotPassword.js +++ b/src/components/structures/login/ForgotPassword.js @@ -93,11 +93,17 @@ module.exports = React.createClass({ description:
      Resetting password will currently reset any end-to-end encryption keys on all devices, - making encrypted chat history unreadable. - In future this may be improved, - but for now be warned. + making encrypted chat history unreadable, unless you first export your room keys + and re-import them afterwards. + In future this will be improved.
      , button: "Continue", + extraButtons: [ + + ], onFinished: (confirmed) => { if (confirmed) { this.submitPasswordReset( @@ -110,6 +116,18 @@ module.exports = React.createClass({ } }, + _onExportE2eKeysClicked: function() { + Modal.createDialogAsync( + (cb) => { + require.ensure(['../../../async-components/views/dialogs/ExportE2eKeysDialog'], () => { + cb(require('../../../async-components/views/dialogs/ExportE2eKeysDialog')); + }, "e2e-export"); + }, { + matrixClient: MatrixClientPeg.get(), + } + ); + }, + onInputChanged: function(stateKey, ev) { this.setState({ [stateKey]: ev.target.value diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index 8b53a0e779..20ce45e5dd 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -73,11 +73,17 @@ module.exports = React.createClass({ description:
      Changing password will currently reset any end-to-end encryption keys on all devices, - making encrypted chat history unreadable. - This will be improved shortly, - but for now be warned. + making encrypted chat history unreadable, unless you first export your room keys + and re-import them afterwards. + In future this will be improved.
      , button: "Continue", + extraButtons: [ + + ], onFinished: (confirmed) => { if (confirmed) { var authDict = { @@ -105,6 +111,18 @@ module.exports = React.createClass({ }); }, + _onExportE2eKeysClicked: function() { + Modal.createDialogAsync( + (cb) => { + require.ensure(['../../../async-components/views/dialogs/ExportE2eKeysDialog'], () => { + cb(require('../../../async-components/views/dialogs/ExportE2eKeysDialog')); + }, "e2e-export"); + }, { + matrixClient: MatrixClientPeg.get(), + } + ); + }, + onClickChange: function() { var old_password = this.refs.old_input.value; var new_password = this.refs.new_input.value; From 8b4836b60ef8dfd8852e982df4abd7bc1f715cf0 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 10 Apr 2017 10:09:26 +0100 Subject: [PATCH 113/284] Refactor roles into Roles.js So that the mapping between a numerical power level and a "role" are done in one place. PowerSelector.js has been modified to use the same mapping. --- src/Roles.js | 29 +++++++++++++++ src/TextForEvent.js | 19 ++-------- .../views/elements/PowerSelector.js | 37 +++++++++++-------- 3 files changed, 54 insertions(+), 31 deletions(-) create mode 100644 src/Roles.js diff --git a/src/Roles.js b/src/Roles.js new file mode 100644 index 0000000000..cef8670aad --- /dev/null +++ b/src/Roles.js @@ -0,0 +1,29 @@ +/* +Copyright 2017 Vector Creations 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. +*/ +export const LEVEL_ROLE_MAP = { + undefined: 'Default', + 0: 'User', + 50: 'Moderator', + 100: 'Admin', +}; + +export function textualPowerLevel(level, userDefault) { + if (LEVEL_ROLE_MAP[level]) { + return LEVEL_ROLE_MAP[level] + (level !== undefined ? ` (${level})` : ` (${userDefault})`); + } else { + return level; + } +} diff --git a/src/TextForEvent.js b/src/TextForEvent.js index 2560264346..40d6a49998 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -17,12 +17,7 @@ limitations under the License. var MatrixClientPeg = require("./MatrixClientPeg"); var CallHandler = require("./CallHandler"); -const roles = { - undefined: 'Default', - 0: 'User', - 50: 'Moderator', - 100: 'Admin', -}; +import * as Roles from './Roles'; function textForMemberEvent(ev) { // XXX: SYJS-16 "sender is sometimes null for join messages" @@ -189,14 +184,6 @@ function textForEncryptionEvent(event) { return senderName + " turned on end-to-end encryption (algorithm " + event.getContent().algorithm + ")"; } -function formatPowerLevel(level, roles, userDefault) { - if (roles[level]) { - return roles[level] + (level !== undefined ? ` (${level})` : ` (${userDefault})`); - } else { - return level; - } -} - // Currently will only display a change if a user's power level is changed function textForPowerEvent(event) { const senderName = event.sender ? event.sender.name : event.getSender(); @@ -225,8 +212,8 @@ function textForPowerEvent(event) { if (to !== from) { diff.push( userId + - ' from ' + formatPowerLevel(from, roles, userDefault) + - ' to ' + formatPowerLevel(to, roles, userDefault) + ' from ' + Roles.textualPowerLevel(from, userDefault) + + ' to ' + Roles.textualPowerLevel(to, userDefault) ); } }); diff --git a/src/components/views/elements/PowerSelector.js b/src/components/views/elements/PowerSelector.js index c7bfd4eec1..5eec464ead 100644 --- a/src/components/views/elements/PowerSelector.js +++ b/src/components/views/elements/PowerSelector.js @@ -16,17 +16,12 @@ limitations under the License. 'use strict'; -var React = require('react'); - -var roles = { - 0: 'User', - 50: 'Moderator', - 100: 'Admin', -}; +import React from 'react'; +import * as Roles from '../../../Roles'; var reverseRoles = {}; -Object.keys(roles).forEach(function(key) { - reverseRoles[roles[key]] = key; +Object.keys(Roles.LEVEL_ROLE_MAP).forEach(function(key) { + reverseRoles[Roles.LEVEL_ROLE_MAP[key]] = key; }); module.exports = React.createClass({ @@ -49,7 +44,7 @@ module.exports = React.createClass({ getInitialState: function() { return { - custom: (roles[this.props.value] === undefined), + custom: (Roles.LEVEL_ROLE_MAP[this.props.value] === undefined), }; }, @@ -99,22 +94,34 @@ module.exports = React.createClass({ selectValue = "Custom"; } else { - selectValue = roles[this.props.value] || "Custom"; + selectValue = Roles.LEVEL_ROLE_MAP[this.props.value] || "Custom"; } var select; if (this.props.disabled) { select = { selectValue }; } else { + // Each level must have a definition in LEVEL_ROLE_MAP + const levels = [0, 50, 100]; + let options = levels.map((level) => { + return { + value: Roles.LEVEL_ROLE_MAP[level], + // Give a userDefault (users_default in the power event) of 0 but + // because level !== undefined, this should never be used. + text: Roles.textualPowerLevel(level, 0), + } + }); + options.push({ value: "Custom", text: "Custom level" }); + options = options.map((op) => { + return ; + }); + select = ; } From 2a4b052fa5b972047c29ec27cae95987e08b36dc Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 10 Apr 2017 12:06:54 +0100 Subject: [PATCH 114/284] unbreak in-app permalinks --- src/linkify-matrix.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/linkify-matrix.js b/src/linkify-matrix.js index 68f7a66bda..e085b1a27a 100644 --- a/src/linkify-matrix.js +++ b/src/linkify-matrix.js @@ -122,7 +122,7 @@ var escapeRegExp = function(string) { // anyone else really should be using matrix.to. matrixLinkify.VECTOR_URL_PATTERN = "^(?:https?:\/\/)?(?:" + escapeRegExp(window.location.host + window.location.pathname) + "|" - + "(?:www\\.)?vector\\.im/(?:beta|staging|develop)/" + + "(?:www\\.)?(riot|vector)\\.im/(?:beta|staging|develop)/" + ")(#.*)"; matrixLinkify.MATRIXTO_URL_PATTERN = "^(?:https?:\/\/)?(?:www\\.)?matrix\\.to/#/((#|@|!).*)"; From 5de71ef504f01c016be02940030de967af8dd84d Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 10 Apr 2017 12:07:39 +0100 Subject: [PATCH 115/284] unbreak in-app permalinks correctly --- src/linkify-matrix.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/linkify-matrix.js b/src/linkify-matrix.js index e085b1a27a..c8e20316a9 100644 --- a/src/linkify-matrix.js +++ b/src/linkify-matrix.js @@ -122,7 +122,7 @@ var escapeRegExp = function(string) { // anyone else really should be using matrix.to. matrixLinkify.VECTOR_URL_PATTERN = "^(?:https?:\/\/)?(?:" + escapeRegExp(window.location.host + window.location.pathname) + "|" - + "(?:www\\.)?(riot|vector)\\.im/(?:beta|staging|develop)/" + + "(?:www\\.)?(?:riot|vector)\\.im/(?:beta|staging|develop)/" + ")(#.*)"; matrixLinkify.MATRIXTO_URL_PATTERN = "^(?:https?:\/\/)?(?:www\\.)?matrix\\.to/#/((#|@|!).*)"; From 4a8173c064e89c3f88ab3b7a8291a83147c061c1 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 10 Apr 2017 16:46:13 +0100 Subject: [PATCH 116/284] Prepare changelog for v0.8.7-rc.2 --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20ee43ff2c..bcb62f8ab4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +Changes in [0.8.7-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.7-rc.2) (2017-04-10) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.7-rc.1...v0.8.7-rc.2) + + * fix the warning shown to users about needing to export e2e keys + [\#797](https://github.com/matrix-org/matrix-react-sdk/pull/797) + Changes in [0.8.7-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.7-rc.1) (2017-04-07) ============================================================================================================= [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.6...v0.8.7-rc.1) From 966dc3a071740e1f07fc8b6da26826af6e69eaf0 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 10 Apr 2017 16:46:14 +0100 Subject: [PATCH 117/284] v0.8.7-rc.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4da45e6f8a..7d0a2b3208 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "0.8.7-rc.1", + "version": "0.8.7-rc.2", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From a77fdd03838f07191a5f51d61d659673ad577d09 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 10 Apr 2017 16:47:45 +0100 Subject: [PATCH 118/284] js-sdk rc.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7d0a2b3208..b7460a6711 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "isomorphic-fetch": "^2.2.1", "linkifyjs": "^2.1.3", "lodash": "^4.13.1", - "matrix-js-sdk": "0.7.6-rc.1", + "matrix-js-sdk": "0.7.6-rc.2", "optimist": "^0.6.1", "q": "^1.4.1", "react": "^15.4.0", From cacb34a37db86d80b1d253d84eb509e0f505459f Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 10 Apr 2017 16:52:30 +0100 Subject: [PATCH 119/284] Prepare changelog for v0.8.7-rc.3 --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bcb62f8ab4..077dec4992 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +Changes in [0.8.7-rc.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.7-rc.3) (2017-04-10) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.7-rc.2...v0.8.7-rc.3) + + * Use matrix-js-sdk v0.7.6-rc.2 + + Changes in [0.8.7-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.7-rc.2) (2017-04-10) ============================================================================================================= [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.7-rc.1...v0.8.7-rc.2) From 46b362b20c92e2e1816311c6b10dc2a57ebecb0f Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 10 Apr 2017 16:52:30 +0100 Subject: [PATCH 120/284] v0.8.7-rc.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b7460a6711..9165a90bde 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "0.8.7-rc.2", + "version": "0.8.7-rc.3", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From 811cd79206a0b5f8ce2bda340a663a7c7a766310 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 10 Apr 2017 17:39:27 +0100 Subject: [PATCH 121/284] Make the clear cache button work on desktop Fixes https://github.com/vector-im/riot-web/issues/3597 --- src/BasePlatform.js | 8 ++++++++ src/components/structures/UserSettings.js | 6 +++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/BasePlatform.js b/src/BasePlatform.js index 8bdf7d0391..6eed22f436 100644 --- a/src/BasePlatform.js +++ b/src/BasePlatform.js @@ -82,4 +82,12 @@ export default class BasePlatform { screenCaptureErrorString() { return "Not implemented"; } + + /** + * Restarts the application, without neccessarily reloading + * any application code + */ + reload() { + throw new Error("reload not implemented!"); + } } diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index b2c2251455..53daa55fd8 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -441,10 +441,10 @@ module.exports = React.createClass({ }, _onClearCacheClicked: function() { + if (!PlatformPeg.get()) return; + MatrixClientPeg.get().store.deleteAllData().done(() => { - // forceReload=false since we don't really need new HTML/JS files - // we just need to restart the JS runtime. - window.location.reload(false); + PlatformPeg.get().reload(); }); }, From d4dc16545acccd6689af1b41a8fd4bafecb274c9 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 11 Apr 2017 18:16:29 +0100 Subject: [PATCH 122/284] Fix people section vanishing on 'clear cache' Stop the client first Fixes https://github.com/vector-im/riot-web/issues/3610 --- src/components/structures/UserSettings.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 53daa55fd8..892865fdf9 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -443,6 +443,7 @@ module.exports = React.createClass({ _onClearCacheClicked: function() { if (!PlatformPeg.get()) return; + MatrixClientPeg.get().stopClient(); MatrixClientPeg.get().store.deleteAllData().done(() => { PlatformPeg.get().reload(); }); From e12d079aa545afc68b6b5c36c06063d2ef0c398a Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 11 Apr 2017 18:47:45 +0100 Subject: [PATCH 123/284] Prepare changelog for v0.8.7-rc.4 --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 077dec4992..56ab8f3a43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +Changes in [0.8.7-rc.4](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.7-rc.4) (2017-04-11) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.7-rc.3...v0.8.7-rc.4) + + * Fix people section vanishing on 'clear cache' + [\#799](https://github.com/matrix-org/matrix-react-sdk/pull/799) + * Make the clear cache button work on desktop + [\#798](https://github.com/matrix-org/matrix-react-sdk/pull/798) + Changes in [0.8.7-rc.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.7-rc.3) (2017-04-10) ============================================================================================================= [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.7-rc.2...v0.8.7-rc.3) From 25665a81aeb597744ccb965af0904433668e2d2f Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 11 Apr 2017 18:47:45 +0100 Subject: [PATCH 124/284] v0.8.7-rc.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9165a90bde..25779681de 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "0.8.7-rc.3", + "version": "0.8.7-rc.4", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From 73888ea616104738edda7b0d32611c0934f417a2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 12 Apr 2017 10:00:35 +0100 Subject: [PATCH 125/284] js-sdk 0.7.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 25779681de..a3159e28e0 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "isomorphic-fetch": "^2.2.1", "linkifyjs": "^2.1.3", "lodash": "^4.13.1", - "matrix-js-sdk": "0.7.6-rc.2", + "matrix-js-sdk": "0.7.6", "optimist": "^0.6.1", "q": "^1.4.1", "react": "^15.4.0", From d728925177807c0ecf55819d933f502d6ffefbf2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 12 Apr 2017 10:02:21 +0100 Subject: [PATCH 126/284] Prepare changelog for v0.8.7 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56ab8f3a43..292e60607d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +Changes in [0.8.7](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.7) (2017-04-12) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.7-rc.4...v0.8.7) + + * No changes + Changes in [0.8.7-rc.4](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.7-rc.4) (2017-04-11) ============================================================================================================= [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.7-rc.3...v0.8.7-rc.4) From 1ff443956605ca818b212072767a1f6e515994b5 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 12 Apr 2017 10:02:21 +0100 Subject: [PATCH 127/284] v0.8.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a3159e28e0..0a0a51fc0b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "0.8.7-rc.4", + "version": "0.8.7", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From 1d836c7d02a6935313bfb05d94fc38ae05439480 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 12 Apr 2017 10:04:25 +0100 Subject: [PATCH 128/284] Back to js-sdk develop --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0a0a51fc0b..cb3cdfa63f 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "isomorphic-fetch": "^2.2.1", "linkifyjs": "^2.1.3", "lodash": "^4.13.1", - "matrix-js-sdk": "0.7.6", + "matrix-js-sdk": "matrix-org/matrix-js-sdk#develop", "optimist": "^0.6.1", "q": "^1.4.1", "react": "^15.4.0", From 424aae6b91283edd1a4e25142cec48dacf3fb6ac Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 12 Apr 2017 15:04:38 +0100 Subject: [PATCH 129/284] Prevent the ghost and real RM tile from both appearing --- src/components/structures/MessagePanel.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 0f8d35f525..6ee308a5a7 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -386,9 +386,7 @@ module.exports = React.createClass({ ret.push(this._getReadMarkerTile(visible)); readMarkerVisible = visible; isVisibleReadMarker = visible; - } - - if (eventId == this.currentGhostEventId) { + } else if (eventId == this.currentGhostEventId) { // if we're showing an animation, continue to show it. ret.push(this._getReadMarkerGhostTile()); } else if (!isVisibleReadMarker && From 1c25ed89b01345da3af185bb1900dd7943c388aa Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 12 Apr 2017 15:05:39 +0100 Subject: [PATCH 130/284] Initial implementation of using new RM API As detailed here https://docs.google.com/document/d/1UWqdS-e1sdwkLDUY0wA4gZyIkRp-ekjsLZ8k6g_Zvso/edit, the RM state is no longer kept locally, but rather server-side. The client now uses it's locally-calculated RM to update the server and receives server updates via the per-room account data. The sending of the RR has been bundled in to reduce traffic when sending both. In effect, whenever a RR is sent the RM is sent with it but using the new API. This uses a js-sdk change which has set to be finalised and so might change. --- src/components/structures/TimelinePanel.js | 55 +++++++++++++++------- 1 file changed, 39 insertions(+), 16 deletions(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 8cd820c284..4fbca4d40a 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -102,9 +102,6 @@ var TimelinePanel = React.createClass({ }, statics: { - // a map from room id to read marker event ID - roomReadMarkerMap: {}, - // a map from room id to read marker event timestamp roomReadMarkerTsMap: {}, }, @@ -121,10 +118,15 @@ var TimelinePanel = React.createClass({ getInitialState: function() { // XXX: we could track RM per TimelineSet rather than per Room. // but for now we just do it per room for simplicity. + let initialReadMarker = null; if (this.props.manageReadMarkers) { - var initialReadMarker = - TimelinePanel.roomReadMarkerMap[this.props.timelineSet.room.roomId] - || this._getCurrentReadReceipt(); + const readmarker = this.props.timelineSet.room.getAccountData('m.read_marker'); + if (readmarker){ + initialReadMarker = readmarker.getContent().marker; + } else { + initialReadMarker = this._getCurrentReadReceipt(); + } + console.info('Read marker initially', initialReadMarker); } return { @@ -180,6 +182,7 @@ var TimelinePanel = React.createClass({ MatrixClientPeg.get().on("Room.redaction", this.onRoomRedaction); MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt); MatrixClientPeg.get().on("Room.localEchoUpdated", this.onLocalEchoUpdated); + MatrixClientPeg.get().on("Room.accountData", this.onAccountData); this._initTimeline(this.props); }, @@ -466,6 +469,21 @@ var TimelinePanel = React.createClass({ this._reloadEvents(); }, + onAccountData: function(ev, room) { + if (this.unmounted) return; + + // ignore events for other rooms + if (room !== this.props.timelineSet.room) return; + + if (ev.getType() !== "m.read_marker") return; + + const markerEventId = ev.getContent().marker; + console.log('TimelinePanel: Read marker received from server', markerEventId); + + this.setState({ + readMarkerEventId: markerEventId, + }, this.props.onReadMarkerUpdated); + }, sendReadReceipt: function() { if (!this.refs.messagePanel) return; @@ -505,13 +523,23 @@ var TimelinePanel = React.createClass({ // we also remember the last read receipt we sent to avoid spamming the // same one at the server repeatedly - if (lastReadEventIndex > currentReadUpToEventIndex - && this.last_rr_sent_event_id != lastReadEvent.getId()) { + if ((lastReadEventIndex > currentReadUpToEventIndex && + this.last_rr_sent_event_id != lastReadEvent.getId()) || + this.last_rm_sent_event_id != this.state.readMarkerEventId) { + this.last_rr_sent_event_id = lastReadEvent.getId(); - MatrixClientPeg.get().sendReadReceipt(lastReadEvent).catch(() => { + this.last_rm_sent_event_id = this.state.readMarkerEventId; + + MatrixClientPeg.get().setRoomReadMarker( + this.props.timelineSet.room.roomId, + this.state.readMarkerEventId, + lastReadEvent + ).catch(() => { // it failed, so allow retries next time the user is active this.last_rr_sent_event_id = undefined; + this.last_rm_sent_event_id = undefined; }); + console.log('TimelinePanel: Read marker sent to the server ', this.state.readMarkerEventId, ); // do a quick-reset of our unreadNotificationCount to avoid having // to wait from the remote echo from the homeserver. @@ -956,16 +984,10 @@ var TimelinePanel = React.createClass({ _setReadMarker: function(eventId, eventTs, inhibitSetState) { var roomId = this.props.timelineSet.room.roomId; - if (TimelinePanel.roomReadMarkerMap[roomId] == eventId) { - // don't update the state (and cause a re-render) if there is - // no change to the RM. + if (eventId === this.state.readMarkerEventId) { return; } - // ideally we'd sync these via the server, but for now just stash them - // in a map. - TimelinePanel.roomReadMarkerMap[roomId] = eventId; - // in order to later figure out if the read marker is // above or below the visible timeline, we stash the timestamp. TimelinePanel.roomReadMarkerTsMap[roomId] = eventTs; @@ -974,6 +996,7 @@ var TimelinePanel = React.createClass({ return; } + // Do the local echo of the RM // run the render cycle before calling the callback, so that // getReadMarkerPosition() returns the right thing. this.setState({ From 249e42747b2435df6d431ee0170dd167133b4479 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 12 Apr 2017 15:09:56 +0100 Subject: [PATCH 131/284] Fix bug where `roomId` was expected to be a property on timelineSet --- src/components/structures/TimelinePanel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 4fbca4d40a..18f52d1f07 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -734,7 +734,7 @@ var TimelinePanel = React.createClass({ // the messagePanel doesn't know where the read marker is. // if we know the timestamp of the read marker, make a guess based on that. - var rmTs = TimelinePanel.roomReadMarkerTsMap[this.props.timelineSet.roomId]; + const rmTs = TimelinePanel.roomReadMarkerTsMap[this.props.timelineSet.room.roomId]; if (rmTs && this.state.events.length > 0) { if (rmTs < this.state.events[0].getTs()) { return -1; From 9c9dc84f45e0b26df4c444babc42a614749c92c0 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 12 Apr 2017 15:12:37 +0100 Subject: [PATCH 132/284] Remove redundant setting of readMarkerEventId --- src/components/structures/TimelinePanel.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 18f52d1f07..9277c3f2b7 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -417,9 +417,10 @@ var TimelinePanel = React.createClass({ } else if(lastEv && this.getReadMarkerPosition() === 0) { // we know we're stuckAtBottom, so we can advance the RM // immediately, to save a later render cycle + + // This call will setState with readMarkerEventId = lastEv.getId() this._setReadMarker(lastEv.getId(), lastEv.getTs(), true); updatedState.readMarkerVisible = false; - updatedState.readMarkerEventId = lastEv.getId(); callback = this.props.onReadMarkerUpdated; } } From 1189368aab8cfb22c1895f8ce6c0d8a8fbe7ca0b Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 15 Apr 2017 00:30:48 +0100 Subject: [PATCH 133/284] add a class to remove evil blue outlines --- src/components/views/elements/AccessibleButton.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/views/elements/AccessibleButton.js b/src/components/views/elements/AccessibleButton.js index 2c23c0d208..ce58b6d5cf 100644 --- a/src/components/views/elements/AccessibleButton.js +++ b/src/components/views/elements/AccessibleButton.js @@ -32,6 +32,8 @@ export default function AccessibleButton(props) { }; restProps.tabIndex = restProps.tabIndex || "0"; restProps.role = "button"; + restProps.className = (restProps.className ? restProps.className + " " : "") + + "mx_AccessibleButton"; return React.createElement(element, restProps, children); } From 0a91511f05c2bf3cc7511cffd8f4487a366caa15 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 15 Apr 2017 12:13:29 +0100 Subject: [PATCH 134/284] cmd-k for quick search --- src/KeyCode.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/KeyCode.js b/src/KeyCode.js index c9cac01239..f164dbc15c 100644 --- a/src/KeyCode.js +++ b/src/KeyCode.js @@ -32,4 +32,5 @@ module.exports = { DELETE: 46, KEY_D: 68, KEY_E: 69, + KEY_K: 75, }; From 691639d1e06e8c7384dfedcc4514f024a77fc0af Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 15 Apr 2017 13:23:52 +0100 Subject: [PATCH 135/284] track RoomTile focus in RoomList, and stop the RoomList from updating during mouseOver --- src/components/views/rooms/RoomList.js | 62 ++++++++++++++++++++++++-- src/components/views/rooms/RoomTile.js | 11 ++++- 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 59346d5f4d..0da741df19 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -63,12 +63,15 @@ module.exports = React.createClass({ var s = this.getRoomLists(); this.setState(s); + + this.focusedRoomTileRoomId = null; }, componentDidMount: function() { this.dispatcherRef = dis.register(this.onAction); // Initialise the stickyHeaders when the component is created this._updateStickyHeaders(true); + document.addEventListener('keydown', this._onKeyDown); }, componentDidUpdate: function() { @@ -100,6 +103,8 @@ module.exports = React.createClass({ // Force an update because the notif count state is too deep to cause // an update. This forces the local echo of reading notifs to be // reflected by the RoomTiles. + // + // FIXME: we should surely just be refreshing the right tile... this.forceUpdate(); break; } @@ -120,6 +125,8 @@ module.exports = React.createClass({ } // cancel any pending calls to the rate_limited_funcs this._delayedRefreshRoomList.cancelPendingCall(); + document.removeEventListener('keydown', this._onKeyDown); + }, onRoom: function(room) { @@ -149,6 +156,35 @@ module.exports = React.createClass({ } }, + _onMouseOver: function(ev) { + this._lastMouseOverTs = Date.now(); + }, + + _onKeyDown: function(ev) { + if (!this.focusedRoomTileRoomId) return; + let handled = false; + + switch (ev.keyCode) { + case KeyCode.UP: + this._onMoveFocus(true); + handled = true; + break; + case KeyCode.DOWN: + this._onMoveFocus(false); + handled = true; + break; + } + + if (handled) { + ev.stopPropagation(); + ev.preventDefault(); + } + }, + + _onMoveFocus: function(up) { + + }, + onSubListHeaderClick: function(isHidden, scrollToPosition) { // The scroll area has expanded or contracted, so re-calculate sticky headers positions this._updateStickyHeaders(true, scrollToPosition); @@ -192,7 +228,15 @@ module.exports = React.createClass({ }, _delayedRefreshRoomList: new rate_limited_func(function() { - this.refreshRoomList(); + // if the mouse has been moving over the RoomList in the last 500ms + // then delay the refresh further to avoid bouncing around under the + // cursor + if (Date.now() - this._lastMouseOverTs > 500) { + this.refreshRoomList(); + } + else { + this._delayedRefreshRoomList(); + } }, 500), refreshRoomList: function() { @@ -207,7 +251,8 @@ module.exports = React.createClass({ // us re-rendering all the sublists every time anything changes anywhere // in the state of the client. this.setState(this.getRoomLists()); - this._lastRefreshRoomListTs = Date.now(); + + // this._lastRefreshRoomListTs = Date.now(); }, getRoomLists: function() { @@ -457,6 +502,10 @@ module.exports = React.createClass({ this.refs.gemscroll.forceUpdate(); }, + onRoomTileFocus: function(roomId) { + this.focusedRoomTileRoomId = roomId; + }, + render: function() { var RoomSubList = sdk.getComponent('structures.RoomSubList'); var self = this; @@ -464,7 +513,7 @@ module.exports = React.createClass({ return ( -
      +
      { Object.keys(self.state.lists).map(function(tagName) { @@ -529,6 +582,7 @@ module.exports = React.createClass({ collapsed={ self.props.collapsed } searchFilter={ self.props.searchFilter } onHeaderClick={ self.onSubListHeaderClick } + onRoomTileFocus={ self.onRoomTileFocus } onShowMoreRooms={ self.onShowMoreRooms } />; } @@ -545,6 +599,7 @@ module.exports = React.createClass({ collapsed={ self.props.collapsed } searchFilter={ self.props.searchFilter } onHeaderClick={ self.onSubListHeaderClick } + onRoomTileFocus={ self.onRoomTileFocus } onShowMoreRooms={ self.onShowMoreRooms } />
      diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 06b05e9299..cff5c2f623 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -35,6 +35,7 @@ module.exports = React.createClass({ connectDragSource: React.PropTypes.func, connectDropTarget: React.PropTypes.func, onClick: React.PropTypes.func, + onFocus: React.PropTypes.func, isDragging: React.PropTypes.bool, room: React.PropTypes.object.isRequired, @@ -104,6 +105,12 @@ module.exports = React.createClass({ } }, + onFocus: function() { + if (this.props.onFocus) { + this.props.onFocus(this.props.room.roomId); + } + }, + onMouseEnter: function() { this.setState( { hover : true }); this.badgeOnMouseEnter(); @@ -255,7 +262,9 @@ module.exports = React.createClass({ let ret = (
      { /* Only native elements can be wrapped in a DnD object. */} - +
      From a0c498e8ba786543fa8efadd8d563af939299b11 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 17 Apr 2017 14:37:24 +0100 Subject: [PATCH 136/284] Make Download behaviour consistent with that of E2E (iframed) download butttons (ACTUALLY DOWNLOAD) Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/messages/MFileBody.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/messages/MFileBody.js b/src/components/views/messages/MFileBody.js index 86aee28269..029a8a9fe4 100644 --- a/src/components/views/messages/MFileBody.js +++ b/src/components/views/messages/MFileBody.js @@ -346,7 +346,7 @@ module.exports = React.createClass({ return (
      - + { fileName }
      @@ -360,7 +360,7 @@ module.exports = React.createClass({ return (
      - + Download {text} From 6f0c3b1c03f92f49a1f90f6edcdcc73283479401 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 17 Apr 2017 14:50:34 +0100 Subject: [PATCH 137/284] Pass file name (as name) to the ImageView modal Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/messages/MImageBody.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index ab163297d7..0b4bc6ecb9 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -56,6 +56,7 @@ module.exports = React.createClass({ const ImageView = sdk.getComponent("elements.ImageView"); const params = { src: httpUrl, + name: content.body && content.body.length > 0 ? content.body : 'Attachment', mxEvent: this.props.mxEvent, }; From da569c2c8d3b8ea031566f4b9b5bd4f40e5cc465 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 17 Apr 2017 20:58:43 +0100 Subject: [PATCH 138/284] add constantTimeDispatcher and use it for strategic refreshes. constantTimeDispatcher lets you poke a specific react component to do something without having to do any O(N) operations. This is useful if you have thousands of RoomTiles in a RoomSubList and want to just tell one of them to update, without either having to do a full comparison of this.props.list or have each and every RoomTile subscribe to a generic event from flux or node's eventemitter *UNTESTED* --- src/ConstantTimeDispatcher.js | 62 +++++++++++++++ src/components/structures/TimelinePanel.js | 3 + src/components/views/rooms/RoomList.js | 88 ++++++++++++++++------ src/components/views/rooms/RoomTile.js | 7 ++ 4 files changed, 136 insertions(+), 24 deletions(-) create mode 100644 src/ConstantTimeDispatcher.js diff --git a/src/ConstantTimeDispatcher.js b/src/ConstantTimeDispatcher.js new file mode 100644 index 0000000000..265ee11fd4 --- /dev/null +++ b/src/ConstantTimeDispatcher.js @@ -0,0 +1,62 @@ +/* +Copyright 2017 Vector Creations 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. +*/ + +// singleton which dispatches invocations of a given type & argument +// rather than just a type (as per EventEmitter and Flux's dispatcher etc) +// +// This means you can have a single point which listens for an EventEmitter event +// and then dispatches out to one of thousands of RoomTiles (for instance) rather than +// having each RoomTile register for the EventEmitter event and having to +// iterate over all of them. +class ConstantTimeDispatcher { + constructor() { + // type -> arg -> [ listener(arg, params) ] + this.listeners = {}; + } + + register(type, arg, listener) { + if (!this.listeners[type]) this.listeners[type] = {}; + if (!this.listeners[type][arg]) this.listeners[type][arg] = []; + this.listeners[type][arg].push(listener); + } + + unregister(type, arg, listener) { + if (this.listeners[type] && this.listeners[type][arg]) { + var i = this.listeners[type][arg].indexOf(listener); + if (i > -1) { + this.listeners[type][arg].splice(i, 1); + } + } + else { + console.warn("Unregistering unrecognised listener (type=" + type + ", arg=" + arg + ")"); + } + } + + dispatch(type, arg, params) { + if (!this.listeners[type] || !this.listeners[type][arg]) { + console.warn("No registered listeners for dispatch (type=" + type + ", arg=" + arg + ")"); + return; + } + this.listeners[type][arg].forEach(listener=>{ + listener.call(arg, params); + }); + } +} + +if (!global.constantTimeDispatcher) { + global.constantTimeDispatcher = new ConstantTimeDispatcher(); +} +module.exports = global.constantTimeDispatcher; diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 8cd820c284..296565488c 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -523,6 +523,9 @@ var TimelinePanel = React.createClass({ this.props.timelineSet.room.setUnreadNotificationCount('highlight', 0); dis.dispatch({ action: 'on_room_read', + payload: { + room: this.props.timelineSet.room + } }); } } diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 0da741df19..2a70f14724 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -28,6 +28,7 @@ var rate_limited_func = require('../../../ratelimitedfunc'); var Rooms = require('../../../Rooms'); import DMRoomMap from '../../../utils/DMRoomMap'; var Receipt = require('../../../utils/Receipt'); +var constantTimeDispatcher = require('../../../ConstantTimeDispatcher'); var HIDE_CONFERENCE_CHANS = true; @@ -57,13 +58,16 @@ module.exports = React.createClass({ cli.on("Room.name", this.onRoomName); cli.on("Room.tags", this.onRoomTags); cli.on("Room.receipt", this.onRoomReceipt); - cli.on("RoomState.events", this.onRoomStateEvents); + // cli.on("RoomState.events", this.onRoomStateEvents); cli.on("RoomMember.name", this.onRoomMemberName); cli.on("accountData", this.onAccountData); var s = this.getRoomLists(); this.setState(s); + // lookup for which lists a given roomId is currently in. + this.listsForRoomId = {}; + this.focusedRoomTileRoomId = null; }, @@ -100,12 +104,13 @@ module.exports = React.createClass({ } break; case 'on_room_read': - // Force an update because the notif count state is too deep to cause - // an update. This forces the local echo of reading notifs to be - // reflected by the RoomTiles. - // - // FIXME: we should surely just be refreshing the right tile... - this.forceUpdate(); + // poke the right RoomTile to refresh, using the constantTimeDispatcher + // to avoid each and every RoomTile registering to the 'on_room_read' event + // XXX: if we like the constantTimeDispatcher we might want to dispatch + // directly from TimelinePanel rather than needlessly bouncing via here. + constantTimeDispatcher.dispatch( + "RoomTile.refresh", payload.room.roomId, {} + ); break; } }, @@ -119,7 +124,7 @@ module.exports = React.createClass({ MatrixClientPeg.get().removeListener("Room.name", this.onRoomName); MatrixClientPeg.get().removeListener("Room.tags", this.onRoomTags); MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt); - MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents); + // MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents); MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName); MatrixClientPeg.get().removeListener("accountData", this.onAccountData); } @@ -130,10 +135,14 @@ module.exports = React.createClass({ }, onRoom: function(room) { + // XXX: this happens rarely; ideally we should only update the correct + // sublists when it does (e.g. via a constantTimeDispatch to the right sublist) this._delayedRefreshRoomList(); }, onDeleteRoom: function(roomId) { + // XXX: this happens rarely; ideally we should only update the correct + // sublists when it does (e.g. via a constantTimeDispatch to the right sublist) this._delayedRefreshRoomList(); }, @@ -194,35 +203,60 @@ module.exports = React.createClass({ if (toStartOfTimeline) return; if (!room) return; if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return; - this._delayedRefreshRoomList(); + + // rather than regenerate our full roomlists, which is very heavy, we poke the + // correct sublists to just re-sort themselves. This isn't enormously reacty, + // but is much faster than the default react reconciler, or having to do voodoo + // with shouldComponentUpdate and a pleaseRefresh property or similar. + var lists = this.listsByRoomId[room.roomId]; + if (lists) { + lists.forEach(list=>{ + constantTimeDispatcher.dispatch("RoomSubList.sort", list, { room: room }); + }); + } }, onRoomReceipt: function(receiptEvent, room) { // because if we read a notification, it will affect notification count // only bother updating if there's a receipt from us if (Receipt.findReadReceiptFromUserId(receiptEvent, MatrixClientPeg.get().credentials.userId)) { - this._delayedRefreshRoomList(); + var lists = this.listsByRoomId[room.roomId]; + if (lists) { + lists.forEach(list=>{ + constantTimeDispatcher.dispatch( + "RoomSubList.refreshHeader", list, { room: room } + ); + }); + } } }, onRoomName: function(room) { - this._delayedRefreshRoomList(); + constantTimeDispatcher.dispatch( + "RoomTile.refresh", room.roomId, {} + ); }, onRoomTags: function(event, room) { + // XXX: this happens rarely; ideally we should only update the correct + // sublists when it does (e.g. via a constantTimeDispatch to the right sublist) this._delayedRefreshRoomList(); }, - onRoomStateEvents: function(ev, state) { - this._delayedRefreshRoomList(); - }, + // onRoomStateEvents: function(ev, state) { + // this._delayedRefreshRoomList(); + // }, onRoomMemberName: function(ev, member) { - this._delayedRefreshRoomList(); + constantTimeDispatcher.dispatch( + "RoomTile.refresh", member.room.roomId, {} + ); }, onAccountData: function(ev) { if (ev.getType() == 'm.direct') { + // XXX: this happens rarely; ideally we should only update the correct + // sublists when it does (e.g. via a constantTimeDispatch to the right sublist) this._delayedRefreshRoomList(); } }, @@ -244,12 +278,10 @@ module.exports = React.createClass({ // (!this._lastRefreshRoomListTs ? "-" : (Date.now() - this._lastRefreshRoomListTs)) // ); - // TODO: rather than bluntly regenerating and re-sorting everything - // every time we see any kind of room change from the JS SDK - // we could do incremental updates on our copy of the state - // based on the room which has actually changed. This would stop - // us re-rendering all the sublists every time anything changes anywhere - // in the state of the client. + // TODO: ideally we'd calculate this once at start, and then maintain + // any changes to it incrementally, updating the appropriate sublists + // as needed. + // Alternatively we'd do something magical with Immutable.js or similar. this.setState(this.getRoomLists()); // this._lastRefreshRoomListTs = Date.now(); @@ -266,18 +298,19 @@ module.exports = React.createClass({ s.lists["m.lowpriority"] = []; s.lists["im.vector.fake.archived"] = []; + this.listsForRoomId = {}; + const dmRoomMap = new DMRoomMap(MatrixClientPeg.get()); MatrixClientPeg.get().getRooms().forEach(function(room) { const me = room.getMember(MatrixClientPeg.get().credentials.userId); if (!me) return; - // console.log("room = " + room.name + ", me.membership = " + me.membership + // ", sender = " + me.events.member.getSender() + // ", target = " + me.events.member.getStateKey() + // ", prevMembership = " + me.events.member.getPrevContent().membership); - if (me.membership == "invite") { + self.listsForRoomId[room.roomId].push("im.vector.fake.invite"); s.lists["im.vector.fake.invite"].push(room); } else if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, me, self.props.ConferenceHandler)) { @@ -288,23 +321,26 @@ module.exports = React.createClass({ { // Used to split rooms via tags var tagNames = Object.keys(room.tags); - if (tagNames.length) { for (var i = 0; i < tagNames.length; i++) { var tagName = tagNames[i]; s.lists[tagName] = s.lists[tagName] || []; s.lists[tagNames[i]].push(room); + self.listsForRoomId[room.roomId].push(tagNames[i]); } } else if (dmRoomMap.getUserIdForRoomId(room.roomId)) { // "Direct Message" rooms (that we're still in and that aren't otherwise tagged) + self.listsForRoomId[room.roomId].push("im.vector.fake.direct"); s.lists["im.vector.fake.direct"].push(room); } else { + self.listsForRoomId[room.roomId].push("im.vector.fake.recent"); s.lists["im.vector.fake.recent"].push(room); } } else if (me.membership === "leave") { + self.listsForRoomId[room.roomId].push("im.vector.fake.archived"); s.lists["im.vector.fake.archived"].push(room); } else { @@ -325,8 +361,10 @@ module.exports = React.createClass({ const me = room.getMember(MatrixClientPeg.get().credentials.userId); if (me && Rooms.looksLikeDirectMessageRoom(room, me)) { + self.listsForRoomId[room.roomId].push("im.vector.fake.direct"); s.lists["im.vector.fake.direct"].push(room); } else { + self.listsForRoomId[room.roomId].push("im.vector.fake.recent"); s.lists["im.vector.fake.recent"].push(room); } } @@ -343,6 +381,8 @@ module.exports = React.createClass({ newMDirectEvent[otherPerson.userId] = roomList; } + console.warn("Resetting room DM state to be " + JSON.stringify(newMDirectEvent)); + // if this fails, fine, we'll just do the same thing next time we get the room lists MatrixClientPeg.get().setAccountData('m.direct', newMDirectEvent).done(); } diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index cff5c2f623..ac682f710a 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -27,6 +27,7 @@ var RoomNotifs = require('../../../RoomNotifs'); var FormattingUtils = require('../../../utils/FormattingUtils'); import AccessibleButton from '../elements/AccessibleButton'; var UserSettingsStore = require('../../../UserSettingsStore'); +var constantTimeDispatcher = require('../../../ConstantTimeDispatcher'); module.exports = React.createClass({ displayName: 'RoomTile', @@ -89,16 +90,22 @@ module.exports = React.createClass({ }, componentWillMount: function() { + constantTimeDispatcher.register("RoomTile.refresh", this.props.room.roomId, this.onRefresh); MatrixClientPeg.get().on("accountData", this.onAccountData); }, componentWillUnmount: function() { + constantTimeDispatcher.unregister("RoomTile.refresh", this.props.room.roomId, this.onRefresh); var cli = MatrixClientPeg.get(); if (cli) { MatrixClientPeg.get().removeListener("accountData", this.onAccountData); } }, + onRefresh: function() { + this.forceUpdate(); + }, + onClick: function() { if (this.props.onClick) { this.props.onClick(this.props.room.roomId); From 9591ad31e6c95d7748072702c45d10bdb1c4d841 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 18 Apr 2017 02:43:29 +0100 Subject: [PATCH 139/284] fix bugs, experiment with focus pulling, make it vaguely work --- src/components/views/rooms/RoomList.js | 143 +++++++++++++++++++++++-- src/components/views/rooms/RoomTile.js | 4 +- 2 files changed, 136 insertions(+), 11 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 2a70f14724..cb692ff253 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -27,6 +27,7 @@ var sdk = require('../../../index'); var rate_limited_func = require('../../../ratelimitedfunc'); var Rooms = require('../../../Rooms'); import DMRoomMap from '../../../utils/DMRoomMap'; +import KeyCode from '../../../KeyCode'; var Receipt = require('../../../utils/Receipt'); var constantTimeDispatcher = require('../../../ConstantTimeDispatcher'); @@ -68,7 +69,13 @@ module.exports = React.createClass({ // lookup for which lists a given roomId is currently in. this.listsForRoomId = {}; - this.focusedRoomTileRoomId = null; + // order of the sublists + this.listOrder = []; + + // this.focusedRoomTileRoomId = null; + this.focusedElement = null; + // this.focusedPosition = null; + // this.focusMoving = false; }, componentDidMount: function() { @@ -170,7 +177,7 @@ module.exports = React.createClass({ }, _onKeyDown: function(ev) { - if (!this.focusedRoomTileRoomId) return; + if (!this.focusedElement) return; let handled = false; switch (ev.keyCode) { @@ -191,7 +198,61 @@ module.exports = React.createClass({ }, _onMoveFocus: function(up) { + // cheat and move focus by faking tab/shift-tab. This lets us do things + // like collapse/uncollapse room headers & truncated lists without having + // to reimplement the entirety of the keyboard navigation logic. + // + // this simply doens't work, as for security apparently you can't inject + // UI events any more - c.f. this note from + // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent + // + // Note: manually firing an event does not generate the default action + // associated with that event. For example, manually firing a key event + // does not cause that letter to appear in a focused text input. In the + // case of UI events, this is important for security reasons, as it + // prevents scripts from simulating user actions that interact with the + // browser itself. +/* + var event = document.createEvent('Event'); + event.initEvent('keydown', true, true); + event.keyCode = 9; + event.shiftKey = up ? true : false; + document.dispatchEvent(event); +*/ + // alternatively, this is the beginning of moving the focus through the list, + // navigating the pure datastructure of the list contents, but doesn't let + // you navigate through other things +/* + this.focusMoving = true; + if (this.focusPosition) { + if (up) { + this.focusPosition.index++; + if (this.focusPosition.index > this.listsForRoomId[this.focusPosition.list].length) { + // move to the next sublist + } + } + else { + this.focusPosition.index--; + if (this.focusPosition.index < 0) { + // move to the previous sublist + } + } + } +*/ + // alternatively, we can just try to manually implementing the focus switch at the DOM level. + // ignores tabindex. + var element = this.focusedElement; + if (up) { + element = element.parentElement.previousElementSibling.firstElementChild; + } + else { + element = element.parentElement.nextElementSibling.firstElementChild; + } + + if (element) { + element.focus(); + } }, onSubListHeaderClick: function(isHidden, scrollToPosition) { @@ -208,19 +269,27 @@ module.exports = React.createClass({ // correct sublists to just re-sort themselves. This isn't enormously reacty, // but is much faster than the default react reconciler, or having to do voodoo // with shouldComponentUpdate and a pleaseRefresh property or similar. - var lists = this.listsByRoomId[room.roomId]; + var lists = this.listsForRoomId[room.roomId]; if (lists) { lists.forEach(list=>{ constantTimeDispatcher.dispatch("RoomSubList.sort", list, { room: room }); }); } + +/* + if (this.focusPosition && lists.indexOf(this.focusPosition.list) > -1) { + // if we're reordering the list which currently have focus, recalculate + // our focus offset + this.focusPosition = null; + } +*/ }, onRoomReceipt: function(receiptEvent, room) { // because if we read a notification, it will affect notification count // only bother updating if there's a receipt from us if (Receipt.findReadReceiptFromUserId(receiptEvent, MatrixClientPeg.get().credentials.userId)) { - var lists = this.listsByRoomId[room.roomId]; + var lists = this.listsForRoomId[room.roomId]; if (lists) { lists.forEach(list=>{ constantTimeDispatcher.dispatch( @@ -274,6 +343,12 @@ module.exports = React.createClass({ }, 500), refreshRoomList: function() { +/* + // if we're regenerating the list, then the chances are the contents + // or ordering is changing - forget our cached focus position + this.focusPosition = null; +*/ + // console.log("DEBUG: Refresh room list delta=%s ms", // (!this._lastRefreshRoomListTs ? "-" : (Date.now() - this._lastRefreshRoomListTs)) // ); @@ -299,16 +374,23 @@ module.exports = React.createClass({ s.lists["im.vector.fake.archived"] = []; this.listsForRoomId = {}; + var otherTagNames = {}; const dmRoomMap = new DMRoomMap(MatrixClientPeg.get()); MatrixClientPeg.get().getRooms().forEach(function(room) { const me = room.getMember(MatrixClientPeg.get().credentials.userId); if (!me) return; + // console.log("room = " + room.name + ", me.membership = " + me.membership + // ", sender = " + me.events.member.getSender() + // ", target = " + me.events.member.getStateKey() + // ", prevMembership = " + me.events.member.getPrevContent().membership); + + if (!self.listsForRoomId[room.roomId]) { + self.listsForRoomId[room.roomId] = []; + } + if (me.membership == "invite") { self.listsForRoomId[room.roomId].push("im.vector.fake.invite"); s.lists["im.vector.fake.invite"].push(room); @@ -325,8 +407,9 @@ module.exports = React.createClass({ for (var i = 0; i < tagNames.length; i++) { var tagName = tagNames[i]; s.lists[tagName] = s.lists[tagName] || []; - s.lists[tagNames[i]].push(room); - self.listsForRoomId[room.roomId].push(tagNames[i]); + s.lists[tagName].push(room); + self.listsForRoomId[room.roomId].push(tagName); + otherTagNames[tagName] = 1; } } else if (dmRoomMap.getUserIdForRoomId(room.roomId)) { @@ -391,6 +474,21 @@ module.exports = React.createClass({ // we actually apply the sorting to this when receiving the prop in RoomSubLists. + // we'll need this when we get to iterating through lists programatically - e.g. ctrl-shift-up/down +/* + this.listOrder = [ + "im.vector.fake.invite", + "m.favourite", + "im.vector.fake.recent", + "im.vector.fake.direct", + Object.keys(otherTagNames).filter(tagName=>{ + return (!tagName.match(/^m\.(favourite|lowpriority)$/)); + }).sort(), + "m.lowpriority", + "im.vector.fake.archived" + ]; +*/ + return s; }, @@ -542,8 +640,35 @@ module.exports = React.createClass({ this.refs.gemscroll.forceUpdate(); }, - onRoomTileFocus: function(roomId) { - this.focusedRoomTileRoomId = roomId; + onRoomTileFocus: function(roomId, event) { + // this.focusedRoomTileRoomId = roomId; + this.focusedElement = event ? event.target : null; + + /* + if (roomId && !this.focusPosition) { + var list = this.listsForRoomId[roomId]; + if (list) { + console.warn("Focused to room " + roomId + " not in a list?!"); + } + else { + this.focusPosition = { + list: list, + index: this.state.lists[list].findIndex(room=>{ + return room.roomId == roomId; + }), + }; + } + } + + if (!roomId) { + if (this.focusMoving) { + this.focusMoving = false; + } + else { + this.focusPosition = null; + } + } + */ }, render: function() { @@ -608,7 +733,7 @@ module.exports = React.createClass({ onRoomTileFocus={ self.onRoomTileFocus } onShowMoreRooms={ self.onShowMoreRooms } /> - { Object.keys(self.state.lists).map(function(tagName) { + { Object.keys(self.state.lists).sort().map(function(tagName) { if (!tagName.match(/^(m\.(favourite|lowpriority)|im\.vector\.fake\.(invite|recent|direct|archived))$/)) { return Date: Tue, 18 Apr 2017 14:44:43 +0100 Subject: [PATCH 140/284] m.read_marker -> m.fully_read --- src/components/structures/TimelinePanel.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 9277c3f2b7..162c474a25 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -120,7 +120,7 @@ var TimelinePanel = React.createClass({ // but for now we just do it per room for simplicity. let initialReadMarker = null; if (this.props.manageReadMarkers) { - const readmarker = this.props.timelineSet.room.getAccountData('m.read_marker'); + const readmarker = this.props.timelineSet.room.getAccountData('m.fully_read'); if (readmarker){ initialReadMarker = readmarker.getContent().marker; } else { @@ -476,7 +476,7 @@ var TimelinePanel = React.createClass({ // ignore events for other rooms if (room !== this.props.timelineSet.room) return; - if (ev.getType() !== "m.read_marker") return; + if (ev.getType() !== "m.fully_read") return; const markerEventId = ev.getContent().marker; console.log('TimelinePanel: Read marker received from server', markerEventId); From d33afa99ab25f85b2932f7d9c9621347eabaa40c Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 18 Apr 2017 15:13:05 +0100 Subject: [PATCH 141/284] marker -> event_id --- src/components/structures/TimelinePanel.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 162c474a25..74cf549c4d 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -122,7 +122,7 @@ var TimelinePanel = React.createClass({ if (this.props.manageReadMarkers) { const readmarker = this.props.timelineSet.room.getAccountData('m.fully_read'); if (readmarker){ - initialReadMarker = readmarker.getContent().marker; + initialReadMarker = readmarker.getContent().event_id; } else { initialReadMarker = this._getCurrentReadReceipt(); } @@ -478,7 +478,7 @@ var TimelinePanel = React.createClass({ if (ev.getType() !== "m.fully_read") return; - const markerEventId = ev.getContent().marker; + const markerEventId = ev.getContent().event_id; console.log('TimelinePanel: Read marker received from server', markerEventId); this.setState({ @@ -1045,7 +1045,6 @@ var TimelinePanel = React.createClass({ // 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); - return (
      ; }, + _showSpoiler: function(event) { + const target = event.target; + const hidden = target.getAttribute('data-spoiler'); + + target.innerHTML = hidden; + + const range = document.createRange(); + range.selectNodeContents(target); + + const selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + }, + nameForMedium: function(medium) { if (medium == 'msisdn') return 'Phone'; return medium[0].toUpperCase() + medium.slice(1); @@ -958,6 +972,9 @@ module.exports = React.createClass({
      Logged in as {this._me}
      +
      + Access Token: <click to reveal> +
      Homeserver is { MatrixClientPeg.get().getHomeserverUrl() }
      From 015a4480e29feb6e7aa9545947b6a03faf7b7ce0 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 18 Apr 2017 22:36:54 +0100 Subject: [PATCH 148/284] oops, wire up Room.receipt again, and refresh roomtiles on Room.timeline --- src/components/views/rooms/RoomList.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 739c288598..64871d1c0f 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -56,6 +56,7 @@ module.exports = React.createClass({ cli.on("Room.timeline", this.onRoomTimeline); cli.on("Room.name", this.onRoomName); cli.on("Room.tags", this.onRoomTags); + cli.on("Room.receipt", this.onRoomReceipt); // cli.on("RoomState.events", this.onRoomStateEvents); cli.on("RoomMember.name", this.onRoomMemberName); cli.on("accountData", this.onAccountData); @@ -147,6 +148,7 @@ module.exports = React.createClass({ MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline); MatrixClientPeg.get().removeListener("Room.name", this.onRoomName); MatrixClientPeg.get().removeListener("Room.tags", this.onRoomTags); + MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt); // MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents); MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName); MatrixClientPeg.get().removeListener("accountData", this.onAccountData); @@ -212,6 +214,11 @@ module.exports = React.createClass({ constantTimeDispatcher.dispatch("RoomSubList.sort", list, { room: room }); }); } + + // we have to explicitly hit the roomtile which just changed + constantTimeDispatcher.dispatch( + "RoomTile.refresh", room.roomId, {} + ); }, onRoomReceipt: function(receiptEvent, room) { @@ -226,6 +233,11 @@ module.exports = React.createClass({ ); }); } + + // we have to explicitly hit the roomtile which just changed + constantTimeDispatcher.dispatch( + "RoomTile.refresh", room.roomId, {} + ); } }, From 8389a67c758d4b9dc352b816eefdf388bed7d938 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 18 Apr 2017 22:54:30 +0100 Subject: [PATCH 149/284] we don't need RoomTile specific focus in the end --- src/components/views/rooms/RoomList.js | 11 ----------- src/components/views/rooms/RoomTile.js | 10 +--------- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 64871d1c0f..25e19da770 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -575,10 +575,6 @@ module.exports = React.createClass({ this.refs.gemscroll.forceUpdate(); }, - onRoomTileFocus: function(roomId, event) { - this.focusedElement = event ? event.target : null; - }, - render: function() { var RoomSubList = sdk.getComponent('structures.RoomSubList'); var self = this; @@ -595,7 +591,6 @@ module.exports = React.createClass({ collapsed={ self.props.collapsed } searchFilter={ self.props.searchFilter } onHeaderClick={ self.onSubListHeaderClick } - onRoomTileFocus={ self.onRoomTileFocus } onShowMoreRooms={ self.onShowMoreRooms } /> { Object.keys(self.state.lists).sort().map(function(tagName) { @@ -650,7 +642,6 @@ module.exports = React.createClass({ collapsed={ self.props.collapsed } searchFilter={ self.props.searchFilter } onHeaderClick={ self.onSubListHeaderClick } - onRoomTileFocus={ self.onRoomTileFocus } onShowMoreRooms={ self.onShowMoreRooms } />; } @@ -666,7 +657,6 @@ module.exports = React.createClass({ collapsed={ self.props.collapsed } searchFilter={ self.props.searchFilter } onHeaderClick={ self.onSubListHeaderClick } - onRoomTileFocus={ self.onRoomTileFocus } onShowMoreRooms={ self.onShowMoreRooms } />
      diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index db997fff3e..f18df52eee 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -37,7 +37,6 @@ module.exports = React.createClass({ connectDragSource: React.PropTypes.func, connectDropTarget: React.PropTypes.func, onClick: React.PropTypes.func, - onFocus: React.PropTypes.func, isDragging: React.PropTypes.bool, room: React.PropTypes.object.isRequired, @@ -121,12 +120,6 @@ module.exports = React.createClass({ } }, - onFocus: function(event) { - if (this.props.onFocus) { - this.props.onFocus(this.props.room.roomId, event); - } - }, - onMouseEnter: function() { this.setState( { hover : true }); this.badgeOnMouseEnter(); @@ -279,8 +272,7 @@ module.exports = React.createClass({ let ret = (
      { /* Only native elements can be wrapped in a DnD object. */} + onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
      From 093b9a0b52a3dfbee682f224e4fe6ca23565a38f Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 18 Apr 2017 23:29:28 +0100 Subject: [PATCH 150/284] kick the roomtile on RoomState.members --- src/components/views/rooms/RoomList.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 25e19da770..e510de08a4 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -57,7 +57,7 @@ module.exports = React.createClass({ cli.on("Room.name", this.onRoomName); cli.on("Room.tags", this.onRoomTags); cli.on("Room.receipt", this.onRoomReceipt); - // cli.on("RoomState.events", this.onRoomStateEvents); + cli.on("RoomState.members", this.onRoomStateMember); cli.on("RoomMember.name", this.onRoomMemberName); cli.on("accountData", this.onAccountData); @@ -149,7 +149,7 @@ module.exports = React.createClass({ MatrixClientPeg.get().removeListener("Room.name", this.onRoomName); MatrixClientPeg.get().removeListener("Room.tags", this.onRoomTags); MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt); - // MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents); + MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember); MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName); MatrixClientPeg.get().removeListener("accountData", this.onAccountData); } @@ -253,9 +253,11 @@ module.exports = React.createClass({ this._delayedRefreshRoomList(); }, - // onRoomStateEvents: function(ev, state) { - // this._delayedRefreshRoomList(); - // }, + onRoomStateMember: function(ev, state, member) { + constantTimeDispatcher.dispatch( + "RoomTile.refresh", member.roomId, {} + ); + }, onRoomMemberName: function(ev, member) { constantTimeDispatcher.dispatch( From abf2300c0d37a8785ff9813dca3972e9e7a090b5 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 19 Apr 2017 00:09:03 +0100 Subject: [PATCH 151/284] highlight invites correctly --- src/components/views/rooms/RoomTile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index f18df52eee..31ffdf7e12 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -104,7 +104,7 @@ module.exports = React.createClass({ onRefresh: function(params) { this.setState({ unread: Unread.doesRoomHaveUnreadMessages(this.props.room), - highlight: this.props.room.getUnreadNotificationCount('highlight') > 0 || this.props.label === 'Invites', + highlight: this.props.room.getUnreadNotificationCount('highlight') > 0 || this.props.isInvite, }); }, From 4a9c16868249a6685e2219189fe2a7186d168362 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 19 Apr 2017 00:13:01 +0100 Subject: [PATCH 152/284] fix invite highlights --- src/components/views/rooms/RoomTile.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 31ffdf7e12..dc2d9a4b25 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -101,6 +101,10 @@ module.exports = React.createClass({ } }, + componentWillReceiveProps: function(nextProps) { + this.onRefresh(); + }, + onRefresh: function(params) { this.setState({ unread: Unread.doesRoomHaveUnreadMessages(this.props.room), From fb6252a16b6d324a7e7b9cf99c716d8eaf2050b7 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 19 Apr 2017 00:16:17 +0100 Subject: [PATCH 153/284] fix invite highlights take 3 --- src/components/views/rooms/RoomTile.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index dc2d9a4b25..1f6063e37c 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -90,6 +90,7 @@ module.exports = React.createClass({ constantTimeDispatcher.register("RoomTile.refresh", this.props.room.roomId, this.onRefresh); constantTimeDispatcher.register("RoomTile.select", this.props.room.roomId, this.onSelect); MatrixClientPeg.get().on("accountData", this.onAccountData); + this.onRefresh(); }, componentWillUnmount: function() { From 566a31524271cc50a76f7af25866468f0eb09911 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 19 Apr 2017 10:08:04 +0100 Subject: [PATCH 154/284] Initial commit on riot-web#3524 (login UI update) --- src/components/views/elements/Dropdown.js | 21 ++++--- src/components/views/login/CountryDropdown.js | 2 +- src/components/views/login/PasswordLogin.js | 59 +++++++++++++------ 3 files changed, 55 insertions(+), 27 deletions(-) diff --git a/src/components/views/elements/Dropdown.js b/src/components/views/elements/Dropdown.js index 3b34d3cac1..907d4b0905 100644 --- a/src/components/views/elements/Dropdown.js +++ b/src/components/views/elements/Dropdown.js @@ -249,7 +249,7 @@ export default class Dropdown extends React.Component { ); }); - if (!this.state.searchQuery) { + if (!this.state.searchQuery && this.props.searchEnabled) { options.push(
      Type to search... @@ -267,16 +267,20 @@ export default class Dropdown extends React.Component { let menu; if (this.state.expanded) { - currentValue = ; + if (this.props.searchEnabled) { + currentValue = ; + } menu =
      {this._getMenuOptions()}
      ; - } else { + } + + if (!currentValue) { const selectedChild = this.props.getShortOption ? this.props.getShortOption(this.props.value) : this.childrenByKey[this.props.value]; @@ -313,6 +317,7 @@ Dropdown.propTypes = { onOptionChange: React.PropTypes.func.isRequired, // Called when the value of the search field changes onSearchChange: React.PropTypes.func, + searchEnabled: React.PropTypes.boolean, // Function that, given the key of an option, returns // a node representing that option to be displayed in the // box itself as the currently-selected option (ie. as diff --git a/src/components/views/login/CountryDropdown.js b/src/components/views/login/CountryDropdown.js index 9729c9e23f..be1ed51b5e 100644 --- a/src/components/views/login/CountryDropdown.js +++ b/src/components/views/login/CountryDropdown.js @@ -111,7 +111,7 @@ export default class CountryDropdown extends React.Component { return {options} diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index 61cb3da652..002de0c2ba 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.js @@ -60,6 +60,7 @@ module.exports = React.createClass({displayName: 'PasswordLogin', password: this.props.initialPassword, phoneCountry: this.props.initialPhoneCountry, phoneNumber: this.props.initialPhoneNumber, + loginType: "mxid", }; }, @@ -88,6 +89,10 @@ module.exports = React.createClass({displayName: 'PasswordLogin', this.props.onUsernameChanged(ev.target.value); }, + onLoginTypeChange: function(loginType) { + this.setState({loginType: loginType}); + }, + onPhoneCountryChanged: function(country) { this.setState({phoneCountry: country}); this.props.onPhoneCountryChanged(country); @@ -120,28 +125,46 @@ module.exports = React.createClass({displayName: 'PasswordLogin', }); const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); - return ( -
      -
      + const Dropdown = sdk.getComponent('elements.Dropdown'); + + const loginType = { + 'email': - or -
      - - + placeholder="Email or user name" autoFocus />, + 'mxid': + , + 'phone':
      + + +
      + }[this.state.loginType]; + + return ( +
      + +
      + + + Matrix ID + Email + Phone +
      -
      + {loginType} {this._passwordField = e;}} type="password" name="password" value={this.state.password} onChange={this.onPasswordChanged} From 81bdfe2126fe9375b937aa1ec39f7acc61e221b2 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 19 Apr 2017 10:14:57 +0100 Subject: [PATCH 155/284] Update to match renamed API --- src/components/structures/TimelinePanel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 74cf549c4d..9dc1b2dead 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -531,7 +531,7 @@ var TimelinePanel = React.createClass({ this.last_rr_sent_event_id = lastReadEvent.getId(); this.last_rm_sent_event_id = this.state.readMarkerEventId; - MatrixClientPeg.get().setRoomReadMarker( + MatrixClientPeg.get().setRoomReadMarkers( this.props.timelineSet.room.roomId, this.state.readMarkerEventId, lastReadEvent From 28818b857acbd0505c66a418c7b56e6b95c395ad Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 19 Apr 2017 10:17:44 +0100 Subject: [PATCH 156/284] Remove log --- src/components/structures/TimelinePanel.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 9dc1b2dead..34f492c585 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -126,7 +126,6 @@ var TimelinePanel = React.createClass({ } else { initialReadMarker = this._getCurrentReadReceipt(); } - console.info('Read marker initially', initialReadMarker); } return { From e32f153573cc36fac793c2253cbb7d3485858615 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 19 Apr 2017 10:18:25 +0100 Subject: [PATCH 157/284] Remove Room.accountData listener on unmount --- src/components/structures/TimelinePanel.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 34f492c585..a9c063b2fa 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -249,6 +249,7 @@ var TimelinePanel = React.createClass({ client.removeListener("Room.redaction", this.onRoomRedaction); client.removeListener("Room.receipt", this.onRoomReceipt); client.removeListener("Room.localEchoUpdated", this.onLocalEchoUpdated); + client.removeListener("Room.accountData", this.onAccountData); } }, From 00cf5b59183252a2650de970566a97c83d2bc21d Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 19 Apr 2017 10:20:24 +0100 Subject: [PATCH 158/284] Revert change --- src/components/structures/TimelinePanel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index a9c063b2fa..4657548a3c 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -418,9 +418,9 @@ var TimelinePanel = React.createClass({ // we know we're stuckAtBottom, so we can advance the RM // immediately, to save a later render cycle - // This call will setState with readMarkerEventId = lastEv.getId() this._setReadMarker(lastEv.getId(), lastEv.getTs(), true); updatedState.readMarkerVisible = false; + updatedState.readMarkerEventId = lastEv.getId(); callback = this.props.onReadMarkerUpdated; } } From a787ee848065adb134d7007ce3cbf15d4f14e35f Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 19 Apr 2017 10:20:53 +0100 Subject: [PATCH 159/284] Remove spammy log --- src/components/structures/TimelinePanel.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 4657548a3c..7d202b7a85 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -478,11 +478,8 @@ var TimelinePanel = React.createClass({ if (ev.getType() !== "m.fully_read") return; - const markerEventId = ev.getContent().event_id; - console.log('TimelinePanel: Read marker received from server', markerEventId); - this.setState({ - readMarkerEventId: markerEventId, + readMarkerEventId: ev.getContent().event_id, }, this.props.onReadMarkerUpdated); }, From 81bf2be13b96baff2796690d799f7444bb8262f6 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 19 Apr 2017 10:27:43 +0100 Subject: [PATCH 160/284] Make note of inconsistant roomReadMarkerTsMap This will become redundant when there is server support for directionality of the RM --- src/components/structures/TimelinePanel.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 7d202b7a85..5a52d57f17 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -478,6 +478,9 @@ var TimelinePanel = React.createClass({ if (ev.getType() !== "m.fully_read") return; + // XXX: roomReadMarkerTsMap not updated here so it is now inconsistent. Replace + // this mechanism of determining where the RM is relative to the view-port with + // one supported by the server (the client needs more than an event ID). this.setState({ readMarkerEventId: ev.getContent().event_id, }, this.props.onReadMarkerUpdated); From edeaef8c2f163ca8053ab817a37e49c0f142cbec Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 19 Apr 2017 10:28:38 +0100 Subject: [PATCH 161/284] Initialise last_rm_sent_event_id --- src/components/structures/TimelinePanel.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 5a52d57f17..92aeb7cc66 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -174,6 +174,7 @@ var TimelinePanel = React.createClass({ debuglog("TimelinePanel: mounting"); this.last_rr_sent_event_id = undefined; + this.last_rm_sent_event_id = undefined; this.dispatcherRef = dis.register(this.onAction); MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); From a4ba5f041c1f80d942984e6ed7fdc25ed022beea Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 19 Apr 2017 10:46:08 +0100 Subject: [PATCH 162/284] Remove log, reinstate comment --- src/components/structures/TimelinePanel.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 92aeb7cc66..787638f966 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -541,7 +541,6 @@ var TimelinePanel = React.createClass({ this.last_rr_sent_event_id = undefined; this.last_rm_sent_event_id = undefined; }); - console.log('TimelinePanel: Read marker sent to the server ', this.state.readMarkerEventId, ); // do a quick-reset of our unreadNotificationCount to avoid having // to wait from the remote echo from the homeserver. @@ -986,6 +985,8 @@ var TimelinePanel = React.createClass({ _setReadMarker: function(eventId, eventTs, inhibitSetState) { var roomId = this.props.timelineSet.room.roomId; + // don't update the state (and cause a re-render) if there is + // no change to the RM. if (eventId === this.state.readMarkerEventId) { return; } From 9f99224a1fee849426ca184e72eedba9c3626f32 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 19 Apr 2017 17:59:06 +0100 Subject: [PATCH 163/284] fix bugs from PR review --- src/components/views/rooms/RoomList.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index e510de08a4..3916261dda 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -75,6 +75,12 @@ module.exports = React.createClass({ this.dispatcherRef = dis.register(this.onAction); // Initialise the stickyHeaders when the component is created this._updateStickyHeaders(true); + + if (this.props.selectedRoom) { + constantTimeDispatcher.dispatch( + "RoomTile.select", this.props.selectedRoom, { selected: true } + ); + } }, componentWillReceiveProps: function(nextProps) { @@ -155,8 +161,6 @@ module.exports = React.createClass({ } // cancel any pending calls to the rate_limited_funcs this._delayedRefreshRoomList.cancelPendingCall(); - document.removeEventListener('keydown', this._onKeyDown); - }, onRoom: function(room) { From 8da07740d1efb3b3b0389b29eacae1e73c86a344 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 19 Apr 2017 23:34:29 +0100 Subject: [PATCH 164/284] bump react-gemini-scrollbar --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cb3cdfa63f..5c96a74f5b 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "react": "^15.4.0", "react-addons-css-transition-group": "15.3.2", "react-dom": "^15.4.0", - "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef", + "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#39d858c", "sanitize-html": "^1.11.1", "text-encoding-utf-8": "^1.0.1", "velocity-vector": "vector-im/velocity#059e3b2", From 90f526bdeba68c201fdd7535ace3fe23fc5034a7 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 20 Apr 2017 00:42:13 +0100 Subject: [PATCH 165/284] autofocus doesn't seem to work on this button --- src/components/views/dialogs/QuestionDialog.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/views/dialogs/QuestionDialog.js b/src/components/views/dialogs/QuestionDialog.js index 6012541b94..8e20b0d2bc 100644 --- a/src/components/views/dialogs/QuestionDialog.js +++ b/src/components/views/dialogs/QuestionDialog.js @@ -47,6 +47,12 @@ export default React.createClass({ this.props.onFinished(false); }, + componentDidMount: function() { + if (this.props.focus) { + this.refs.button.focus(); + } + }, + render: function() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const cancelButton = this.props.hasCancelButton ? ( @@ -63,7 +69,7 @@ export default React.createClass({ {this.props.description}
      - {this.props.extraButtons} From 5a3b4b6a60bcc198e8d15d15c0e8e8499522854f Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 20 Apr 2017 01:12:57 +0100 Subject: [PATCH 166/284] various bug fixes: don't redraw RoomList when the selectedRoom changes keep passing selectedRoom through to RoomTiles so they have correct initial state handle onAccountData at the RoomList, not RoomTile level Fix some typos --- src/components/views/rooms/RoomList.js | 26 ++++++++++++++++++-------- src/components/views/rooms/RoomTile.js | 21 +++++---------------- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 3916261dda..979b14eaaf 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -41,6 +41,12 @@ module.exports = React.createClass({ searchFilter: React.PropTypes.string, }, + shouldComponentUpdate: function(nextProps, nextState) { + if (nextProps.collapsed !== this.props.collapsed) return true; + if (nextProps.searchFilter !== this.props.searchFilter) return true; + return false; + }, + getInitialState: function() { return { isLoadingLeftRooms: false, @@ -75,12 +81,6 @@ module.exports = React.createClass({ this.dispatcherRef = dis.register(this.onAction); // Initialise the stickyHeaders when the component is created this._updateStickyHeaders(true); - - if (this.props.selectedRoom) { - constantTimeDispatcher.dispatch( - "RoomTile.select", this.props.selectedRoom, { selected: true } - ); - } }, componentWillReceiveProps: function(nextProps) { @@ -98,7 +98,7 @@ module.exports = React.createClass({ } }, - componentDidUpdate: function() { + componentDidUpdate: function(prevProps, prevState) { // Reinitialise the stickyHeaders when the component is updated this._updateStickyHeaders(true); this._repositionIncomingCallBox(undefined, false); @@ -265,7 +265,7 @@ module.exports = React.createClass({ onRoomMemberName: function(ev, member) { constantTimeDispatcher.dispatch( - "RoomTile.refresh", member.room.roomId, {} + "RoomTile.refresh", member.roomId, {} ); }, @@ -275,6 +275,9 @@ module.exports = React.createClass({ // sublists when it does (e.g. via a constantTimeDispatch to the right sublist) this._delayedRefreshRoomList(); } + else if (ev.getType() == 'm.push_rules') { + this._delayedRefreshRoomList(); + } }, _delayedRefreshRoomList: new rate_limited_func(function() { @@ -595,6 +598,7 @@ module.exports = React.createClass({ order="recent" incomingCall={ self.state.incomingCall } collapsed={ self.props.collapsed } + selectedRoom={ self.props.selectedRoom } searchFilter={ self.props.searchFilter } onHeaderClick={ self.onSubListHeaderClick } onShowMoreRooms={ self.onShowMoreRooms } /> @@ -607,6 +611,7 @@ module.exports = React.createClass({ order="manual" incomingCall={ self.state.incomingCall } collapsed={ self.props.collapsed } + selectedRoom={ self.props.selectedRoom } searchFilter={ self.props.searchFilter } onHeaderClick={ self.onSubListHeaderClick } onShowMoreRooms={ self.onShowMoreRooms } /> @@ -619,6 +624,7 @@ module.exports = React.createClass({ order="recent" incomingCall={ self.state.incomingCall } collapsed={ self.props.collapsed } + selectedRoom={ self.props.selectedRoom } alwaysShowHeader={ true } searchFilter={ self.props.searchFilter } onHeaderClick={ self.onSubListHeaderClick } @@ -631,6 +637,7 @@ module.exports = React.createClass({ order="recent" incomingCall={ self.state.incomingCall } collapsed={ self.props.collapsed } + selectedRoom={ self.props.selectedRoom } searchFilter={ self.props.searchFilter } onHeaderClick={ self.onSubListHeaderClick } onShowMoreRooms={ self.onShowMoreRooms } /> @@ -646,6 +653,7 @@ module.exports = React.createClass({ order="manual" incomingCall={ self.state.incomingCall } collapsed={ self.props.collapsed } + selectedRoom={ self.props.selectedRoom } searchFilter={ self.props.searchFilter } onHeaderClick={ self.onSubListHeaderClick } onShowMoreRooms={ self.onShowMoreRooms } />; @@ -661,6 +669,7 @@ module.exports = React.createClass({ order="recent" incomingCall={ self.state.incomingCall } collapsed={ self.props.collapsed } + selectedRoom={ self.props.selectedRoom } searchFilter={ self.props.searchFilter } onHeaderClick={ self.onSubListHeaderClick } onShowMoreRooms={ self.onShowMoreRooms } /> @@ -670,6 +679,7 @@ module.exports = React.createClass({ editable={ false } order="recent" collapsed={ self.props.collapsed } + selectedRoom={ self.props.selectedRoom } alwaysShowHeader={ true } startAsHidden={ true } showSpinner={ self.state.isLoadingLeftRooms } diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 1f6063e37c..5d896e8beb 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -38,6 +38,7 @@ module.exports = React.createClass({ connectDropTarget: React.PropTypes.func, onClick: React.PropTypes.func, isDragging: React.PropTypes.bool, + selectedRoom: React.PropTypes.string, room: React.PropTypes.object.isRequired, collapsed: React.PropTypes.bool.isRequired, @@ -53,10 +54,11 @@ module.exports = React.createClass({ getInitialState: function() { return({ - hover : false, - badgeHover : false, + hover: false, + badgeHover: false, menuDisplayed: false, notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId), + selected: this.props.room ? (this.props.selectedRoom === this.props.room.roomId) : false, }); }, @@ -78,28 +80,15 @@ module.exports = React.createClass({ } }, - onAccountData: function(accountDataEvent) { - if (accountDataEvent.getType() == 'm.push_rules') { - this.setState({ - notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId), - }); - } - }, - componentWillMount: function() { constantTimeDispatcher.register("RoomTile.refresh", this.props.room.roomId, this.onRefresh); constantTimeDispatcher.register("RoomTile.select", this.props.room.roomId, this.onSelect); - MatrixClientPeg.get().on("accountData", this.onAccountData); - this.onRefresh(); + this.onRefresh(); }, componentWillUnmount: function() { constantTimeDispatcher.unregister("RoomTile.refresh", this.props.room.roomId, this.onRefresh); constantTimeDispatcher.unregister("RoomTile.select", this.props.room.roomId, this.onSelect); - var cli = MatrixClientPeg.get(); - if (cli) { - MatrixClientPeg.get().removeListener("accountData", this.onAccountData); - } }, componentWillReceiveProps: function(nextProps) { From e69ea68133bb01dfd2093ffc5644edef24fbed70 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 20 Apr 2017 13:53:36 +0100 Subject: [PATCH 167/284] unbreak stack overflow which fires on tests due to mocked timers --- src/components/views/rooms/RoomList.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 979b14eaaf..3d80c335ca 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -74,7 +74,11 @@ module.exports = React.createClass({ this.setState(s); // order of the sublists - this.listOrder = []; + //this.listOrder = []; + + // loop count to stop a stack overflow if the user keeps waggling the + // mouse for >30s in a row, or if running under mocha + this._delayedRefreshRoomListLoopCount = 0 }, componentDidMount: function() { @@ -284,10 +288,12 @@ module.exports = React.createClass({ // if the mouse has been moving over the RoomList in the last 500ms // then delay the refresh further to avoid bouncing around under the // cursor - if (Date.now() - this._lastMouseOverTs > 500) { + if (Date.now() - this._lastMouseOverTs > 500 || this._delayedRefreshRoomListLoopCount > 60) { this.refreshRoomList(); + this._delayedRefreshRoomListLoopCount = 0; } else { + this._delayedRefreshRoomListLoopCount++; this._delayedRefreshRoomList(); } }, 500), From 238f59dc87195164f0b7b58e1787fb2fd00b6a38 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 20 Apr 2017 14:16:45 +0100 Subject: [PATCH 168/284] return the event from RoomTile's onClick to distinguish clicks from keypresses --- src/components/views/rooms/RoomTile.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 5d896e8beb..3b37d4608f 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -108,9 +108,9 @@ module.exports = React.createClass({ }); }, - onClick: function() { + onClick: function(ev) { if (this.props.onClick) { - this.props.onClick(this.props.room.roomId); + this.props.onClick(this.props.room.roomId, ev); } }, From 67089cb5279c80afabdce4cdb0707f0183a1185a Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 20 Apr 2017 14:34:59 +0100 Subject: [PATCH 169/284] If new RR-RM API not implemented, fallback to RR-only API --- src/components/structures/TimelinePanel.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 787638f966..e8774cec62 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -536,9 +536,16 @@ var TimelinePanel = React.createClass({ this.props.timelineSet.room.roomId, this.state.readMarkerEventId, lastReadEvent - ).catch(() => { + ).catch((e) => { + // /read_markers API is not implemented on this HS, fallback to just RR + if (e.errcode === 'M_UNRECOGNIZED') { + return MatrixClientPeg.get().sendReadReceipt( + lastReadEvent + ).catch(() => { + this.last_rr_sent_event_id = undefined; + }); + } // it failed, so allow retries next time the user is active - this.last_rr_sent_event_id = undefined; this.last_rm_sent_event_id = undefined; }); From 0d8d3c67106a3e84fd30de4016b9c852870f99b3 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 20 Apr 2017 15:15:20 +0100 Subject: [PATCH 170/284] HOW DID THIS EVER WORK? --- src/components/views/rooms/RoomList.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 3d80c335ca..c2778edc7c 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -600,6 +600,7 @@ module.exports = React.createClass({
      Date: Thu, 20 Apr 2017 15:47:59 +0100 Subject: [PATCH 171/284] oops, actually refresh roomlist when its state changes! --- src/components/views/rooms/RoomList.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index c2778edc7c..a7dda40c6e 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -44,6 +44,9 @@ module.exports = React.createClass({ shouldComponentUpdate: function(nextProps, nextState) { if (nextProps.collapsed !== this.props.collapsed) return true; if (nextProps.searchFilter !== this.props.searchFilter) return true; + if (nextState.lists !== this.props.lists || + nextState.isLoadingLeftRooms !== this.isLoadingLeftRooms || + nextState.incomingCall !== this.incomingCall) return true; return false; }, From 3d507e98409af91c9b8cadec511f6e5d253bdbed Mon Sep 17 00:00:00 2001 From: Robert Swain Date: Fri, 21 Apr 2017 00:05:52 +0200 Subject: [PATCH 172/284] (Room)?Avatar: Request 96x96 avatars on high DPI screens --- src/Avatar.js | 9 +++++---- src/components/views/avatars/RoomAvatar.js | 18 +++++++++++++----- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/Avatar.js b/src/Avatar.js index 76f5e55ff0..cb5e6965e3 100644 --- a/src/Avatar.js +++ b/src/Avatar.js @@ -22,8 +22,8 @@ module.exports = { avatarUrlForMember: function(member, width, height, resizeMethod) { var url = member.getAvatarUrl( MatrixClientPeg.get().getHomeserverUrl(), - width, - height, + window.devicePixelRatio > 1.2 ? 96 : width, + window.devicePixelRatio > 1.2 ? 96 : height, resizeMethod, false, false @@ -40,7 +40,9 @@ module.exports = { avatarUrlForUser: function(user, width, height, resizeMethod) { var url = ContentRepo.getHttpUriForMxc( MatrixClientPeg.get().getHomeserverUrl(), user.avatarUrl, - width, height, resizeMethod + window.devicePixelRatio > 1.2 ? 96 : width, + window.devicePixelRatio > 1.2 ? 96 : height, + resizeMethod ); if (!url || url.length === 0) { return null; @@ -57,4 +59,3 @@ module.exports = { return 'img/' + images[total % images.length] + '.png'; } }; - diff --git a/src/components/views/avatars/RoomAvatar.js b/src/components/views/avatars/RoomAvatar.js index bfa7575b0c..7ed7bfa9fa 100644 --- a/src/components/views/avatars/RoomAvatar.js +++ b/src/components/views/avatars/RoomAvatar.js @@ -59,7 +59,9 @@ module.exports = React.createClass({ ContentRepo.getHttpUriForMxc( MatrixClientPeg.get().getHomeserverUrl(), props.oobData.avatarUrl, - props.width, props.height, props.resizeMethod + window.devicePixelRatio > 1.2 ? 96 : props.width, + window.devicePixelRatio > 1.2 ? 96 : props.height, + props.resizeMethod ), // highest priority this.getRoomAvatarUrl(props), this.getOneToOneAvatar(props), @@ -74,7 +76,9 @@ module.exports = React.createClass({ return props.room.getAvatarUrl( MatrixClientPeg.get().getHomeserverUrl(), - props.width, props.height, props.resizeMethod, + window.devicePixelRatio > 1.2 ? 96 : props.width, + window.devicePixelRatio > 1.2 ? 96 : props.height, + props.resizeMethod, false ); }, @@ -103,14 +107,18 @@ module.exports = React.createClass({ } return theOtherGuy.getAvatarUrl( MatrixClientPeg.get().getHomeserverUrl(), - props.width, props.height, props.resizeMethod, + window.devicePixelRatio > 1.2 ? 96 : props.width, + window.devicePixelRatio > 1.2 ? 96 : props.height, + props.resizeMethod, false ); } else if (userIds.length == 1) { return mlist[userIds[0]].getAvatarUrl( MatrixClientPeg.get().getHomeserverUrl(), - props.width, props.height, props.resizeMethod, - false + window.devicePixelRatio > 1.2 ? 96 : props.width, + window.devicePixelRatio > 1.2 ? 96 : props.height, + props.resizeMethod, + false ); } else { return null; From be9b858193fe4dd47b18db4e657aad9dd1a07721 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 21 Apr 2017 01:06:00 +0100 Subject: [PATCH 173/284] focus on composer after jumping to bottom Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/RoomView.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index b22d867acf..b09b101b8a 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1254,6 +1254,7 @@ module.exports = React.createClass({ // jump down to the bottom of this room, where new events are arriving jumpToLiveTimeline: function() { this.refs.messagePanel.jumpToLiveTimeline(); + dis.dispatch({action: 'focus_composer'}); }, // jump up to wherever our read marker is From bbd1f3433683dd64119406898de205b6deaa4762 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 21 Apr 2017 03:04:34 +0100 Subject: [PATCH 174/284] Prepend REACT_SDK_VERSION with a v to match riot-web version output Add simple helper to construct version/commit hash urls var -> let/const and prepend olmVersionString with v for same reason for both matrix-react-sdk and riot-web, if unknown/local don't do anything else try to create a link to the commit hash/tag name Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/UserSettings.js | 26 ++++++++++++++++------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 892865fdf9..881817acab 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -31,10 +31,14 @@ var SdkConfig = require('../../SdkConfig'); import AccessibleButton from '../views/elements/AccessibleButton'; // if this looks like a release, use the 'version' from package.json; else use -// the git sha. -const REACT_SDK_VERSION = - 'dist' in package_json ? package_json.version : package_json.gitHead || ""; +// the git sha. Prepend version with v, to look like riot-web version +const REACT_SDK_VERSION = 'dist' in package_json ? `v${package_json.version}` : package_json.gitHead || ''; +// Simple method to help prettify GH Release Tags and Commit Hashes. +const GHVersionUrl = function(repo, token) { + const uriTail = (token.startsWith('v') && token.includes('.')) ? `releases/tag/${token}` : `commit/${token}`; + return `https://github.com/${repo}/${uriTail}`; +} // Enumerate some simple 'flip a bit' UI settings (if any). // 'id' gives the key name in the im.vector.web.settings account data event @@ -880,12 +884,12 @@ module.exports = React.createClass({
      ); } - var olmVersion = MatrixClientPeg.get().olmVersion; + const olmVersion = MatrixClientPeg.get().olmVersion; // If the olmVersion is not defined then either crypto is disabled, or // we are using a version old version of olm. We assume the former. - var olmVersionString = ""; + let olmVersionString = ""; if (olmVersion !== undefined) { - olmVersionString = olmVersion[0] + "." + olmVersion[1] + "." + olmVersion[2]; + olmVersionString = `v${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}`; } return ( @@ -965,8 +969,14 @@ module.exports = React.createClass({ Identity Server is { MatrixClientPeg.get().getIdentityServerUrl() }
      - matrix-react-sdk version: {REACT_SDK_VERSION}
      - riot-web version: {this.state.vectorVersion !== null ? this.state.vectorVersion : 'unknown'}
      + matrix-react-sdk version: {(REACT_SDK_VERSION !== '') + ? {REACT_SDK_VERSION} + : REACT_SDK_VERSION + }
      + riot-web version: {(this.state.vectorVersion !== null) + ? {this.state.vectorVersion} + : 'unknown' + }
      olm version: {olmVersionString}
      From 9cd7914ea51dbfb60f8b84a80cb800282476d3e4 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 21 Apr 2017 11:37:08 +0100 Subject: [PATCH 175/284] Finishing off the first iteration on login UI This makes the following changes: - Improve CountryDropdown by allowing all countries to be displayed at once and using PNGs for performance (trading of quality - the pngs are scaled down from 32px to 25px) - "I want to sign in with" dropdown to select login method - MXID login field that suffixes HS domain (whether custom or matrix.org) and prefixes "@" - Email field which is secretly the same as the username field but with a different placeholder - No more login flickering when changing ServerConfig (!) fixes https://github.com/vector-im/riot-web/issues/1517 This implements most of the design in https://github.com/vector-im/riot-web/issues/3524 but neglects the phone number login: ![login_with_msisdn](https://cloud.githubusercontent.com/assets/1922197/24864469/30a921fc-1dfc-11e7-95d1-76f619da1402.png) This will be updated in another PR to implement desired things: - Country code visible once a country has been selected (propbably but as a prefix to the phone number input box. - Use square flags - Move CountryDropdown above phone input and make it show the full country name when not expanded - Auto-select country based on IP --- src/HtmlUtils.js | 14 +- src/components/structures/login/Login.js | 85 +++---- src/components/views/elements/Dropdown.js | 13 +- src/components/views/login/CountryDropdown.js | 8 +- src/components/views/login/PasswordLogin.js | 210 +++++++++++------- src/components/views/login/ServerConfig.js | 28 ++- 6 files changed, 207 insertions(+), 151 deletions(-) diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index a8e20f5ec1..96934d205e 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -25,6 +25,9 @@ import emojione from 'emojione'; import classNames from 'classnames'; emojione.imagePathSVG = 'emojione/svg/'; +// Store PNG path for displaying many flags at once (for increased performance over SVG) +emojione.imagePathPNG = 'emojione/png/'; +// Use SVGs for emojis emojione.imageType = 'svg'; const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp+"+", "gi"); @@ -64,16 +67,23 @@ export function unicodeToImage(str) { * emoji. * * @param alt {string} String to use for the image alt text + * @param useSvg {boolean} Whether to use SVG image src. If False, PNG will be used. * @param unicode {integer} One or more integers representing unicode characters * @returns A img node with the corresponding emoji */ -export function charactersToImageNode(alt, ...unicode) { +export function charactersToImageNode(alt, useSvg, ...unicode) { const fileName = unicode.map((u) => { return u.toString(16); }).join('-'); - return {alt}; + const path = useSvg ? emojione.imagePathSVG : emojione.imagePathPNG; + const fileType = useSvg ? 'svg' : 'png'; + return {alt}; } + export function stripParagraphs(html: string): string { const contentDiv = document.createElement('div'); contentDiv.innerHTML = html; diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index 7e1a5f9d35..d9a7039686 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -17,13 +17,11 @@ limitations under the License. 'use strict'; -var React = require('react'); -var ReactDOM = require('react-dom'); -var sdk = require('../../../index'); -var Login = require("../../../Login"); -var PasswordLogin = require("../../views/login/PasswordLogin"); -var CasLogin = require("../../views/login/CasLogin"); -var ServerConfig = require("../../views/login/ServerConfig"); +import React from 'react'; +import ReactDOM from 'react-dom'; +import url from 'url'; +import sdk from '../../../index'; +import Login from '../../../Login'; /** * A wire component which glues together login UI components and Login logic @@ -67,6 +65,7 @@ module.exports = React.createClass({ username: "", phoneCountry: null, phoneNumber: "", + currentFlow: "m.login.password", }; }, @@ -129,23 +128,19 @@ module.exports = React.createClass({ this.setState({ phoneNumber: phoneNumber }); }, - onHsUrlChanged: function(newHsUrl) { + onServerConfigChange: function(config) { var self = this; - this.setState({ - enteredHomeserverUrl: newHsUrl, + let newState = { errorText: null, // reset err messages - }, function() { - self._initLoginLogic(newHsUrl); - }); - }, - - onIsUrlChanged: function(newIsUrl) { - var self = this; - this.setState({ - enteredIdentityServerUrl: newIsUrl, - errorText: null, // reset err messages - }, function() { - self._initLoginLogic(null, newIsUrl); + }; + if (config.hsUrl !== undefined) { + newState.enteredHomeserverUrl = config.hsUrl; + } + if (config.isUrl !== undefined) { + newState.enteredIdentityServerUrl = config.isUrl; + } + this.setState(newState, function() { + self._initLoginLogic(config.hsUrl || null, config.isUrl); }); }, @@ -161,25 +156,28 @@ module.exports = React.createClass({ }); this._loginLogic = loginLogic; - loginLogic.getFlows().then(function(flows) { - // old behaviour was to always use the first flow without presenting - // options. This works in most cases (we don't have a UI for multiple - // logins so let's skip that for now). - loginLogic.chooseFlow(0); - }, function(err) { - self._setStateFromError(err, false); - }).finally(function() { - self.setState({ - busy: false - }); - }); - this.setState({ enteredHomeserverUrl: hsUrl, enteredIdentityServerUrl: isUrl, busy: true, loginIncorrect: false, }); + + loginLogic.getFlows().then(function(flows) { + // old behaviour was to always use the first flow without presenting + // options. This works in most cases (we don't have a UI for multiple + // logins so let's skip that for now). + loginLogic.chooseFlow(0); + self.setState({ + currentFlow: self._getCurrentFlowStep(), + }); + }, function(err) { + self._setStateFromError(err, false); + }).finally(function() { + self.setState({ + busy: false, + }); + }); }, _getCurrentFlowStep: function() { @@ -231,6 +229,7 @@ module.exports = React.createClass({ componentForStep: function(step) { switch (step) { case 'm.login.password': + const PasswordLogin = sdk.getComponent('login.PasswordLogin'); return ( ); case 'm.login.cas': + const CasLogin = sdk.getComponent('login.CasLogin'); return ( ); @@ -262,10 +263,11 @@ module.exports = React.createClass({ }, render: function() { - var Loader = sdk.getComponent("elements.Spinner"); - var LoginHeader = sdk.getComponent("login.LoginHeader"); - var LoginFooter = sdk.getComponent("login.LoginFooter"); - var loader = this.state.busy ?
      : null; + const Loader = sdk.getComponent("elements.Spinner"); + const LoginHeader = sdk.getComponent("login.LoginHeader"); + const LoginFooter = sdk.getComponent("login.LoginFooter"); + const ServerConfig = sdk.getComponent("login.ServerConfig"); + const loader = this.state.busy ?
      : null; var loginAsGuestJsx; if (this.props.enableGuest) { @@ -291,15 +293,14 @@ module.exports = React.createClass({

      Sign in { loader }

      - { this.componentForStep(this._getCurrentFlowStep()) } + { this.componentForStep(this.state.currentFlow) }
      { this.state.errorText } diff --git a/src/components/views/elements/Dropdown.js b/src/components/views/elements/Dropdown.js index 907d4b0905..a9ecf5b669 100644 --- a/src/components/views/elements/Dropdown.js +++ b/src/components/views/elements/Dropdown.js @@ -248,13 +248,10 @@ export default class Dropdown extends React.Component { ); }); - - if (!this.state.searchQuery && this.props.searchEnabled) { - options.push( -
      - Type to search... -
      - ); + if (options.length === 0) { + return [
      + No results +
      ]; } return options; } @@ -317,7 +314,7 @@ Dropdown.propTypes = { onOptionChange: React.PropTypes.func.isRequired, // Called when the value of the search field changes onSearchChange: React.PropTypes.func, - searchEnabled: React.PropTypes.boolean, + searchEnabled: React.PropTypes.bool, // Function that, given the key of an option, returns // a node representing that option to be displayed in the // box itself as the currently-selected option (ie. as diff --git a/src/components/views/login/CountryDropdown.js b/src/components/views/login/CountryDropdown.js index be1ed51b5e..7f6b21650d 100644 --- a/src/components/views/login/CountryDropdown.js +++ b/src/components/views/login/CountryDropdown.js @@ -33,8 +33,6 @@ function countryMatchesSearchQuery(query, country) { return false; } -const MAX_DISPLAYED_ROWS = 2; - export default class CountryDropdown extends React.Component { constructor(props) { super(props); @@ -64,7 +62,7 @@ export default class CountryDropdown extends React.Component { // Unicode Regional Indicator Symbol letter 'A' const RIS_A = 0x1F1E6; const ASCII_A = 65; - return charactersToImageNode(iso2, + return charactersToImageNode(iso2, true, RIS_A + (iso2.charCodeAt(0) - ASCII_A), RIS_A + (iso2.charCodeAt(1) - ASCII_A), ); @@ -93,10 +91,6 @@ export default class CountryDropdown extends React.Component { displayedCountries = COUNTRIES; } - if (displayedCountries.length > MAX_DISPLAYED_ROWS) { - displayedCountries = displayedCountries.slice(0, MAX_DISPLAYED_ROWS); - } - const options = displayedCountries.map((country) => { return
      {this._flagImgForIso2(country.iso2)} diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index 002de0c2ba..fc063efbe9 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.js @@ -25,56 +25,49 @@ import {field_input_incorrect} from '../../../UiEffects'; /** * A pure UI component which displays a username/password form. */ -module.exports = React.createClass({displayName: 'PasswordLogin', - propTypes: { - onSubmit: React.PropTypes.func.isRequired, // fn(username, password) - onForgotPasswordClick: React.PropTypes.func, // fn() - initialUsername: React.PropTypes.string, - initialPhoneCountry: React.PropTypes.string, - initialPhoneNumber: React.PropTypes.string, - initialPassword: React.PropTypes.string, - onUsernameChanged: React.PropTypes.func, - onPhoneCountryChanged: React.PropTypes.func, - onPhoneNumberChanged: React.PropTypes.func, - onPasswordChanged: React.PropTypes.func, - loginIncorrect: React.PropTypes.bool, - }, +class PasswordLogin extends React.Component { + static defaultProps = { + onUsernameChanged: function() {}, + onPasswordChanged: function() {}, + onPhoneCountryChanged: function() {}, + onPhoneNumberChanged: function() {}, + initialUsername: "", + initialPhoneCountry: "", + initialPhoneNumber: "", + initialPassword: "", + loginIncorrect: false, + hsDomain: "", + } - getDefaultProps: function() { - return { - onUsernameChanged: function() {}, - onPasswordChanged: function() {}, - onPhoneCountryChanged: function() {}, - onPhoneNumberChanged: function() {}, - initialUsername: "", - initialPhoneCountry: "", - initialPhoneNumber: "", - initialPassword: "", - loginIncorrect: false, - }; - }, - - getInitialState: function() { - return { + constructor(props) { + super(props); + this.state = { username: this.props.initialUsername, password: this.props.initialPassword, phoneCountry: this.props.initialPhoneCountry, phoneNumber: this.props.initialPhoneNumber, - loginType: "mxid", + loginType: PasswordLogin.LOGIN_FIELD_MXID, }; - }, - componentWillMount: function() { + this.onSubmitForm = this.onSubmitForm.bind(this); + this.onUsernameChanged = this.onUsernameChanged.bind(this); + this.onLoginTypeChange = this.onLoginTypeChange.bind(this); + this.onPhoneCountryChanged = this.onPhoneCountryChanged.bind(this); + this.onPhoneNumberChanged = this.onPhoneNumberChanged.bind(this); + this.onPasswordChanged = this.onPasswordChanged.bind(this); + } + + componentWillMount() { this._passwordField = null; - }, + } - componentWillReceiveProps: function(nextProps) { + componentWillReceiveProps(nextProps) { if (!this.props.loginIncorrect && nextProps.loginIncorrect) { field_input_incorrect(this._passwordField); } - }, + } - onSubmitForm: function(ev) { + onSubmitForm(ev) { ev.preventDefault(); this.props.onSubmit( this.state.username, @@ -82,33 +75,87 @@ module.exports = React.createClass({displayName: 'PasswordLogin', this.state.phoneNumber, this.state.password, ); - }, + } - onUsernameChanged: function(ev) { + onUsernameChanged(ev) { this.setState({username: ev.target.value}); this.props.onUsernameChanged(ev.target.value); - }, + } - onLoginTypeChange: function(loginType) { - this.setState({loginType: loginType}); - }, + onLoginTypeChange(loginType) { + this.setState({ + loginType: loginType, + username: "" // Reset because email and username use the same state + }); + } - onPhoneCountryChanged: function(country) { + onPhoneCountryChanged(country) { this.setState({phoneCountry: country}); this.props.onPhoneCountryChanged(country); - }, + } - onPhoneNumberChanged: function(ev) { + onPhoneNumberChanged(ev) { this.setState({phoneNumber: ev.target.value}); this.props.onPhoneNumberChanged(ev.target.value); - }, + } - onPasswordChanged: function(ev) { + onPasswordChanged(ev) { this.setState({password: ev.target.value}); this.props.onPasswordChanged(ev.target.value); - }, + } - render: function() { + renderLoginField(loginType) { + switch(loginType) { + case PasswordLogin.LOGIN_FIELD_EMAIL: + return ; + case PasswordLogin.LOGIN_FIELD_MXID: + return
      +
      @
      + +
      :{this.props.hsDomain}
      +
      ; + case PasswordLogin.LOGIN_FIELD_PHONE: + const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); + return
      + + +
      ; + } + } + + render() { var forgotPasswordJsx; if (this.props.onForgotPasswordClick) { @@ -124,47 +171,25 @@ module.exports = React.createClass({displayName: 'PasswordLogin', error: this.props.loginIncorrect, }); - const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); const Dropdown = sdk.getComponent('elements.Dropdown'); - const loginType = { - 'email': - , - 'mxid': - , - 'phone':
      - - -
      - }[this.state.loginType]; + const loginField = this.renderLoginField(this.state.loginType); return (
      - - Matrix ID - Email - Phone + + Matrix ID + Email Address + Phone
      - {loginType} + {loginField} {this._passwordField = e;}} type="password" name="password" value={this.state.password} onChange={this.onPasswordChanged} @@ -176,4 +201,25 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
      ); } -}); +} + +PasswordLogin.LOGIN_FIELD_EMAIL = "login_field_email"; +PasswordLogin.LOGIN_FIELD_MXID = "login_field_mxid"; +PasswordLogin.LOGIN_FIELD_PHONE = "login_field_phone"; + +PasswordLogin.propTypes = { + onSubmit: React.PropTypes.func.isRequired, // fn(username, password) + onForgotPasswordClick: React.PropTypes.func, // fn() + initialUsername: React.PropTypes.string, + initialPhoneCountry: React.PropTypes.string, + initialPhoneNumber: React.PropTypes.string, + initialPassword: React.PropTypes.string, + onUsernameChanged: React.PropTypes.func, + onPhoneCountryChanged: React.PropTypes.func, + onPhoneNumberChanged: React.PropTypes.func, + onPasswordChanged: React.PropTypes.func, + loginIncorrect: React.PropTypes.bool, + hsDomain: React.PropTypes.string, +}; + +module.exports = PasswordLogin; diff --git a/src/components/views/login/ServerConfig.js b/src/components/views/login/ServerConfig.js index 4e6ed12f9e..2853945425 100644 --- a/src/components/views/login/ServerConfig.js +++ b/src/components/views/login/ServerConfig.js @@ -27,8 +27,7 @@ module.exports = React.createClass({ displayName: 'ServerConfig', propTypes: { - onHsUrlChanged: React.PropTypes.func, - onIsUrlChanged: React.PropTypes.func, + onServerConfigChange: React.PropTypes.func, // default URLs are defined in config.json (or the hardcoded defaults) // they are used if the user has not overridden them with a custom URL. @@ -50,8 +49,7 @@ module.exports = React.createClass({ getDefaultProps: function() { return { - onHsUrlChanged: function() {}, - onIsUrlChanged: function() {}, + onServerConfigChange: function() {}, customHsUrl: "", customIsUrl: "", withToggleButton: false, @@ -75,7 +73,10 @@ module.exports = React.createClass({ this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, function() { var hsUrl = this.state.hs_url.trim().replace(/\/$/, ""); if (hsUrl === "") hsUrl = this.props.defaultHsUrl; - this.props.onHsUrlChanged(hsUrl); + this.props.onServerConfigChange({ + hsUrl : this.state.hs_url, + isUrl : this.state.is_url, + }); }); }); }, @@ -85,7 +86,10 @@ module.exports = React.createClass({ this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, function() { var isUrl = this.state.is_url.trim().replace(/\/$/, ""); if (isUrl === "") isUrl = this.props.defaultIsUrl; - this.props.onIsUrlChanged(isUrl); + this.props.onServerConfigChange({ + hsUrl : this.state.hs_url, + isUrl : this.state.is_url, + }); }); }); }, @@ -102,12 +106,16 @@ module.exports = React.createClass({ configVisible: visible }); if (!visible) { - this.props.onHsUrlChanged(this.props.defaultHsUrl); - this.props.onIsUrlChanged(this.props.defaultIsUrl); + this.props.onServerConfigChange({ + hsUrl : this.props.defaultHsUrl, + isUrl : this.props.defaultIsUrl, + }); } else { - this.props.onHsUrlChanged(this.state.hs_url); - this.props.onIsUrlChanged(this.state.is_url); + this.props.onServerConfigChange({ + hsUrl : this.state.hs_url, + isUrl : this.state.is_url, + }); } }, From 2b9cb999baebc04bc8d62f1159714278dd67711f Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 21 Apr 2017 11:50:19 +0100 Subject: [PATCH 176/284] autoFocus PasswordLogin --- src/components/views/login/PasswordLogin.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index fc063efbe9..ffb86636ca 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.js @@ -150,6 +150,7 @@ class PasswordLogin extends React.Component { onChange={this.onPhoneNumberChanged} placeholder="Mobile phone number" value={this.state.phoneNumber} + autoFocus />
      ; } From b0288ebd89841ad362a6804b41839ef9d3bdca7f Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 21 Apr 2017 12:40:13 +0100 Subject: [PATCH 177/284] fix stupid typos in RoomList's shouldComponentUpdate --- src/components/views/rooms/RoomList.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index a7dda40c6e..394de8876b 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -44,9 +44,9 @@ module.exports = React.createClass({ shouldComponentUpdate: function(nextProps, nextState) { if (nextProps.collapsed !== this.props.collapsed) return true; if (nextProps.searchFilter !== this.props.searchFilter) return true; - if (nextState.lists !== this.props.lists || - nextState.isLoadingLeftRooms !== this.isLoadingLeftRooms || - nextState.incomingCall !== this.incomingCall) return true; + if (nextState.lists !== this.state.lists || + nextState.isLoadingLeftRooms !== this.state.isLoadingLeftRooms || + nextState.incomingCall !== this.state.incomingCall) return true; return false; }, From 9c4c706120497ad677cff3f1820c31cb628516e1 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 21 Apr 2017 16:09:11 +0100 Subject: [PATCH 178/284] Remove :server.name for custom servers Custom servers may not be configured such that their domain name === domain part. --- src/components/structures/login/Login.js | 8 +++++++- src/components/views/login/PasswordLogin.js | 15 +++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index d9a7039686..315a0ea242 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -230,6 +230,12 @@ module.exports = React.createClass({ switch (step) { case 'm.login.password': const PasswordLogin = sdk.getComponent('login.PasswordLogin'); + // HSs that are not matrix.org may not be configured to have their + // domain name === domain part. + let hsDomain = url.parse(this.state.enteredHomeserverUrl).hostname; + if (hsDomain !== 'matrix.org') { + hsDomain = null; + } return ( ); case 'm.login.cas': diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index ffb86636ca..568461817c 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.js @@ -118,10 +118,21 @@ class PasswordLogin extends React.Component { autoFocus />; case PasswordLogin.LOGIN_FIELD_MXID: + const mxidInputClasses = classNames({ + "mx_Login_field": true, + "mx_Login_username": true, + "mx_Login_field_has_suffix": Boolean(this.props.hsDomain), + }); + let suffix = null; + if (this.props.hsDomain) { + suffix =
      + :{this.props.hsDomain} +
      ; + } return
      @
      -
      :{this.props.hsDomain}
      + {suffix}
      ; case PasswordLogin.LOGIN_FIELD_PHONE: const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); From 29c2bd3d18beb22f619f297127ddf28fd3e1e9ab Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 21 Apr 2017 16:46:36 +0100 Subject: [PATCH 179/284] reset last_rr_sent on error Indicate that setting the RR was a failure and that hitting the API should be retried (in the case where the errcode !== "M_UNRECOGNISED") --- src/components/structures/TimelinePanel.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index e8774cec62..872d30ac8c 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -546,6 +546,7 @@ var TimelinePanel = React.createClass({ }); } // it failed, so allow retries next time the user is active + this.last_rr_sent_event_id = undefined; this.last_rm_sent_event_id = undefined; }); From 665f507537602878bff2373490ede4a7366ae482 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 21 Apr 2017 18:18:35 +0100 Subject: [PATCH 180/284] Update js-sdk dependency --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0a0a51fc0b..67dfa165af 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "isomorphic-fetch": "^2.2.1", "linkifyjs": "^2.1.3", "lodash": "^4.13.1", - "matrix-js-sdk": "0.7.6", + "matrix-js-sdk": "0.7.7-rc.1", "optimist": "^0.6.1", "q": "^1.4.1", "react": "^15.4.0", From 2f08340ff0ee2bf767998fbe6d8e2bfee7187752 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 21 Apr 2017 18:22:39 +0100 Subject: [PATCH 181/284] Prepare changelog for v0.8.8-rc.1 --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 292e60607d..7d4a69fb5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +Changes in [0.8.8-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.8-rc.1) (2017-04-21) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.7...v0.8.8-rc.1) + + * Update js-sdk to fix registration without a captcha (https://github.com/vector-im/riot-web/issues/3621) + + Changes in [0.8.7](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.7) (2017-04-12) =================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.7-rc.4...v0.8.7) From a55eb00dad61e6485ace58737d44a898c08a41f2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 21 Apr 2017 18:22:39 +0100 Subject: [PATCH 182/284] v0.8.8-rc.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 67dfa165af..38d08344e0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "0.8.7", + "version": "0.8.8-rc.1", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From fdc26a490ad8b9525fbc0633d192472a4af431d4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 21 Apr 2017 18:45:28 +0100 Subject: [PATCH 183/284] On return to RoomView from auxPanel, send focus back to Composer Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/RoomView.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index b09b101b8a..9d5d50e9b1 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1181,6 +1181,7 @@ module.exports = React.createClass({ console.log("updateTint from onCancelClick"); this.updateTint(); this.setState({editingRoomSettings: false}); + dis.dispatch({action: 'focus_composer'}); }, onLeaveClick: function() { From 8e9f52e2172e8118ca5767c15729dbf08c16ce4a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 21 Apr 2017 19:46:19 +0100 Subject: [PATCH 184/284] Disable Scalar Integrations if urls passed to it are falsey Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/RoomSettings.js | 76 ++++++++++++---------- 1 file changed, 41 insertions(+), 35 deletions(-) diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index 2c7e1d7140..2c29dd433c 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -129,14 +129,17 @@ module.exports = React.createClass({ console.error("Failed to get room visibility: " + err); }); - this.scalarClient = new ScalarAuthClient(); - this.scalarClient.connect().done(() => { - this.forceUpdate(); - }, (err) => { - this.setState({ - scalar_error: err + this.scalarClient = null; + if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) { + this.scalarClient = new ScalarAuthClient(); + this.scalarClient.connect().done(() => { + this.forceUpdate(); + }, (err) => { + this.setState({ + scalar_error: err + }); }); - }); + } dis.dispatch({ action: 'ui_opacity', @@ -490,7 +493,7 @@ module.exports = React.createClass({ ev.preventDefault(); var IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager"); Modal.createDialog(IntegrationsManager, { - src: this.scalarClient.hasCredentials() ? + src: (this.scalarClient !== null && this.scalarClient.hasCredentials()) ? this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId) : null, onFinished: ()=>{ @@ -765,36 +768,39 @@ module.exports = React.createClass({
      ; } - var integrationsButton; - var integrationsError; - if (this.state.showIntegrationsError && this.state.scalar_error) { - console.error(this.state.scalar_error); - integrationsError = ( - - Could not connect to the integration server - - ); - } + let integrationsButton; + let integrationsError; - if (this.scalarClient.hasCredentials()) { - integrationsButton = ( + if (this.scalarClient !== null) { + if (this.state.showIntegrationsError && this.state.scalar_error) { + console.error(this.state.scalar_error); + integrationsError = ( + + Could not connect to the integration server + + ); + } + + if (this.scalarClient.hasCredentials()) { + integrationsButton = (
      - Manage Integrations -
      - ); - } else if (this.state.scalar_error) { - integrationsButton = ( + Manage Integrations +
      + ); + } else if (this.state.scalar_error) { + integrationsButton = (
      - Integrations Error - { integrationsError } -
      - ); - } else { - integrationsButton = ( -
      - Manage Integrations -
      - ); + Integrations Error + { integrationsError } +
      + ); + } else { + integrationsButton = ( +
      + Manage Integrations +
      + ); + } } return ( From 2d39b5955616a74d148cb4d797fb8446d4c7b7ad Mon Sep 17 00:00:00 2001 From: turt2live Date: Fri, 21 Apr 2017 13:41:37 -0600 Subject: [PATCH 185/284] Change presence status labels to be more clear. As per vector-im/riot-web#3626 the current labels are unclear. Changing the verbage should make it more clear. Signed-off-by: Travis Ralston --- src/components/views/rooms/PresenceLabel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/PresenceLabel.js b/src/components/views/rooms/PresenceLabel.js index 2ece4c771e..52d831fcf6 100644 --- a/src/components/views/rooms/PresenceLabel.js +++ b/src/components/views/rooms/PresenceLabel.js @@ -75,7 +75,7 @@ module.exports = React.createClass({ render: function() { if (this.props.activeAgo >= 0) { - var ago = this.props.currentlyActive ? "now" : (this.getDuration(this.props.activeAgo) + " ago"); + var ago = this.props.currentlyActive ? "" : "for " + (this.getDuration(this.props.activeAgo)); // var ago = this.getDuration(this.props.activeAgo) + " ago"; // if (this.props.currentlyActive) ago += " (now?)"; return ( From e4c4adc5177fc9f33bcd39070ba79cab38cf3055 Mon Sep 17 00:00:00 2001 From: turt2live Date: Fri, 21 Apr 2017 14:28:28 -0600 Subject: [PATCH 186/284] Add option to hide other people's read receipts. Addresses vector-im/riot-web#2526 Signed-off-by: Travis Ralston --- src/components/structures/UserSettings.js | 4 ++++ src/components/views/rooms/EventTile.js | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 892865fdf9..b2ee29a1da 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -44,6 +44,10 @@ const SETTINGS_LABELS = [ id: 'autoplayGifsAndVideos', label: 'Autoplay GIFs and videos', }, + { + id: 'hideReadReceipts', + label: 'Hide read receipts' + }, /* { id: 'alwaysShowTimestamps', diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 9df0499eb2..e4234fc0bc 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -23,6 +23,7 @@ var Modal = require('../../../Modal'); var sdk = require('../../../index'); var TextForEvent = require('../../../TextForEvent'); import WithMatrixClient from '../../../wrappers/WithMatrixClient'; +import * as UserSettingsStore from "../../../UserSettingsStore"; var ContextualMenu = require('../../structures/ContextualMenu'); import dis from '../../../dispatcher'; @@ -284,6 +285,11 @@ module.exports = WithMatrixClient(React.createClass({ }, getReadAvatars: function() { + // return early if the user doesn't want any read receipts + if (UserSettingsStore.getSyncedSetting('hideReadReceipts', false)) { + return (); + } + const ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker'); const avatars = []; const receiptOffset = 15; From 64e416e11745a9fc5513926f3738d0cac728ac1f Mon Sep 17 00:00:00 2001 From: turt2live Date: Fri, 21 Apr 2017 14:50:26 -0600 Subject: [PATCH 187/284] Add option to not send typing notifications Addresses vector-im/riot-web#3220 Fix applies to both the RTE and plain editor. Signed-off-by: Travis Ralston --- src/components/structures/UserSettings.js | 4 ++++ src/components/views/rooms/MessageComposerInput.js | 1 + src/components/views/rooms/MessageComposerInputOld.js | 2 ++ 3 files changed, 7 insertions(+) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 892865fdf9..619d8f32c8 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -44,6 +44,10 @@ const SETTINGS_LABELS = [ id: 'autoplayGifsAndVideos', label: 'Autoplay GIFs and videos', }, + { + id: 'dontSendTypingNotifications', + label: "Don't send typing notifications", + }, /* { id: 'alwaysShowTimestamps', diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 51c9ba881b..a7c20b02b5 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -355,6 +355,7 @@ export default class MessageComposerInput extends React.Component { } sendTyping(isTyping) { + if (UserSettingsStore.getSyncedSetting('dontSendTypingNotifications', false)) return; MatrixClientPeg.get().sendTyping( this.props.room.roomId, this.isTyping, TYPING_SERVER_TIMEOUT diff --git a/src/components/views/rooms/MessageComposerInputOld.js b/src/components/views/rooms/MessageComposerInputOld.js index f0b650eb04..f5366c36ad 100644 --- a/src/components/views/rooms/MessageComposerInputOld.js +++ b/src/components/views/rooms/MessageComposerInputOld.js @@ -20,6 +20,7 @@ var SlashCommands = require("../../../SlashCommands"); var Modal = require("../../../Modal"); var MemberEntry = require("../../../TabCompleteEntries").MemberEntry; var sdk = require('../../../index'); +import UserSettingsStore from "../../../UserSettingsStore"; var dis = require("../../../dispatcher"); var KeyCode = require("../../../KeyCode"); @@ -420,6 +421,7 @@ export default React.createClass({ }, sendTyping: function(isTyping) { + if (UserSettingsStore.getSyncedSetting('dontSendTypingNotifications', false)) return; MatrixClientPeg.get().sendTyping( this.props.room.roomId, this.isTyping, TYPING_SERVER_TIMEOUT From 80b8be64d1692f9546d00b8fa167e7dfffe38695 Mon Sep 17 00:00:00 2001 From: turt2live Date: Fri, 21 Apr 2017 15:09:56 -0600 Subject: [PATCH 188/284] Transform h1 and h2 tags to h3 tags Addresses vector-im/riot-web#1772 Signed-off-by: Travis Ralston --- src/HtmlUtils.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index 96934d205e..632542ac43 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -165,6 +165,12 @@ var sanitizeHtmlParams = { attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/ return { tagName: tagName, attribs : attribs }; }, + 'h1': function(tagName, attribs) { + return { tagName: 'h3', attribs: attribs }; + }, + 'h2': function(tagName, attribs) { + return { tagName: 'h3', attribs: attribs }; + }, '*': function(tagName, attribs) { // Delete any style previously assigned, style is an allowedTag for font and span // because attributes are stripped after transforming From ec6a1c4c750f959017cdf823402a6c9d86b16fe2 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 22 Apr 2017 01:16:16 +0100 Subject: [PATCH 189/284] recalculate roomlist when your invites change --- src/components/views/rooms/RoomList.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 394de8876b..f36078e47d 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -265,9 +265,16 @@ module.exports = React.createClass({ }, onRoomStateMember: function(ev, state, member) { - constantTimeDispatcher.dispatch( - "RoomTile.refresh", member.roomId, {} - ); + if (ev.getStateKey() === MatrixClientPeg.get().credentials.userId && + ev.getPrevContent() && ev.getPrevContent().membership === "invite") + { + this._delayedRefreshRoomList(); + } + else { + constantTimeDispatcher.dispatch( + "RoomTile.refresh", member.roomId, {} + ); + } }, onRoomMemberName: function(ev, member) { From 1faecfd0f7b5cd48bea0166cf73d273fd201d38f Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 22 Apr 2017 01:29:48 +0100 Subject: [PATCH 190/284] fix sticky headers on resize --- src/components/views/rooms/RoomList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index f36078e47d..5372135f95 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -606,7 +606,7 @@ module.exports = React.createClass({ return ( + autoshow={true} onScroll={ self._whenScrolling } onResize={ self._whenScrolling } ref="gemscroll">
      Date: Sat, 22 Apr 2017 04:57:27 +0100 Subject: [PATCH 191/284] Remember element that was in focus before rendering dialog restore focus to that element when we unmount also remove some whitespace because ESLint is a big bad bully... Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/dialogs/BaseDialog.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js index 0b2ca5225d..d0f34c5fbd 100644 --- a/src/components/views/dialogs/BaseDialog.js +++ b/src/components/views/dialogs/BaseDialog.js @@ -47,6 +47,16 @@ export default React.createClass({ children: React.PropTypes.node, }, + componentWillMount: function() { + this.priorActiveElement = document.activeElement; + }, + + componentWillUnmount: function() { + if (this.priorActiveElement !== null) { + this.priorActiveElement.focus(); + } + }, + _onKeyDown: function(e) { if (e.keyCode === KeyCode.ESCAPE) { e.stopPropagation(); @@ -67,7 +77,7 @@ export default React.createClass({ render: function() { const TintableSvg = sdk.getComponent("elements.TintableSvg"); - + return (
      Date: Sat, 22 Apr 2017 14:52:20 +0100 Subject: [PATCH 192/284] Specify cross platform regexes and add olm to noParse Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- karma.conf.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/karma.conf.js b/karma.conf.js index 6d3047bb3b..3495a981be 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -135,17 +135,24 @@ module.exports = function (config) { }, ], noParse: [ + // for cross platform compatibility use [\\\/] as the path separator + // this ensures that the regex trips on both Windows and *nix + // don't parse the languages within highlight.js. They // cause stack overflows // (https://github.com/webpack/webpack/issues/1721), and // there is no need for webpack to parse them - they can // just be included as-is. - /highlight\.js\/lib\/languages/, + /highlight\.js[\\\/]lib[\\\/]languages/, + + // olm takes ages for webpack to process, and it's already heavily + // optimised, so there is little to gain by us uglifying it. + /olm[\\\/](javascript[\\\/])?olm\.js$/, // also disable parsing for sinon, because it // tries to do voodoo with 'require' which upsets // webpack (https://github.com/webpack/webpack/issues/304) - /sinon\/pkg\/sinon\.js$/, + /sinon[\\\/]pkg[\\\/]sinon\.js$/, ], }, resolve: { From 33e841a786b507a705cc54b735c29ec9427c2ae8 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 22 Apr 2017 15:40:29 +0100 Subject: [PATCH 193/284] move user settings outward and use built in read receipts disabling Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/RoomView.js | 3 ++- src/components/views/rooms/EventTile.js | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 9d5d50e9b1..ea8b6e2ae0 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -26,6 +26,7 @@ var q = require("q"); var classNames = require("classnames"); var Matrix = require("matrix-js-sdk"); +var UserSettingsStore = require('../../UserSettingsStore'); var MatrixClientPeg = require("../../MatrixClientPeg"); var ContentMessages = require("../../ContentMessages"); var Modal = require("../../Modal"); @@ -1727,7 +1728,7 @@ module.exports = React.createClass({ var messagePanel = (
      , button: "Sign out", extraButtons: [ - From 0e5006b0415b4b42789661eace8aab26b2f0dc48 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 22 Apr 2017 17:28:28 +0100 Subject: [PATCH 195/284] typo --- src/components/views/rooms/RoomList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 5372135f95..3810f7d4d6 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -483,7 +483,7 @@ module.exports = React.createClass({ // Use the offset of the top of the scroll area from the window // as this is used to calculate the CSS fixed top position for the stickies var scrollAreaOffset = scrollArea.getBoundingClientRect().top + window.pageYOffset; - // Use the offset of the top of the componet from the window + // Use the offset of the top of the component from the window // as this is used to calculate the CSS fixed top position for the stickies var scrollAreaHeight = ReactDOM.findDOMNode(this).getBoundingClientRect().height; From 34c1a8f3cf7965ac77819b41b61c0702bb28ede4 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 22 Apr 2017 17:28:48 +0100 Subject: [PATCH 196/284] make autofocus explicit on errordialog as it autoFocus attr seems unreliable --- src/components/views/dialogs/ErrorDialog.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/views/dialogs/ErrorDialog.js b/src/components/views/dialogs/ErrorDialog.js index 937595dfa8..ef6fdbbead 100644 --- a/src/components/views/dialogs/ErrorDialog.js +++ b/src/components/views/dialogs/ErrorDialog.js @@ -50,6 +50,12 @@ export default React.createClass({ }; }, + componentDidMount: function() { + if (this.props.focus) { + this.refs.button.focus(); + } + }, + render: function() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); return ( @@ -59,7 +65,7 @@ export default React.createClass({ {this.props.description}
      -
      From 6a63c7e50c46ffbe16a4d1df500bac8565b03b90 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 22 Apr 2017 21:06:38 +0100 Subject: [PATCH 197/284] fix deep-linking to riot.im/app --- src/linkify-matrix.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/linkify-matrix.js b/src/linkify-matrix.js index c8e20316a9..d9b0b78982 100644 --- a/src/linkify-matrix.js +++ b/src/linkify-matrix.js @@ -122,7 +122,7 @@ var escapeRegExp = function(string) { // anyone else really should be using matrix.to. matrixLinkify.VECTOR_URL_PATTERN = "^(?:https?:\/\/)?(?:" + escapeRegExp(window.location.host + window.location.pathname) + "|" - + "(?:www\\.)?(?:riot|vector)\\.im/(?:beta|staging|develop)/" + + "(?:www\\.)?(?:riot|vector)\\.im/(?:app|beta|staging|develop)/" + ")(#.*)"; matrixLinkify.MATRIXTO_URL_PATTERN = "^(?:https?:\/\/)?(?:www\\.)?matrix\\.to/#/((#|@|!).*)"; From fa033e6116e65b5b1e40042eecf537cc2d7a70a0 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 23 Apr 2017 00:49:14 +0100 Subject: [PATCH 198/284] limit our keyboard shortcut modifiers correctly fixes https://github.com/vector-im/riot-web/issues/3614 --- src/components/structures/LoggedInView.js | 11 +++++++---- src/components/structures/ScrollPanel.js | 12 ++++++++---- src/components/structures/TimelinePanel.js | 4 +++- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index ef9d8d112a..318a5d7805 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -117,9 +117,10 @@ export default React.createClass({ } break; + case KeyCode.UP: case KeyCode.DOWN: - if (ev.altKey) { + if (ev.altKey && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) { var action = ev.keyCode == KeyCode.UP ? 'view_prev_room' : 'view_next_room'; dis.dispatch({action: action}); @@ -129,13 +130,15 @@ export default React.createClass({ case KeyCode.PAGE_UP: case KeyCode.PAGE_DOWN: - this._onScrollKeyPressed(ev); - handled = true; + if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { + this._onScrollKeyPressed(ev); + handled = true; + } break; case KeyCode.HOME: case KeyCode.END: - if (ev.ctrlKey) { + if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { this._onScrollKeyPressed(ev); handled = true; } diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index 83bec03e9e..d43e22e2f1 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -483,21 +483,25 @@ module.exports = React.createClass({ handleScrollKey: function(ev) { switch (ev.keyCode) { case KeyCode.PAGE_UP: - this.scrollRelative(-1); + if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { + this.scrollRelative(-1); + } break; case KeyCode.PAGE_DOWN: - this.scrollRelative(1); + if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { + this.scrollRelative(1); + } break; case KeyCode.HOME: - if (ev.ctrlKey) { + if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { this.scrollToTop(); } break; case KeyCode.END: - if (ev.ctrlKey) { + if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { this.scrollToBottom(); } break; diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 7325cea2da..8babdaae4a 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -766,7 +766,9 @@ var TimelinePanel = React.createClass({ // jump to the live timeline on ctrl-end, rather than the end of the // timeline window. - if (ev.ctrlKey && ev.keyCode == KeyCode.END) { + if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey && + ev.keyCode == KeyCode.END) + { this.jumpToLiveTimeline(); } else { this.refs.messagePanel.handleScrollKey(ev); From 7854cac61d823f8c98e3a30caec5276e90a98823 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 23 Apr 2017 01:00:44 +0100 Subject: [PATCH 199/284] hook up keyb shortcuts for roomdir --- src/components/structures/LoggedInView.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 318a5d7805..4c012b42a8 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -156,6 +156,9 @@ export default React.createClass({ if (this.refs.roomView) { this.refs.roomView.handleScrollKey(ev); } + else if (this.refs.roomDirectory) { + this.refs.roomDirectory.handleScrollKey(ev); + } }, render: function() { @@ -216,6 +219,7 @@ export default React.createClass({ case PageTypes.RoomDirectory: page_element = ; From db996f678c9d0f31c30e44b221250431af89d89c Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 23 Apr 2017 01:32:51 +0100 Subject: [PATCH 200/284] show better errors when slash commands fail --- src/components/views/rooms/MessageComposerInput.js | 2 +- src/components/views/rooms/MessageComposerInputOld.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index a7c20b02b5..417d003226 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -510,7 +510,7 @@ export default class MessageComposerInput extends React.Component { var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { title: "Server error", - description: "Server unavailable, overloaded, or something else went wrong.", + description: ((err && err.message) ? err.message : "Server unavailable, overloaded, or something else went wrong."), }); }); } diff --git a/src/components/views/rooms/MessageComposerInputOld.js b/src/components/views/rooms/MessageComposerInputOld.js index f5366c36ad..378644478c 100644 --- a/src/components/views/rooms/MessageComposerInputOld.js +++ b/src/components/views/rooms/MessageComposerInputOld.js @@ -312,7 +312,7 @@ export default React.createClass({ var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { title: "Server error", - description: "Server unavailable, overloaded, or something else went wrong.", + description: ((err && err.message) ? err.message : "Server unavailable, overloaded, or something else went wrong."), }); }); } From a2be764681240644ba5cbfaa7965adc308094dbf Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 23 Apr 2017 01:48:27 +0100 Subject: [PATCH 201/284] display err.message to user if available in error msgs --- src/CallHandler.js | 2 +- src/components/structures/MatrixChat.js | 2 +- src/components/structures/RoomView.js | 4 ++-- src/components/structures/UserSettings.js | 18 +++++++++--------- .../views/dialogs/ChatInviteDialog.js | 12 ++++++------ src/components/views/rooms/MemberInfo.js | 4 ++-- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/CallHandler.js b/src/CallHandler.js index 42cc681d08..5199ef0a67 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -313,7 +313,7 @@ function _onAction(payload) { console.error("Conference call failed: " + err); Modal.createDialog(ErrorDialog, { title: "Failed to set up conference call", - description: "Conference call failed.", + description: "Conference call failed. " + ((err && err.message) ? err.message : ""), }); }); } diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index b449ff3094..9b8aa3426a 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -413,7 +413,7 @@ module.exports = React.createClass({ console.error("Failed to leave room " + payload.room_id + " " + err); Modal.createDialog(ErrorDialog, { title: "Failed to leave room", - description: "Server may be unavailable, overloaded, or you hit a bug." + description: (err && err.message ? err.message : "Server may be unavailable, overloaded, or you hit a bug."), }); }); } diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index ea8b6e2ae0..c158b87ff3 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -947,7 +947,7 @@ module.exports = React.createClass({ console.error("Failed to upload file " + file + " " + error); Modal.createDialog(ErrorDialog, { title: "Failed to upload file", - description: "Server may be unavailable, overloaded, or the file too big", + description: ((error && error.message) ? error.message : "Server may be unavailable, overloaded, or the file too big"), }); }); }, @@ -1034,7 +1034,7 @@ module.exports = React.createClass({ console.error("Search failed: " + error); Modal.createDialog(ErrorDialog, { title: "Search failed", - description: "Server may be unavailable, overloaded, or search timed out :(" + description: ((error && error.message) ? error.message : "Server may be unavailable, overloaded, or search timed out :("), }); }).finally(function() { self.setState({ diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 3e636c3eb1..ba5d5780b4 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -223,7 +223,7 @@ module.exports = React.createClass({ console.error("Failed to load user settings: " + error); Modal.createDialog(ErrorDialog, { title: "Can't load user settings", - description: "Server may be unavailable or overloaded", + description: ((error && error.message) ? error.message : "Server may be unavailable or overloaded"), }); }); }, @@ -264,8 +264,8 @@ module.exports = React.createClass({ console.error("Failed to set avatar: " + err); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - title: "Error", - description: "Failed to set avatar." + title: "Failed to set avatar", + description: ((err && err.message) ? err.message : "Operation failed"), }); }); }, @@ -366,8 +366,8 @@ module.exports = React.createClass({ this.setState({email_add_pending: false}); console.error("Unable to add email address " + email_address + " " + err); Modal.createDialog(ErrorDialog, { - title: "Error", - description: "Unable to add email address" + title: "Unable to add email address", + description: ((err && err.message) ? err.message : "Operation failed"), }); }); ReactDOM.findDOMNode(this.refs.add_email_input).blur(); @@ -391,8 +391,8 @@ module.exports = React.createClass({ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Unable to remove contact information: " + err); Modal.createDialog(ErrorDialog, { - title: "Error", - description: "Unable to remove contact information", + title: "Unable to remove contact information", + description: ((err && err.message) ? err.message : "Operation failed"), }); }).done(); } @@ -432,8 +432,8 @@ module.exports = React.createClass({ var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Unable to verify email address: " + err); Modal.createDialog(ErrorDialog, { - title: "Error", - description: "Unable to verify email address", + title: "Unable to verify email address", + description: ((err && err.message) ? err.message : "Operation failed"), }); } }); diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index 16f756a773..7ba503099a 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -308,8 +308,8 @@ module.exports = React.createClass({ console.error(err.stack); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - title: "Error", - description: "Failed to invite", + title: "Failed to invite", + description: ((err && err.message) ? err.message : "Operation failed"), }); return null; }) @@ -321,8 +321,8 @@ module.exports = React.createClass({ console.error(err.stack); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - title: "Error", - description: "Failed to invite user", + title: "Failed to invite user", + description: ((err && err.message) ? err.message : "Operation failed"), }); return null; }) @@ -342,8 +342,8 @@ module.exports = React.createClass({ console.error(err.stack); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - title: "Error", - description: "Failed to invite", + title: "Failed to invite", + description: ((err && err.message) ? err.message : "Operation failed"), }); return null; }) diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 1459ad3eb7..1a9a8d5e0f 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -241,8 +241,8 @@ module.exports = WithMatrixClient(React.createClass({ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Kick error: " + err); Modal.createDialog(ErrorDialog, { - title: "Error", - description: "Failed to kick user", + title: "Failed to kick", + description: ((err && err.message) ? err.message : "Operation failed"), }); } ).finally(()=>{ From 24f2aed45f890a630a9c095e44c7f86d57ffe32e Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 23 Apr 2017 04:05:50 +0100 Subject: [PATCH 202/284] summarise profile changes in MELS fixes https://github.com/vector-im/riot-web/issues/3463 --- src/components/structures/MessagePanel.js | 4 +--- .../views/elements/MemberEventListSummary.js | 21 ++++++++++++++++++- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 8d50789eb0..87f444d607 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -279,9 +279,7 @@ module.exports = React.createClass({ this.currentGhostEventId = null; } - var isMembershipChange = (e) => - e.getType() === 'm.room.member' - && (!e.getPrevContent() || e.getContent().membership !== e.getPrevContent().membership); + var isMembershipChange = (e) => e.getType() === 'm.room.member'; for (i = 0; i < this.props.events.length; i++) { var mxEv = this.props.events[i]; diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.js index d7f876c16e..8eb81ae5f1 100644 --- a/src/components/views/elements/MemberEventListSummary.js +++ b/src/components/views/elements/MemberEventListSummary.js @@ -221,6 +221,8 @@ module.exports = React.createClass({ "banned": beConjugated + " banned", "unbanned": beConjugated + " unbanned", "kicked": beConjugated + " kicked", + "changed_name": "changed name", + "changed_avatar": "changed avatar", }; if (Object.keys(map).includes(t)) { @@ -289,7 +291,24 @@ module.exports = React.createClass({ switch (e.mxEvent.getContent().membership) { case 'invite': return 'invited'; case 'ban': return 'banned'; - case 'join': return 'joined'; + case 'join': + if (e.mxEvent.getPrevContent().membership === 'join') { + if (e.mxEvent.getContent().displayname !== + e.mxEvent.getPrevContent().displayname) + { + return 'changed_name'; + } + else if (e.mxEvent.getContent().avatar_url !== + e.mxEvent.getPrevContent().avatar_url) + { + return 'changed_avatar'; + } + console.info("MELS ignoring duplicate membership join event"); + return null; + } + else { + return 'joined'; + } case 'leave': if (e.mxEvent.getSender() === e.mxEvent.getStateKey()) { switch (e.mxEvent.getPrevContent().membership) { From 0590ce7faf9680d9d720a43de786545e7da5e7e6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sun, 23 Apr 2017 06:06:23 +0100 Subject: [PATCH 203/284] Conform damn you (mostly) Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/Notifier.js | 53 ++++++++++++++++++++++++------------------------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/src/Notifier.js b/src/Notifier.js index 92770877b7..617135a2c8 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -15,11 +15,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -var MatrixClientPeg = require("./MatrixClientPeg"); -var PlatformPeg = require("./PlatformPeg"); -var TextForEvent = require('./TextForEvent'); -var Avatar = require('./Avatar'); -var dis = require("./dispatcher"); +import MatrixClientPeg from './MatrixClientPeg'; +import PlatformPeg from './PlatformPeg'; +import TextForEvent from './TextForEvent'; +import Avatar from './Avatar'; +import dis from './dispatcher'; /* * Dispatches: @@ -29,7 +29,7 @@ var dis = require("./dispatcher"); * } */ -var Notifier = { +const Notifier = { notifsByRoom: {}, notificationMessageForEvent: function(ev) { @@ -48,16 +48,16 @@ var Notifier = { return; } - var msg = this.notificationMessageForEvent(ev); + let msg = this.notificationMessageForEvent(ev); if (!msg) return; - var title; - if (!ev.sender || room.name == ev.sender.name) { + let title; + if (!ev.sender || room.name === ev.sender.name) { title = room.name; // notificationMessageForEvent includes sender, // but we already have the sender here if (ev.getContent().body) msg = ev.getContent().body; - } else if (ev.getType() == 'm.room.member') { + } else if (ev.getType() === 'm.room.member') { // context is all in the message here, we don't need // to display sender info title = room.name; @@ -68,7 +68,7 @@ var Notifier = { if (ev.getContent().body) msg = ev.getContent().body; } - var avatarUrl = ev.sender ? Avatar.avatarUrlForMember( + const avatarUrl = ev.sender ? Avatar.avatarUrlForMember( ev.sender, 40, 40, 'crop' ) : null; @@ -83,7 +83,7 @@ var Notifier = { }, _playAudioNotification: function(ev, room) { - var e = document.getElementById("messageAudio"); + const e = document.getElementById("messageAudio"); if (e) { e.load(); e.play(); @@ -95,7 +95,7 @@ var Notifier = { this.boundOnSyncStateChange = this.onSyncStateChange.bind(this); this.boundOnRoomReceipt = this.onRoomReceipt.bind(this); MatrixClientPeg.get().on('Room.timeline', this.boundOnRoomTimeline); - MatrixClientPeg.get().on("Room.receipt", this.boundOnRoomReceipt); + MatrixClientPeg.get().on('Room.receipt', this.boundOnRoomReceipt); MatrixClientPeg.get().on("sync", this.boundOnSyncStateChange); this.toolbarHidden = false; this.isSyncing = false; @@ -104,7 +104,7 @@ var Notifier = { stop: function() { if (MatrixClientPeg.get() && this.boundOnRoomTimeline) { MatrixClientPeg.get().removeListener('Room.timeline', this.boundOnRoomTimeline); - MatrixClientPeg.get().removeListener("Room.receipt", this.boundOnRoomReceipt); + MatrixClientPeg.get().removeListener('Room.receipt', this.boundOnRoomReceipt); MatrixClientPeg.get().removeListener('sync', this.boundOnSyncStateChange); } this.isSyncing = false; @@ -121,7 +121,7 @@ var Notifier = { // make sure that we persist the current setting audio_enabled setting // before changing anything if (global.localStorage) { - if(global.localStorage.getItem('audio_notifications_enabled') == null) { + if (global.localStorage.getItem('audio_notifications_enabled') === null) { this.setAudioEnabled(this.isEnabled()); } } @@ -141,7 +141,7 @@ var Notifier = { if (callback) callback(); dis.dispatch({ action: "notifier_enabled", - value: true + value: true, }); }); // clear the notifications_hidden flag, so that if notifications are @@ -152,7 +152,7 @@ var Notifier = { global.localStorage.setItem('notifications_enabled', 'false'); dis.dispatch({ action: "notifier_enabled", - value: false + value: false, }); } }, @@ -165,7 +165,7 @@ var Notifier = { if (!global.localStorage) return true; - var enabled = global.localStorage.getItem('notifications_enabled'); + const enabled = global.localStorage.getItem('notifications_enabled'); if (enabled === null) return true; return enabled === 'true'; }, @@ -173,12 +173,12 @@ var Notifier = { setAudioEnabled: function(enable) { if (!global.localStorage) return; global.localStorage.setItem('audio_notifications_enabled', - enable ? 'true' : 'false'); + enable ? 'true' : 'false'); }, isAudioEnabled: function(enable) { if (!global.localStorage) return true; - var enabled = global.localStorage.getItem( + const enabled = global.localStorage.getItem( 'audio_notifications_enabled'); // default to true if the popups are enabled if (enabled === null) return this.isEnabled(); @@ -192,7 +192,7 @@ var Notifier = { // this is nothing to do with notifier_enabled dis.dispatch({ action: "notifier_enabled", - value: this.isEnabled() + value: this.isEnabled(), }); // update the info to localStorage for persistent settings @@ -215,8 +215,7 @@ var Notifier = { onSyncStateChange: function(state) { if (state === "SYNCING") { this.isSyncing = true; - } - else if (state === "STOPPED" || state === "ERROR") { + } else if (state === "STOPPED" || state === "ERROR") { this.isSyncing = false; } }, @@ -225,10 +224,10 @@ var Notifier = { if (toStartOfTimeline) return; if (!room) return; if (!this.isSyncing) return; // don't alert for any messages initially - if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) return; + if (ev.sender && ev.sender.userId === MatrixClientPeg.get().credentials.userId) return; if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return; - var actions = MatrixClientPeg.get().getPushActionsForEvent(ev); + const actions = MatrixClientPeg.get().getPushActionsForEvent(ev); if (actions && actions.notify) { if (this.isEnabled()) { this._displayPopupNotification(ev, room); @@ -240,7 +239,7 @@ var Notifier = { }, onRoomReceipt: function(ev, room) { - if (room.getUnreadNotificationCount() == 0) { + if (room.getUnreadNotificationCount() === 0) { // ideally we would clear each notification when it was read, // but we have no way, given a read receipt, to know whether // the receipt comes before or after an event, so we can't @@ -255,7 +254,7 @@ var Notifier = { } delete this.notifsByRoom[room.roomId]; } - } + }, }; if (!global.mxNotifier) { From 5e8b43f3edc70cd8d73a920c9a231c0f681c43fb Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sun, 23 Apr 2017 06:16:25 +0100 Subject: [PATCH 204/284] if we're not granted, show an ErrorDialog with some text which needs changing Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/Notifier.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Notifier.js b/src/Notifier.js index 617135a2c8..fed2760732 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -20,6 +20,8 @@ import PlatformPeg from './PlatformPeg'; import TextForEvent from './TextForEvent'; import Avatar from './Avatar'; import dis from './dispatcher'; +import sdk from './index'; +import Modal from './Modal'; /* * Dispatches: @@ -131,6 +133,14 @@ const Notifier = { plaf.requestNotificationPermission().done((result) => { if (result !== 'granted') { // The permission request was dismissed or denied + const description = result === 'denied' + ? 'Your browser is not permitting this app to send you notifications.' + : 'It seems you didn\'t accept notifications when your browser asked'; + const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); + Modal.createDialog(ErrorDialog, { + title: 'Unable to enable Notifications', + description, + }); return; } From 6f461f0ebbb084a73f545b8008b34d96c351312b Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 24 Apr 2017 01:09:54 +0100 Subject: [PATCH 205/284] add in scrollto button --- src/components/views/rooms/TopUnreadMessagesBar.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/TopUnreadMessagesBar.js b/src/components/views/rooms/TopUnreadMessagesBar.js index 5bef8c0b0a..72b489a406 100644 --- a/src/components/views/rooms/TopUnreadMessagesBar.js +++ b/src/components/views/rooms/TopUnreadMessagesBar.js @@ -1,5 +1,6 @@ /* Copyright 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -32,7 +33,10 @@ module.exports = React.createClass({
      - Jump to first unread message. Mark all read + Scroll to unread messages + Jump to first unread message.
      Date: Mon, 24 Apr 2017 12:53:53 +0100 Subject: [PATCH 206/284] fix scroll behaviour on macs with no gemini --- src/components/views/rooms/RoomList.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 3810f7d4d6..96ff65498f 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -456,11 +456,10 @@ module.exports = React.createClass({ var panel = ReactDOM.findDOMNode(this); if (!panel) return null; - if (panel.classList.contains('gm-prevented')) { - return panel; - } else { - return panel.children[2]; // XXX: Fragile! - } + // empirically, if we have gm-prevented for some reason, the scroll node + // is still the 3rd child (i.e. the view child). This looks to be due + // to vdh's improved resize updater logic...? + return panel.children[2]; // XXX: Fragile! }, _whenScrolling: function(e) { @@ -506,7 +505,7 @@ module.exports = React.createClass({ // Use the offset of the top of the scroll area from the window // as this is used to calculate the CSS fixed top position for the stickies var scrollAreaOffset = scrollArea.getBoundingClientRect().top + window.pageYOffset; - // Use the offset of the top of the componet from the window + // Use the offset of the top of the component from the window // as this is used to calculate the CSS fixed top position for the stickies var scrollAreaHeight = ReactDOM.findDOMNode(this).getBoundingClientRect().height; From 74e92d6c235e629802184c86d0323587dec9f82f Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 24 Apr 2017 15:44:45 +0100 Subject: [PATCH 207/284] Remove DM-guessing code --- src/components/views/rooms/RoomList.js | 53 +++----------------------- 1 file changed, 6 insertions(+), 47 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 3810f7d4d6..5839b66d1c 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -97,7 +97,7 @@ module.exports = React.createClass({ if (this.props.selectedRoom) { constantTimeDispatcher.dispatch( "RoomTile.select", this.props.selectedRoom, {} - ); + ); } constantTimeDispatcher.dispatch( "RoomTile.select", nextProps.selectedRoom, { selected: true } @@ -265,7 +265,7 @@ module.exports = React.createClass({ }, onRoomStateMember: function(ev, state, member) { - if (ev.getStateKey() === MatrixClientPeg.get().credentials.userId && + if (ev.getStateKey() === MatrixClientPeg.get().credentials.userId && ev.getPrevContent() && ev.getPrevContent().membership === "invite") { this._delayedRefreshRoomList(); @@ -290,7 +290,7 @@ module.exports = React.createClass({ this._delayedRefreshRoomList(); } else if (ev.getType() == 'm.push_rules') { - this._delayedRefreshRoomList(); + this._delayedRefreshRoomList(); } }, @@ -318,7 +318,7 @@ module.exports = React.createClass({ // as needed. // Alternatively we'd do something magical with Immutable.js or similar. this.setState(this.getRoomLists()); - + // this._lastRefreshRoomListTs = Date.now(); }, @@ -341,7 +341,7 @@ module.exports = React.createClass({ MatrixClientPeg.get().getRooms().forEach(function(room) { const me = room.getMember(MatrixClientPeg.get().credentials.userId); if (!me) return; - + // console.log("room = " + room.name + ", me.membership = " + me.membership + // ", sender = " + me.events.member.getSender() + // ", target = " + me.events.member.getStateKey() + @@ -391,51 +391,10 @@ module.exports = React.createClass({ } }); - if (s.lists["im.vector.fake.direct"].length == 0 && - MatrixClientPeg.get().getAccountData('m.direct') === undefined && - !MatrixClientPeg.get().isGuest()) - { - // scan through the 'recents' list for any rooms which look like DM rooms - // and make them DM rooms - const oldRecents = s.lists["im.vector.fake.recent"]; - s.lists["im.vector.fake.recent"] = []; - - for (const room of oldRecents) { - const me = room.getMember(MatrixClientPeg.get().credentials.userId); - - if (me && Rooms.looksLikeDirectMessageRoom(room, me)) { - self.listsForRoomId[room.roomId].push("im.vector.fake.direct"); - s.lists["im.vector.fake.direct"].push(room); - } else { - self.listsForRoomId[room.roomId].push("im.vector.fake.recent"); - s.lists["im.vector.fake.recent"].push(room); - } - } - - // save these new guessed DM rooms into the account data - const newMDirectEvent = {}; - for (const room of s.lists["im.vector.fake.direct"]) { - const me = room.getMember(MatrixClientPeg.get().credentials.userId); - const otherPerson = Rooms.getOnlyOtherMember(room, me); - if (!otherPerson) continue; - - const roomList = newMDirectEvent[otherPerson.userId] || []; - roomList.push(room.roomId); - newMDirectEvent[otherPerson.userId] = roomList; - } - - console.warn("Resetting room DM state to be " + JSON.stringify(newMDirectEvent)); - - // if this fails, fine, we'll just do the same thing next time we get the room lists - MatrixClientPeg.get().setAccountData('m.direct', newMDirectEvent).done(); - } - - //console.log("calculated new roomLists; im.vector.fake.recent = " + s.lists["im.vector.fake.recent"]); - // we actually apply the sorting to this when receiving the prop in RoomSubLists. // we'll need this when we get to iterating through lists programatically - e.g. ctrl-shift-up/down -/* +/* this.listOrder = [ "im.vector.fake.invite", "m.favourite", From 3bd77d56db1a2910e3e8872a84876a1533810a0d Mon Sep 17 00:00:00 2001 From: turt2live Date: Mon, 24 Apr 2017 08:43:51 -0600 Subject: [PATCH 208/284] Allow h1 and h2 tags again. CSS handled by riot-web Signed-off-by: Travis Ralston --- src/HtmlUtils.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index 632542ac43..a31601790f 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -111,8 +111,7 @@ var sanitizeHtmlParams = { allowedTags: [ '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', + 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol', 'nl', 'li', 'b', 'i', 'u', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div', 'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'span', ], @@ -165,12 +164,6 @@ var sanitizeHtmlParams = { attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/ return { tagName: tagName, attribs : attribs }; }, - 'h1': function(tagName, attribs) { - return { tagName: 'h3', attribs: attribs }; - }, - 'h2': function(tagName, attribs) { - return { tagName: 'h3', attribs: attribs }; - }, '*': function(tagName, attribs) { // Delete any style previously assigned, style is an allowedTag for font and span // because attributes are stripped after transforming From bb6dd363d70d9ebeaa650a8349c233251c0b00be Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 10 Apr 2017 12:07:39 +0100 Subject: [PATCH 209/284] unbreak in-app permalinks correctly --- src/linkify-matrix.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/linkify-matrix.js b/src/linkify-matrix.js index e085b1a27a..c8e20316a9 100644 --- a/src/linkify-matrix.js +++ b/src/linkify-matrix.js @@ -122,7 +122,7 @@ var escapeRegExp = function(string) { // anyone else really should be using matrix.to. matrixLinkify.VECTOR_URL_PATTERN = "^(?:https?:\/\/)?(?:" + escapeRegExp(window.location.host + window.location.pathname) + "|" - + "(?:www\\.)?(riot|vector)\\.im/(?:beta|staging|develop)/" + + "(?:www\\.)?(?:riot|vector)\\.im/(?:beta|staging|develop)/" + ")(#.*)"; matrixLinkify.MATRIXTO_URL_PATTERN = "^(?:https?:\/\/)?(?:www\\.)?matrix\\.to/#/((#|@|!).*)"; From cf2cf66caebea125912b1ff228935d2fc2497213 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 22 Apr 2017 21:06:38 +0100 Subject: [PATCH 210/284] fix deep-linking to riot.im/app --- src/linkify-matrix.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/linkify-matrix.js b/src/linkify-matrix.js index c8e20316a9..d9b0b78982 100644 --- a/src/linkify-matrix.js +++ b/src/linkify-matrix.js @@ -122,7 +122,7 @@ var escapeRegExp = function(string) { // anyone else really should be using matrix.to. matrixLinkify.VECTOR_URL_PATTERN = "^(?:https?:\/\/)?(?:" + escapeRegExp(window.location.host + window.location.pathname) + "|" - + "(?:www\\.)?(?:riot|vector)\\.im/(?:beta|staging|develop)/" + + "(?:www\\.)?(?:riot|vector)\\.im/(?:app|beta|staging|develop)/" + ")(#.*)"; matrixLinkify.MATRIXTO_URL_PATTERN = "^(?:https?:\/\/)?(?:www\\.)?matrix\\.to/#/((#|@|!).*)"; From 8e6981db44c7a765cc26f21c3fde76d5a723debe Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 24 Apr 2017 18:24:28 +0100 Subject: [PATCH 211/284] Prepare changelog for v0.8.8-rc.2 --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d4a69fb5b..32f16c46ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +Changes in [0.8.8-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.8-rc.2) (2017-04-24) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.8-rc.1...v0.8.8-rc.2) + + * Fix bug where links to Riot would fail to open. + + Changes in [0.8.8-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.8-rc.1) (2017-04-21) ============================================================================================================= [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.7...v0.8.8-rc.1) From e569144d6f700cf05e52f67b66aa92237ee7546f Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 24 Apr 2017 18:24:29 +0100 Subject: [PATCH 212/284] v0.8.8-rc.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 38d08344e0..5fc8bc2750 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "0.8.8-rc.1", + "version": "0.8.8-rc.2", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From 63dac026a89954f4487a5a95e697051a1e75b108 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 25 Apr 2017 00:17:46 +0100 Subject: [PATCH 213/284] remove spammy log --- src/components/views/elements/MemberEventListSummary.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.js index 8eb81ae5f1..63bd2a7c39 100644 --- a/src/components/views/elements/MemberEventListSummary.js +++ b/src/components/views/elements/MemberEventListSummary.js @@ -303,7 +303,7 @@ module.exports = React.createClass({ { return 'changed_avatar'; } - console.info("MELS ignoring duplicate membership join event"); + // console.log("MELS ignoring duplicate membership join event"); return null; } else { From e64b647799e57045d25aabc6e3c448f1a7e57cd5 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 Apr 2017 09:25:10 +0100 Subject: [PATCH 214/284] show the room name in the UDE Dialog especially useful when it appears after you switch rooms Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/dialogs/UnknownDeviceDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/UnknownDeviceDialog.js b/src/components/views/dialogs/UnknownDeviceDialog.js index da9c8e8f65..4f3d4301f9 100644 --- a/src/components/views/dialogs/UnknownDeviceDialog.js +++ b/src/components/views/dialogs/UnknownDeviceDialog.js @@ -149,7 +149,7 @@ export default React.createClass({ >

      - This room contains devices that you haven't seen before. + "{this.props.room.name}" contains devices that you haven't seen before.

      { warning } Unknown devices: From 74dabb20873cb4650041710085d2ed06b3476218 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 25 Apr 2017 10:52:44 +0100 Subject: [PATCH 215/284] Released js-sdk --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5fc8bc2750..d14bc2b766 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "isomorphic-fetch": "^2.2.1", "linkifyjs": "^2.1.3", "lodash": "^4.13.1", - "matrix-js-sdk": "0.7.7-rc.1", + "matrix-js-sdk": "0.7.7", "optimist": "^0.6.1", "q": "^1.4.1", "react": "^15.4.0", From f6fd7b04ac01d7a7e3ef83ca186d9c462b047cb1 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 25 Apr 2017 10:53:58 +0100 Subject: [PATCH 216/284] Prepare changelog for v0.8.8 --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32f16c46ae..97dda666de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +Changes in [0.8.8](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.8) (2017-04-25) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.8-rc.2...v0.8.8) + + * No changes + + Changes in [0.8.8-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.8-rc.2) (2017-04-24) ============================================================================================================= [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.8-rc.1...v0.8.8-rc.2) From d81adb234a7581169a2ddbf2f0ee6375e5c340ce Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 25 Apr 2017 10:53:59 +0100 Subject: [PATCH 217/284] v0.8.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d14bc2b766..00dc902cc9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "0.8.8-rc.2", + "version": "0.8.8", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From 336462366e033a43ac76c133bd74dea1b0222c35 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 25 Apr 2017 11:21:47 +0100 Subject: [PATCH 218/284] Improve country dropdown UX and expose +prefix A prefix is now exposed through a change to the API for onOptionChange. This now returns the entire country object which includes iso2, prefix etc. This also shows the prefix in the Registration and Login screens as a prefix to the phone number field. --- src/components/views/login/CountryDropdown.js | 18 +++++++-- src/components/views/login/PasswordLogin.js | 40 +++++++++++-------- .../views/login/RegistrationForm.js | 28 ++++++++----- .../views/settings/AddPhoneNumber.js | 7 ++-- 4 files changed, 61 insertions(+), 32 deletions(-) diff --git a/src/components/views/login/CountryDropdown.js b/src/components/views/login/CountryDropdown.js index 7f6b21650d..da4e770093 100644 --- a/src/components/views/login/CountryDropdown.js +++ b/src/components/views/login/CountryDropdown.js @@ -37,6 +37,7 @@ export default class CountryDropdown extends React.Component { constructor(props) { super(props); this._onSearchChange = this._onSearchChange.bind(this); + this._onOptionChange = this._onOptionChange.bind(this); this.state = { searchQuery: '', @@ -48,7 +49,7 @@ export default class CountryDropdown extends React.Component { // If no value is given, we start with the first // country selected, but our parent component // doesn't know this, therefore we do this. - this.props.onOptionChange(COUNTRIES[0].iso2); + this.props.onOptionChange(COUNTRIES[0]); } } @@ -58,6 +59,10 @@ export default class CountryDropdown extends React.Component { }); } + _onOptionChange(iso2) { + this.props.onOptionChange(COUNTRIES_BY_ISO2[iso2]); + } + _flagImgForIso2(iso2) { // Unicode Regional Indicator Symbol letter 'A' const RIS_A = 0x1F1E6; @@ -68,6 +73,10 @@ export default class CountryDropdown extends React.Component { ); } + getCountryPrefix(iso2) { + return COUNTRIES_BY_ISO2[iso2].prefix; + } + render() { const Dropdown = sdk.getComponent('elements.Dropdown'); @@ -102,9 +111,11 @@ export default class CountryDropdown extends React.Component { // values between mounting and the initial value propgating const value = this.props.value || COUNTRIES[0].iso2; + const getShortOption = this.props.isSmall ? this._flagImgForIso2 : undefined; + return {options} @@ -114,6 +125,7 @@ export default class CountryDropdown extends React.Component { CountryDropdown.propTypes = { className: React.PropTypes.string, + isSmall: React.PropTypes.bool, onOptionChange: React.PropTypes.func.isRequired, value: React.PropTypes.string, }; diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index 568461817c..349dd0d139 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.js @@ -90,8 +90,11 @@ class PasswordLogin extends React.Component { } onPhoneCountryChanged(country) { - this.setState({phoneCountry: country}); - this.props.onPhoneCountryChanged(country); + this.setState({ + phoneCountry: country.iso2, + phonePrefix: country.prefix, + }); + this.props.onPhoneCountryChanged(country.iso2); } onPhoneNumberChanged(ev) { @@ -121,16 +124,17 @@ class PasswordLogin extends React.Component { const mxidInputClasses = classNames({ "mx_Login_field": true, "mx_Login_username": true, + "mx_Login_field_has_prefix": true, "mx_Login_field_has_suffix": Boolean(this.props.hsDomain), }); let suffix = null; if (this.props.hsDomain) { - suffix =
      + suffix =
      :{this.props.hsDomain}
      ; } - return
      -
      @
      + return
      +
      @
      ; case PasswordLogin.LOGIN_FIELD_PHONE: const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); + const prefix = this.state.phonePrefix; return
      - +
      +
      +{prefix}
      + +
      ; } } diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js index 4868c9de63..a0b56e30ac 100644 --- a/src/components/views/login/RegistrationForm.js +++ b/src/components/views/login/RegistrationForm.js @@ -270,7 +270,8 @@ module.exports = React.createClass({ _onPhoneCountryChange(newVal) { this.setState({ - phoneCountry: newVal, + phoneCountry: newVal.iso2, + phonePrefix: newVal.prefix, }); }, @@ -316,15 +317,22 @@ module.exports = React.createClass({ className="mx_Login_phoneCountry" value={this.state.phoneCountry} /> - + +
      +
      +{this.state.phonePrefix}
      + +
      ); diff --git a/src/components/views/settings/AddPhoneNumber.js b/src/components/views/settings/AddPhoneNumber.js index 3a348393aa..35dd5548d1 100644 --- a/src/components/views/settings/AddPhoneNumber.js +++ b/src/components/views/settings/AddPhoneNumber.js @@ -50,7 +50,7 @@ export default WithMatrixClient(React.createClass({ }, _onPhoneCountryChange: function(phoneCountry) { - this.setState({ phoneCountry: phoneCountry }); + this.setState({ phoneCountry: phoneCountry.iso2 }); }, _onPhoneNumberChange: function(ev) { @@ -149,10 +149,11 @@ export default WithMatrixClient(React.createClass({
      -
      +
      Date: Tue, 25 Apr 2017 11:25:14 +0100 Subject: [PATCH 219/284] Remove redundant API for getting country prefix --- src/components/views/login/CountryDropdown.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/components/views/login/CountryDropdown.js b/src/components/views/login/CountryDropdown.js index da4e770093..6323b3f558 100644 --- a/src/components/views/login/CountryDropdown.js +++ b/src/components/views/login/CountryDropdown.js @@ -73,10 +73,6 @@ export default class CountryDropdown extends React.Component { ); } - getCountryPrefix(iso2) { - return COUNTRIES_BY_ISO2[iso2].prefix; - } - render() { const Dropdown = sdk.getComponent('elements.Dropdown'); From 0d4ab072500d6ec2187b901f6a7c38f1d046cfa1 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 25 Apr 2017 11:53:14 +0100 Subject: [PATCH 220/284] Fix not autoSelecting first item in dropdown Fixes https://github.com/vector-im/riot-web/issues/3686 --- src/components/views/elements/Dropdown.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/Dropdown.js b/src/components/views/elements/Dropdown.js index a9ecf5b669..074853bb92 100644 --- a/src/components/views/elements/Dropdown.js +++ b/src/components/views/elements/Dropdown.js @@ -115,7 +115,7 @@ export default class Dropdown extends React.Component { componentWillReceiveProps(nextProps) { this._reindexChildren(nextProps.children); - const firstChild = React.Children.toArray(nextProps.children)[0]; + const firstChild = nextProps.children[0]; this.setState({ highlightedOption: firstChild ? firstChild.key : null, }); From 1e9a2e80e99cc160fee60c45ad660aff05755ed8 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 25 Apr 2017 11:57:03 +0100 Subject: [PATCH 221/284] Remove empty line --- src/components/views/login/RegistrationForm.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js index a0b56e30ac..2bc2b8946a 100644 --- a/src/components/views/login/RegistrationForm.js +++ b/src/components/views/login/RegistrationForm.js @@ -317,7 +317,6 @@ module.exports = React.createClass({ className="mx_Login_phoneCountry" value={this.state.phoneCountry} /> -
      +{this.state.phonePrefix}
      Date: Tue, 25 Apr 2017 17:05:54 +0100 Subject: [PATCH 222/284] Guard against no children --- src/components/views/elements/Dropdown.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/views/elements/Dropdown.js b/src/components/views/elements/Dropdown.js index 074853bb92..b4d2545e04 100644 --- a/src/components/views/elements/Dropdown.js +++ b/src/components/views/elements/Dropdown.js @@ -114,6 +114,9 @@ export default class Dropdown extends React.Component { } componentWillReceiveProps(nextProps) { + if (!nextProps.children || nextProps.children.length === 0) { + return; + } this._reindexChildren(nextProps.children); const firstChild = nextProps.children[0]; this.setState({ From 96e7479d8b038a21a94d33107c1617811dc38d48 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 25 Apr 2017 17:19:36 +0100 Subject: [PATCH 223/284] Show "jump to message" when message is not paginated --- src/components/structures/RoomView.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index c158b87ff3..8a355a8f6d 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1280,7 +1280,8 @@ module.exports = React.createClass({ // we want to show the bar if the read-marker is off the top of the // screen. - var showBar = (pos < 0); + // If pos is null, the event might not be paginated, so show the unread bar! + var showBar = pos < 0 || pos === null; if (this.state.showTopUnreadMessagesBar != showBar) { this.setState({showTopUnreadMessagesBar: showBar}, From bc045698b959f7fe111fa25f345a95b996a1a416 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 25 Apr 2017 18:01:56 +0100 Subject: [PATCH 224/284] Fix for fuse 2.7.2 As of v2.7.2, fuse.js introduces a regression where the second argument to the constructor `Fuse` is assumed to be an object. There was one instance where we were not passing any argument. This fixes that. --- src/autocomplete/EmojiProvider.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/autocomplete/EmojiProvider.js b/src/autocomplete/EmojiProvider.js index a2d77f02a1..d488ac53ae 100644 --- a/src/autocomplete/EmojiProvider.js +++ b/src/autocomplete/EmojiProvider.js @@ -14,7 +14,7 @@ let instance = null; export default class EmojiProvider extends AutocompleteProvider { constructor() { super(EMOJI_REGEX); - this.fuse = new Fuse(EMOJI_SHORTNAMES); + this.fuse = new Fuse(EMOJI_SHORTNAMES, {}); } async getCompletions(query: string, selection: SelectionRange) { From fa9c2d137330e6e90891132fa3b4a42c4ee03afd Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 25 Apr 2017 19:21:09 +0100 Subject: [PATCH 225/284] Fix specifying custom server for registration Broken by https://github.com/matrix-org/matrix-react-sdk/commit/9cd7914ea51dbfb60f8b84a80cb800282476d3e4 (ServerConfig interface changed but Registration not updated) --- .../structures/login/Registration.js | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index 4e0d61e716..5501a39b58 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -123,18 +123,17 @@ module.exports = React.createClass({ } }, - onHsUrlChanged: function(newHsUrl) { - this.setState({ - hsUrl: newHsUrl, + onServerConfigChange: function(config) { + let newState = {}; + if (config.hsUrl !== undefined) { + newState.hsUrl = config.hsUrl; + } + if (config.isUrl !== undefined) { + newState.isUrl = config.isUrl; + } + this.setState(newState, function() { + this._replaceClient(); }); - this._replaceClient(); - }, - - onIsUrlChanged: function(newIsUrl) { - this.setState({ - isUrl: newIsUrl, - }); - this._replaceClient(); }, _replaceClient: function() { @@ -390,8 +389,7 @@ module.exports = React.createClass({ customIsUrl={this.props.customIsUrl} defaultHsUrl={this.props.defaultHsUrl} defaultIsUrl={this.props.defaultIsUrl} - onHsUrlChanged={this.onHsUrlChanged} - onIsUrlChanged={this.onIsUrlChanged} + onServerConfigChange={this.onServerConfigChange} delayTimeMs={1000} />
      From de89c1f7105232478c879d4828c687e14572d02e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 Apr 2017 22:00:50 +0100 Subject: [PATCH 226/284] lets make eslint at least somewhat happy Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/UserSettingsStore.js | 54 ++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js index 66a872958c..4d52fa00f2 100644 --- a/src/UserSettingsStore.js +++ b/src/UserSettingsStore.js @@ -15,9 +15,9 @@ limitations under the License. */ 'use strict'; -var q = require("q"); -var MatrixClientPeg = require("./MatrixClientPeg"); -var Notifier = require("./Notifier"); +import q from 'q'; +import MatrixClientPeg from './MatrixClientPeg'; +import Notifier from './Notifier'; /* * TODO: Make things use this. This is all WIP - see UserSettings.js for usage. @@ -33,7 +33,7 @@ module.exports = { ], loadProfileInfo: function() { - var cli = MatrixClientPeg.get(); + const cli = MatrixClientPeg.get(); return cli.getProfileInfo(cli.credentials.userId); }, @@ -44,7 +44,7 @@ module.exports = { loadThreePids: function() { if (MatrixClientPeg.get().isGuest()) { return q({ - threepids: [] + threepids: [], }); // guests can't poke 3pid endpoint } return MatrixClientPeg.get().getThreePids(); @@ -73,19 +73,19 @@ module.exports = { Notifier.setAudioEnabled(enable); }, - changePassword: function(old_password, new_password) { - var cli = MatrixClientPeg.get(); + changePassword: function(oldPassword, newPassword) { + const cli = MatrixClientPeg.get(); - var authDict = { + const authDict = { type: 'm.login.password', user: cli.credentials.userId, - password: old_password + password: oldPassword, }; - return cli.setPassword(authDict, new_password); + return cli.setPassword(authDict, newPassword); }, - /** + /* * Returns the email pusher (pusher of type 'email') for a given * email address. Email pushers all have the same app ID, so since * pushers are unique over (app ID, pushkey), there will be at most @@ -95,8 +95,8 @@ module.exports = { if (pushers === undefined) { return undefined; } - for (var i = 0; i < pushers.length; ++i) { - if (pushers[i].kind == 'email' && pushers[i].pushkey == address) { + for (let i = 0; i < pushers.length; ++i) { + if (pushers[i].kind === 'email' && pushers[i].pushkey === address) { return pushers[i]; } } @@ -110,7 +110,7 @@ module.exports = { addEmailPusher: function(address, data) { return MatrixClientPeg.get().setPusher({ kind: 'email', - app_id: "m.email", + app_id: 'm.email', pushkey: address, app_display_name: 'Email Notifications', device_display_name: address, @@ -121,46 +121,46 @@ module.exports = { }, getUrlPreviewsDisabled: function() { - var event = MatrixClientPeg.get().getAccountData("org.matrix.preview_urls"); + const event = MatrixClientPeg.get().getAccountData('org.matrix.preview_urls'); return (event && event.getContent().disable); }, setUrlPreviewsDisabled: function(disabled) { // FIXME: handle errors - return MatrixClientPeg.get().setAccountData("org.matrix.preview_urls", { - disable: disabled + return MatrixClientPeg.get().setAccountData('org.matrix.preview_urls', { + disable: disabled, }); }, getSyncedSettings: function() { - var event = MatrixClientPeg.get().getAccountData("im.vector.web.settings"); + const event = MatrixClientPeg.get().getAccountData('im.vector.web.settings'); return event ? event.getContent() : {}; }, getSyncedSetting: function(type, defaultValue = null) { - var settings = this.getSyncedSettings(); + const settings = this.getSyncedSettings(); return settings.hasOwnProperty(type) ? settings[type] : null; }, setSyncedSetting: function(type, value) { - var settings = this.getSyncedSettings(); + const settings = this.getSyncedSettings(); settings[type] = value; // FIXME: handle errors - return MatrixClientPeg.get().setAccountData("im.vector.web.settings", settings); + return MatrixClientPeg.get().setAccountData('im.vector.web.settings', settings); }, getLocalSettings: function() { - var localSettingsString = localStorage.getItem('mx_local_settings') || '{}'; + const localSettingsString = localStorage.getItem('mx_local_settings') || '{}'; return JSON.parse(localSettingsString); }, getLocalSetting: function(type, defaultValue = null) { - var settings = this.getLocalSettings(); + const settings = this.getLocalSettings(); return settings.hasOwnProperty(type) ? settings[type] : null; }, setLocalSetting: function(type, value) { - var settings = this.getLocalSettings(); + const settings = this.getLocalSettings(); settings[type] = value; // FIXME: handle errors localStorage.setItem('mx_local_settings', JSON.stringify(settings)); @@ -171,8 +171,8 @@ module.exports = { if (MatrixClientPeg.get().isGuest()) return false; if (localStorage.getItem(`mx_labs_feature_${feature}`) === null) { - for (var i = 0; i < this.LABS_FEATURES.length; i++) { - var f = this.LABS_FEATURES[i]; + for (let i = 0; i < this.LABS_FEATURES.length; i++) { + const f = this.LABS_FEATURES[i]; if (f.id === feature) { return f.default; } @@ -183,5 +183,5 @@ module.exports = { setFeatureEnabled: function(feature: string, enabled: boolean) { localStorage.setItem(`mx_labs_feature_${feature}`, enabled); - } + }, }; From cc53825b062c59e9fc02f7e7a429571c4067c3ff Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 Apr 2017 22:01:35 +0100 Subject: [PATCH 227/284] fix defaultValue on getLocalSetting and getSyncedSetting Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/UserSettingsStore.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js index 4d52fa00f2..9de291249f 100644 --- a/src/UserSettingsStore.js +++ b/src/UserSettingsStore.js @@ -139,7 +139,7 @@ module.exports = { getSyncedSetting: function(type, defaultValue = null) { const settings = this.getSyncedSettings(); - return settings.hasOwnProperty(type) ? settings[type] : null; + return settings.hasOwnProperty(type) ? settings[type] : defaultValue; }, setSyncedSetting: function(type, value) { @@ -156,7 +156,7 @@ module.exports = { getLocalSetting: function(type, defaultValue = null) { const settings = this.getLocalSettings(); - return settings.hasOwnProperty(type) ? settings[type] : null; + return settings.hasOwnProperty(type) ? settings[type] : defaultValue; }, setLocalSetting: function(type, value) { From 6cbd04045d67197679a318944bdd40eb9d68c697 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 Apr 2017 22:17:25 +0100 Subject: [PATCH 228/284] change the now working defaults to what they effectively were when defaultValue was broken (hopefully tests now pass) Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/MessageComposer.js | 2 +- src/components/views/rooms/MessageComposerInput.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 8a3b128908..88230062fe 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -50,7 +50,7 @@ export default class MessageComposer extends React.Component { inputState: { style: [], blockType: null, - isRichtextEnabled: UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', true), + isRichtextEnabled: UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false), wordCount: 0, }, showFormatting: UserSettingsStore.getSyncedSetting('MessageComposer.showFormatting', false), diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 417d003226..8efd2fa579 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -94,7 +94,7 @@ export default class MessageComposerInput extends React.Component { this.setDisplayedCompletion = this.setDisplayedCompletion.bind(this); this.onMarkdownToggleClicked = this.onMarkdownToggleClicked.bind(this); - const isRichtextEnabled = UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', true); + const isRichtextEnabled = UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false); this.state = { // whether we're in rich text or markdown mode From 04f44e920192f8528b6d016a0bef2232d2467043 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 26 Apr 2017 13:48:03 +0100 Subject: [PATCH 229/284] Style fixes for LoggedInView PRing this becaise I was going to change LoggedInView, so did some code style updates, but then decided the do the change elsewhere. --- src/components/structures/LoggedInView.js | 25 ++++++++++++----------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 4c012b42a8..9f01b0082b 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -162,19 +163,19 @@ export default React.createClass({ }, render: function() { - var LeftPanel = sdk.getComponent('structures.LeftPanel'); - var RightPanel = sdk.getComponent('structures.RightPanel'); - var RoomView = sdk.getComponent('structures.RoomView'); - var UserSettings = sdk.getComponent('structures.UserSettings'); - var CreateRoom = sdk.getComponent('structures.CreateRoom'); - var RoomDirectory = sdk.getComponent('structures.RoomDirectory'); - var HomePage = sdk.getComponent('structures.HomePage'); - var MatrixToolbar = sdk.getComponent('globals.MatrixToolbar'); - var GuestWarningBar = sdk.getComponent('globals.GuestWarningBar'); - var NewVersionBar = sdk.getComponent('globals.NewVersionBar'); + const LeftPanel = sdk.getComponent('structures.LeftPanel'); + const RightPanel = sdk.getComponent('structures.RightPanel'); + const RoomView = sdk.getComponent('structures.RoomView'); + const UserSettings = sdk.getComponent('structures.UserSettings'); + const CreateRoom = sdk.getComponent('structures.CreateRoom'); + const RoomDirectory = sdk.getComponent('structures.RoomDirectory'); + const HomePage = sdk.getComponent('structures.HomePage'); + const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar'); + const GuestWarningBar = sdk.getComponent('globals.GuestWarningBar'); + const NewVersionBar = sdk.getComponent('globals.NewVersionBar'); - var page_element; - var right_panel = ''; + let page_element; + let right_panel = ''; switch (this.props.page_type) { case PageTypes.RoomView: From df283dae4767cbfcb4b073a71a1b384790dae70b Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 26 Apr 2017 14:05:09 +0100 Subject: [PATCH 230/284] Show spinner until first sync has completed Shows the 'forward paginating' spinner until the first sync has completed. Fixes https://github.com/vector-im/riot-web/issues/3318 --- src/components/structures/TimelinePanel.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 8babdaae4a..dde86e1ce9 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -1058,11 +1058,18 @@ var TimelinePanel = React.createClass({ // 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); + + // If the state is PREPARED, we're still waiting for the js-sdk to sync with + // the HS and fetch the latest events, so we are effectively forward paginating. + const forwardPaginating = ( + this.state.forwardPaginating || MatrixClientPeg.get().getSyncState() == 'PREPARED' + ); + return (