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 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"; import styles from "./Avatar.module.css";
const backgroundColors = [ const backgroundColors = [
@ -14,6 +17,22 @@ const backgroundColors = [
"#74D12C", "#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) { function hashStringToArrIndex(str: string, arrLength: number) {
let sum = 0; let sum = 0;
@ -24,24 +43,51 @@ function hashStringToArrIndex(str: string, arrLength: number) {
return sum % arrLength; 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> { interface Props extends React.HTMLAttributes<HTMLDivElement> {
bgKey?: string; bgKey?: string;
src: string; src: string;
fallback: string; fallback: string;
size?: number; size?: Size | number;
className: string; className: string;
style: React.CSSProperties; style?: CSSProperties;
} }
export const Avatar: React.FC<Props> = ({ export const Avatar: React.FC<Props> = ({
bgKey, bgKey,
src, src,
fallback, fallback,
size, size = Size.MD,
className, className,
style, style = {},
...rest ...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 backgroundColor = useMemo(() => {
const index = hashStringToArrIndex( const index = hashStringToArrIndex(
bgKey || fallback || src || "", bgKey || fallback || src || "",
@ -53,12 +99,12 @@ export const Avatar: React.FC<Props> = ({
/* eslint-disable jsx-a11y/alt-text */ /* eslint-disable jsx-a11y/alt-text */
return ( return (
<div <div
className={classNames(styles.avatar, styles[size || "md"], className)} className={classNames(styles.avatar, sizeClass, className)}
style={{ backgroundColor, ...style }} style={{ backgroundColor, ...sizeStyle, ...style }}
{...rest} {...rest}
> >
{src ? ( {resolvedSrc ? (
<img src={src} /> <img src={resolvedSrc} />
) : typeof fallback === "string" ? ( ) : typeof fallback === "string" ? (
<span>{fallback}</span> <span>{fallback}</span>
) : ( ) : (

View file

@ -1,8 +1,7 @@
import React from "react"; import React from "react";
import styles from "./Facepile.module.css"; import styles from "./Facepile.module.css";
import classNames from "classnames"; import classNames from "classnames";
import { Avatar } from "./Avatar"; import { Avatar, sizes } from "./Avatar";
import { getAvatarUrl } from "./matrix-utils";
const overlapMap = { const overlapMap = {
xs: 2, xs: 2,
@ -10,12 +9,6 @@ const overlapMap = {
md: 8, md: 8,
}; };
const sizeMap = {
xs: 24,
sm: 32,
md: 36,
};
export function Facepile({ export function Facepile({
className, className,
client, client,
@ -24,7 +17,7 @@ export function Facepile({
size, size,
...rest ...rest
}) { }) {
const _size = sizeMap[size]; const _size = sizes.get(size);
const _overlap = overlapMap[size]; const _overlap = overlapMap[size];
return ( return (
@ -40,7 +33,7 @@ export function Facepile({
<Avatar <Avatar
key={member.userId} key={member.userId}
size={size} size={size}
src={avatarUrl && getAvatarUrl(client, avatarUrl, _size)} src={avatarUrl}
fallback={member.name.slice(0, 1).toUpperCase()} fallback={member.name.slice(0, 1).toUpperCase()}
className={styles.avatar} className={styles.avatar}
style={{ left: i * (_size - _overlap) }} 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 ( return (
<> <>
<div className={styles.roomAvatar}> <div className={styles.roomAvatar}>
<Avatar <Avatar
size="md" size="md"
src={avatarUrl}
bgKey={roomName} bgKey={roomName}
fallback={roomName.slice(0, 1).toUpperCase()} 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 ref = useRef();
const { buttonProps } = useButton(rest, ref); const { buttonProps } = useButton(rest, ref);
return ( return (
<button className={styles.backButton} ref={ref} {...buttonProps}> <button className={styles.backButton} ref={ref} {...buttonProps}>
<ArrowLeftIcon width={16} height={16} /> <ArrowLeftIcon width={16} height={16} />
<RoomHeaderInfo roomName={roomName} /> <RoomHeaderInfo roomName={roomName} avatarUrl={avatarUrl} />
</button> </button>
); );
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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