From dc44b9ef59afc7bbef5c64547523d1bf6071d5a5 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Fri, 11 Sep 2020 19:49:48 -0600
Subject: [PATCH] Store and thread 3pid invite through the app

This doesn't do anything with the stored value (yet), but enables us to do something with it in a future commit.
---
 src/components/structures/LoggedInView.tsx |   5 +-
 src/components/structures/MatrixChat.tsx   |  44 +++++----
 src/components/structures/RoomView.tsx     |  26 ++---
 src/stores/ThreepidInviteStore.ts          | 107 +++++++++++++++++++++
 4 files changed, 141 insertions(+), 41 deletions(-)
 create mode 100644 src/stores/ThreepidInviteStore.ts

diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx
index 1ac15caa4c..81b8da2cad 100644
--- a/src/components/structures/LoggedInView.tsx
+++ b/src/components/structures/LoggedInView.tsx
@@ -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'}
diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx
index d3d5835dae..13e0c21858 100644
--- a/src/components/structures/MatrixChat.tsx
+++ b/src/components/structures/MatrixChat.tsx
@@ -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();
@@ -835,10 +844,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 +903,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,
@@ -1639,16 +1646,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 +1671,12 @@ 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,
+                oob_data: {
+                    name: threepidInvite?.roomName,
+                    avatarUrl: threepidInvite?.roomAvatarUrl,
+                    inviterName: threepidInvite?.inviterName,
+                },
                 room_alias: undefined,
                 room_id: undefined,
             };
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 738042c680..039d36a8de 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -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
diff --git a/src/stores/ThreepidInviteStore.ts b/src/stores/ThreepidInviteStore.ts
new file mode 100644
index 0000000000..dc6d97817b
--- /dev/null
+++ b/src/stores/ThreepidInviteStore.ts
@@ -0,0 +1,107 @@
+/*
+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 { ComponentClass } from "../@types/common";
+import { UPDATE_EVENT } from "./AsyncStore";
+import { base32, base64 } 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;
+    room_avatar_url: string;
+    inviter_name: string;
+
+    // TODO: Figure out if these are ever populated
+    guest_access_token?: string;
+    guest_user_id?: string;
+}
+
+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 {
+        console.log("Storing invite: ", {roomId, ...wireInvite});
+        const invite = <IPersistedThreepidInvite>{roomId, ...wireInvite};
+        const id = this.generateIdOf(invite);
+        localStorage.setItem(`${STORAGE_PREFIX}${id}`, JSON.stringify(invite));
+        return this.translateInvite(invite);
+    }
+
+    public getInvites(): IThreepidInvite[] {
+        const result: IThreepidInvite[] = [];
+        for (let i = 0; i < localStorage.length; i++) {
+            const keyName = localStorage.key(i);
+            if (!keyName.startsWith(STORAGE_PREFIX)) continue;
+
+            const persisted = JSON.parse(localStorage.getItem(keyName)) as IPersistedThreepidInvite;
+            result.push(this.translateInvite(persisted));
+        }
+        return result;
+    }
+
+    // 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,
+        };
+    }
+}