From 897ff05d8748ac3d783655aaf4e5f69a896d92ed Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 17 Jul 2017 15:53:29 +0100 Subject: [PATCH] Implement composer completion user/room pill insertion This modifies the composer completion such that completing a room or user will insert an IMMUTABLE matrix.to LINK Entity for the range that was replaced. Display names will not have a colon after their name anymore as it seemed strange that we would insert one after a pill. --- src/autocomplete/Autocompleter.js | 6 +++ src/autocomplete/RoomProvider.js | 11 +++++- src/autocomplete/UserProvider.js | 19 ++++----- .../views/rooms/MessageComposerInput.js | 39 ++++++++++++++----- 4 files changed, 54 insertions(+), 21 deletions(-) diff --git a/src/autocomplete/Autocompleter.js b/src/autocomplete/Autocompleter.js index e47c5dd59c..6fadf53a61 100644 --- a/src/autocomplete/Autocompleter.js +++ b/src/autocomplete/Autocompleter.js @@ -34,6 +34,12 @@ export type Completion = { component: ?Component, range: SelectionRange, command: ?string, + // An entity applied during the replacement (using draftjs@0.8.1 Entity.create) + entity: ? { + type: string, + mutability: string, + data: ?Object, + }, }; const PROVIDERS = [ diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js index bf8495a90e..ed737a7d96 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.js @@ -52,9 +52,16 @@ export default class RoomProvider extends AutocompleteProvider { }; })); completions = this.matcher.match(command[0]).map(room => { - let displayAlias = getDisplayAliasForRoom(room.room) || room.roomId; + const displayAlias = getDisplayAliasForRoom(room.room) || room.roomId; return { - completion: displayAlias + ' ', + completion: displayAlias, + entity: { + type: 'LINK', + mutability: 'IMMUTABLE', + data: { + url: 'https://matrix.to/#/' + displayAlias, + }, + }, component: ( } title={room.name} description={displayAlias} /> ), diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index f052716b4b..c17d56312b 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -51,16 +51,17 @@ export default class UserProvider extends AutocompleteProvider { let completions = []; let {command, range} = this.getCurrentCommand(query, selection, force); if (command) { - completions = this.matcher.match(command[0]).slice(0, 4).map((user) => { - let displayName = (user.name || user.userId || '').replace(' (IRC)', ''); // FIXME when groups are done - let completion = displayName; - if (range.start === 0) { - completion += ': '; - } else { - completion += ' '; - } + completions = this.matcher.match(command[0]).map((user) => { + const displayName = (user.name || user.userId || '').replace(' (IRC)', ''); // FIXME when groups are done return { - completion, + completion: displayName, + entity: { + type: 'LINK', + mutability: 'IMMUTABLE', + data: { + url: 'https://matrix.to/#/' + user.userId, + }, + }, component: ( } diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index d4ae55f03a..6463573a2a 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -190,9 +190,16 @@ export default class MessageComposerInput extends React.Component { const MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); const RoomAvatar = sdk.getComponent('avatars.RoomAvatar'); const {url} = Entity.get(props.entityKey).getData(); - const matrixToMatch = REGEX_MATRIXTO.exec(url); - const isUserPill = matrixToMatch[2] === '@'; - const isRoomPill = matrixToMatch[2] === '#' || matrixToMatch[2] === '!'; + + // Default to the empty array if no match for simplicity + // resource and prefix will be undefined instead of throwing + const matrixToMatch = REGEX_MATRIXTO.exec(url) || []; + + const resource = matrixToMatch[1]; // The room/user ID + const prefix = matrixToMatch[2]; // The first character of prefix + + const isUserPill = prefix === '@'; + const isRoomPill = prefix === '#' || prefix === '!'; const classes = classNames({ "mx_UserPill": isUserPill, @@ -203,14 +210,14 @@ export default class MessageComposerInput extends React.Component { if (isUserPill) { // If this user is not a member of this room, default to the empty // member. This could be improved by doing an async profile lookup. - const member = this.props.room.getMember(matrixToMatch[1]) || - new RoomMember(null, matrixToMatch[1]); + const member = this.props.room.getMember(resource) || + new RoomMember(null, resource); avatar = member ? : null; } else if (isRoomPill) { - const room = matrixToMatch[2] === '#' ? + const room = prefix === '#' ? MatrixClientPeg.get().getRooms().find((r) => { - return r.getCanonicalAlias() === matrixToMatch[1]; - }) : MatrixClientPeg.get().getRoom(matrixToMatch[1]); + return r.getCanonicalAlias() === resource; + }) : MatrixClientPeg.get().getRoom(resource); avatar = room ? : null; } @@ -906,12 +913,24 @@ export default class MessageComposerInput extends React.Component { return false; } - const {range = {}, completion = ''} = displayedCompletion; + const {range = {}, completion = '', entity = null} = displayedCompletion; + let entityKey; + if (entity) { + entityKey = Entity.create( + entity.type, + entity.mutability, + entity.data, + ); + } const contentState = Modifier.replaceText( activeEditorState.getCurrentContent(), - RichText.textOffsetsToSelectionState(range, activeEditorState.getCurrentContent().getBlocksAsArray()), + RichText.textOffsetsToSelectionState( + range, activeEditorState.getCurrentContent().getBlocksAsArray(), + ), completion, + null, + entityKey, ); let editorState = EditorState.push(activeEditorState, contentState, 'insert-characters');