Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 14bc7f4

Browse files
committed
check profiles before starting a DM
1 parent 567248d commit 14bc7f4

File tree

7 files changed

+405
-68
lines changed

7 files changed

+405
-68
lines changed

src/components/views/dialogs/AskInviteAnywayDialog.tsx

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,21 +22,31 @@ import SettingsStore from "../../../settings/SettingsStore";
2222
import { SettingLevel } from "../../../settings/SettingLevel";
2323
import BaseDialog from "./BaseDialog";
2424

25+
export interface UnknownProfile {
26+
userId: string;
27+
errorText: string;
28+
}
29+
30+
export type UnknownProfiles = UnknownProfile[];
31+
2532
export interface AskInviteAnywayDialogProps {
26-
unknownProfileUsers: Array<{
27-
userId: string;
28-
errorText: string;
29-
}>;
33+
unknownProfileUsers: UnknownProfiles;
3034
onInviteAnyways: () => void;
3135
onGiveUp: () => void;
3236
onFinished: (success: boolean) => void;
37+
description?: string;
38+
inviteNeverWarnLabel?: string;
39+
inviteLabel?: string;
3340
}
3441

3542
export default function AskInviteAnywayDialog({
3643
onFinished,
3744
onGiveUp,
3845
onInviteAnyways,
3946
unknownProfileUsers,
47+
description: descriptionProp,
48+
inviteNeverWarnLabel,
49+
inviteLabel,
4050
}: AskInviteAnywayDialogProps): JSX.Element {
4151
const onInviteClicked = useCallback((): void => {
4252
onInviteAnyways();
@@ -60,6 +70,10 @@ export default function AskInviteAnywayDialog({
6070
</li>
6171
));
6272

73+
const description =
74+
descriptionProp ??
75+
_t("Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?");
76+
6377
return (
6478
<BaseDialog
6579
className="mx_RetryInvitesDialog"
@@ -68,20 +82,17 @@ export default function AskInviteAnywayDialog({
6882
contentId="mx_Dialog_content"
6983
>
7084
<div id="mx_Dialog_content">
71-
<p>
72-
{_t(
73-
"Unable to find profiles for the Matrix IDs listed below - " +
74-
"would you like to invite them anyway?",
75-
)}
76-
</p>
85+
<p>{description}</p>
7786
<ul>{errorList}</ul>
7887
</div>
7988

8089
<div className="mx_Dialog_buttons">
8190
<button onClick={onGiveUpClicked}>{_t("Close")}</button>
82-
<button onClick={onInviteNeverWarnClicked}>{_t("Invite anyway and never warn me again")}</button>
91+
<button onClick={onInviteNeverWarnClicked}>
92+
{inviteNeverWarnLabel ?? _t("Invite anyway and never warn me again")}
93+
</button>
8394
<button onClick={onInviteClicked} autoFocus={true}>
84-
{_t("Invite anyway")}
95+
{inviteLabel ?? _t("Invite anyway")}
8596
</button>
8697
</div>
8798
</BaseDialog>

src/components/views/dialogs/InviteDialog.tsx

Lines changed: 81 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2019 - 2022 The Matrix.org Foundation C.I.C.
2+
Copyright 2019 - 2023 The Matrix.org Foundation C.I.C.
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@ import { RoomMember } from "matrix-js-sdk/src/models/room-member";
2020
import { Room } from "matrix-js-sdk/src/models/room";
2121
import { MatrixCall } from "matrix-js-sdk/src/webrtc/call";
2222
import { logger } from "matrix-js-sdk/src/logger";
23+
import { MatrixError } from "matrix-js-sdk/src/matrix";
2324

2425
import { Icon as InfoIcon } from "../../../../res/img/element-icons/info.svg";
2526
import { Icon as EmailPillAvatarIcon } from "../../../../res/img/icon-email-pill-avatar.svg";
@@ -75,10 +76,38 @@ import Modal from "../../../Modal";
7576
import dis from "../../../dispatcher/dispatcher";
7677
import { privateShouldBeEncrypted } from "../../../utils/rooms";
7778
import { NonEmptyArray } from "../../../@types/common";
79+
import { UNKNOWN_PROFILE_ERRORS } from "../../../utils/MultiInviter";
80+
import AskInviteAnywayDialog, { UnknownProfiles } from "./AskInviteAnywayDialog";
81+
import { SdkContextClass } from "../../../contexts/SDKContext";
82+
import { UserProfilesStore } from "../../../stores/UserProfilesStore";
7883

7984
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
8085
/* eslint-disable camelcase */
8186

87+
const extractTargetUnknownProfiles = async (
88+
targets: Member[],
89+
profilesStores: UserProfilesStore,
90+
): Promise<UnknownProfiles> => {
91+
const directoryMembers = targets.filter((t): t is DirectoryMember => t instanceof DirectoryMember);
92+
await Promise.all(directoryMembers.map((t) => profilesStores.getOrFetchProfile(t.userId)));
93+
return directoryMembers.reduce<UnknownProfiles>((unknownProfiles: UnknownProfiles, target: DirectoryMember) => {
94+
const lookupError = profilesStores.getProfileLookupError(target.userId);
95+
96+
if (
97+
lookupError instanceof MatrixError &&
98+
lookupError.errcode &&
99+
UNKNOWN_PROFILE_ERRORS.includes(lookupError.errcode)
100+
) {
101+
unknownProfiles.push({
102+
userId: target.userId,
103+
errorText: lookupError.data.error || "",
104+
});
105+
}
106+
107+
return unknownProfiles;
108+
}, []);
109+
};
110+
82111
interface Result {
83112
userId: string;
84113
user: Member;
@@ -331,6 +360,7 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
331360
private numberEntryFieldRef: React.RefObject<Field> = createRef();
332361
private unmounted = false;
333362
private encryptionByDefault = false;
363+
private profilesStore: UserProfilesStore;
334364

335365
public constructor(props: Props) {
336366
super(props);
@@ -341,6 +371,8 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
341371
throw new Error("When using InviteKind.CallTransfer a call is required for an InviteDialog");
342372
}
343373

374+
this.profilesStore = SdkContextClass.instance.userProfilesStore;
375+
344376
const alreadyInvited = new Set([MatrixClientPeg.get().getUserId()!]);
345377
const welcomeUserId = SdkConfig.get("welcome_user_id");
346378
if (welcomeUserId) alreadyInvited.add(welcomeUserId);
@@ -504,10 +536,28 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
504536
return newTargets;
505537
}
506538

539+
/**
540+
* Check if there are unknown profiles if promptBeforeInviteUnknownUsers setting is enabled.
541+
* If so show the "invite anyway?" dialog. Otherwise directly create the DM local room.
542+
*/
543+
private checkProfileAndStartDm = async (): Promise<void> => {
544+
this.setBusy(true);
545+
const targets = this.convertFilter();
546+
547+
if (SettingsStore.getValue("promptBeforeInviteUnknownUsers")) {
548+
const unknownProfileUsers = await extractTargetUnknownProfiles(targets, this.profilesStore);
549+
550+
if (unknownProfileUsers.length) {
551+
this.showAskInviteAnywayDialog(unknownProfileUsers);
552+
return;
553+
}
554+
}
555+
556+
await this.startDm();
557+
};
558+
507559
private startDm = async (): Promise<void> => {
508-
this.setState({
509-
busy: true,
510-
});
560+
this.setBusy(true);
511561

512562
try {
513563
const cli = MatrixClientPeg.get();
@@ -523,6 +573,27 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
523573
}
524574
};
525575

576+
private setBusy(busy: boolean): void {
577+
this.setState({
578+
busy,
579+
});
580+
}
581+
582+
private showAskInviteAnywayDialog(unknownProfileUsers: { userId: string; errorText: string }[]): void {
583+
Modal.createDialog(AskInviteAnywayDialog, {
584+
unknownProfileUsers,
585+
onInviteAnyways: () => this.startDm(),
586+
onGiveUp: () => {
587+
this.setBusy(false);
588+
},
589+
description: _t(
590+
"Unable to find profiles for the Matrix IDs listed below - would you like to start a DM anyway?",
591+
),
592+
inviteNeverWarnLabel: _t("Start DM anyway and never warn me again"),
593+
inviteLabel: _t("Start DM anyway"),
594+
});
595+
}
596+
526597
private inviteUsers = async (): Promise<void> => {
527598
if (this.props.kind !== InviteKind.Invite) return;
528599
this.setState({ busy: true });
@@ -639,7 +710,8 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
639710
// if there's no matches (and the input looks like a mxid).
640711
if (term[0] === "@" && term.indexOf(":") > 1) {
641712
try {
642-
const profile = await MatrixClientPeg.get().getProfileInfo(term);
713+
const profile = await this.profilesStore.getOrFetchProfile(term, { shouldThrow: true });
714+
643715
if (profile) {
644716
// If we have a profile, we have enough information to assume that
645717
// the mxid can be invited - add it to the list. We stick it at the
@@ -651,8 +723,7 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
651723
});
652724
}
653725
} catch (e) {
654-
logger.warn("Non-fatal error trying to make an invite for a user ID");
655-
logger.warn(e);
726+
logger.warn("Non-fatal error trying to make an invite for a user ID", e);
656727

657728
// Reuse logic from Permalinks as a basic MXID validity check
658729
const serverName = getServerName(term);
@@ -716,7 +787,7 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
716787
// the email anyways, and so we don't cause things to jump around. In
717788
// theory, the user would see the user pop up and think "ah yes, that
718789
// person!"
719-
const profile = await MatrixClientPeg.get().getProfileInfo(lookup.mxid);
790+
const profile = await this.profilesStore.getOrFetchProfile(lookup.mxid);
720791
if (term !== this.state.filterText || !profile) return; // abandon hope
721792
this.setState({
722793
threepidResultsMixin: [
@@ -861,7 +932,7 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
861932
}
862933

863934
try {
864-
const profile = await MatrixClientPeg.get().getProfileInfo(address);
935+
const profile = await this.profilesStore.getOrFetchProfile(address);
865936
toAdd.push(
866937
new DirectoryMember({
867938
user_id: address,
@@ -1252,7 +1323,7 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
12521323
}
12531324

12541325
buttonText = _t("Go");
1255-
goButtonFn = this.startDm;
1326+
goButtonFn = this.checkProfileAndStartDm;
12561327
extraSection = (
12571328
<div className="mx_InviteDialog_section_hidden_suggestions_disclaimer">
12581329
<span>{_t("Some suggestions may be hidden for privacy.")}</span>

src/i18n/strings/en_EN.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2707,8 +2707,8 @@
27072707
"Get it on F-Droid": "Get it on F-Droid",
27082708
"App Store® and the Apple logo® are trademarks of Apple Inc.": "App Store® and the Apple logo® are trademarks of Apple Inc.",
27092709
"Google Play and the Google Play logo are trademarks of Google LLC.": "Google Play and the Google Play logo are trademarks of Google LLC.",
2710-
"The following users may not exist": "The following users may not exist",
27112710
"Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?": "Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?",
2711+
"The following users may not exist": "The following users may not exist",
27122712
"Invite anyway and never warn me again": "Invite anyway and never warn me again",
27132713
"Invite anyway": "Invite anyway",
27142714
"Close dialog": "Close dialog",
@@ -2872,6 +2872,9 @@
28722872
"Click the button below to confirm your identity.": "Click the button below to confirm your identity.",
28732873
"Invite by email": "Invite by email",
28742874
"We couldn't create your DM.": "We couldn't create your DM.",
2875+
"Unable to find profiles for the Matrix IDs listed below - would you like to start a DM anyway?": "Unable to find profiles for the Matrix IDs listed below - would you like to start a DM anyway?",
2876+
"Start DM anyway and never warn me again": "Start DM anyway and never warn me again",
2877+
"Start DM anyway": "Start DM anyway",
28752878
"Something went wrong trying to invite the users.": "Something went wrong trying to invite the users.",
28762879
"We couldn't invite those users. Please check the users you want to invite and try again.": "We couldn't invite those users. Please check the users you want to invite and try again.",
28772880
"A call can only be transferred to a single user.": "A call can only be transferred to a single user.",

src/stores/UserProfilesStore.ts

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,33 @@ limitations under the License.
1515
*/
1616

1717
import { logger } from "matrix-js-sdk/src/logger";
18-
import { IMatrixProfile, MatrixClient, MatrixEvent, RoomMember, RoomMemberEvent } from "matrix-js-sdk/src/matrix";
18+
import {
19+
IMatrixProfile,
20+
MatrixClient,
21+
MatrixError,
22+
MatrixEvent,
23+
RoomMember,
24+
RoomMemberEvent,
25+
} from "matrix-js-sdk/src/matrix";
1926

2027
import { LruCache } from "../utils/LruCache";
2128

2229
const cacheSize = 500;
2330

2431
type StoreProfileValue = IMatrixProfile | undefined | null;
2532

33+
interface GetOptions {
34+
/** Whether calling the function shouuld raise an Error. */
35+
shouldThrow: boolean;
36+
}
37+
2638
/**
2739
* This store provides cached access to user profiles.
2840
* Listens for membership events and invalidates the cache for a profile on update with different profile values.
2941
*/
3042
export class UserProfilesStore {
3143
private profiles = new LruCache<string, IMatrixProfile | null>(cacheSize);
44+
private profileLookupErrors = new LruCache<string, MatrixError>(cacheSize);
3245
private knownProfiles = new LruCache<string, IMatrixProfile | null>(cacheSize);
3346

3447
public constructor(private client: MatrixClient) {
@@ -48,6 +61,32 @@ export class UserProfilesStore {
4861
return this.profiles.get(userId);
4962
}
5063

64+
/**
65+
* Async shortcut function that returns the profile from cache or
66+
* or fetches it on cache miss.
67+
*
68+
* @param userId - User Id of the profile to get or fetch
69+
* @returns The profile, if cached by the store or fetched from the API.
70+
* Null if the profile does not exist or an error occurred during fetch.
71+
*/
72+
public async getOrFetchProfile(userId: string, options?: GetOptions): Promise<IMatrixProfile | null> {
73+
const cachedProfile = this.profiles.get(userId);
74+
75+
if (cachedProfile) return cachedProfile;
76+
77+
return this.fetchProfile(userId, options);
78+
}
79+
80+
/**
81+
* Get a profile lookup error.
82+
*
83+
* @param userId - User Id for which to get the lookup error
84+
* @returns The lookup error or undefined if there was no error or the profile was not fetched.
85+
*/
86+
public getProfileLookupError(userId: string): MatrixError | undefined {
87+
return this.profileLookupErrors.get(userId);
88+
}
89+
5190
/**
5291
* Synchronously get a profile from known users from the store cache.
5392
* Known user means that at least one shared room with the user exists.
@@ -70,8 +109,8 @@ export class UserProfilesStore {
70109
* @returns The profile, if found.
71110
* Null if the profile does not exist or there was an error fetching it.
72111
*/
73-
public async fetchProfile(userId: string): Promise<IMatrixProfile | null> {
74-
const profile = await this.fetchProfileFromApi(userId);
112+
public async fetchProfile(userId: string, options?: GetOptions): Promise<IMatrixProfile | null> {
113+
const profile = await this.fetchProfileFromApi(userId, options);
75114
this.profiles.set(userId, profile);
76115
return profile;
77116
}
@@ -96,17 +135,31 @@ export class UserProfilesStore {
96135
return profile;
97136
}
98137

138+
public flush(): void {
139+
this.profiles = new LruCache<string, IMatrixProfile | null>(cacheSize);
140+
this.profileLookupErrors = new LruCache<string, MatrixError>(cacheSize);
141+
this.knownProfiles = new LruCache<string, IMatrixProfile | null>(cacheSize);
142+
}
143+
99144
/**
100145
* Looks up a user profile via API.
101146
*
102147
* @param userId - User Id for which the profile should be fetched for
103148
* @returns The profile information or null on errors
104149
*/
105-
private async fetchProfileFromApi(userId: string): Promise<IMatrixProfile | null> {
150+
private async fetchProfileFromApi(userId: string, options?: GetOptions): Promise<IMatrixProfile | null> {
106151
try {
107152
return (await this.client.getProfileInfo(userId)) ?? null;
108153
} catch (e) {
109154
logger.warn(`Error retrieving profile for userId ${userId}`, e);
155+
156+
if (e instanceof MatrixError) {
157+
this.profileLookupErrors.set(userId, e);
158+
}
159+
160+
if (options?.shouldThrow) {
161+
throw e;
162+
}
110163
}
111164

112165
return null;

src/utils/MultiInviter.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,12 @@ interface IError {
3838
errcode: string;
3939
}
4040

41-
const UNKNOWN_PROFILE_ERRORS = ["M_NOT_FOUND", "M_USER_NOT_FOUND", "M_PROFILE_UNDISCLOSED", "M_PROFILE_NOT_FOUND"];
41+
export const UNKNOWN_PROFILE_ERRORS = [
42+
"M_NOT_FOUND",
43+
"M_USER_NOT_FOUND",
44+
"M_PROFILE_UNDISCLOSED",
45+
"M_PROFILE_NOT_FOUND",
46+
];
4247

4348
export type CompletionStates = Record<string, InviteState>;
4449

0 commit comments

Comments
 (0)