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
This commit is contained in:
Luke Barnard 2017-06-01 14:16:25 +01:00
parent 8192374481
commit defecb1b14
7 changed files with 202 additions and 47 deletions

View file

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

View file

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

View file

@ -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 = <AccessibleButton
className="mx_MemberInfo_createRoom"
onClick={this.onNewDMClick}
>
<div className="mx_RoomTile_avatar">
<img src="img/create-big.svg" width="26" height="26" />
</div>
<div className={labelClasses}><i>{_("Start new chat")}</i></div>
</AccessibleButton>;
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 = <AccessibleButton
className="mx_MemberInfo_createRoom"
onClick={this.props.onNewDMClick}
>
<div className="mx_RoomTile_avatar">
<img src="img/create-big.svg" width="26" height="26" />
</div>
<div className={labelClasses}><i>{ _t("Start new chat") }</i></div>
</AccessibleButton>;
content = <div className="mx_Dialog_content">
{ _t('You already have existing direct chats with this user:') }
<div className="mx_ChatCreateOrReuseDialog_tiles">
{ this.state.tiles }
{ startNewChat }
</div>
</div>;
} 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 = <Spinner />;
} else {
profile = <div className="mx_ChatCreateOrReuseDialog_profile">
<BaseAvatar
name={this.state.profile.displayName || this.props.userId}
url={this.state.profile.avatarUrl}
width={48} height={48}
/>
<div className="mx_ChatCreateOrReuseDialog_profile_name">
{this.state.profile.displayName || this.props.userId}
</div>
</div>;
}
content = <div>
<div className="mx_Dialog_content">
<p>
{ _t('Click on the button below to start chatting!') }
</p>
{ profile }
</div>
<div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" onClick={this.props.onNewDMClick}>
{ _t('Start Chatting') }
</button>
</div>
</div>;
}
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
<BaseDialog className='mx_ChatCreateOrReuseDialog'
onFinished={() => {
this.props.onFinished(false)
}}
title='Create a new chat or reuse an existing one'
onFinished={ this.props.onFinished.bind(false) }
title={title}
>
<div className="mx_Dialog_content">
You already have existing direct chats with this user:
<div className="mx_ChatCreateOrReuseDialog_tiles">
{tiles}
{startNewChat}
</div>
</div>
{ content }
</BaseDialog>
);
}
@ -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,
};

View file

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

View file

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

View file

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

View file

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