diff --git a/src/stores/OwnProfileStore.ts b/src/stores/OwnProfileStore.ts index bae9534846..ce69a8f547 100644 --- a/src/stores/OwnProfileStore.ts +++ b/src/stores/OwnProfileStore.ts @@ -19,6 +19,7 @@ import { User, UserEvent } from "matrix-js-sdk/src/models/user"; import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state"; import { throttle } from "lodash"; import { EventType } from "matrix-js-sdk/src/@types/event"; +import { MatrixError } from "matrix-js-sdk/src/matrix"; import { ActionPayload } from "../dispatcher/payloads"; import { AsyncStoreWithClient } from "./AsyncStoreWithClient"; @@ -45,7 +46,7 @@ export class OwnProfileStore extends AsyncStoreWithClient { private monitoredUser: User | null = null; - private constructor() { + public constructor() { // seed from localstorage because otherwise we won't get these values until a whole network // round-trip after the client is ready, and we often load widgets in that time, and we'd // and up passing them an incorrect display name @@ -136,12 +137,32 @@ export class OwnProfileStore extends AsyncStoreWithClient { if (!this.matrixClient) return; // We specifically do not use the User object we stored for profile info as it // could easily be wrong (such as per-room instead of global profile). - const profileInfo = await this.matrixClient.getProfileInfo(this.matrixClient.getSafeUserId()); + + let profileInfo: { displayname?: string; avatar_url?: string } = { + displayname: undefined, + avatar_url: undefined, + }; + + try { + profileInfo = await this.matrixClient.getProfileInfo(this.matrixClient.getSafeUserId()); + } catch (error: unknown) { + if (!(error instanceof MatrixError) || error.errcode !== "M_NOT_FOUND") { + /** + * Raise any other error than M_NOT_FOUND. + * M_NOT_FOUND could occur if there is no user profile. + * {@link https://spec.matrix.org/v1.7/client-server-api/#get_matrixclientv3profileuserid} + * We should then assume an empty profile, emit UPDATE_EVENT etc.. + */ + throw error; + } + } + if (profileInfo.displayname) { window.localStorage.setItem(KEY_DISPLAY_NAME, profileInfo.displayname); } else { window.localStorage.removeItem(KEY_DISPLAY_NAME); } + if (profileInfo.avatar_url) { window.localStorage.setItem(KEY_AVATAR_URL, profileInfo.avatar_url); } else { diff --git a/test/stores/OwnProfileStore-test.ts b/test/stores/OwnProfileStore-test.ts new file mode 100644 index 0000000000..1e0f8188d9 --- /dev/null +++ b/test/stores/OwnProfileStore-test.ts @@ -0,0 +1,87 @@ +/* +Copyright 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. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix"; +import { MockedObject, mocked } from "jest-mock"; + +import { stubClient } from "../test-utils"; +import { OwnProfileStore } from "../../src/stores/OwnProfileStore"; +import { UPDATE_EVENT } from "../../src/stores/AsyncStore"; + +describe("OwnProfileStore", () => { + let client: MockedObject; + let ownProfileStore: OwnProfileStore; + let onUpdate: ReturnType; + + beforeEach(() => { + client = mocked(stubClient()); + onUpdate = jest.fn(); + ownProfileStore = new OwnProfileStore(); + ownProfileStore.addListener(UPDATE_EVENT, onUpdate); + }); + + afterEach(() => { + ownProfileStore.removeListener(UPDATE_EVENT, onUpdate); + }); + + it("if the client has not yet been started, the displayname and avatar should be null", () => { + expect(onUpdate).not.toHaveBeenCalled(); + expect(ownProfileStore.displayName).toBeNull(); + expect(ownProfileStore.avatarMxc).toBeNull(); + }); + + it("if the client has been started and there is a profile, it should return the profile display name and avatar", async () => { + client.getProfileInfo.mockResolvedValue({ + displayname: "Display Name", + avatar_url: "mxc://example.com/abc123", + }); + await ownProfileStore.start(); + + expect(onUpdate).toHaveBeenCalled(); + expect(ownProfileStore.displayName).toBe("Display Name"); + expect(ownProfileStore.avatarMxc).toBe("mxc://example.com/abc123"); + }); + + it("if there is a M_NOT_FOUND error, it should report ready, displayname = MXID and avatar = null", async () => { + client.getProfileInfo.mockRejectedValue( + new MatrixError({ + error: "Not found", + errcode: "M_NOT_FOUND", + }), + ); + await ownProfileStore.start(); + + expect(onUpdate).toHaveBeenCalled(); + expect(ownProfileStore.displayName).toBe(client.getSafeUserId()); + expect(ownProfileStore.avatarMxc).toBeNull(); + }); + + it("if there is any other error, it should not report ready, displayname = MXID and avatar = null", async () => { + client.getProfileInfo.mockRejectedValue( + new MatrixError({ + error: "Forbidden", + errcode: "M_FORBIDDEN", + }), + ); + try { + await ownProfileStore.start(); + } catch (ignore) {} + + expect(onUpdate).not.toHaveBeenCalled(); + expect(ownProfileStore.displayName).toBe(client.getSafeUserId()); + expect(ownProfileStore.avatarMxc).toBeNull(); + }); +});