Merge pull request #5201 from matrix-org/travis/3pid-invites
Tactical improvements to 3PID invites
This commit is contained in:
commit
3d9c520af8
6 changed files with 192 additions and 48 deletions
|
@ -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>
|
||||
|
|
116
src/stores/ThreepidInviteStore.ts
Normal file
116
src/stores/ThreepidInviteStore.ts
Normal file
|
@ -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 a new issue