Merge pull request #339 from robintown/room-avatars

Display room avatars
This commit is contained in:
Robin 2022-05-19 10:46:24 -04:00 committed by GitHub
commit 7a2d64c0ef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 112 additions and 63 deletions

View file

@ -1,6 +1,9 @@
import React, { useMemo } from "react";
import React, { useMemo, CSSProperties } from "react";
import classNames from "classnames";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { getAvatarUrl } from "./matrix-utils";
import { useClient } from "./ClientContext";
import styles from "./Avatar.module.css";
const backgroundColors = [
@ -14,6 +17,22 @@ const backgroundColors = [
"#74D12C",
];
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],
]);
function hashStringToArrIndex(str: string, arrLength: number) {
let sum = 0;
@ -24,24 +43,51 @@ function hashStringToArrIndex(str: string, arrLength: number) {
return sum % arrLength;
}
const resolveAvatarSrc = (client: MatrixClient, src: string, size: number) =>
src?.startsWith("mxc://") ? client && getAvatarUrl(client, src, size) : src;
interface Props extends React.HTMLAttributes<HTMLDivElement> {
bgKey?: string;
src: string;
fallback: string;
size?: number;
size?: Size | number;
className: string;
style: React.CSSProperties;
style?: CSSProperties;
}
export const Avatar: React.FC<Props> = ({
bgKey,
src,
fallback,
size,
size = Size.MD,
className,
style,
style = {},
...rest
}) => {
const { client } = useClient();
const [sizeClass, sizePx, sizeStyle] = useMemo(
() =>
Object.values(Size).includes(size as Size)
? [styles[size as string], sizes.get(size as Size), {}]
: [
null,
size as number,
{
width: size,
height: size,
borderRadius: size,
fontSize: Math.round((size as number) / 2),
},
],
[size]
);
const resolvedSrc = useMemo(
() => resolveAvatarSrc(client, src, sizePx),
[client, src, sizePx]
);
const backgroundColor = useMemo(() => {
const index = hashStringToArrIndex(
bgKey || fallback || src || "",
@ -53,12 +99,12 @@ export const Avatar: React.FC<Props> = ({
/* eslint-disable jsx-a11y/alt-text */
return (
<div
className={classNames(styles.avatar, styles[size || "md"], className)}
style={{ backgroundColor, ...style }}
className={classNames(styles.avatar, sizeClass, className)}
style={{ backgroundColor, ...sizeStyle, ...style }}
{...rest}
>
{src ? (
<img src={src} />
{resolvedSrc ? (
<img src={resolvedSrc} />
) : typeof fallback === "string" ? (
<span>{fallback}</span>
) : (

View file

@ -1,8 +1,7 @@
import React from "react";
import styles from "./Facepile.module.css";
import classNames from "classnames";
import { Avatar } from "./Avatar";
import { getAvatarUrl } from "./matrix-utils";
import { Avatar, sizes } from "./Avatar";
const overlapMap = {
xs: 2,
@ -10,12 +9,6 @@ const overlapMap = {
md: 8,
};
const sizeMap = {
xs: 24,
sm: 32,
md: 36,
};
export function Facepile({
className,
client,
@ -24,7 +17,7 @@ export function Facepile({
size,
...rest
}) {
const _size = sizeMap[size];
const _size = sizes.get(size);
const _overlap = overlapMap[size];
return (
@ -40,7 +33,7 @@ export function Facepile({
<Avatar
key={member.userId}
size={size}
src={avatarUrl && getAvatarUrl(client, avatarUrl, _size)}
src={avatarUrl}
fallback={member.name.slice(0, 1).toUpperCase()}
className={styles.avatar}
style={{ left: i * (_size - _overlap) }}

View file

@ -57,12 +57,13 @@ export function HeaderLogo({ className }) {
);
}
export function RoomHeaderInfo({ roomName }) {
export function RoomHeaderInfo({ roomName, avatarUrl }) {
return (
<>
<div className={styles.roomAvatar}>
<Avatar
size="md"
src={avatarUrl}
bgKey={roomName}
fallback={roomName.slice(0, 1).toUpperCase()}
/>
@ -73,13 +74,13 @@ export function RoomHeaderInfo({ roomName }) {
);
}
export function RoomSetupHeaderInfo({ roomName, ...rest }) {
export function RoomSetupHeaderInfo({ roomName, avatarUrl, ...rest }) {
const ref = useRef();
const { buttonProps } = useButton(rest, ref);
return (
<button className={styles.backButton} ref={ref} {...buttonProps}>
<ArrowLeftIcon width={16} height={16} />
<RoomHeaderInfo roomName={roomName} />
<RoomHeaderInfo roomName={roomName} avatarUrl={avatarUrl} />
</button>
);
}

View file

@ -79,7 +79,7 @@ export function useGroupCallRooms(client) {
return {
roomId: room.getCanonicalAlias() || room.roomId,
roomName: room.name,
avatarUrl: null,
avatarUrl: room.getMxcAvatarUrl(),
room,
groupCall,
participants: [...groupCall.participants],

View file

@ -15,7 +15,6 @@ limitations under the License.
*/
import { useState, useCallback, useEffect } from "react";
import { getAvatarUrl } from "../matrix-utils";
export function useProfile(client) {
const [{ loading, displayName, avatarUrl, error, success }, setState] =
@ -26,7 +25,7 @@ export function useProfile(client) {
success: false,
loading: false,
displayName: user?.rawDisplayName,
avatarUrl: user && client && getAvatarUrl(client, user.avatarUrl),
avatarUrl: user?.avatarUrl,
error: null,
};
});
@ -37,7 +36,7 @@ export function useProfile(client) {
success: false,
loading: false,
displayName,
avatarUrl: getAvatarUrl(client, avatarUrl),
avatarUrl,
error: null,
});
};
@ -84,11 +83,7 @@ export function useProfile(client) {
setState((prev) => ({
...prev,
displayName,
avatarUrl: removeAvatar
? null
: mxcAvatarUrl
? getAvatarUrl(client, mxcAvatarUrl)
: prev.avatarUrl,
avatarUrl: removeAvatar ? null : mxcAvatarUrl ?? prev.avatarUrl,
loading: false,
success: true,
}));

View file

@ -23,6 +23,7 @@ import { LobbyView } from "./LobbyView";
import { InCallView } from "./InCallView";
import { PTTCallView } from "./PTTCallView";
import { CallEndedView } from "./CallEndedView";
import { useRoomAvatar } from "./useRoomAvatar";
import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler";
import { useLocationNavigation } from "../useLocationNavigation";
@ -67,6 +68,8 @@ export function GroupCallView({
participants,
} = useGroupCall(groupCall);
const avatarUrl = useRoomAvatar(groupCall.room);
useEffect(() => {
window.groupCall = groupCall;
}, [groupCall]);
@ -96,6 +99,7 @@ export function GroupCallView({
client={client}
roomId={roomId}
roomName={groupCall.room.name}
avatarUrl={avatarUrl}
groupCall={groupCall}
participants={participants}
userMediaFeeds={userMediaFeeds}
@ -110,6 +114,7 @@ export function GroupCallView({
groupCall={groupCall}
client={client}
roomName={groupCall.room.name}
avatarUrl={avatarUrl}
microphoneMuted={microphoneMuted}
localVideoMuted={localVideoMuted}
toggleLocalVideoMuted={toggleLocalVideoMuted}
@ -142,6 +147,7 @@ export function GroupCallView({
groupCall={groupCall}
hasLocalParticipant={hasLocalParticipant}
roomName={groupCall.room.name}
avatarUrl={avatarUrl}
state={state}
onInitLocalCallFeed={initLocalCallFeed}
localCallFeed={localCallFeed}

View file

@ -25,7 +25,6 @@ import {
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
import { VideoGrid, useVideoGridLayout } from "../video-grid/VideoGrid";
import { VideoTileContainer } from "../video-grid/VideoTileContainer";
import { getAvatarUrl } from "../matrix-utils";
import { GroupCallInspector } from "./GroupCallInspector";
import { OverflowMenu } from "./OverflowMenu";
import { GridLayoutMenu } from "./GridLayoutMenu";
@ -47,6 +46,7 @@ export function InCallView({
client,
groupCall,
roomName,
avatarUrl,
microphoneMuted,
localVideoMuted,
toggleLocalVideoMuted,
@ -129,13 +129,8 @@ export function InCallView({
return (
<Avatar
key={roomMember.userId}
style={{
width: size,
height: size,
borderRadius: size,
fontSize: Math.round(size / 2),
}}
src={avatarUrl && getAvatarUrl(client, avatarUrl, 96)}
size={size}
src={avatarUrl}
fallback={roomMember.name.slice(0, 1).toUpperCase()}
className={styles.avatar}
/>
@ -153,7 +148,7 @@ export function InCallView({
<div className={styles.inRoom}>
<Header>
<LeftNav>
<RoomHeaderInfo roomName={roomName} />
<RoomHeaderInfo roomName={roomName} avatarUrl={avatarUrl} />
</LeftNav>
<RightNav>
<GridLayoutMenu layout={layout} setLayout={setLayout} />

View file

@ -32,6 +32,7 @@ export function LobbyView({
client,
groupCall,
roomName,
avatarUrl,
state,
onInitLocalCallFeed,
onEnter,
@ -72,7 +73,7 @@ export function LobbyView({
<div className={styles.room}>
<Header>
<LeftNav>
<RoomHeaderInfo roomName={roomName} />
<RoomHeaderInfo roomName={roomName} avatarUrl={avatarUrl} />
</LeftNav>
<RightNav>
<UserMenuContainer />

View file

@ -148,12 +148,7 @@ export const PTTButton: React.FC<Props> = ({
) : (
<Avatar
key={activeSpeakerUserId}
style={{
width: size - 12,
height: size - 12,
borderRadius: size - 12,
fontSize: Math.round((size - 12) / 2),
}}
size={size - 12}
src={activeSpeakerAvatarUrl}
fallback={activeSpeakerDisplayName.slice(0, 1).toUpperCase()}
className={styles.avatar}

View file

@ -32,7 +32,6 @@ import { useMediaHandler } from "../settings/useMediaHandler";
import { usePTT } from "./usePTT";
import { Timer } from "./Timer";
import { Toggle } from "../input/Toggle";
import { getAvatarUrl } from "../matrix-utils";
import { ReactComponent as AudioIcon } from "../icons/Audio.svg";
import { usePTTSounds } from "../sound/usePttSounds";
import { PTTClips } from "../sound/PTTClips";
@ -80,6 +79,7 @@ interface Props {
client: MatrixClient;
roomId: string;
roomName: string;
avatarUrl: string;
groupCall: GroupCall;
participants: RoomMember[];
userMediaFeeds: CallFeed[];
@ -92,6 +92,7 @@ export const PTTCallView: React.FC<Props> = ({
client,
roomId,
roomName,
avatarUrl,
groupCall,
participants,
userMediaFeeds,
@ -106,7 +107,6 @@ export const PTTCallView: React.FC<Props> = ({
const [containerRef, bounds] = useMeasure({ polyfill: ResizeObserver });
const facepileSize = bounds.width < 800 ? "sm" : "md";
const pttButtonSize = 232;
const pttBorderWidth = 6;
const { audioOutput } = useMediaHandler();
@ -142,13 +142,7 @@ export const PTTCallView: React.FC<Props> = ({
const activeSpeakerUser = activeSpeakerUserId
? client.getUser(activeSpeakerUserId)
: null;
const activeSpeakerAvatarUrl = activeSpeakerUser
? getAvatarUrl(
client,
activeSpeakerUser.avatarUrl,
pttButtonSize - pttBorderWidth * 2
)
: null;
const activeSpeakerAvatarUrl = activeSpeakerUser?.avatarUrl;
const activeSpeakerDisplayName = activeSpeakerUser
? activeSpeakerUser.displayName
: "";
@ -170,7 +164,11 @@ export const PTTCallView: React.FC<Props> = ({
/>
<Header className={styles.header}>
<LeftNav>
<RoomSetupHeaderInfo roomName={roomName} onPress={onLeave} />
<RoomSetupHeaderInfo
roomName={roomName}
avatarUrl={avatarUrl}
onPress={onLeave}
/>
</LeftNav>
<RightNav />
</Header>

View file

@ -66,12 +66,7 @@ export function VideoPreview({
{localVideoMuted && (
<div className={styles.avatarContainer}>
<Avatar
style={{
width: avatarSize,
height: avatarSize,
borderRadius: avatarSize,
fontSize: Math.round(avatarSize / 2),
}}
size={avatarSize}
src={avatarUrl}
fallback={displayName.slice(0, 1).toUpperCase()}
/>

24
src/room/useRoomAvatar.ts Normal file
View file

@ -0,0 +1,24 @@
import { useState, useEffect } from "react";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Room } from "matrix-js-sdk/src/models/room";
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
import { EventType } from "matrix-js-sdk/src/@types/event";
export const useRoomAvatar = (room: Room) => {
const [avatarUrl, setAvatarUrl] = useState(room.getMxcAvatarUrl());
useEffect(() => {
const update = (ev: MatrixEvent) => {
if (ev.getType() === EventType.RoomAvatar) {
setAvatarUrl(room.getMxcAvatarUrl());
}
};
room.currentState.on(RoomStateEvent.Events, update);
return () => {
room.currentState.off(RoomStateEvent.Events, update);
};
}, [room]);
return avatarUrl;
};