Merge pull request #5201 from matrix-org/travis/3pid-invites
Tactical improvements to 3PID invitespull/21833/head
						commit
						3d9c520af8
					
				|  | @ -42,6 +42,7 @@ import {Mjolnir} from "./mjolnir/Mjolnir"; | |||
| import DeviceListener from "./DeviceListener"; | ||||
| import {Jitsi} from "./widgets/Jitsi"; | ||||
| import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "./BasePlatform"; | ||||
| import ThreepidInviteStore from "./stores/ThreepidInviteStore"; | ||||
| 
 | ||||
| const HOMESERVER_URL_KEY = "mx_hs_url"; | ||||
| const ID_SERVER_URL_KEY = "mx_is_url"; | ||||
|  | @ -666,17 +667,30 @@ export async function onLoggedOut() { | |||
|     // that can occur when components try to use a null client.
 | ||||
|     dis.dispatch({action: 'on_logged_out'}, true); | ||||
|     stopMatrixClient(); | ||||
|     await _clearStorage(); | ||||
|     await _clearStorage({deleteEverything: true}); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * @param {object} opts Options for how to clear storage. | ||||
|  * @returns {Promise} promise which resolves once the stores have been cleared | ||||
|  */ | ||||
| async function _clearStorage() { | ||||
| async function _clearStorage(opts: {deleteEverything: boolean}) { | ||||
|     Analytics.disable(); | ||||
| 
 | ||||
|     if (window.localStorage) { | ||||
|         // try to save any 3pid invites from being obliterated
 | ||||
|         const pendingInvites = ThreepidInviteStore.instance.getWireInvites(); | ||||
| 
 | ||||
|         window.localStorage.clear(); | ||||
| 
 | ||||
|         // now restore those invites
 | ||||
|         if (!opts?.deleteEverything) { | ||||
|             pendingInvites.forEach(i => { | ||||
|                 const roomId = i.roomId; | ||||
|                 delete i.roomId; // delete to avoid confusing the store
 | ||||
|                 ThreepidInviteStore.instance.storeInvite(roomId, i); | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     if (window.sessionStorage) { | ||||
|  |  | |||
|  | @ -56,6 +56,7 @@ import { ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPay | |||
| import RoomListStore from "../../stores/room-list/RoomListStore"; | ||||
| import NonUrgentToastContainer from "./NonUrgentToastContainer"; | ||||
| import { ToggleRightPanelPayload } from "../../dispatcher/payloads/ToggleRightPanelPayload"; | ||||
| import { IThreepidInvite } from "../../stores/ThreepidInviteStore"; | ||||
| 
 | ||||
| // We need to fetch each pinned message individually (if we don't already have it)
 | ||||
| // so each pinned message may trigger a request. Limit the number per room for sanity.
 | ||||
|  | @ -81,7 +82,7 @@ interface IProps { | |||
|     // eslint-disable-next-line camelcase
 | ||||
|     page_type: string; | ||||
|     autoJoin: boolean; | ||||
|     thirdPartyInvite?: object; | ||||
|     threepidInvite?: IThreepidInvite; | ||||
|     roomOobData?: object; | ||||
|     currentRoomId: string; | ||||
|     ConferenceHandler?: object; | ||||
|  | @ -631,7 +632,7 @@ class LoggedInView extends React.Component<IProps, IState> { | |||
|                     ref={this._roomView} | ||||
|                     autoJoin={this.props.autoJoin} | ||||
|                     onRegistered={this.props.onRegistered} | ||||
|                     thirdPartyInvite={this.props.thirdPartyInvite} | ||||
|                     threepidInvite={this.props.threepidInvite} | ||||
|                     oobData={this.props.roomOobData} | ||||
|                     viaServers={this.props.viaServers} | ||||
|                     key={this.props.currentRoomId || 'roomview'} | ||||
|  |  | |||
|  | @ -78,6 +78,7 @@ import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotif | |||
| import { SettingLevel } from "../../settings/SettingLevel"; | ||||
| import { leaveRoomBehaviour } from "../../utils/membership"; | ||||
| import CreateCommunityPrototypeDialog from "../views/dialogs/CreateCommunityPrototypeDialog"; | ||||
| import ThreepidInviteStore, { IThreepidInvite, IThreepidInviteWireFormat } from "../../stores/ThreepidInviteStore"; | ||||
| 
 | ||||
| /** constants for MatrixChat.state.view */ | ||||
| export enum Views { | ||||
|  | @ -137,9 +138,9 @@ interface IRoomInfo { | |||
| 
 | ||||
|     auto_join?: boolean; | ||||
|     highlighted?: boolean; | ||||
|     third_party_invite?: object; | ||||
|     oob_data?: object; | ||||
|     via_servers?: string[]; | ||||
|     threepid_invite?: IThreepidInvite; | ||||
| } | ||||
| /* eslint-enable camelcase */ | ||||
| 
 | ||||
|  | @ -196,7 +197,7 @@ interface IState { | |||
|     resizeNotifier: ResizeNotifier; | ||||
|     serverConfig?: ValidatedServerConfig; | ||||
|     ready: boolean; | ||||
|     thirdPartyInvite?: object; | ||||
|     threepidInvite?: IThreepidInvite, | ||||
|     roomOobData?: object; | ||||
|     viaServers?: string[]; | ||||
|     pendingInitialSync?: boolean; | ||||
|  | @ -260,6 +261,14 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> { | |||
|         // outside this.state because updating it should never trigger a
 | ||||
|         // rerender.
 | ||||
|         this.screenAfterLogin = this.props.initialScreenAfterLogin; | ||||
|         if (this.screenAfterLogin) { | ||||
|             const params = this.screenAfterLogin.params || {}; | ||||
|             if (this.screenAfterLogin.screen.startsWith("room/") && params['signurl'] && params['email']) { | ||||
|                 // probably a threepid invite - try to store it
 | ||||
|                 const roomId = this.screenAfterLogin.screen.substring("room/".length); | ||||
|                 ThreepidInviteStore.instance.storeInvite(roomId, params as IThreepidInviteWireFormat); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         this.windowWidth = 10000; | ||||
|         this.handleResize(); | ||||
|  | @ -404,8 +413,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> { | |||
|             }); | ||||
|         }).then((loadedSession) => { | ||||
|             if (!loadedSession) { | ||||
|                 // fall back to showing the welcome screen
 | ||||
|                 dis.dispatch({action: "view_welcome_page"}); | ||||
|                 // fall back to showing the welcome screen... unless we have a 3pid invite pending
 | ||||
|                 if (ThreepidInviteStore.instance.pickBestInvite()) { | ||||
|                     dis.dispatch({action: 'start_registration'}); | ||||
|                 } else { | ||||
|                     dis.dispatch({action: "view_welcome_page"}); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|         // Note we don't catch errors from this: we catch everything within
 | ||||
|  | @ -835,10 +848,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> { | |||
|     //                                    context of that particular event.
 | ||||
|     // @param {boolean=} roomInfo.highlighted If true, add event_id to the hash of the URL
 | ||||
|     //                                        and alter the EventTile to appear highlighted.
 | ||||
|     // @param {Object=} roomInfo.third_party_invite Object containing data about the third party
 | ||||
|     //                                    we received to join the room, if any.
 | ||||
|     // @param {string=} roomInfo.third_party_invite.inviteSignUrl 3pid invite sign URL
 | ||||
|     // @param {string=} roomInfo.third_party_invite.invitedEmail The email address the invite was sent to
 | ||||
|     // @param {Object=} roomInfo.threepid_invite Object containing data about the third party
 | ||||
|     //                                           we received to join the room, if any.
 | ||||
|     // @param {Object=} roomInfo.oob_data Object of additional data about the room
 | ||||
|     //                               that has been passed out-of-band (eg.
 | ||||
|     //                               room name and avatar from an invite email)
 | ||||
|  | @ -896,7 +907,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> { | |||
|                 view: Views.LOGGED_IN, | ||||
|                 currentRoomId: roomInfo.room_id || null, | ||||
|                 page_type: PageTypes.RoomView, | ||||
|                 thirdPartyInvite: roomInfo.third_party_invite, | ||||
|                 threepidInvite: roomInfo.threepid_invite, | ||||
|                 roomOobData: roomInfo.oob_data, | ||||
|                 viaServers: roomInfo.via_servers, | ||||
|                 ready: true, | ||||
|  | @ -1203,6 +1214,14 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> { | |||
|                     // the homepage.
 | ||||
|                     dis.dispatch({action: 'view_home_page'}); | ||||
|                 } | ||||
|             } else if (ThreepidInviteStore.instance.pickBestInvite()) { | ||||
|                 // The user has a 3pid invite pending - show them that
 | ||||
|                 const threepidInvite = ThreepidInviteStore.instance.pickBestInvite(); | ||||
| 
 | ||||
|                 // HACK: This is a pretty brutal way of threading the invite back through
 | ||||
|                 // our systems, but it's the safest we have for now.
 | ||||
|                 const params = ThreepidInviteStore.instance.translateToWireFormat(threepidInvite); | ||||
|                 this.showScreen(`room/${threepidInvite.roomId}`, params) | ||||
|             } else { | ||||
|                 // The user has just logged in after registering,
 | ||||
|                 // so show the homepage.
 | ||||
|  | @ -1639,16 +1658,11 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> { | |||
| 
 | ||||
|             // TODO: Handle encoded room/event IDs: https://github.com/vector-im/element-web/issues/9149
 | ||||
| 
 | ||||
|             // FIXME: sort_out caseConsistency
 | ||||
|             const thirdPartyInvite = { | ||||
|                 inviteSignUrl: params.signurl, | ||||
|                 invitedEmail: params.email, | ||||
|             }; | ||||
|             const oobData = { | ||||
|                 name: params.room_name, | ||||
|                 avatarUrl: params.room_avatar_url, | ||||
|                 inviterName: params.inviter_name, | ||||
|             }; | ||||
|             let threepidInvite: IThreepidInvite; | ||||
|             if (params.signurl && params.email) { | ||||
|                 threepidInvite = ThreepidInviteStore.instance | ||||
|                     .storeInvite(roomString, params as IThreepidInviteWireFormat); | ||||
|             } | ||||
| 
 | ||||
|             // on our URLs there might be a ?via=matrix.org or similar to help
 | ||||
|             // joins to the room succeed. We'll pass these through as an array
 | ||||
|  | @ -1669,8 +1683,15 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> { | |||
|                 // it as highlighted, which will propagate to RoomView and highlight the
 | ||||
|                 // associated EventTile.
 | ||||
|                 highlighted: Boolean(eventId), | ||||
|                 third_party_invite: thirdPartyInvite, | ||||
|                 oob_data: oobData, | ||||
|                 threepid_invite: threepidInvite, | ||||
|                 // TODO: Replace oob_data with the threepidInvite (which has the same info).
 | ||||
|                 // This isn't done yet because it's threaded through so many more places.
 | ||||
|                 // See https://github.com/vector-im/element-web/issues/15157
 | ||||
|                 oob_data: { | ||||
|                     name: threepidInvite?.roomName, | ||||
|                     avatarUrl: threepidInvite?.roomAvatarUrl, | ||||
|                     inviterName: threepidInvite?.inviterName, | ||||
|                 }, | ||||
|                 room_alias: undefined, | ||||
|                 room_id: undefined, | ||||
|             }; | ||||
|  | @ -2002,12 +2023,13 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> { | |||
|             view = <Welcome />; | ||||
|         } else if (this.state.view === Views.REGISTER) { | ||||
|             const Registration = sdk.getComponent('structures.auth.Registration'); | ||||
|             const email = ThreepidInviteStore.instance.pickBestInvite()?.toEmail; | ||||
|             view = ( | ||||
|                 <Registration | ||||
|                     clientSecret={this.state.register_client_secret} | ||||
|                     sessionId={this.state.register_session_id} | ||||
|                     idSid={this.state.register_id_sid} | ||||
|                     email={this.props.startingFragmentQueryParams.email} | ||||
|                     email={email} | ||||
|                     brand={this.props.config.brand} | ||||
|                     makeRegistrationUrl={this.makeRegistrationUrl} | ||||
|                     onLoggedIn={this.onRegisterFlowComplete} | ||||
|  |  | |||
|  | @ -72,6 +72,7 @@ import RoomHeader from "../views/rooms/RoomHeader"; | |||
| import TintableSvg from "../views/elements/TintableSvg"; | ||||
| import type * as ConferenceHandler from '../../VectorConferenceHandler'; | ||||
| import {XOR} from "../../@types/common"; | ||||
| import { IThreepidInvite } from "../../stores/ThreepidInviteStore"; | ||||
| 
 | ||||
| const DEBUG = false; | ||||
| let debuglog = function(msg: string) {}; | ||||
|  | @ -86,15 +87,7 @@ if (DEBUG) { | |||
| interface IProps { | ||||
|     ConferenceHandler?: ConferenceHandler; | ||||
| 
 | ||||
|     // An object representing a third party invite to join this room
 | ||||
|     // Fields:
 | ||||
|     // * inviteSignUrl (string) The URL used to join this room from an email invite
 | ||||
|     //                          (given as part of the link in the invite email)
 | ||||
|     // * invitedEmail (string) The email address that was invited to this room
 | ||||
|     thirdPartyInvite?: { | ||||
|         inviteSignUrl: string; | ||||
|         invitedEmail: string; | ||||
|     }; | ||||
|     threepidInvite: IThreepidInvite, | ||||
| 
 | ||||
|     // Any data about the room that would normally come from the homeserver
 | ||||
|     // but has been passed out-of-band, eg. the room name and avatar URL
 | ||||
|  | @ -1178,8 +1171,7 @@ export default class RoomView extends React.Component<IProps, IState> { | |||
|             // return;
 | ||||
|         } else { | ||||
|             Promise.resolve().then(() => { | ||||
|                 const signUrl = this.props.thirdPartyInvite ? | ||||
|                     this.props.thirdPartyInvite.inviteSignUrl : undefined; | ||||
|                 const signUrl = this.props.threepidInvite?.signUrl; | ||||
|                 dis.dispatch({ | ||||
|                     action: 'join_room', | ||||
|                     opts: { inviteSignUrl: signUrl, viaServers: this.props.viaServers }, | ||||
|  | @ -1752,10 +1744,7 @@ export default class RoomView extends React.Component<IProps, IState> { | |||
|                 if (this.props.oobData) { | ||||
|                     inviterName = this.props.oobData.inviterName; | ||||
|                 } | ||||
|                 let invitedEmail = undefined; | ||||
|                 if (this.props.thirdPartyInvite) { | ||||
|                     invitedEmail = this.props.thirdPartyInvite.invitedEmail; | ||||
|                 } | ||||
|                 const invitedEmail = this.props.threepidInvite?.toEmail; | ||||
| 
 | ||||
|                 // We have no room object for this room, only the ID.
 | ||||
|                 // We've got to this room by following a link, possibly a third party invite.
 | ||||
|  | @ -1773,7 +1762,7 @@ export default class RoomView extends React.Component<IProps, IState> { | |||
|                                 inviterName={inviterName} | ||||
|                                 invitedEmail={invitedEmail} | ||||
|                                 oobData={this.props.oobData} | ||||
|                                 signUrl={this.props.thirdPartyInvite ? this.props.thirdPartyInvite.inviteSignUrl : null} | ||||
|                                 signUrl={this.props.threepidInvite?.signUrl} | ||||
|                                 room={this.state.room} | ||||
|                             /> | ||||
|                         </ErrorBoundary> | ||||
|  | @ -1907,10 +1896,7 @@ export default class RoomView extends React.Component<IProps, IState> { | |||
|             if (this.props.oobData) { | ||||
|                 inviterName = this.props.oobData.inviterName; | ||||
|             } | ||||
|             let invitedEmail = undefined; | ||||
|             if (this.props.thirdPartyInvite) { | ||||
|                 invitedEmail = this.props.thirdPartyInvite.invitedEmail; | ||||
|             } | ||||
|             const invitedEmail = this.props.threepidInvite?.toEmail; | ||||
|             hideCancel = true; | ||||
|             previewBar = ( | ||||
|                 <RoomPreviewBar | ||||
|  |  | |||
|  | @ -25,6 +25,7 @@ import * as sdk from '../../../index'; | |||
| import { _t } from '../../../languageHandler'; | ||||
| import SettingsStore from "../../../settings/SettingsStore"; | ||||
| import AccessibleButton from "../elements/AccessibleButton"; | ||||
| import Spinner from "../elements/Spinner"; | ||||
| 
 | ||||
| /* This file contains a collection of components which are used by the | ||||
|  * InteractiveAuth to prompt the user to enter the information needed | ||||
|  | @ -404,8 +405,12 @@ export class EmailIdentityAuthEntry extends React.Component { | |||
|         // the validation link, we won't know the email address, so if we don't have it,
 | ||||
|         // assume that the link has been clicked and the server will realise when we poll.
 | ||||
|         if (this.props.inputs.emailAddress === undefined) { | ||||
|             const Loader = sdk.getComponent("elements.Spinner"); | ||||
|             return <Loader />; | ||||
|             return <Spinner />; | ||||
|         } else if (this.props.stageState?.emailSid) { | ||||
|             // we only have a session ID if the user has clicked the link in their email,
 | ||||
|             // so show a loading state instead of "an email has been sent to..." because
 | ||||
|             // that's confusing when you've already read that email.
 | ||||
|             return <Spinner />; | ||||
|         } else { | ||||
|             return ( | ||||
|                 <div> | ||||
|  |  | |||
|  | @ -0,0 +1,116 @@ | |||
| /* | ||||
| Copyright 2020 The Matrix.org Foundation C.I.C. | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
|     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| import EventEmitter from "events"; | ||||
| import { base32 } from "rfc4648"; | ||||
| 
 | ||||
| // Dev note: the interface is split in two so we don't have to disable the
 | ||||
| // linter across the whole project.
 | ||||
| export interface IThreepidInviteWireFormat { | ||||
|     email: string; | ||||
|     signurl: string; | ||||
|     room_name: string; // eslint-disable-line camelcase
 | ||||
|     room_avatar_url: string; // eslint-disable-line camelcase
 | ||||
|     inviter_name: string; // eslint-disable-line camelcase
 | ||||
| 
 | ||||
|     // TODO: Figure out if these are ever populated
 | ||||
|     guest_access_token?: string; // eslint-disable-line camelcase
 | ||||
|     guest_user_id?: string; // eslint-disable-line camelcase
 | ||||
| } | ||||
| 
 | ||||
| interface IPersistedThreepidInvite extends IThreepidInviteWireFormat { | ||||
|     roomId: string; | ||||
| } | ||||
| 
 | ||||
| export interface IThreepidInvite { | ||||
|     id: string; // generated by us
 | ||||
|     roomId: string; | ||||
|     toEmail: string; | ||||
|     signUrl: string; | ||||
|     roomName: string; | ||||
|     roomAvatarUrl: string; | ||||
|     inviterName: string; | ||||
| } | ||||
| 
 | ||||
| const STORAGE_PREFIX = "mx_threepid_invite_"; | ||||
| 
 | ||||
| export default class ThreepidInviteStore extends EventEmitter { | ||||
|     private static _instance: ThreepidInviteStore; | ||||
| 
 | ||||
|     public static get instance(): ThreepidInviteStore { | ||||
|         if (!ThreepidInviteStore._instance) { | ||||
|             ThreepidInviteStore._instance = new ThreepidInviteStore(); | ||||
|         } | ||||
|         return ThreepidInviteStore._instance; | ||||
|     } | ||||
| 
 | ||||
|     public storeInvite(roomId: string, wireInvite: IThreepidInviteWireFormat): IThreepidInvite { | ||||
|         const invite = <IPersistedThreepidInvite>{roomId, ...wireInvite}; | ||||
|         const id = this.generateIdOf(invite); | ||||
|         localStorage.setItem(`${STORAGE_PREFIX}${id}`, JSON.stringify(invite)); | ||||
|         return this.translateInvite(invite); | ||||
|     } | ||||
| 
 | ||||
|     public getWireInvites(): IPersistedThreepidInvite[] { | ||||
|         const results: IPersistedThreepidInvite[] = []; | ||||
|         for (let i = 0; i < localStorage.length; i++) { | ||||
|             const keyName = localStorage.key(i); | ||||
|             if (!keyName.startsWith(STORAGE_PREFIX)) continue; | ||||
|             results.push(JSON.parse(localStorage.getItem(keyName)) as IPersistedThreepidInvite); | ||||
|         } | ||||
|         return results; | ||||
|     } | ||||
| 
 | ||||
|     public getInvites(): IThreepidInvite[] { | ||||
|         return this.getWireInvites().map(i => this.translateInvite(i)); | ||||
|     } | ||||
| 
 | ||||
|     // Currently Element can only handle one invite at a time, so handle that
 | ||||
|     public pickBestInvite(): IThreepidInvite { | ||||
|         return this.getInvites()[0]; | ||||
|     } | ||||
| 
 | ||||
|     public resolveInvite(invite: IThreepidInvite) { | ||||
|         localStorage.removeItem(`${STORAGE_PREFIX}${invite.id}`); | ||||
|     } | ||||
| 
 | ||||
|     private generateIdOf(persisted: IPersistedThreepidInvite): string { | ||||
|         // Use a consistent "hash" to form an ID.
 | ||||
|         return base32.stringify(Buffer.from(JSON.stringify(persisted))); | ||||
|     } | ||||
| 
 | ||||
|     private translateInvite(persisted: IPersistedThreepidInvite): IThreepidInvite { | ||||
|         return { | ||||
|             id: this.generateIdOf(persisted), | ||||
|             roomId: persisted.roomId, | ||||
|             toEmail: persisted.email, | ||||
|             signUrl: persisted.signurl, | ||||
|             roomName: persisted.room_name, | ||||
|             roomAvatarUrl: persisted.room_avatar_url, | ||||
|             inviterName: persisted.inviter_name, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     public translateToWireFormat(invite: IThreepidInvite): IThreepidInviteWireFormat { | ||||
|         return { | ||||
|             email: invite.toEmail, | ||||
|             signurl: invite.signUrl, | ||||
|             room_name: invite.roomName, | ||||
|             room_avatar_url: invite.roomAvatarUrl, | ||||
|             inviter_name: invite.inviterName, | ||||
|         }; | ||||
|     } | ||||
| } | ||||
		Loading…
	
		Reference in New Issue
	
	 Travis Ralston
						Travis Ralston