2023-01-03 16:55:26 +00:00
/*
2024-09-06 10:22:13 +02:00
Copyright 2022-2024 New Vector Ltd.
2023-01-03 16:55:26 +00:00
2025-02-18 17:59:58 +00:00
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
2024-09-06 10:22:13 +02:00
Please see LICENSE in the repository root for full details.
2023-01-03 16:55:26 +00:00
*/
2024-12-11 09:27:55 +00:00
import {
useMemo ,
type FC ,
type CSSProperties ,
useState ,
useEffect ,
} from "react" ;
2023-08-31 15:46:09 +02:00
import { Avatar as CompoundAvatar } from "@vector-im/compound-web" ;
2025-03-13 13:58:43 +01:00
import { type MatrixClient } from "matrix-js-sdk" ;
2026-03-13 11:31:55 -05:00
import { type WidgetApi } from "matrix-widget-api" ;
2022-05-06 11:32:09 +01:00
2026-03-09 22:25:54 -05:00
import { useClientState } from "./ClientContext" ;
2026-03-07 19:44:00 -06:00
import { widget } from "./widget" ;
2021-12-07 17:59:55 -08:00
2022-05-18 19:00:59 -04:00
export enum Size {
XS = "xs" ,
SM = "sm" ,
MD = "md" ,
LG = "lg" ,
XL = "xl" ,
}
export const sizes = new Map ([
[ Size . XS , 22 ],
[ Size . SM , 32 ],
[ Size . MD , 36 ],
[ Size . LG , 42 ],
[ Size . XL , 90 ],
]);
2024-12-18 15:31:45 +00:00
export interface Props {
2023-08-31 15:46:09 +02:00
id : string ;
name : string ;
className? : string ;
2022-07-30 10:06:28 +02:00
src? : string ;
2022-05-18 19:00:59 -04:00
size? : Size | number ;
2024-11-25 20:22:02 +00:00
style? : CSSProperties ;
2022-05-06 11:32:09 +01:00
}
2024-12-06 18:30:05 +00:00
export function getAvatarUrl (
client : MatrixClient ,
mxcUrl : string | null ,
avatarSize = 96 ,
) : string | null {
const width = Math . floor ( avatarSize * window . devicePixelRatio );
const height = Math . floor ( avatarSize * window . devicePixelRatio );
// scale is more suitable for larger sizes
const resizeMethod = avatarSize <= 96 ? "crop" : "scale" ;
return mxcUrl
? client . mxcUrlToHttp (
mxcUrl ,
width ,
height ,
resizeMethod ,
false ,
true ,
true ,
)
: null ;
}
2023-06-30 18:21:18 -04:00
export const Avatar : FC < Props > = ({
2023-08-31 15:46:09 +02:00
className ,
id ,
name ,
2021-12-07 17:59:55 -08:00
src ,
2022-05-18 19:00:59 -04:00
size = Size . MD ,
2024-11-25 20:22:02 +00:00
style ,
... props
2022-05-06 11:32:09 +01:00
}) => {
2024-12-06 18:30:05 +00:00
const clientState = useClientState ();
2022-05-18 19:00:59 -04:00
2023-08-31 15:46:09 +02:00
const sizePx = useMemo (
2022-05-18 19:00:59 -04:00
() =>
Object . values ( Size ). includes ( size as Size )
2026-03-09 22:25:54 -05:00
? sizes . get ( size as Size ) !
2023-08-31 15:46:09 +02:00
: ( size as number ),
2023-10-11 10:42:04 -04:00
[ size ],
2022-05-18 19:00:59 -04:00
);
2024-12-06 18:30:05 +00:00
const [ avatarUrl , setAvatarUrl ] = useState < string | undefined >( undefined );
2026-03-12 22:18:38 -05:00
// In theory, a change in `clientState` or `sizePx` could run extra getAvatarFromWidgetAPI calls, but in practice they should be stable long before this code runs.
2024-12-06 18:30:05 +00:00
useEffect (() => {
2026-03-12 22:18:38 -05:00
if ( ! src ) {
2024-12-06 18:30:05 +00:00
setAvatarUrl ( undefined );
return ;
}
2026-03-09 22:25:54 -05:00
let blob : Promise < Blob >;
2026-03-12 22:18:38 -05:00
if ( widget ? . api ) {
blob = getAvatarFromWidgetAPI ( widget . api , src );
} else if (
clientState ? . state === "valid" &&
clientState . authenticated ? . client &&
2026-03-09 22:25:54 -05:00
sizePx
) {
2026-03-12 22:18:38 -05:00
blob = getAvatarFromServer ( clientState . authenticated . client , src , sizePx );
2026-03-09 22:25:54 -05:00
} else {
setAvatarUrl ( undefined );
return ;
}
2026-03-07 19:44:00 -06:00
2026-03-09 22:25:54 -05:00
let objectUrl : string | undefined ;
2026-03-12 22:20:19 -05:00
let stale = false ;
2026-03-09 22:25:54 -05:00
blob
2024-12-06 18:30:05 +00:00
. then (( blob ) => {
2026-03-12 22:20:19 -05:00
if ( stale ) {
return ;
}
2024-12-06 18:30:05 +00:00
objectUrl = URL . createObjectURL ( blob );
setAvatarUrl ( objectUrl );
})
. catch (( ex ) => {
2026-03-12 22:20:19 -05:00
if ( stale ) {
return ;
}
2024-12-06 18:30:05 +00:00
setAvatarUrl ( undefined );
});
return () : void => {
2026-03-12 22:20:19 -05:00
stale = true ;
2024-12-06 18:30:05 +00:00
if ( objectUrl ) {
URL . revokeObjectURL ( objectUrl );
}
};
}, [ clientState , src , sizePx ]);
2022-05-18 19:00:59 -04:00
2021-12-07 17:59:55 -08:00
return (
2023-08-31 15:46:09 +02:00
< CompoundAvatar
className = { className }
id = { id }
name = { name }
size = { ` ${ sizePx } px` }
2024-12-06 18:30:05 +00:00
src = { avatarUrl }
2024-11-25 20:22:02 +00:00
style = { style }
{ ...props }
2023-08-31 15:46:09 +02:00
/>
2021-12-07 17:59:55 -08:00
);
2022-05-06 11:32:09 +01:00
};
2026-03-07 19:44:00 -06:00
async function getAvatarFromServer (
2026-03-09 22:25:54 -05:00
client : MatrixClient ,
2026-03-07 19:44:00 -06:00
src : string ,
2026-03-09 22:25:54 -05:00
sizePx : number ,
2026-03-07 19:44:00 -06:00
) : Promise < Blob > {
const httpSrc = getAvatarUrl ( client , src , sizePx );
if ( ! httpSrc ) {
throw new Error ( "Failed to get http avatar URL" );
}
const token = client . getAccessToken ();
if ( ! token ) {
throw new Error ( "Failed to get access token" );
}
const request = await fetch ( httpSrc , {
headers : {
Authorization : `Bearer ${ token } ` ,
},
});
const blob = await request . blob ();
return blob ;
}
2026-03-26 18:14:51 +01:00
// export for testing
export async function getAvatarFromWidgetAPI (
2026-03-09 22:25:54 -05:00
api : WidgetApi ,
2026-03-07 19:44:00 -06:00
src : string ,
) : Promise < Blob > {
const response = await api . downloadFile ( src );
const file = response . file ;
// element-web sends a Blob, and the MSC4039 is considering changing the spec to strictly Blob, so only handling that
2026-03-26 15:29:51 +01:00
if ( file instanceof Blob ) {
return file ;
} else if ( typeof file === "string" ) {
// it is a base64 string
2026-03-26 18:14:51 +01:00
const bytes = Uint8Array . from ( atob ( file ), ( c ) => c . charCodeAt ( 0 ));
2026-03-26 15:29:51 +01:00
return new Blob ([ bytes ]);
2026-03-07 19:44:00 -06:00
}
2026-03-26 18:14:51 +01:00
throw new Error (
"Downloaded file format is not supported: " + typeof file + "" ,
);
2026-03-07 19:44:00 -06:00
}