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

Commit

Permalink
check profiles before starting a DM
Browse files Browse the repository at this point in the history
  • Loading branch information
weeman1337 committed Mar 30, 2023
1 parent 567248d commit 14bc7f4
Show file tree
Hide file tree
Showing 7 changed files with 405 additions and 68 deletions.
35 changes: 23 additions & 12 deletions src/components/views/dialogs/AskInviteAnywayDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,31 @@ import SettingsStore from "../../../settings/SettingsStore";
import { SettingLevel } from "../../../settings/SettingLevel";
import BaseDialog from "./BaseDialog";

export interface UnknownProfile {
userId: string;
errorText: string;
}

export type UnknownProfiles = UnknownProfile[];

export interface AskInviteAnywayDialogProps {
unknownProfileUsers: Array<{
userId: string;
errorText: string;
}>;
unknownProfileUsers: UnknownProfiles;
onInviteAnyways: () => void;
onGiveUp: () => void;
onFinished: (success: boolean) => void;
description?: string;
inviteNeverWarnLabel?: string;
inviteLabel?: string;
}

export default function AskInviteAnywayDialog({
onFinished,
onGiveUp,
onInviteAnyways,
unknownProfileUsers,
description: descriptionProp,
inviteNeverWarnLabel,
inviteLabel,
}: AskInviteAnywayDialogProps): JSX.Element {
const onInviteClicked = useCallback((): void => {
onInviteAnyways();
Expand All @@ -60,6 +70,10 @@ export default function AskInviteAnywayDialog({
</li>
));

const description =
descriptionProp ??
_t("Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?");

return (
<BaseDialog
className="mx_RetryInvitesDialog"
Expand All @@ -68,20 +82,17 @@ export default function AskInviteAnywayDialog({
contentId="mx_Dialog_content"
>
<div id="mx_Dialog_content">
<p>
{_t(
"Unable to find profiles for the Matrix IDs listed below - " +
"would you like to invite them anyway?",
)}
</p>
<p>{description}</p>
<ul>{errorList}</ul>
</div>

<div className="mx_Dialog_buttons">
<button onClick={onGiveUpClicked}>{_t("Close")}</button>
<button onClick={onInviteNeverWarnClicked}>{_t("Invite anyway and never warn me again")}</button>
<button onClick={onInviteNeverWarnClicked}>
{inviteNeverWarnLabel ?? _t("Invite anyway and never warn me again")}
</button>
<button onClick={onInviteClicked} autoFocus={true}>
{_t("Invite anyway")}
{inviteLabel ?? _t("Invite anyway")}
</button>
</div>
</BaseDialog>
Expand Down
91 changes: 81 additions & 10 deletions src/components/views/dialogs/InviteDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2019 - 2022 The Matrix.org Foundation C.I.C.
Copyright 2019 - 2023 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.
Expand All @@ -20,6 +20,7 @@ import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixCall } from "matrix-js-sdk/src/webrtc/call";
import { logger } from "matrix-js-sdk/src/logger";
import { MatrixError } from "matrix-js-sdk/src/matrix";

import { Icon as InfoIcon } from "../../../../res/img/element-icons/info.svg";
import { Icon as EmailPillAvatarIcon } from "../../../../res/img/icon-email-pill-avatar.svg";
Expand Down Expand Up @@ -75,10 +76,38 @@ import Modal from "../../../Modal";
import dis from "../../../dispatcher/dispatcher";
import { privateShouldBeEncrypted } from "../../../utils/rooms";
import { NonEmptyArray } from "../../../@types/common";
import { UNKNOWN_PROFILE_ERRORS } from "../../../utils/MultiInviter";
import AskInviteAnywayDialog, { UnknownProfiles } from "./AskInviteAnywayDialog";
import { SdkContextClass } from "../../../contexts/SDKContext";
import { UserProfilesStore } from "../../../stores/UserProfilesStore";

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

const extractTargetUnknownProfiles = async (
targets: Member[],
profilesStores: UserProfilesStore,
): Promise<UnknownProfiles> => {
const directoryMembers = targets.filter((t): t is DirectoryMember => t instanceof DirectoryMember);
await Promise.all(directoryMembers.map((t) => profilesStores.getOrFetchProfile(t.userId)));
return directoryMembers.reduce<UnknownProfiles>((unknownProfiles: UnknownProfiles, target: DirectoryMember) => {
const lookupError = profilesStores.getProfileLookupError(target.userId);

if (
lookupError instanceof MatrixError &&
lookupError.errcode &&
UNKNOWN_PROFILE_ERRORS.includes(lookupError.errcode)
) {
unknownProfiles.push({
userId: target.userId,
errorText: lookupError.data.error || "",
});
}

return unknownProfiles;
}, []);
};

interface Result {
userId: string;
user: Member;
Expand Down Expand Up @@ -331,6 +360,7 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
private numberEntryFieldRef: React.RefObject<Field> = createRef();
private unmounted = false;
private encryptionByDefault = false;
private profilesStore: UserProfilesStore;

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

this.profilesStore = SdkContextClass.instance.userProfilesStore;

const alreadyInvited = new Set([MatrixClientPeg.get().getUserId()!]);
const welcomeUserId = SdkConfig.get("welcome_user_id");
if (welcomeUserId) alreadyInvited.add(welcomeUserId);
Expand Down Expand Up @@ -504,10 +536,28 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
return newTargets;
}

/**
* Check if there are unknown profiles if promptBeforeInviteUnknownUsers setting is enabled.
* If so show the "invite anyway?" dialog. Otherwise directly create the DM local room.
*/
private checkProfileAndStartDm = async (): Promise<void> => {
this.setBusy(true);
const targets = this.convertFilter();

if (SettingsStore.getValue("promptBeforeInviteUnknownUsers")) {
const unknownProfileUsers = await extractTargetUnknownProfiles(targets, this.profilesStore);

if (unknownProfileUsers.length) {
this.showAskInviteAnywayDialog(unknownProfileUsers);
return;
}
}

await this.startDm();
};

private startDm = async (): Promise<void> => {
this.setState({
busy: true,
});
this.setBusy(true);

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

private setBusy(busy: boolean): void {
this.setState({
busy,
});
}

private showAskInviteAnywayDialog(unknownProfileUsers: { userId: string; errorText: string }[]): void {
Modal.createDialog(AskInviteAnywayDialog, {
unknownProfileUsers,
onInviteAnyways: () => this.startDm(),
onGiveUp: () => {
this.setBusy(false);
},
description: _t(
"Unable to find profiles for the Matrix IDs listed below - would you like to start a DM anyway?",
),
inviteNeverWarnLabel: _t("Start DM anyway and never warn me again"),
inviteLabel: _t("Start DM anyway"),
});
}

private inviteUsers = async (): Promise<void> => {
if (this.props.kind !== InviteKind.Invite) return;
this.setState({ busy: true });
Expand Down Expand Up @@ -639,7 +710,8 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
// if there's no matches (and the input looks like a mxid).
if (term[0] === "@" && term.indexOf(":") > 1) {
try {
const profile = await MatrixClientPeg.get().getProfileInfo(term);
const profile = await this.profilesStore.getOrFetchProfile(term, { shouldThrow: true });

if (profile) {
// If we have a profile, we have enough information to assume that
// the mxid can be invited - add it to the list. We stick it at the
Expand All @@ -651,8 +723,7 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
});
}
} catch (e) {
logger.warn("Non-fatal error trying to make an invite for a user ID");
logger.warn(e);
logger.warn("Non-fatal error trying to make an invite for a user ID", e);

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

try {
const profile = await MatrixClientPeg.get().getProfileInfo(address);
const profile = await this.profilesStore.getOrFetchProfile(address);
toAdd.push(
new DirectoryMember({
user_id: address,
Expand Down Expand Up @@ -1252,7 +1323,7 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
}

buttonText = _t("Go");
goButtonFn = this.startDm;
goButtonFn = this.checkProfileAndStartDm;
extraSection = (
<div className="mx_InviteDialog_section_hidden_suggestions_disclaimer">
<span>{_t("Some suggestions may be hidden for privacy.")}</span>
Expand Down
5 changes: 4 additions & 1 deletion src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -2707,8 +2707,8 @@
"Get it on F-Droid": "Get it on F-Droid",
"App Store® and the Apple logo® are trademarks of Apple Inc.": "App Store® and the Apple logo® are trademarks of Apple Inc.",
"Google Play and the Google Play logo are trademarks of Google LLC.": "Google Play and the Google Play logo are trademarks of Google LLC.",
"The following users may not exist": "The following users may not exist",
"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?",
"The following users may not exist": "The following users may not exist",
"Invite anyway and never warn me again": "Invite anyway and never warn me again",
"Invite anyway": "Invite anyway",
"Close dialog": "Close dialog",
Expand Down Expand Up @@ -2872,6 +2872,9 @@
"Click the button below to confirm your identity.": "Click the button below to confirm your identity.",
"Invite by email": "Invite by email",
"We couldn't create your DM.": "We couldn't create your DM.",
"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?",
"Start DM anyway and never warn me again": "Start DM anyway and never warn me again",
"Start DM anyway": "Start DM anyway",
"Something went wrong trying to invite the users.": "Something went wrong trying to invite the users.",
"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.",
"A call can only be transferred to a single user.": "A call can only be transferred to a single user.",
Expand Down
61 changes: 57 additions & 4 deletions src/stores/UserProfilesStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,33 @@ limitations under the License.
*/

import { logger } from "matrix-js-sdk/src/logger";
import { IMatrixProfile, MatrixClient, MatrixEvent, RoomMember, RoomMemberEvent } from "matrix-js-sdk/src/matrix";
import {
IMatrixProfile,
MatrixClient,
MatrixError,
MatrixEvent,
RoomMember,
RoomMemberEvent,
} from "matrix-js-sdk/src/matrix";

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

const cacheSize = 500;

type StoreProfileValue = IMatrixProfile | undefined | null;

interface GetOptions {
/** Whether calling the function shouuld raise an Error. */
shouldThrow: boolean;
}

/**
* This store provides cached access to user profiles.
* Listens for membership events and invalidates the cache for a profile on update with different profile values.
*/
export class UserProfilesStore {
private profiles = new LruCache<string, IMatrixProfile | null>(cacheSize);
private profileLookupErrors = new LruCache<string, MatrixError>(cacheSize);
private knownProfiles = new LruCache<string, IMatrixProfile | null>(cacheSize);

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

/**
* Async shortcut function that returns the profile from cache or
* or fetches it on cache miss.
*
* @param userId - User Id of the profile to get or fetch
* @returns The profile, if cached by the store or fetched from the API.
* Null if the profile does not exist or an error occurred during fetch.
*/
public async getOrFetchProfile(userId: string, options?: GetOptions): Promise<IMatrixProfile | null> {
const cachedProfile = this.profiles.get(userId);

if (cachedProfile) return cachedProfile;

return this.fetchProfile(userId, options);
}

/**
* Get a profile lookup error.
*
* @param userId - User Id for which to get the lookup error
* @returns The lookup error or undefined if there was no error or the profile was not fetched.
*/
public getProfileLookupError(userId: string): MatrixError | undefined {
return this.profileLookupErrors.get(userId);
}

/**
* Synchronously get a profile from known users from the store cache.
* Known user means that at least one shared room with the user exists.
Expand All @@ -70,8 +109,8 @@ export class UserProfilesStore {
* @returns The profile, if found.
* Null if the profile does not exist or there was an error fetching it.
*/
public async fetchProfile(userId: string): Promise<IMatrixProfile | null> {
const profile = await this.fetchProfileFromApi(userId);
public async fetchProfile(userId: string, options?: GetOptions): Promise<IMatrixProfile | null> {
const profile = await this.fetchProfileFromApi(userId, options);
this.profiles.set(userId, profile);
return profile;
}
Expand All @@ -96,17 +135,31 @@ export class UserProfilesStore {
return profile;
}

public flush(): void {
this.profiles = new LruCache<string, IMatrixProfile | null>(cacheSize);
this.profileLookupErrors = new LruCache<string, MatrixError>(cacheSize);
this.knownProfiles = new LruCache<string, IMatrixProfile | null>(cacheSize);
}

/**
* Looks up a user profile via API.
*
* @param userId - User Id for which the profile should be fetched for
* @returns The profile information or null on errors
*/
private async fetchProfileFromApi(userId: string): Promise<IMatrixProfile | null> {
private async fetchProfileFromApi(userId: string, options?: GetOptions): Promise<IMatrixProfile | null> {
try {
return (await this.client.getProfileInfo(userId)) ?? null;
} catch (e) {
logger.warn(`Error retrieving profile for userId ${userId}`, e);

if (e instanceof MatrixError) {
this.profileLookupErrors.set(userId, e);
}

if (options?.shouldThrow) {
throw e;
}
}

return null;
Expand Down
7 changes: 6 additions & 1 deletion src/utils/MultiInviter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,12 @@ interface IError {
errcode: string;
}

const UNKNOWN_PROFILE_ERRORS = ["M_NOT_FOUND", "M_USER_NOT_FOUND", "M_PROFILE_UNDISCLOSED", "M_PROFILE_NOT_FOUND"];
export const UNKNOWN_PROFILE_ERRORS = [
"M_NOT_FOUND",
"M_USER_NOT_FOUND",
"M_PROFILE_UNDISCLOSED",
"M_PROFILE_NOT_FOUND",
];

export type CompletionStates = Record<string, InviteState>;

Expand Down
Loading

0 comments on commit 14bc7f4

Please sign in to comment.