Check profiles before starting a DM (#10472)
This commit is contained in:
parent
78e03e0617
commit
df89d2ce28
7 changed files with 438 additions and 72 deletions
|
@ -22,14 +22,21 @@ import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||||
import BaseDialog from "./BaseDialog";
|
import BaseDialog from "./BaseDialog";
|
||||||
|
|
||||||
export interface AskInviteAnywayDialogProps {
|
export interface UnknownProfile {
|
||||||
unknownProfileUsers: Array<{
|
|
||||||
userId: string;
|
userId: string;
|
||||||
errorText: string;
|
errorText: string;
|
||||||
}>;
|
}
|
||||||
|
|
||||||
|
export type UnknownProfiles = UnknownProfile[];
|
||||||
|
|
||||||
|
export interface AskInviteAnywayDialogProps {
|
||||||
|
unknownProfileUsers: UnknownProfiles;
|
||||||
onInviteAnyways: () => void;
|
onInviteAnyways: () => void;
|
||||||
onGiveUp: () => void;
|
onGiveUp: () => void;
|
||||||
onFinished: (success: boolean) => void;
|
onFinished: (success: boolean) => void;
|
||||||
|
description?: string;
|
||||||
|
inviteNeverWarnLabel?: string;
|
||||||
|
inviteLabel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AskInviteAnywayDialog({
|
export default function AskInviteAnywayDialog({
|
||||||
|
@ -37,6 +44,9 @@ export default function AskInviteAnywayDialog({
|
||||||
onGiveUp,
|
onGiveUp,
|
||||||
onInviteAnyways,
|
onInviteAnyways,
|
||||||
unknownProfileUsers,
|
unknownProfileUsers,
|
||||||
|
description: descriptionProp,
|
||||||
|
inviteNeverWarnLabel,
|
||||||
|
inviteLabel,
|
||||||
}: AskInviteAnywayDialogProps): JSX.Element {
|
}: AskInviteAnywayDialogProps): JSX.Element {
|
||||||
const onInviteClicked = useCallback((): void => {
|
const onInviteClicked = useCallback((): void => {
|
||||||
onInviteAnyways();
|
onInviteAnyways();
|
||||||
|
@ -60,6 +70,10 @@ export default function AskInviteAnywayDialog({
|
||||||
</li>
|
</li>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
const description =
|
||||||
|
descriptionProp ??
|
||||||
|
_t("Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
className="mx_RetryInvitesDialog"
|
className="mx_RetryInvitesDialog"
|
||||||
|
@ -68,20 +82,17 @@ export default function AskInviteAnywayDialog({
|
||||||
contentId="mx_Dialog_content"
|
contentId="mx_Dialog_content"
|
||||||
>
|
>
|
||||||
<div id="mx_Dialog_content">
|
<div id="mx_Dialog_content">
|
||||||
<p>
|
<p>{description}</p>
|
||||||
{_t(
|
|
||||||
"Unable to find profiles for the Matrix IDs listed below - " +
|
|
||||||
"would you like to invite them anyway?",
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<ul>{errorList}</ul>
|
<ul>{errorList}</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button onClick={onGiveUpClicked}>{_t("Close")}</button>
|
<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}>
|
<button onClick={onInviteClicked} autoFocus={true}>
|
||||||
{_t("Invite anyway")}
|
{inviteLabel ?? _t("Invite anyway")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
|
|
|
@ -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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
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";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
import { MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
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 InfoIcon } from "../../../../res/img/element-icons/info.svg";
|
||||||
import { Icon as EmailPillAvatarIcon } from "../../../../res/img/icon-email-pill-avatar.svg";
|
import { Icon as EmailPillAvatarIcon } from "../../../../res/img/icon-email-pill-avatar.svg";
|
||||||
|
@ -75,10 +76,38 @@ import Modal from "../../../Modal";
|
||||||
import dis from "../../../dispatcher/dispatcher";
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
import { privateShouldBeEncrypted } from "../../../utils/rooms";
|
import { privateShouldBeEncrypted } from "../../../utils/rooms";
|
||||||
import { NonEmptyArray } from "../../../@types/common";
|
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.
|
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
|
||||||
/* eslint-disable camelcase */
|
/* 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 {
|
interface Result {
|
||||||
userId: string;
|
userId: string;
|
||||||
user: Member;
|
user: Member;
|
||||||
|
@ -331,6 +360,7 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
|
||||||
private numberEntryFieldRef: React.RefObject<Field> = createRef();
|
private numberEntryFieldRef: React.RefObject<Field> = createRef();
|
||||||
private unmounted = false;
|
private unmounted = false;
|
||||||
private encryptionByDefault = false;
|
private encryptionByDefault = false;
|
||||||
|
private profilesStore: UserProfilesStore;
|
||||||
|
|
||||||
public constructor(props: Props) {
|
public constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -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");
|
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 alreadyInvited = new Set([MatrixClientPeg.get().getUserId()!]);
|
||||||
const welcomeUserId = SdkConfig.get("welcome_user_id");
|
const welcomeUserId = SdkConfig.get("welcome_user_id");
|
||||||
if (welcomeUserId) alreadyInvited.add(welcomeUserId);
|
if (welcomeUserId) alreadyInvited.add(welcomeUserId);
|
||||||
|
@ -504,10 +536,28 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
|
||||||
return newTargets;
|
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> => {
|
private startDm = async (): Promise<void> => {
|
||||||
this.setState({
|
this.setBusy(true);
|
||||||
busy: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
|
@ -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> => {
|
private inviteUsers = async (): Promise<void> => {
|
||||||
if (this.props.kind !== InviteKind.Invite) return;
|
if (this.props.kind !== InviteKind.Invite) return;
|
||||||
this.setState({ busy: true });
|
this.setState({ busy: true });
|
||||||
|
@ -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 there's no matches (and the input looks like a mxid).
|
||||||
if (term[0] === "@" && term.indexOf(":") > 1) {
|
if (term[0] === "@" && term.indexOf(":") > 1) {
|
||||||
try {
|
try {
|
||||||
const profile = await MatrixClientPeg.get().getProfileInfo(term);
|
const profile = await this.profilesStore.getOrFetchProfile(term, { shouldThrow: true });
|
||||||
|
|
||||||
if (profile) {
|
if (profile) {
|
||||||
// If we have a profile, we have enough information to assume that
|
// 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
|
// 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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.warn("Non-fatal error trying to make an invite for a user ID");
|
logger.warn("Non-fatal error trying to make an invite for a user ID", e);
|
||||||
logger.warn(e);
|
|
||||||
|
|
||||||
// Reuse logic from Permalinks as a basic MXID validity check
|
// Reuse logic from Permalinks as a basic MXID validity check
|
||||||
const serverName = getServerName(term);
|
const serverName = getServerName(term);
|
||||||
|
@ -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
|
// 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
|
// theory, the user would see the user pop up and think "ah yes, that
|
||||||
// person!"
|
// 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
|
if (term !== this.state.filterText || !profile) return; // abandon hope
|
||||||
this.setState({
|
this.setState({
|
||||||
threepidResultsMixin: [
|
threepidResultsMixin: [
|
||||||
|
@ -861,7 +932,7 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const profile = await MatrixClientPeg.get().getProfileInfo(address);
|
const profile = await this.profilesStore.getOrFetchProfile(address);
|
||||||
toAdd.push(
|
toAdd.push(
|
||||||
new DirectoryMember({
|
new DirectoryMember({
|
||||||
user_id: address,
|
user_id: address,
|
||||||
|
@ -1252,7 +1323,7 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
|
||||||
}
|
}
|
||||||
|
|
||||||
buttonText = _t("Go");
|
buttonText = _t("Go");
|
||||||
goButtonFn = this.startDm;
|
goButtonFn = this.checkProfileAndStartDm;
|
||||||
extraSection = (
|
extraSection = (
|
||||||
<div className="mx_InviteDialog_section_hidden_suggestions_disclaimer">
|
<div className="mx_InviteDialog_section_hidden_suggestions_disclaimer">
|
||||||
<span>{_t("Some suggestions may be hidden for privacy.")}</span>
|
<span>{_t("Some suggestions may be hidden for privacy.")}</span>
|
||||||
|
|
|
@ -2711,8 +2711,8 @@
|
||||||
"Get it on F-Droid": "Get it on F-Droid",
|
"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.",
|
"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.",
|
"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?",
|
"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 and never warn me again": "Invite anyway and never warn me again",
|
||||||
"Invite anyway": "Invite anyway",
|
"Invite anyway": "Invite anyway",
|
||||||
"Close dialog": "Close dialog",
|
"Close dialog": "Close dialog",
|
||||||
|
@ -2876,6 +2876,9 @@
|
||||||
"Click the button below to confirm your identity.": "Click the button below to confirm your identity.",
|
"Click the button below to confirm your identity.": "Click the button below to confirm your identity.",
|
||||||
"Invite by email": "Invite by email",
|
"Invite by email": "Invite by email",
|
||||||
"We couldn't create your DM.": "We couldn't create your DM.",
|
"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.",
|
"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.",
|
"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.",
|
"A call can only be transferred to a single user.": "A call can only be transferred to a single user.",
|
||||||
|
|
|
@ -15,7 +15,14 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
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";
|
import { LruCache } from "../utils/LruCache";
|
||||||
|
|
||||||
|
@ -23,12 +30,18 @@ const cacheSize = 500;
|
||||||
|
|
||||||
type StoreProfileValue = IMatrixProfile | undefined | null;
|
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.
|
* 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.
|
* Listens for membership events and invalidates the cache for a profile on update with different profile values.
|
||||||
*/
|
*/
|
||||||
export class UserProfilesStore {
|
export class UserProfilesStore {
|
||||||
private profiles = new LruCache<string, IMatrixProfile | null>(cacheSize);
|
private profiles = new LruCache<string, IMatrixProfile | null>(cacheSize);
|
||||||
|
private profileLookupErrors = new LruCache<string, MatrixError>(cacheSize);
|
||||||
private knownProfiles = new LruCache<string, IMatrixProfile | null>(cacheSize);
|
private knownProfiles = new LruCache<string, IMatrixProfile | null>(cacheSize);
|
||||||
|
|
||||||
public constructor(private client: MatrixClient) {
|
public constructor(private client: MatrixClient) {
|
||||||
|
@ -48,6 +61,32 @@ export class UserProfilesStore {
|
||||||
return this.profiles.get(userId);
|
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.
|
* Synchronously get a profile from known users from the store cache.
|
||||||
* Known user means that at least one shared room with the user exists.
|
* Known user means that at least one shared room with the user exists.
|
||||||
|
@ -70,8 +109,8 @@ export class UserProfilesStore {
|
||||||
* @returns The profile, if found.
|
* @returns The profile, if found.
|
||||||
* Null if the profile does not exist or there was an error fetching it.
|
* Null if the profile does not exist or there was an error fetching it.
|
||||||
*/
|
*/
|
||||||
public async fetchProfile(userId: string): Promise<IMatrixProfile | null> {
|
public async fetchProfile(userId: string, options?: GetOptions): Promise<IMatrixProfile | null> {
|
||||||
const profile = await this.fetchProfileFromApi(userId);
|
const profile = await this.fetchProfileFromApi(userId, options);
|
||||||
this.profiles.set(userId, profile);
|
this.profiles.set(userId, profile);
|
||||||
return profile;
|
return profile;
|
||||||
}
|
}
|
||||||
|
@ -96,17 +135,34 @@ export class UserProfilesStore {
|
||||||
return profile;
|
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.
|
* Looks up a user profile via API.
|
||||||
*
|
*
|
||||||
* @param userId - User Id for which the profile should be fetched for
|
* @param userId - User Id for which the profile should be fetched for
|
||||||
* @returns The profile information or null on errors
|
* @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> {
|
||||||
|
// invalidate cached profile errors
|
||||||
|
this.profileLookupErrors.delete(userId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return (await this.client.getProfileInfo(userId)) ?? null;
|
return (await this.client.getProfileInfo(userId)) ?? null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.warn(`Error retrieving profile for userId ${userId}`, 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;
|
return null;
|
||||||
|
|
|
@ -38,7 +38,12 @@ interface IError {
|
||||||
errcode: string;
|
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>;
|
export type CompletionStates = Record<string, InviteState>;
|
||||||
|
|
||||||
|
|
|
@ -18,15 +18,28 @@ import React from "react";
|
||||||
import { render, screen } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import { RoomType } from "matrix-js-sdk/src/@types/event";
|
import { RoomType } from "matrix-js-sdk/src/@types/event";
|
||||||
import { Room } from "matrix-js-sdk/src/matrix";
|
import { MatrixClient, MatrixError, Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { sleep } from "matrix-js-sdk/src/utils";
|
||||||
|
import { mocked, Mocked } from "jest-mock";
|
||||||
|
|
||||||
import InviteDialog from "../../../../src/components/views/dialogs/InviteDialog";
|
import InviteDialog from "../../../../src/components/views/dialogs/InviteDialog";
|
||||||
import { InviteKind } from "../../../../src/components/views/dialogs/InviteDialogTypes";
|
import { InviteKind } from "../../../../src/components/views/dialogs/InviteDialogTypes";
|
||||||
import { getMockClientWithEventEmitter, mkMembership, mkMessage, mkRoomCreateEvent } from "../../../test-utils";
|
import {
|
||||||
|
filterConsole,
|
||||||
|
getMockClientWithEventEmitter,
|
||||||
|
mkMembership,
|
||||||
|
mkMessage,
|
||||||
|
mkRoomCreateEvent,
|
||||||
|
} from "../../../test-utils";
|
||||||
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
||||||
import SdkConfig from "../../../../src/SdkConfig";
|
import SdkConfig from "../../../../src/SdkConfig";
|
||||||
import { ValidatedServerConfig } from "../../../../src/utils/ValidatedServerConfig";
|
import { ValidatedServerConfig } from "../../../../src/utils/ValidatedServerConfig";
|
||||||
import { IConfigOptions } from "../../../../src/IConfigOptions";
|
import { IConfigOptions } from "../../../../src/IConfigOptions";
|
||||||
|
import { SdkContextClass } from "../../../../src/contexts/SDKContext";
|
||||||
|
import { IProfileInfo } from "../../../../src/hooks/useProfileInfo";
|
||||||
|
import { DirectoryMember, startDmOnFirstMessage } from "../../../../src/utils/direct-messages";
|
||||||
|
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||||
|
import Modal from "../../../../src/Modal";
|
||||||
|
|
||||||
const mockGetAccessToken = jest.fn().mockResolvedValue("getAccessToken");
|
const mockGetAccessToken = jest.fn().mockResolvedValue("getAccessToken");
|
||||||
jest.mock("../../../../src/IdentityAuthClient", () =>
|
jest.mock("../../../../src/IdentityAuthClient", () =>
|
||||||
|
@ -35,6 +48,12 @@ jest.mock("../../../../src/IdentityAuthClient", () =>
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
jest.mock("../../../../src/utils/direct-messages", () => ({
|
||||||
|
...jest.requireActual("../../../../src/utils/direct-messages"),
|
||||||
|
__esModule: true,
|
||||||
|
startDmOnFirstMessage: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
const getSearchField = () => screen.getByTestId("invite-dialog-input");
|
const getSearchField = () => screen.getByTestId("invite-dialog-input");
|
||||||
|
|
||||||
const enterIntoSearchField = async (value: string) => {
|
const enterIntoSearchField = async (value: string) => {
|
||||||
|
@ -60,13 +79,37 @@ const expectNoPill = (value: string) => {
|
||||||
expect(getSearchField()).toHaveValue(value);
|
expect(getSearchField()).toHaveValue(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const roomId = "!111111111111111111:example.org";
|
||||||
|
const aliceId = "@alice:example.org";
|
||||||
|
const aliceEmail = "foobar@email.com";
|
||||||
|
const bobId = "@bob:example.org";
|
||||||
|
const bobEmail = "bobbob@example.com"; // bob@example.com is already used as an example in the invite dialog
|
||||||
|
const carolId = "@carol:example.com";
|
||||||
|
|
||||||
|
const aliceProfileInfo: IProfileInfo = {
|
||||||
|
user_id: aliceId,
|
||||||
|
display_name: "Alice",
|
||||||
|
};
|
||||||
|
|
||||||
|
const bobProfileInfo: IProfileInfo = {
|
||||||
|
user_id: bobId,
|
||||||
|
display_name: "Bob",
|
||||||
|
};
|
||||||
|
|
||||||
describe("InviteDialog", () => {
|
describe("InviteDialog", () => {
|
||||||
const roomId = "!111111111111111111:example.org";
|
let mockClient: Mocked<MatrixClient>;
|
||||||
const aliceId = "@alice:example.org";
|
let room: Room;
|
||||||
const aliceEmail = "foobar@email.com";
|
|
||||||
const bobId = "@bob:example.org";
|
filterConsole(
|
||||||
const bobEmail = "bobbob@example.com"; // bob@example.com is already used as an example in the invite dialog
|
"Error retrieving profile for userId @carol:example.com",
|
||||||
const mockClient = getMockClientWithEventEmitter({
|
"Error retrieving profile for userId @localpart:server.tld",
|
||||||
|
"Error retrieving profile for userId @localpart:server:tld",
|
||||||
|
"Starting load of AsyncWrapper for modal",
|
||||||
|
"[Invite:Recents] Excluding @alice:example.org from recents",
|
||||||
|
);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockClient = getMockClientWithEventEmitter({
|
||||||
getUserId: jest.fn().mockReturnValue(bobId),
|
getUserId: jest.fn().mockReturnValue(bobId),
|
||||||
getSafeUserId: jest.fn().mockReturnValue(bobId),
|
getSafeUserId: jest.fn().mockReturnValue(bobId),
|
||||||
isGuest: jest.fn().mockReturnValue(false),
|
isGuest: jest.fn().mockReturnValue(false),
|
||||||
|
@ -77,7 +120,15 @@ describe("InviteDialog", () => {
|
||||||
getPushActionsForEvent: jest.fn(),
|
getPushActionsForEvent: jest.fn(),
|
||||||
mxcUrlToHttp: jest.fn().mockReturnValue(""),
|
mxcUrlToHttp: jest.fn().mockReturnValue(""),
|
||||||
isRoomEncrypted: jest.fn().mockReturnValue(false),
|
isRoomEncrypted: jest.fn().mockReturnValue(false),
|
||||||
getProfileInfo: jest.fn().mockRejectedValue({ errcode: "" }),
|
getProfileInfo: jest.fn().mockImplementation(async (userId: string) => {
|
||||||
|
if (userId === aliceId) return aliceProfileInfo;
|
||||||
|
if (userId === bobId) return bobProfileInfo;
|
||||||
|
|
||||||
|
throw new MatrixError({
|
||||||
|
errcode: "M_NOT_FOUND",
|
||||||
|
error: "Profile not found",
|
||||||
|
});
|
||||||
|
}),
|
||||||
getIdentityServerUrl: jest.fn(),
|
getIdentityServerUrl: jest.fn(),
|
||||||
searchUserDirectory: jest.fn().mockResolvedValue({}),
|
searchUserDirectory: jest.fn().mockResolvedValue({}),
|
||||||
lookupThreePid: jest.fn(),
|
lookupThreePid: jest.fn(),
|
||||||
|
@ -90,16 +141,11 @@ describe("InviteDialog", () => {
|
||||||
getTerms: jest.fn().mockResolvedValue({ policies: [] }),
|
getTerms: jest.fn().mockResolvedValue({ policies: [] }),
|
||||||
supportsThreads: jest.fn().mockReturnValue(false),
|
supportsThreads: jest.fn().mockReturnValue(false),
|
||||||
isInitialSyncComplete: jest.fn().mockReturnValue(true),
|
isInitialSyncComplete: jest.fn().mockReturnValue(true),
|
||||||
getClientWellKnown: jest.fn(),
|
getClientWellKnown: jest.fn().mockResolvedValue({}),
|
||||||
});
|
});
|
||||||
let room: Room;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
SdkConfig.put({ validated_server_config: {} as ValidatedServerConfig } as IConfigOptions);
|
SdkConfig.put({ validated_server_config: {} as ValidatedServerConfig } as IConfigOptions);
|
||||||
DMRoomMap.makeShared();
|
DMRoomMap.makeShared();
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
mockClient.getUserId.mockReturnValue(bobId);
|
|
||||||
mockClient.getClientWellKnown.mockReturnValue({});
|
|
||||||
|
|
||||||
room = new Room(roomId, mockClient, mockClient.getSafeUserId());
|
room = new Room(roomId, mockClient, mockClient.getSafeUserId());
|
||||||
room.addLiveEvents([
|
room.addLiveEvents([
|
||||||
|
@ -127,6 +173,14 @@ describe("InviteDialog", () => {
|
||||||
});
|
});
|
||||||
mockClient.getRooms.mockReturnValue([room]);
|
mockClient.getRooms.mockReturnValue([room]);
|
||||||
mockClient.getRoom.mockReturnValue(room);
|
mockClient.getRoom.mockReturnValue(room);
|
||||||
|
|
||||||
|
SdkContextClass.instance.client = mockClient;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
Modal.closeCurrentModal();
|
||||||
|
SdkContextClass.instance.onLoggedOut();
|
||||||
|
SdkContextClass.instance.client = undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
|
@ -232,7 +286,7 @@ describe("InviteDialog", () => {
|
||||||
input.focus();
|
input.focus();
|
||||||
await userEvent.paste(`${bobId} ${aliceEmail}`);
|
await userEvent.paste(`${bobId} ${aliceEmail}`);
|
||||||
|
|
||||||
await screen.findByText(bobId);
|
await screen.findAllByText(bobId);
|
||||||
await screen.findByText(aliceEmail);
|
await screen.findByText(aliceEmail);
|
||||||
expect(input).toHaveValue("");
|
expect(input).toHaveValue("");
|
||||||
});
|
});
|
||||||
|
@ -291,13 +345,94 @@ describe("InviteDialog", () => {
|
||||||
render(<InviteDialog kind={InviteKind.Dm} onFinished={jest.fn()} />);
|
render(<InviteDialog kind={InviteKind.Dm} onFinished={jest.fn()} />);
|
||||||
|
|
||||||
// Start with a MXID → should convert to a pill
|
// Start with a MXID → should convert to a pill
|
||||||
await enterIntoSearchField("@carol:example.com");
|
await enterIntoSearchField(carolId);
|
||||||
expect(screen.queryByText("Invites by email can only be sent one at a time")).not.toBeInTheDocument();
|
expect(screen.queryByText("Invites by email can only be sent one at a time")).not.toBeInTheDocument();
|
||||||
expectPill("@carol:example.com");
|
expectPill(carolId);
|
||||||
|
|
||||||
// Add an email → should not convert to a pill
|
// Add an email → should not convert to a pill
|
||||||
await enterIntoSearchField(bobEmail);
|
await enterIntoSearchField(bobEmail);
|
||||||
expect(screen.getByText("Invites by email can only be sent one at a time")).toBeInTheDocument();
|
expect(screen.getByText("Invites by email can only be sent one at a time")).toBeInTheDocument();
|
||||||
expectNoPill(bobEmail);
|
expectNoPill(bobEmail);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should start a DM if the profile is available", async () => {
|
||||||
|
render(<InviteDialog kind={InviteKind.Dm} onFinished={jest.fn()} />);
|
||||||
|
await enterIntoSearchField(aliceId);
|
||||||
|
await userEvent.click(screen.getByRole("button", { name: "Go" }));
|
||||||
|
expect(startDmOnFirstMessage).toHaveBeenCalledWith(mockClient, [
|
||||||
|
new DirectoryMember({
|
||||||
|
user_id: aliceId,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when inviting a user with an unknown profile", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
render(<InviteDialog kind={InviteKind.Dm} onFinished={jest.fn()} />);
|
||||||
|
await enterIntoSearchField(carolId);
|
||||||
|
await userEvent.click(screen.getByRole("button", { name: "Go" }));
|
||||||
|
// Wait for the »invite anyway« modal to show up
|
||||||
|
await screen.findByText("The following users may not exist");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not start the DM", () => {
|
||||||
|
expect(startDmOnFirstMessage).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show the »invite anyway« dialog if the profile is not available", () => {
|
||||||
|
expect(screen.getByText("The following users may not exist")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(`${carolId}: Profile not found`)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when clicking »Start DM anyway«", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await userEvent.click(screen.getByRole("button", { name: "Start DM anyway", exact: true }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should start the DM", () => {
|
||||||
|
expect(startDmOnFirstMessage).toHaveBeenCalledWith(mockClient, [
|
||||||
|
new DirectoryMember({
|
||||||
|
user_id: carolId,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when clicking »Close«", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
mocked(startDmOnFirstMessage).mockClear();
|
||||||
|
await userEvent.click(screen.getByRole("button", { name: "Close" }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not start the DM", () => {
|
||||||
|
expect(startDmOnFirstMessage).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when inviting a user with an unknown profile and »promptBeforeInviteUnknownUsers« setting = false", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
mocked(startDmOnFirstMessage).mockClear();
|
||||||
|
jest.spyOn(SettingsStore, "getValue").mockImplementation(
|
||||||
|
(settingName) => settingName !== "promptBeforeInviteUnknownUsers",
|
||||||
|
);
|
||||||
|
render(<InviteDialog kind={InviteKind.Dm} onFinished={jest.fn()} />);
|
||||||
|
await enterIntoSearchField(carolId);
|
||||||
|
await userEvent.click(screen.getByRole("button", { name: "Go" }));
|
||||||
|
// modal rendering has some weird sleeps - fake timers will mess up the entire test
|
||||||
|
await sleep(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not show the »invite anyway« dialog", () => {
|
||||||
|
expect(screen.queryByText("The following users may not exist")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should start the DM directly", () => {
|
||||||
|
expect(startDmOnFirstMessage).toHaveBeenCalledWith(mockClient, [
|
||||||
|
new DirectoryMember({
|
||||||
|
user_id: carolId,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,13 +15,25 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { mocked, Mocked } from "jest-mock";
|
import { mocked, Mocked } from "jest-mock";
|
||||||
import { IMatrixProfile, MatrixClient, MatrixEvent, Room, RoomMemberEvent } from "matrix-js-sdk/src/matrix";
|
import {
|
||||||
|
IMatrixProfile,
|
||||||
|
MatrixClient,
|
||||||
|
MatrixError,
|
||||||
|
MatrixEvent,
|
||||||
|
Room,
|
||||||
|
RoomMemberEvent,
|
||||||
|
} from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import { UserProfilesStore } from "../../src/stores/UserProfilesStore";
|
import { UserProfilesStore } from "../../src/stores/UserProfilesStore";
|
||||||
import { filterConsole, mkRoomMember, mkRoomMemberJoinEvent, stubClient } from "../test-utils";
|
import { filterConsole, mkRoomMember, mkRoomMemberJoinEvent, stubClient } from "../test-utils";
|
||||||
|
|
||||||
describe("UserProfilesStore", () => {
|
describe("UserProfilesStore", () => {
|
||||||
const userIdDoesNotExist = "@unknown:example.com";
|
const userIdDoesNotExist = "@unknown:example.com";
|
||||||
|
const userDoesNotExistError = new MatrixError({
|
||||||
|
errcode: "M_NOT_FOUND",
|
||||||
|
error: "Profile not found",
|
||||||
|
});
|
||||||
|
|
||||||
const user1Id = "@user1:example.com";
|
const user1Id = "@user1:example.com";
|
||||||
const user1Profile: IMatrixProfile = { displayname: "User 1", avatar_url: undefined };
|
const user1Profile: IMatrixProfile = { displayname: "User 1", avatar_url: undefined };
|
||||||
const user2Id = "@user2:example.com";
|
const user2Id = "@user2:example.com";
|
||||||
|
@ -50,7 +62,7 @@ describe("UserProfilesStore", () => {
|
||||||
if (userId === user1Id) return user1Profile;
|
if (userId === user1Id) return user1Profile;
|
||||||
if (userId === user2Id) return user2Profile;
|
if (userId === user2Id) return user2Profile;
|
||||||
|
|
||||||
throw new Error("User not found");
|
throw userDoesNotExistError;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -65,10 +77,69 @@ describe("UserProfilesStore", () => {
|
||||||
expect(userProfilesStore.getProfile(user1Id)).toBe(user1Profile);
|
expect(userProfilesStore.getProfile(user1Id)).toBe(user1Profile);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("for an user that does not exist should return null and cache it", async () => {
|
it("when shouldThrow = true and there is an error it should raise an error", async () => {
|
||||||
const profile = await userProfilesStore.fetchProfile(userIdDoesNotExist);
|
await expect(userProfilesStore.fetchProfile(userIdDoesNotExist, { shouldThrow: true })).rejects.toThrow(
|
||||||
|
userDoesNotExistError.message,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when fetching a profile that does not exist", () => {
|
||||||
|
let profile: IMatrixProfile | null | undefined;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
profile = await userProfilesStore.fetchProfile(userIdDoesNotExist);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return null", () => {
|
||||||
expect(profile).toBeNull();
|
expect(profile).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should cache the error and result", () => {
|
||||||
expect(userProfilesStore.getProfile(userIdDoesNotExist)).toBeNull();
|
expect(userProfilesStore.getProfile(userIdDoesNotExist)).toBeNull();
|
||||||
|
expect(userProfilesStore.getProfileLookupError(userIdDoesNotExist)).toBe(userDoesNotExistError);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when the profile does not exist and fetching it again", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
mockClient.getProfileInfo.mockResolvedValue(user1Profile);
|
||||||
|
profile = await userProfilesStore.fetchProfile(userIdDoesNotExist);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return the profile", () => {
|
||||||
|
expect(profile).toBe(user1Profile);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should clear the error", () => {
|
||||||
|
expect(userProfilesStore.getProfileLookupError(userIdDoesNotExist)).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getOrFetchProfile", () => {
|
||||||
|
it("should return a profile from the API and cache it", async () => {
|
||||||
|
const profile = await userProfilesStore.getOrFetchProfile(user1Id);
|
||||||
|
expect(profile).toBe(user1Profile);
|
||||||
|
// same method again
|
||||||
|
expect(await userProfilesStore.getOrFetchProfile(user1Id)).toBe(user1Profile);
|
||||||
|
// assert that the profile is cached
|
||||||
|
expect(userProfilesStore.getProfile(user1Id)).toBe(user1Profile);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getProfileLookupError", () => {
|
||||||
|
it("should return undefined if a profile was not fetched", () => {
|
||||||
|
expect(userProfilesStore.getProfileLookupError(user1Id)).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return undefined if a profile was successfully fetched", async () => {
|
||||||
|
await userProfilesStore.fetchProfile(user1Id);
|
||||||
|
expect(userProfilesStore.getProfileLookupError(user1Id)).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return the error if there was one", async () => {
|
||||||
|
await userProfilesStore.fetchProfile(userIdDoesNotExist);
|
||||||
|
expect(userProfilesStore.getProfileLookupError(userIdDoesNotExist)).toBe(userDoesNotExistError);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -121,4 +192,18 @@ describe("UserProfilesStore", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("flush", () => {
|
||||||
|
it("should clear profiles, known profiles and errors", async () => {
|
||||||
|
await userProfilesStore.fetchOnlyKnownProfile(user1Id);
|
||||||
|
await userProfilesStore.fetchProfile(user1Id);
|
||||||
|
await userProfilesStore.fetchProfile(userIdDoesNotExist);
|
||||||
|
|
||||||
|
userProfilesStore.flush();
|
||||||
|
|
||||||
|
expect(userProfilesStore.getProfile(user1Id)).toBeUndefined();
|
||||||
|
expect(userProfilesStore.getOnlyKnownProfile(user1Id)).toBeUndefined();
|
||||||
|
expect(userProfilesStore.getProfileLookupError(userIdDoesNotExist)).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue