mirror of
https://github.com/vector-im/element-call.git
synced 2026-09-22 22:29:30 +00:00
ErrorView needed a widget only to decide whether to offer a close button or a link home. That prop was threaded down through ErrorPage, RichError and GroupCallErrorBoundary from seven call sites, to answer the single question of whether the host can dismiss Element Call — which the host bridge now answers directly through the presence of close(). Read the bridge from context in ErrorView and remove the prop, along with the plumbing that carried it. No functional change: the rendered output is unchanged, as the existing snapshot confirms. Move GroupCallView onto the host bridge GroupCallView asked the widget API to keep it on screen, to close it, and to tell it when a preloaded call should join. Route all three through the host bridge and drop the widget prop.
176 lines
3.6 KiB
TypeScript
176 lines
3.6 KiB
TypeScript
/*
|
|
Copyright 2022-2024 New Vector Ltd.
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
Please see LICENSE in the repository root for full details.
|
|
*/
|
|
|
|
import {
|
|
useMemo,
|
|
type FC,
|
|
type CSSProperties,
|
|
useState,
|
|
useEffect,
|
|
} from "react";
|
|
import { Avatar as CompoundAvatar } from "@vector-im/compound-web";
|
|
import { type MatrixClient } from "matrix-js-sdk";
|
|
|
|
import { useClientState } from "./ClientContext";
|
|
import { useHostBridge } from "./HostBridge";
|
|
|
|
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],
|
|
]);
|
|
|
|
export interface Props {
|
|
id: string;
|
|
name: string;
|
|
className?: string;
|
|
src?: string;
|
|
size?: Size | number;
|
|
style?: CSSProperties;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
export const Avatar: FC<Props> = ({
|
|
className,
|
|
id,
|
|
name,
|
|
src,
|
|
size = Size.MD,
|
|
style,
|
|
...props
|
|
}) => {
|
|
const clientState = useClientState();
|
|
const hostBridge = useHostBridge();
|
|
|
|
const sizePx = useMemo(
|
|
() =>
|
|
Object.values(Size).includes(size as Size)
|
|
? sizes.get(size as Size)!
|
|
: (size as number),
|
|
[size],
|
|
);
|
|
|
|
const [avatarUrl, setAvatarUrl] = useState<string | undefined>(undefined);
|
|
|
|
// In theory, a change in `clientState` or `sizePx` could run extra media
|
|
// downloads, but in practice they should be stable long before this code runs.
|
|
useEffect(() => {
|
|
if (!src) {
|
|
setAvatarUrl(undefined);
|
|
return;
|
|
}
|
|
|
|
let blob: Promise<Blob>;
|
|
|
|
if (hostBridge.downloadMedia) {
|
|
blob = hostBridge.downloadMedia(src);
|
|
} else if (
|
|
clientState?.state === "valid" &&
|
|
clientState.authenticated?.client &&
|
|
sizePx
|
|
) {
|
|
blob = getAvatarFromServer(clientState.authenticated.client, src, sizePx);
|
|
} else {
|
|
setAvatarUrl(undefined);
|
|
return;
|
|
}
|
|
|
|
let objectUrl: string | undefined;
|
|
let stale = false;
|
|
blob
|
|
.then((blob) => {
|
|
if (stale) {
|
|
return;
|
|
}
|
|
objectUrl = URL.createObjectURL(blob);
|
|
setAvatarUrl(objectUrl);
|
|
})
|
|
.catch((ex) => {
|
|
if (stale) {
|
|
return;
|
|
}
|
|
setAvatarUrl(undefined);
|
|
});
|
|
|
|
return (): void => {
|
|
stale = true;
|
|
if (objectUrl) {
|
|
URL.revokeObjectURL(objectUrl);
|
|
}
|
|
};
|
|
}, [clientState, hostBridge, src, sizePx]);
|
|
|
|
return (
|
|
<CompoundAvatar
|
|
className={className}
|
|
id={id}
|
|
name={name}
|
|
size={`${sizePx}px`}
|
|
src={avatarUrl}
|
|
style={style}
|
|
{...props}
|
|
/>
|
|
);
|
|
};
|
|
|
|
async function getAvatarFromServer(
|
|
client: MatrixClient,
|
|
src: string,
|
|
sizePx: number,
|
|
): 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;
|
|
}
|