Merge pull request #339 from robintown/room-avatars
Display room avatars
This commit is contained in:
commit
7a2d64c0ef
12 changed files with 112 additions and 63 deletions
|
@ -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>
|
||||
) : (
|
||||
|
|
|
@ -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) }}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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,
|
||||
}));
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
24
src/room/useRoomAvatar.ts
Normal 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;
|
||||
};
|
Loading…
Reference in a new issue