mirror of https://github.com/vector-im/riot-web
				
				
				
			Merge pull request #3819 from matrix-org/travis/ftue/user-lists/4.5-3pids
Support 3PIDs (email addresses) in the invite dialogpull/21833/head
						commit
						ad33a2322e
					
				|  | @ -184,6 +184,10 @@ limitations under the License. | |||
|         .mx_DMInviteDialog_userTile_name { | ||||
|             vertical-align: top; | ||||
|         } | ||||
| 
 | ||||
|         .mx_DMInviteDialog_userTile_threepidAvatar { | ||||
|             background-color: #ffffff; // this is fine without a var because it's for both themes | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     .mx_DMInviteDialog_userTile_remove { | ||||
|  |  | |||
|  | @ -1,37 +1 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <svg width="69px" height="68px" viewBox="0 0 69 68" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||||
|     <!-- Generator: Sketch 58 (84663) - https://sketch.com --> | ||||
|     <title>at-sign</title> | ||||
|     <desc>Created with Sketch.</desc> | ||||
|     <defs> | ||||
|         <filter x="-5.9%" y="-7.9%" width="111.8%" height="115.8%" filterUnits="objectBoundingBox" id="filter-1"> | ||||
|             <feOffset dx="0" dy="2" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset> | ||||
|             <feGaussianBlur stdDeviation="16" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur> | ||||
|             <feColorMatrix values="0 0 0 0 0   0 0 0 0 0.473684211   0 0 0 0 1  0 0 0 0.241258741 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix> | ||||
|             <feMerge> | ||||
|                 <feMergeNode in="shadowMatrixOuter1"></feMergeNode> | ||||
|                 <feMergeNode in="SourceGraphic"></feMergeNode> | ||||
|             </feMerge> | ||||
|         </filter> | ||||
|     </defs> | ||||
|     <g id="FTUE" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"> | ||||
|         <g id="FTUE---User-list-suggestions" transform="translate(-5161.000000, -1379.000000)" stroke="#368BD6"> | ||||
|             <g id="Group-14-Copy-10" transform="translate(4748.000000, 1168.000000)"> | ||||
|                 <g id="Web-Copy-6" filter="url(#filter-1)"> | ||||
|                     <g id="modal-copy" transform="translate(253.000000, 118.000000)"> | ||||
|                         <g id="content" transform="translate(40.000000, 107.000000)"> | ||||
|                             <g id="Group-43"> | ||||
|                                 <g id="Group-15-Copy-6" transform="translate(143.000000, 6.000000)"> | ||||
|                                     <g id="at-sign" transform="translate(5.500000, 6.500000)"> | ||||
|                                         <circle id="Oval" cx="6.28571429" cy="5.71428571" r="2.28571429"></circle> | ||||
|                                         <path d="M8.57142857,3.42857143 L8.57142857,6.28571429 C8.57142857,7.23248814 9.33894043,8 10.2857143,8 C11.2324881,8 12,7.23248814 12,6.28571429 L12,5.71428571 C11.9998328,3.05880261 10.1703625,0.75337961 7.58436487,0.149884297 C4.9983672,-0.453611015 2.33741804,0.803881013 1.16186053,3.18498476 C-0.0136969889,5.5660885 0.605971794,8.4432307 2.65750183,10.1292957 C4.70903186,11.8153606 7.65171364,11.8659623 9.76,10.2514286" id="Path"></path> | ||||
|                                     </g> | ||||
|                                 </g> | ||||
|                             </g> | ||||
|                         </g> | ||||
|                     </g> | ||||
|                 </g> | ||||
|             </g> | ||||
|         </g> | ||||
|     </g> | ||||
| </svg> | ||||
| <svg xmlns="http://www.w3.org/2000/svg" width="65.631" height="67.981"><defs><filter x="-.059" y="-.079" width="1.118" height="1.158" filterUnits="objectBoundingBox" id="a"><feOffset dy="2" in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur stdDeviation="16" in="shadowOffsetOuter1" result="shadowBlurOuter1"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0.473684211 0 0 0 0 1 0 0 0 0.241258741 0" in="shadowBlurOuter1" result="shadowMatrixOuter1"/><feMerge><feMergeNode in="shadowMatrixOuter1"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs><g filter="url(#a)" transform="matrix(3.40907 0 0 3.40907 -1493.716 -795.144)" fill="none" fill-rule="evenodd" stroke="#368bd6" stroke-linecap="round" stroke-linejoin="round"><g transform="translate(441.5 237.5)"><circle r="2.286" cy="5.714" cx="6.286"/><path d="M8.571 3.429v2.857a1.714 1.714 0 103.429 0v-.572a5.714 5.714 0 10-2.24 4.537"/></g></g></svg> | ||||
| Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 918 B | 
|  | @ -25,6 +25,11 @@ import {RoomMember} from "matrix-js-sdk/lib/matrix"; | |||
| import * as humanize from "humanize"; | ||||
| import SdkConfig from "../../../SdkConfig"; | ||||
| import {getHttpUriForMxc} from "matrix-js-sdk/lib/content-repo"; | ||||
| import * as Email from "../../../email"; | ||||
| import {getDefaultIdentityServerUrl, useDefaultIdentityServer} from "../../../utils/IdentityServerUtils"; | ||||
| import {abbreviateUrl} from "../../../utils/UrlUtils"; | ||||
| import dis from "../../../dispatcher"; | ||||
| import IdentityAuthClient from "../../../IdentityAuthClient"; | ||||
| 
 | ||||
| // TODO: [TravisR] Make this generic for all kinds of invites
 | ||||
| 
 | ||||
|  | @ -82,6 +87,35 @@ class DirectoryMember extends Member { | |||
|     } | ||||
| } | ||||
| 
 | ||||
| class ThreepidMember extends Member { | ||||
|     _id: string; | ||||
| 
 | ||||
|     constructor(id: string) { | ||||
|         super(); | ||||
|         this._id = id; | ||||
|     } | ||||
| 
 | ||||
|     // This is a getter that would be falsey on all other implementations. Until we have
 | ||||
|     // better type support in the react-sdk we can use this trick to determine the kind
 | ||||
|     // of 3PID we're dealing with, if any.
 | ||||
|     get isEmail(): boolean { | ||||
|         return this._id.includes('@'); | ||||
|     } | ||||
| 
 | ||||
|     // These next class members are for the Member interface
 | ||||
|     get name(): string { | ||||
|         return this._id; | ||||
|     } | ||||
| 
 | ||||
|     get userId(): string { | ||||
|         return this._id; | ||||
|     } | ||||
| 
 | ||||
|     getMxcAvatarUrl(): string { | ||||
|         return null; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| class DMUserTile extends React.PureComponent { | ||||
|     static propTypes = { | ||||
|         member: PropTypes.object.isRequired, // Should be a Member (see interface above)
 | ||||
|  | @ -103,7 +137,7 @@ class DMUserTile extends React.PureComponent { | |||
|         const avatarSize = 20; | ||||
|         const avatar = this.props.member.isEmail | ||||
|             ? <img | ||||
|                 className='mx_DMInviteDialog_userTile_avatar' | ||||
|                 className='mx_DMInviteDialog_userTile_avatar mx_DMInviteDialog_userTile_threepidAvatar' | ||||
|                 src={require("../../../../res/img/icon-email-pill-avatar.svg")} | ||||
|                 width={avatarSize} height={avatarSize} /> | ||||
|             : <BaseAvatar | ||||
|  | @ -257,6 +291,9 @@ export default class DMInviteDialog extends React.PureComponent { | |||
|             suggestions: this._buildSuggestions(), | ||||
|             numSuggestionsShown: INITIAL_ROOMS_SHOWN, | ||||
|             serverResultsMixin: [], // { user: DirectoryMember, userId: string }[], like recents and suggestions
 | ||||
|             threepidResultsMixin: [], // { user: ThreepidMember, userId: string}[], like recents and suggestions
 | ||||
|             canUseIdentityServer: !!MatrixClientPeg.get().getIdentityServerUrl(), | ||||
|             tryingIdentityServer: false, | ||||
|         }; | ||||
| 
 | ||||
|         this._editorRef = createRef(); | ||||
|  | @ -360,7 +397,7 @@ export default class DMInviteDialog extends React.PureComponent { | |||
|         if (this._debounceTimer) { | ||||
|             clearTimeout(this._debounceTimer); | ||||
|         } | ||||
|         this._debounceTimer = setTimeout(() => { | ||||
|         this._debounceTimer = setTimeout(async () => { | ||||
|             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
 | ||||
|  | @ -379,6 +416,62 @@ export default class DMInviteDialog extends React.PureComponent { | |||
|                 console.error(e); | ||||
|                 this.setState({serverResultsMixin: []}); // clear results because it's moderately fatal
 | ||||
|             }); | ||||
| 
 | ||||
|             // Whenever we search the directory, also try to search the identity server. It's
 | ||||
|             // all debounced the same anyways.
 | ||||
|             if (!this.state.canUseIdentityServer) { | ||||
|                 // The user doesn't have an identity server set - warn them of that.
 | ||||
|                 this.setState({tryingIdentityServer: true}); | ||||
|                 return; | ||||
|             } | ||||
|             if (term.indexOf('@') > 0 && Email.looksValid(term)) { | ||||
|                 // Start off by suggesting the plain email while we try and resolve it
 | ||||
|                 // to a real account.
 | ||||
|                 this.setState({ | ||||
|                     // per above: the userId is a lie here - it's just a regular identifier
 | ||||
|                     threepidResultsMixin: [{user: new ThreepidMember(term), userId: term}], | ||||
|                 }); | ||||
|                 try { | ||||
|                     const authClient = new IdentityAuthClient(); | ||||
|                     const token = await authClient.getAccessToken(); | ||||
|                     if (term !== this.state.filterText) return; // abandon hope
 | ||||
| 
 | ||||
|                     const lookup = await MatrixClientPeg.get().lookupThreePid( | ||||
|                         'email', | ||||
|                         term, | ||||
|                         undefined, // callback
 | ||||
|                         token, | ||||
|                     ); | ||||
|                     if (term !== this.state.filterText) return; // abandon hope
 | ||||
| 
 | ||||
|                     if (!lookup || !lookup.mxid) { | ||||
|                         // We weren't able to find anyone - we're already suggesting the plain email
 | ||||
|                         // as an alternative, so do nothing.
 | ||||
|                         return; | ||||
|                     } | ||||
| 
 | ||||
|                     // We append the user suggestion to give the user an option to click
 | ||||
|                     // the email anyways, and so we don't cause things to jump around. In
 | ||||
|                     // theory, the user would see the user pop up and think "ah yes, that
 | ||||
|                     // person!"
 | ||||
|                     const profile = await MatrixClientPeg.get().getProfileInfo(lookup.mxid); | ||||
|                     if (term !== this.state.filterText || !profile) return; // abandon hope
 | ||||
|                     this.setState({ | ||||
|                         threepidResultsMixin: [...this.state.threepidResultsMixin, { | ||||
|                             user: new DirectoryMember({ | ||||
|                                 user_id: lookup.mxid, | ||||
|                                 display_name: profile.displayname, | ||||
|                                 avatar_url: profile.avatar_url, | ||||
|                             }), | ||||
|                             userId: lookup.mxid, | ||||
|                         }], | ||||
|                     }); | ||||
|                 } catch (e) { | ||||
|                     console.error("Error searching identity server:"); | ||||
|                     console.error(e); | ||||
|                     this.setState({threepidResultsMixin: []}); // clear results because it's moderately fatal
 | ||||
|                 } | ||||
|             } | ||||
|         }, 150); // 150ms debounce (human reaction time + some)
 | ||||
|     }; | ||||
| 
 | ||||
|  | @ -417,6 +510,21 @@ export default class DMInviteDialog extends React.PureComponent { | |||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     _onUseDefaultIdentityServerClick = (e) => { | ||||
|         e.preventDefault(); | ||||
| 
 | ||||
|         // Update the IS in account data. Actually using it may trigger terms.
 | ||||
|         // eslint-disable-next-line react-hooks/rules-of-hooks
 | ||||
|         useDefaultIdentityServer(); | ||||
|         this.setState({canUseIdentityServer: true, tryingIdentityServer: false}); | ||||
|     }; | ||||
| 
 | ||||
|     _onManageSettingsClick = (e) => { | ||||
|         e.preventDefault(); | ||||
|         dis.dispatch({ action: 'view_user_settings' }); | ||||
|         this._cancel(); | ||||
|     }; | ||||
| 
 | ||||
|     _renderSection(kind: "recents"|"suggestions") { | ||||
|         let sourceMembers = kind === 'recents' ? this.state.recents : this.state.suggestions; | ||||
|         let showNum = kind === 'recents' ? this.state.numRecentsShown : this.state.numSuggestionsShown; | ||||
|  | @ -424,17 +532,27 @@ 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)); | ||||
|         // Mix in the server results if we have any, but only if we're searching. We track the additional
 | ||||
|         // members separately because we want to filter sourceMembers but trust the mixin arrays to have
 | ||||
|         // the right members in them.
 | ||||
|         let additionalMembers = []; | ||||
|         const hasMixins = this.state.serverResultsMixin || this.state.threepidResultsMixin; | ||||
|         if (this.state.filterText && hasMixins && kind === 'suggestions') { | ||||
|             // We don't want to duplicate members though, so just exclude anyone we've already seen.
 | ||||
|             const notAlreadyExists = (u: Member): boolean => { | ||||
|                 return !sourceMembers.some(m => m.userId === u.userId) | ||||
|                     && !additionalMembers.some(m => m.userId === u.userId); | ||||
|             }; | ||||
| 
 | ||||
|             sourceMembers = sourceMembers.concat(uniqueServerResults); | ||||
|             const uniqueServerResults = this.state.serverResultsMixin.filter(notAlreadyExists); | ||||
|             additionalMembers = additionalMembers.concat(...uniqueServerResults); | ||||
| 
 | ||||
|             const uniqueThreepidResults = this.state.threepidResultsMixin.filter(notAlreadyExists); | ||||
|             additionalMembers = additionalMembers.concat(...uniqueThreepidResults); | ||||
|         } | ||||
| 
 | ||||
|         // Hide the section if there's nothing to filter by
 | ||||
|         if (!sourceMembers || sourceMembers.length === 0) return null; | ||||
|         if (sourceMembers.length === 0 && additionalMembers.length === 0) return null; | ||||
| 
 | ||||
|         // Do some simple filtering on the input before going much further. If we get no results, say so.
 | ||||
|         if (this.state.filterText) { | ||||
|  | @ -442,7 +560,7 @@ export default class DMInviteDialog extends React.PureComponent { | |||
|             sourceMembers = sourceMembers | ||||
|                 .filter(m => m.user.name.toLowerCase().includes(filterBy) || m.userId.toLowerCase().includes(filterBy)); | ||||
| 
 | ||||
|             if (sourceMembers.length === 0) { | ||||
|             if (sourceMembers.length === 0 && additionalMembers.length === 0) { | ||||
|                 return ( | ||||
|                     <div className='mx_DMInviteDialog_section'> | ||||
|                         <h3>{sectionName}</h3> | ||||
|  | @ -452,6 +570,10 @@ export default class DMInviteDialog extends React.PureComponent { | |||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // Now we mix in the additional members. Again, we presume these have already been filtered. We
 | ||||
|         // also assume they are more relevant than our suggestions and prepend them to the list.
 | ||||
|         sourceMembers = [...additionalMembers, ...sourceMembers]; | ||||
| 
 | ||||
|         // If we're going to hide one member behind 'show more', just use up the space of the button
 | ||||
|         // with the member's tile instead.
 | ||||
|         if (showNum === sourceMembers.length - 1) showNum++; | ||||
|  | @ -510,6 +632,40 @@ export default class DMInviteDialog extends React.PureComponent { | |||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     _renderIdentityServerWarning() { | ||||
|         if (!this.state.tryingIdentityServer || this.state.canUseIdentityServer) { | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         const defaultIdentityServerUrl = getDefaultIdentityServerUrl(); | ||||
|         if (defaultIdentityServerUrl) { | ||||
|             return ( | ||||
|                 <div className="mx_AddressPickerDialog_identityServer">{_t( | ||||
|                     "Use an identity server to invite by email. " + | ||||
|                     "<default>Use the default (%(defaultIdentityServerName)s)</default> " + | ||||
|                     "or manage in <settings>Settings</settings>.", | ||||
|                     { | ||||
|                         defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl), | ||||
|                     }, | ||||
|                     { | ||||
|                         default: sub => <a href="#" onClick={this._onUseDefaultIdentityServerClick}>{sub}</a>, | ||||
|                         settings: sub => <a href="#" onClick={this._onManageSettingsClick}>{sub}</a>, | ||||
|                     }, | ||||
|                 )}</div> | ||||
|             ); | ||||
|         } else { | ||||
|             return ( | ||||
|                 <div className="mx_AddressPickerDialog_identityServer">{_t( | ||||
|                     "Use an identity server to invite by email. " + | ||||
|                     "Manage in <settings>Settings</settings>.", | ||||
|                     {}, { | ||||
|                         settings: sub => <a href="#" onClick={this._onManageSettingsClick}>{sub}</a>, | ||||
|                     }, | ||||
|                 )}</div> | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     render() { | ||||
|         const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); | ||||
|         const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); | ||||
|  | @ -533,6 +689,7 @@ export default class DMInviteDialog extends React.PureComponent { | |||
|                     </p> | ||||
|                     <div className='mx_DMInviteDialog_addressBar'> | ||||
|                         {this._renderEditor()} | ||||
|                         {this._renderIdentityServerWarning()} | ||||
|                         <AccessibleButton | ||||
|                             kind="primary" | ||||
|                             onClick={this._startDm} | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Travis Ralston
						Travis Ralston