From 699e31f59a00c358f0a9fe6b68ac2fa19c909a45 Mon Sep 17 00:00:00 2001 From: JephDiel Date: Mon, 9 Mar 2026 22:25:54 -0500 Subject: [PATCH] Download Avatar from relevent source Instead of relying on failures directly use the available method to download the avatar. --- src/Avatar.test.tsx | 50 ++++++--------------- src/Avatar.tsx | 56 ++++++++++-------------- src/ClientContext.tsx | 15 ++++--- src/settings/useSubmitRageshake.test.tsx | 2 +- 4 files changed, 47 insertions(+), 76 deletions(-) diff --git a/src/Avatar.test.tsx b/src/Avatar.test.tsx index 1ad0f4be..3d75d4c1 100644 --- a/src/Avatar.test.tsx +++ b/src/Avatar.test.tsx @@ -17,8 +17,15 @@ import EventEmitter from "events"; import { widget } from "./widget"; const TestComponent: FC< - PropsWithChildren<{ client: MatrixClient; supportsThumbnails?: boolean }> -> = ({ client, children, supportsThumbnails }) => { + PropsWithChildren<{ + client: MatrixClient; + supportsAuthenticatedMedia?: boolean; + }> +> = ({ + client, + children, + supportsAuthenticatedMedia: supportsAuthenticatedMedia, +}) => { return ( { expect(client.mxcUrlToHttp).toBeCalledTimes(0); }); -test("should just render a placeholder when thumbnails are not supported", () => { - const client = vi.mocked({ - getAccessToken: () => "my-access-token", - mxcUrlToHttp: () => vi.fn(), - } as unknown as MatrixClient); - - vi.spyOn(client, "mxcUrlToHttp"); - const member = mockMatrixRoomMember( - mockRtcMembership("@alice:example.org", "AAAA"), - { - getMxcAvatarUrl: () => "mxc://example.org/alice-avatar", - }, - ); - const displayName = "Alice"; - render( - - - , - ); - const element = screen.getByRole("img", { name: "@alice:example.org" }); - expect(element.tagName).toEqual("SPAN"); - expect(client.mxcUrlToHttp).toBeCalledTimes(0); -}); - -test("should attempt to fetch authenticated media", async () => { +test("should attempt to fetch authenticated media if supported", async () => { const expectedAuthUrl = "http://example.org/media/alice-avatar"; const expectedObjectURL = "my-object-url"; const accessToken = "my-access-token"; @@ -142,7 +120,7 @@ test("should attempt to fetch authenticated media", async () => { ); const displayName = "Alice"; render( - + { }); }); -test("should use widget API when unable to authenticate media", async () => { +test("should attempt to use widget API if authenticate media is not supported", async () => { const expectedMXCUrl = "mxc://example.org/alice-avatar"; const expectedObjectURL = "my-object-url"; const theBlob = new Blob([]); @@ -188,7 +166,7 @@ test("should use widget API when unable to authenticate media", async () => { ); const displayName = "Alice"; render( - + = ({ const sizePx = useMemo( () => Object.values(Size).includes(size as Size) - ? sizes.get(size as Size) + ? sizes.get(size as Size)! : (size as number), [size], ); @@ -88,17 +88,29 @@ export const Avatar: FC = ({ const [avatarUrl, setAvatarUrl] = useState(undefined); useEffect(() => { - if (!src) { + if (!src || clientState?.state !== "valid") { + setAvatarUrl(undefined); + return; + } + + const { authenticated, supportedFeatures } = clientState; + let blob: Promise; + + if ( + supportedFeatures.authenticatedMedia && + authenticated?.client && + sizePx + ) { + blob = getAvatarFromServer(authenticated.client, src, sizePx); + } else if (widget?.api) { + blob = getAvatarFromWidgetAPI(widget.api, src); + } else { setAvatarUrl(undefined); return; } let objectUrl: string | undefined; - - getAvatarFromServer(clientState, src, sizePx) // Try to download directly from the mxc:// - .catch((ex) => { - return getAvatarFromWidget(widget?.api, src); // Fallback to trying to use the MSC4039 Widget API - }) + blob .then((blob) => { objectUrl = URL.createObjectURL(blob); setAvatarUrl(objectUrl); @@ -128,26 +140,10 @@ export const Avatar: FC = ({ }; async function getAvatarFromServer( - clientState: ClientState | undefined, + client: MatrixClient, src: string, - sizePx: number | undefined, + sizePx: number, ): Promise { - if (clientState?.state !== "valid") { - throw new Error("Client state must be valid"); - } - if (!sizePx) { - throw new Error("size must be supplied"); - } - - const { authenticated, supportedFeatures } = clientState; - const client = authenticated?.client; - if (!client) { - throw new Error("Client must be supplied"); - } - if (!supportedFeatures.thumbnails) { - throw new Error("Thumbnails are not supported"); - } - const httpSrc = getAvatarUrl(client, src, sizePx); if (!httpSrc) { throw new Error("Failed to get http avatar URL"); @@ -169,14 +165,10 @@ async function getAvatarFromServer( return blob; } -async function getAvatarFromWidget( - api: WidgetApi | undefined, +async function getAvatarFromWidgetAPI( + api: WidgetApi, src: string, ): Promise { - if (!api) { - throw new Error("No widget api given"); - } - const response = await api.downloadFile(src); const file = response.file; diff --git a/src/ClientContext.tsx b/src/ClientContext.tsx index 1488965a..7059cd69 100644 --- a/src/ClientContext.tsx +++ b/src/ClientContext.tsx @@ -48,7 +48,7 @@ export type ValidClientState = { disconnected: boolean; supportedFeatures: { reactions: boolean; - thumbnails: boolean; + authenticatedMedia: boolean; }; setClient: (client: MatrixClient, session: Session) => void; }; @@ -249,7 +249,8 @@ export const ClientProvider: FC = ({ children }) => { const [isDisconnected, setIsDisconnected] = useState(false); const [supportsReactions, setSupportsReactions] = useState(false); - const [supportsThumbnails, setSupportsThumbnails] = useState(false); + const [supportsAuthenticatedMedia, setSupportsAuthenticatedMedia] = + useState(false); const state: ClientState | undefined = useMemo(() => { if (alreadyOpenedErr) { @@ -275,7 +276,7 @@ export const ClientProvider: FC = ({ children }) => { disconnected: isDisconnected, supportedFeatures: { reactions: supportsReactions, - thumbnails: supportsThumbnails, + authenticatedMedia: supportsAuthenticatedMedia, }, }; }, [ @@ -286,7 +287,7 @@ export const ClientProvider: FC = ({ children }) => { setClient, isDisconnected, supportsReactions, - supportsThumbnails, + supportsAuthenticatedMedia, ]); const onSync = useCallback( @@ -312,8 +313,8 @@ export const ClientProvider: FC = ({ children }) => { } if (initClientState.widgetApi) { - // There is currently no widget API for authenticated media thumbnails. - setSupportsThumbnails(false); + // There is currently no way for widgets to request authenticated media directly from the server. + setSupportsAuthenticatedMedia(false); const reactSend = initClientState.widgetApi.hasCapability( "org.matrix.msc2762.send.event:m.reaction", ); @@ -335,7 +336,7 @@ export const ClientProvider: FC = ({ children }) => { } } else { setSupportsReactions(true); - setSupportsThumbnails(true); + setSupportsAuthenticatedMedia(true); } return (): void => { diff --git a/src/settings/useSubmitRageshake.test.tsx b/src/settings/useSubmitRageshake.test.tsx index b278d4b1..d69255e0 100644 --- a/src/settings/useSubmitRageshake.test.tsx +++ b/src/settings/useSubmitRageshake.test.tsx @@ -78,7 +78,7 @@ function renderWithMockClient( disconnected: false, supportedFeatures: { reactions: true, - thumbnails: true, + authenticatedMedia: true, }, setClient: vi.fn(), authenticated: {