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

Commit 6e26f1e

Browse files
committed
check profiles before starting a DM
1 parent f5115e0 commit 6e26f1e

File tree

7 files changed

+368
-64
lines changed

7 files changed

+368
-64
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: 76 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -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,34 @@ 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 instanceof DirectoryMember);
92+
await Promise.all(directoryMembers.map((t) => profilesStores.getOrFetchProfile(t.userId)));
93+
return directoryMembers.reduce((unknownProfiles: UnknownProfiles, target: DirectoryMember) => {
94+
const lookupError = profilesStores.getProfileLookupError(target.userId);
95+
96+
if (lookupError instanceof MatrixError && UNKNOWN_PROFILE_ERRORS.includes(lookupError.errcode)) {
97+
unknownProfiles.push({
98+
userId: target.userId,
99+
errorText: lookupError.data.error || "",
100+
});
101+
}
102+
103+
return unknownProfiles;
104+
}, []);
105+
};
106+
82107
interface Result {
83108
userId: string;
84109
user: Member;
@@ -331,6 +356,7 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
331356
private numberEntryFieldRef: React.RefObject<Field> = createRef();
332357
private unmounted = false;
333358
private encryptionByDefault = false;
359+
private profilesStore: UserProfilesStore;
334360

335361
public constructor(props: Props) {
336362
super(props);
@@ -341,6 +367,8 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
341367
throw new Error("When using InviteKind.CallTransfer a call is required for an InviteDialog");
342368
}
343369

370+
this.profilesStore = SdkContextClass.instance.userProfilesStore;
371+
344372
const alreadyInvited = new Set([MatrixClientPeg.get().getUserId()!]);
345373
const welcomeUserId = SdkConfig.get("welcome_user_id");
346374
if (welcomeUserId) alreadyInvited.add(welcomeUserId);
@@ -504,10 +532,28 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
504532
return newTargets;
505533
}
506534

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

512558
try {
513559
const cli = MatrixClientPeg.get();
@@ -523,6 +569,27 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
523569
}
524570
};
525571

572+
private setBusy(busy: boolean): void {
573+
this.setState({
574+
busy,
575+
});
576+
}
577+
578+
private showAskInviteAnywayDialog(unknownProfileUsers: { userId: string; errorText: string }[]): void {
579+
Modal.createDialog(AskInviteAnywayDialog, {
580+
unknownProfileUsers,
581+
onInviteAnyways: () => this.startDm(),
582+
onGiveUp: () => {
583+
this.setBusy(false);
584+
},
585+
description: _t(
586+
"Unable to find profiles for the Matrix IDs listed below - would you like to start a DM anyway?",
587+
),
588+
inviteNeverWarnLabel: _t("Start DM anyway and never warn me again"),
589+
inviteLabel: _t("Start DM anyway"),
590+
});
591+
}
592+
526593
private inviteUsers = async (): Promise<void> => {
527594
if (this.props.kind !== InviteKind.Invite) return;
528595
this.setState({ busy: true });
@@ -639,7 +706,8 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
639706
// if there's no matches (and the input looks like a mxid).
640707
if (term[0] === "@" && term.indexOf(":") > 1) {
641708
try {
642-
const profile = await MatrixClientPeg.get().getProfileInfo(term);
709+
const profile = this.profilesStore.getOrFetchProfile(term);
710+
643711
if (profile) {
644712
// If we have a profile, we have enough information to assume that
645713
// the mxid can be invited - add it to the list. We stick it at the
@@ -651,8 +719,7 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
651719
});
652720
}
653721
} catch (e) {
654-
logger.warn("Non-fatal error trying to make an invite for a user ID");
655-
logger.warn(e);
722+
logger.warn("Non-fatal error trying to make an invite for a user ID", e);
656723

657724
// Reuse logic from Permalinks as a basic MXID validity check
658725
const serverName = getServerName(term);
@@ -716,7 +783,7 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
716783
// the email anyways, and so we don't cause things to jump around. In
717784
// theory, the user would see the user pop up and think "ah yes, that
718785
// person!"
719-
const profile = await MatrixClientPeg.get().getProfileInfo(lookup.mxid);
786+
const profile = await this.profilesStore.getOrFetchProfile(lookup.mxid);
720787
if (term !== this.state.filterText || !profile) return; // abandon hope
721788
this.setState({
722789
threepidResultsMixin: [
@@ -861,7 +928,7 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
861928
}
862929

863930
try {
864-
const profile = await MatrixClientPeg.get().getProfileInfo(address);
931+
const profile = await this.profilesStore.getOrFetchProfile(address);
865932
toAdd.push(
866933
new DirectoryMember({
867934
user_id: address,
@@ -1252,7 +1319,7 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
12521319
}
12531320

12541321
buttonText = _t("Go");
1255-
goButtonFn = this.startDm;
1322+
goButtonFn = this.checkProfileAndStartDm;
12561323
extraSection = (
12571324
<div className="mx_InviteDialog_section_hidden_suggestions_disclaimer">
12581325
<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: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,14 @@ 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

@@ -29,6 +36,7 @@ type StoreProfileValue = IMatrixProfile | undefined | null;
2936
*/
3037
export class UserProfilesStore {
3138
private profiles = new LruCache<string, IMatrixProfile | null>(cacheSize);
39+
private profileLookupErrors = new LruCache<string, MatrixError>(cacheSize);
3240
private knownProfiles = new LruCache<string, IMatrixProfile | null>(cacheSize);
3341

3442
public constructor(private client: MatrixClient) {
@@ -48,6 +56,32 @@ export class UserProfilesStore {
4856
return this.profiles.get(userId);
4957
}
5058

59+
/**
60+
* Async shortcut function that returns the profile from cache or
61+
* or fetches it on cache miss.
62+
*
63+
* @param userId - User Id of the profile to get or fetch
64+
* @returns The profile, if cached by the store or fetched from the API.
65+
* Null if the profile does not exist or an error occurred during fetch.
66+
*/
67+
public async getOrFetchProfile(userId: string): Promise<IMatrixProfile | null> {
68+
const cachedProfile = this.profiles.get(userId);
69+
70+
if (cachedProfile) return cachedProfile;
71+
72+
return this.fetchProfile(userId);
73+
}
74+
75+
/**
76+
* Get a profile lookup error.
77+
*
78+
* @param userId - User Id for which to get the lookup error
79+
* @returns The lookup error or undefined if there was no error or the profile was not fetched.
80+
*/
81+
public getProfileLookupError(userId: string): MatrixError | undefined {
82+
return this.profileLookupErrors.get(userId);
83+
}
84+
5185
/**
5286
* Synchronously get a profile from known users from the store cache.
5387
* Known user means that at least one shared room with the user exists.
@@ -96,6 +130,12 @@ export class UserProfilesStore {
96130
return profile;
97131
}
98132

133+
public flush(): void {
134+
this.profiles = new LruCache<string, IMatrixProfile | null>(cacheSize);
135+
this.profileLookupErrors = new LruCache<string, MatrixError>(cacheSize);
136+
this.knownProfiles = new LruCache<string, IMatrixProfile | null>(cacheSize);
137+
}
138+
99139
/**
100140
* Looks up a user profile via API.
101141
*
@@ -107,6 +147,10 @@ export class UserProfilesStore {
107147
return (await this.client.getProfileInfo(userId)) ?? null;
108148
} catch (e) {
109149
logger.warn(`Error retrieving profile for userId ${userId}`, e);
150+
151+
if (e instanceof MatrixError) {
152+
this.profileLookupErrors.set(userId, e);
153+
}
110154
}
111155

112156
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)