From defecb1b147bb2525af9d9fe20325be2311cec02 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 1 Jun 2017 14:16:25 +0100 Subject: [PATCH 1/5] Implement /user/@userid:domain?action=chat This is a URL that can be used to start a chat with a user. - If the user is a guest, setMxId dialog will appear before anything and a defered action will cause `ChatCreateOrReuseDialog` to appear once they've logged in. - If the user is registered, they will not see the setMxId dialog. fixes https://github.com/vector-im/riot-web/issues/4034 --- src/components/structures/MatrixChat.js | 64 +++++++- src/components/views/avatars/BaseAvatar.js | 1 + .../views/dialogs/ChatCreateOrReuseDialog.js | 152 +++++++++++++----- .../views/dialogs/ChatInviteDialog.js | 21 ++- src/createRoom.js | 1 + src/i18n/strings/en_EN.json | 8 +- src/stores/LifecycleStore.js | 2 + 7 files changed, 202 insertions(+), 47 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index ff864379fa..a081ce6fe7 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -139,6 +139,10 @@ module.exports = React.createClass({ register_hs_url: null, register_is_url: null, register_id_sid: null, + + // Whether a DM should be created with welcomeUserId (prop) on registration + // see _onLoggedIn + shouldCreateWelcomeDm: true, }; return s; }, @@ -378,6 +382,11 @@ module.exports = React.createClass({ }); this.notifyNewScreen('forgot_password'); break; + case 'start_chat': + createRoom({ + dmUserId: payload.user_id, + }); + break; case 'leave_room': this._leaveRoom(payload.room_id); break; @@ -474,6 +483,9 @@ module.exports = React.createClass({ case 'view_set_mxid': this._setMxId(); break; + case 'view_start_chat_or_reuse': + this._chatCreateOrReuse(payload.user_id); + break; case 'view_create_chat': this._createChat(); break; @@ -707,6 +719,50 @@ module.exports = React.createClass({ }); }, + _chatCreateOrReuse: function(userId) { + const ChatCreateOrReuseDialog = sdk.getComponent( + 'views.dialogs.ChatCreateOrReuseDialog', + ); + if (MatrixClientPeg.get().isGuest()) { + dis.dispatch({ + action: 'do_after_sync_prepared', + deferred_action: { + action: 'view_start_chat_or_reuse', + user_id: userId, + }, + }); + dis.dispatch({ + action: 'view_set_mxid', + }); + return; + } + + const close = Modal.createDialog(ChatCreateOrReuseDialog, { + userId: userId, + onFinished: (success) => { + if (!success) { + // Dialog cancelled, default to home + dis.dispatch({ action: 'view_home_page' }); + } + }, + onNewDMClick: () => { + dis.dispatch({ + action: 'start_chat', + user_id: userId, + }); + // Close the dialog, indicate success (calls onFinished(true)) + close(true); + }, + onExistingRoomSelected: (roomId) => { + dis.dispatch({ + action: 'view_room', + room_id: roomId, + }); + close(true); + }, + }).close; + }, + _invite: function(roomId) { const ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog"); Modal.createDialog(ChatInviteDialog, { @@ -838,7 +894,7 @@ module.exports = React.createClass({ MatrixClientPeg.get().getUserIdLocalpart() ); - if (this.props.config.welcomeUserId) { + if (this.props.config.welcomeUserId && this.state.shouldCreateWelcomeDm) { createRoom({ dmUserId: this.props.config.welcomeUserId, andView: false, @@ -1043,6 +1099,12 @@ module.exports = React.createClass({ } } else if (screen.indexOf('user/') == 0) { const userId = screen.substring(5); + + if (params.action === 'chat') { + this._chatCreateOrReuse(userId); + return; + } + this.setState({ viewUserId: userId }); this._setPage(PageTypes.UserView); this.notifyNewScreen('user/' + userId); diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.js index 65730be40b..a4443430f4 100644 --- a/src/components/views/avatars/BaseAvatar.js +++ b/src/components/views/avatars/BaseAvatar.js @@ -32,6 +32,7 @@ module.exports = React.createClass({ urls: React.PropTypes.array, // [highest_priority, ... , lowest_priority] width: React.PropTypes.number, height: React.PropTypes.number, + // XXX resizeMethod not actually used. resizeMethod: React.PropTypes.string, defaultToInitialLetter: React.PropTypes.bool // true to add default url }, diff --git a/src/components/views/dialogs/ChatCreateOrReuseDialog.js b/src/components/views/dialogs/ChatCreateOrReuseDialog.js index f563af6691..93ebf8cc2f 100644 --- a/src/components/views/dialogs/ChatCreateOrReuseDialog.js +++ b/src/components/views/dialogs/ChatCreateOrReuseDialog.js @@ -18,34 +18,30 @@ import React from 'react'; import sdk from '../../../index'; import dis from '../../../dispatcher'; import MatrixClientPeg from '../../../MatrixClientPeg'; +import { _t } from '../../../languageHandler'; import DMRoomMap from '../../../utils/DMRoomMap'; import AccessibleButton from '../elements/AccessibleButton'; import Unread from '../../../Unread'; import classNames from 'classnames'; import createRoom from '../../../createRoom'; +import { RoomMember } from "matrix-js-sdk"; export default class ChatCreateOrReuseDialog extends React.Component { constructor(props) { super(props); - this.onNewDMClick = this.onNewDMClick.bind(this); this.onRoomTileClick = this.onRoomTileClick.bind(this); + + this.state = { + tiles: [], + profile: { + displayName: null, + avatarUrl: null, + }, + }; } - 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() { + componentWillMount() { const client = MatrixClientPeg.get(); const dmRoomMap = new DMRoomMap(client); @@ -70,40 +66,115 @@ export default class ChatCreateOrReuseDialog extends React.Component { highlight={highlight} isInvite={me.membership == "invite"} onClick={this.onRoomTileClick} - /> + />, ); } } - const labelClasses = classNames({ - mx_MemberInfo_createRoom_label: true, - mx_RoomTile_name: true, + this.setState({ + tiles: tiles, }); - const startNewChat = -
- -
-
{_("Start new chat")}
-
; + + if (tiles.length === 0) { + this.setState({ + busyProfile: true, + }); + MatrixClientPeg.get().getProfileInfo(this.props.userId).then((resp) => { + const profile = { + displayName: resp.displayname, + avatarUrl: null, + }; + if (resp.avatar_url) { + profile.avatarUrl = MatrixClientPeg.get().mxcUrlToHttp( + resp.avatar_url, 48, 48, "crop", + ); + } + this.setState({ + profile: profile, + }); + }, (err) => { + console.error('Unable to get profile for user', this.props.userId, err); + }).finally(() => { + this.setState({ + busyProfile: false, + }); + }); + } + } + + onRoomTileClick(roomId) { + this.props.onExistingRoomSelected(roomId); + } + + render() { + let title = ''; + let content = null; + if (this.state.tiles.length > 0) { + // Show the existing rooms with a "+" to add a new dm + title = _t('Create a new chat or reuse an existing one'); + const labelClasses = classNames({ + mx_MemberInfo_createRoom_label: true, + mx_RoomTile_name: true, + }); + const startNewChat = +
+ +
+
{ _t("Start new chat") }
+
; + content =
+ { _t('You already have existing direct chats with this user:') } +
+ { this.state.tiles } + { startNewChat } +
+
; + } else { + // Show the avatar, name and a button to confirm that a new chat is requested + const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); + const Spinner = sdk.getComponent('elements.Spinner'); + title = _t('Start chatting'); + + let profile = null; + if (this.state.busyProfile) { + profile = ; + } else { + profile =
+ +
+ {this.state.profile.displayName || this.props.userId} +
+
; + } + content =
+
+

+ { _t('Click on the button below to start chatting!') } +

+ { profile } +
+
+ +
+
; + } const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); return ( { - this.props.onFinished(false) - }} - title='Create a new chat or reuse an existing one' + onFinished={ this.props.onFinished.bind(false) } + title={title} > -
- You already have existing direct chats with this user: -
- {tiles} - {startNewChat} -
-
+ { content }
); } @@ -111,5 +182,8 @@ export default class ChatCreateOrReuseDialog extends React.Component { ChatCreateOrReuseDialog.propTyps = { userId: React.PropTypes.string.isRequired, + // Called when clicking outside of the dialog onFinished: React.PropTypes.func.isRequired, + onNewDMClick: React.PropTypes.func.isRequired, + onExistingRoomSelected: React.PropTypes.func.isRequired, }; diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index b50a782073..f475635212 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -95,16 +95,25 @@ module.exports = React.createClass({ // 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" + "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(success); + }, + onNewDMClick: () => { + dis.dispatch({ + action: 'start_chat', + user_id: userId, + }); + }, + onExistingRoomSelected: (roomId) => { + dis.dispatch({ + action: 'view_room', + user_id: roomId, + }); + }, }); } else { this._startChat(inviteList); diff --git a/src/createRoom.js b/src/createRoom.js index f476a86dcc..1522dbc9a8 100644 --- a/src/createRoom.js +++ b/src/createRoom.js @@ -97,6 +97,7 @@ function createRoom(opts) { // the room exists, causing things like // https://github.com/vector-im/vector-web/issues/1813 if (opts.andView) { + console.info('And viewing'); dis.dispatch({ action: 'view_room', room_id: roomId, diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 11727223e3..c8d9614961 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -771,5 +771,11 @@ "Idle": "Idle", "Offline": "Offline", "disabled": "disabled", - "enabled": "enabled" + "enabled": "enabled", + "Start chatting": "Start chatting", + "Start Chatting": "Start Chatting", + "Click on the button below to start chatting!": "Click on the button below to start chatting!", + "Create a new chat or reuse an existing one": "Create a new chat or reuse an existing one", + "You already have existing direct chats with this user:": "You already have existing direct chats with this user:", + "Start new chat": "Start new chat" } diff --git a/src/stores/LifecycleStore.js b/src/stores/LifecycleStore.js index d38138b3ef..b06c03b778 100644 --- a/src/stores/LifecycleStore.js +++ b/src/stores/LifecycleStore.js @@ -41,6 +41,7 @@ class LifecycleStore extends Store { __onDispatch(payload) { switch (payload.action) { case 'do_after_sync_prepared': + console.info('Will do after sync', payload.deferred_action); this._setState({ deferred_action: payload.deferred_action, }); @@ -49,6 +50,7 @@ class LifecycleStore extends Store { if (payload.state !== 'PREPARED') { break; } + console.info('Doing', payload.deferred_action); if (!this._state.deferred_action) break; const deferredAction = Object.assign({}, this._state.deferred_action); this._setState({ From 6a9781f0231c532fde87f99e78ce84a5b3cfbdc2 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 2 Jun 2017 11:41:09 +0100 Subject: [PATCH 2/5] Remove redundant state --- src/components/structures/MatrixChat.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index a081ce6fe7..7b51fdb524 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -139,10 +139,6 @@ module.exports = React.createClass({ register_hs_url: null, register_is_url: null, register_id_sid: null, - - // Whether a DM should be created with welcomeUserId (prop) on registration - // see _onLoggedIn - shouldCreateWelcomeDm: true, }; return s; }, @@ -894,7 +890,7 @@ module.exports = React.createClass({ MatrixClientPeg.get().getUserIdLocalpart() ); - if (this.props.config.welcomeUserId && this.state.shouldCreateWelcomeDm) { + if (this.props.config.welcomeUserId) { createRoom({ dmUserId: this.props.config.welcomeUserId, andView: false, From e88b52fa8fd49cfca3517338c2a1da4558875fff Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 2 Jun 2017 11:42:47 +0100 Subject: [PATCH 3/5] Add comment --- src/components/structures/MatrixChat.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 7b51fdb524..5791ffe49a 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -719,6 +719,7 @@ module.exports = React.createClass({ const ChatCreateOrReuseDialog = sdk.getComponent( 'views.dialogs.ChatCreateOrReuseDialog', ); + // Use a deferred action to reshow the dialog once the user has registered if (MatrixClientPeg.get().isGuest()) { dis.dispatch({ action: 'do_after_sync_prepared', From 6e84b6e996db49511a9ec5315ce210ef07275cbc Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 2 Jun 2017 11:50:58 +0100 Subject: [PATCH 4/5] Display profile errors better --- .../views/dialogs/ChatCreateOrReuseDialog.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/components/views/dialogs/ChatCreateOrReuseDialog.js b/src/components/views/dialogs/ChatCreateOrReuseDialog.js index 93ebf8cc2f..fdb4e994ae 100644 --- a/src/components/views/dialogs/ChatCreateOrReuseDialog.js +++ b/src/components/views/dialogs/ChatCreateOrReuseDialog.js @@ -38,6 +38,7 @@ export default class ChatCreateOrReuseDialog extends React.Component { displayName: null, avatarUrl: null, }, + profileError: null, }; } @@ -79,7 +80,7 @@ export default class ChatCreateOrReuseDialog extends React.Component { this.setState({ busyProfile: true, }); - MatrixClientPeg.get().getProfileInfo(this.props.userId).then((resp) => { + MatrixClientPeg.get().getProfileInfo(this.props.userId).done((resp) => { const profile = { displayName: resp.displayname, avatarUrl: null, @@ -90,13 +91,17 @@ export default class ChatCreateOrReuseDialog extends React.Component { ); } this.setState({ + busyProfile: false, profile: profile, }); }, (err) => { - console.error('Unable to get profile for user', this.props.userId, err); - }).finally(() => { + console.error( + 'Unable to get profile for user ' + this.props.userId + ':', + err, + ); this.setState({ busyProfile: false, + profileError: err, }); }); } @@ -141,6 +146,10 @@ export default class ChatCreateOrReuseDialog extends React.Component { let profile = null; if (this.state.busyProfile) { profile = ; + } else if (this.state.profileError) { + profile =
+ Unable to load profile information for { this.props.userId } +
; } else { profile =
Date: Fri, 2 Jun 2017 12:02:37 +0100 Subject: [PATCH 5/5] Remove cryptic log --- src/createRoom.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/createRoom.js b/src/createRoom.js index 1522dbc9a8..f476a86dcc 100644 --- a/src/createRoom.js +++ b/src/createRoom.js @@ -97,7 +97,6 @@ function createRoom(opts) { // the room exists, causing things like // https://github.com/vector-im/vector-web/issues/1813 if (opts.andView) { - console.info('And viewing'); dis.dispatch({ action: 'view_room', room_id: roomId,