From 3e254c758b70a7ccf89ba625480576857385bf81 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 3 Jan 2020 21:17:48 -0700 Subject: [PATCH] Support searching in the user directory for invite targets Part of https://github.com/vector-im/riot-web/issues/11200 --- .../views/dialogs/DMInviteDialog.js | 84 ++++++++++++++++++- 1 file changed, 81 insertions(+), 3 deletions(-) diff --git a/src/components/views/dialogs/DMInviteDialog.js b/src/components/views/dialogs/DMInviteDialog.js index 8b96e4838418..264f6c729319 100644 --- a/src/components/views/dialogs/DMInviteDialog.js +++ b/src/components/views/dialogs/DMInviteDialog.js @@ -25,14 +25,41 @@ import {RoomMember} from "matrix-js-sdk/lib/matrix"; import * as humanize from "humanize"; import SdkConfig from "../../../SdkConfig"; import {htmlEntitiesEncode} from "../../../HtmlUtils"; +import {getHttpUriForMxc} from "matrix-js-sdk/lib/content-repo"; // TODO: [TravisR] Make this generic for all kinds of invites const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first const INCREMENT_ROOMS_SHOWN = 5; // Number of rooms to add when 'show more' is clicked +class DirectoryMember { + _userId: string; + _displayName: string; + _avatarUrl: string; + + constructor(userDirResult: {user_id: string, display_name: string, avatar_url: string}) { + this._userId = userDirResult.user_id; + this._displayName = userDirResult.display_name; + this._avatarUrl = userDirResult.avatar_url; + } + + // These next members are to implement the contract expected by DMRoomTile + get name(): string { + return this._displayName || this._userId; + } + + get userId(): string { + return this._userId; + } + + getMxcAvatarUrl(): string { + return this._avatarUrl; + } +} + class DMRoomTile extends React.PureComponent { static propTypes = { + // Has properties to match RoomMember: userId (str), name (str), getMxcAvatarUrl(): string member: PropTypes.object.isRequired, lastActiveTs: PropTypes.number, onToggle: PropTypes.func.isRequired, @@ -86,7 +113,7 @@ class DMRoomTile extends React.PureComponent { } render() { - const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar"); + const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar"); let timestamp = null; if (this.props.lastActiveTs) { @@ -96,9 +123,20 @@ class DMRoomTile extends React.PureComponent { timestamp = {humanTs}; } + const avatarSize = 36; + const avatarUrl = getHttpUriForMxc( + MatrixClientPeg.get().getHomeserverUrl(), this.props.member.getMxcAvatarUrl(), + avatarSize, avatarSize, "crop"); + return (
- + {this._highlightName(this.props.member.name)} {this._highlightName(this.props.member.userId)} {timestamp} @@ -113,6 +151,8 @@ export default class DMInviteDialog extends React.PureComponent { onFinished: PropTypes.func.isRequired, }; + _debounceTimer: number = null; + constructor() { super(); @@ -123,6 +163,7 @@ export default class DMInviteDialog extends React.PureComponent { numRecentsShown: INITIAL_ROOMS_SHOWN, suggestions: this._buildSuggestions(), numSuggestionsShown: INITIAL_ROOMS_SHOWN, + serverResultsMixin: [], // { user: DirectoryMember, userId: string }[], like recents and suggestions }; } @@ -210,7 +251,35 @@ export default class DMInviteDialog extends React.PureComponent { }; _updateFilter = (e) => { - this.setState({filterText: e.target.value}); + const term = e.target.value; + this.setState({filterText: term}); + + // Debounce server lookups to reduce spam. We don't clear the existing server + // results because they might still be vaguely accurate, likewise for races which + // could happen here. + if (this._debounceTimer) { + clearTimeout(this._debounceTimer); + } + this._debounceTimer = setTimeout(() => { + MatrixClientPeg.get().searchUserDirectory({term}).then(r => { + if (term !== this.state.filterText) { + // Discard the results - we were probably too slow on the server-side to make + // these results useful. This is a race we want to avoid because we could overwrite + // more accurate results. + return; + } + this.setState({ + serverResultsMixin: r.results.map(u => ({ + userId: u.user_id, + user: new DirectoryMember(u), + })), + }); + }).catch(e => { + console.error("Error searching user directory:"); + console.error(e); + this.setState({serverResultsMixin: []}); // clear results because it's moderately fatal + }); + }, 150); // 150ms debounce (human reaction time + some) }; _showMoreRecents = () => { @@ -236,6 +305,15 @@ export default class DMInviteDialog extends React.PureComponent { const lastActive = (m) => kind === 'recents' ? m.lastActive : null; const sectionName = kind === 'recents' ? _t("Recent Conversations") : _t("Suggestions"); + // Mix in the server results if we have any, but only if we're searching + if (this.state.filterText && this.state.serverResultsMixin && kind === 'suggestions') { + // only pick out the server results that aren't already covered though + const uniqueServerResults = this.state.serverResultsMixin + .filter(u => !sourceMembers.some(m => m.userId === u.userId)); + + sourceMembers = sourceMembers.concat(uniqueServerResults); + } + // Hide the section if there's nothing to filter by if (!sourceMembers || sourceMembers.length === 0) return null;