diff --git a/public/locales/en-GB/app.json b/public/locales/en-GB/app.json index 279ffd3..b861fd2 100644 --- a/public/locales/en-GB/app.json +++ b/public/locales/en-GB/app.json @@ -1,8 +1,8 @@ { "{{count}} stars|one": "{{count}} star", "{{count}} stars|other": "{{count}} stars", + "{{displayName}} is presenting": "{{displayName}} is presenting", "{{displayName}}, your call has ended.": "{{displayName}}, your call has ended.", - "{{name}} is presenting": "{{name}} is presenting", "{{names}}, {{name}}": "{{names}}, {{name}}", "<0><1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0><1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.", "<0>Already have an account?<1><0>Log in Or <2>Access as a guest": "<0>Already have an account?<1><0>Log in Or <2>Access as a guest", diff --git a/src/room/InCallView.module.css b/src/room/InCallView.module.css index 5638b10..0ed59e6 100644 --- a/src/room/InCallView.module.css +++ b/src/room/InCallView.module.css @@ -82,23 +82,6 @@ limitations under the License. bottom: 0; } -/* CSS makes us put a condition here, even though all we want to do is -unconditionally select the container so we can use cqmin units */ -@container videoTile (width > 0) { - .avatar { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - /* To make avatars scale smoothly with their tiles during animations, we - override the styles set on the element */ - --avatarSize: 50cqmin; /* Half of the smallest dimension of the tile */ - width: var(--avatarSize) !important; - height: var(--avatarSize) !important; - border-radius: 10000px !important; - } -} - @media (min-height: 300px) { .inRoom { --footerPadding: 24px; diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 611f130..d1ca2d3 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -23,12 +23,7 @@ import { } from "@livekit/components-react"; import { usePreventScroll } from "@react-aria/overlays"; import classNames from "classnames"; -import { - LocalParticipant, - RemoteParticipant, - Room, - Track, -} from "livekit-client"; +import { Room, Track } from "livekit-client"; import { MatrixClient } from "matrix-js-sdk/src/client"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall"; @@ -59,7 +54,6 @@ import { useVideoGridLayout, TileDescriptor, } from "../video-grid/VideoGrid"; -import { Avatar } from "../Avatar"; import { useShowInspector } from "../settings/useSetting"; import { useModalTriggerState } from "../Modal"; import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; @@ -275,26 +269,9 @@ export function InCallView({ [noControls, items] ); - const renderAvatar = useCallback( - (roomMember: RoomMember, width: number, height: number) => { - const avatarUrl = roomMember.getMxcAvatarUrl(); - const size = Math.round(Math.min(width, height) / 2); - - return ( - - ); - }, - [] - ); - const Grid = items.length > 12 && layout === "freedom" ? NewVideoGrid : VideoGrid; + const prefersReducedMotion = usePrefersReducedMotion(); const renderContent = (): JSX.Element => { @@ -312,8 +289,6 @@ export function InCallView({ targetWidth={bounds.width} key={maximisedParticipant.id} data={maximisedParticipant.data} - getAvatar={renderAvatar} - showSpeakingIndicator={false} /> ); } @@ -325,12 +300,7 @@ export function InCallView({ disableAnimations={prefersReducedMotion || isSafari} > {(props) => ( - 2} - {...props} - ref={props.ref as Ref} - /> + } /> )} ); @@ -484,33 +454,29 @@ function useParticipantTiles( }); const items = useMemo(() => { - const tiles: TileDescriptor[] = []; - const screenshareExists = sfuParticipants.some( - (p) => p.isScreenShareEnabled + // The IDs of the participants who published membership event to the room (i.e. are present from Matrix perspective). + const matrixParticipants: Map = new Map( + [...participants.entries()].flatMap(([user, devicesMap]) => { + return [...devicesMap.keys()].map((deviceId) => [ + `${user.userId}:${deviceId}`, + user, + ]); + }) ); - const participantsById = new Map< - string, - LocalParticipant | RemoteParticipant - >(); - for (const p of sfuParticipants) participantsById.set(p.identity, p); - for (const [member, participantMap] of participants) { - for (const [deviceId] of participantMap) { - const id = `${member.userId}:${deviceId}`; - const sfuParticipant = participantsById.get(id); + const someoneIsPresenting = sfuParticipants.some((p) => { + !p.isLocal && p.isScreenShareEnabled; + }); - // Skip rendering participants that did not connect to the SFU. - if (!sfuParticipant) { - continue; - } + // Iterate over SFU participants (those who actually are present from the SFU perspective) and create tiles for them. + const tiles: TileDescriptor[] = sfuParticipants.flatMap( + (sfuParticipant) => { + const id = sfuParticipant.identity; + const member = matrixParticipants.get(id); const userMediaTile = { id, - // Screenshare feeds take precedence for focus - focused: - !screenshareExists && - sfuParticipant.isSpeaking && - !sfuParticipant.isLocal, + focused: !someoneIsPresenting && sfuParticipant.isSpeaking, local: sfuParticipant.isLocal, data: { member, @@ -519,12 +485,10 @@ function useParticipantTiles( }, }; - // Add a tile for user media. - tiles.push(userMediaTile); - // If there is a screen sharing enabled for this participant, create a tile for it as well. + let screenShareTile: TileDescriptor | undefined; if (sfuParticipant.isScreenShareEnabled) { - const screenShareTile = { + screenShareTile = { ...userMediaTile, id: `${id}:screen-share`, focused: true, @@ -533,10 +497,13 @@ function useParticipantTiles( content: TileContent.ScreenShare, }, }; - tiles.push(screenShareTile); } + + return screenShareTile + ? [userMediaTile, screenShareTile] + : [userMediaTile]; } - } + ); PosthogAnalytics.instance.eventCallEnded.cacheParticipantCountChanged( tiles.length diff --git a/src/video-grid/VideoTile.module.css b/src/video-grid/VideoTile.module.css index 62d4545..20849a9 100644 --- a/src/video-grid/VideoTile.module.css +++ b/src/video-grid/VideoTile.module.css @@ -193,3 +193,20 @@ limitations under the License. --tileRadius: 20px; } } + +/* CSS makes us put a condition here, even though all we want to do is +unconditionally select the container so we can use cqmin units */ +@container videoTile (width > 0) { + .avatar { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + /* To make avatars scale smoothly with their tiles during animations, we + override the styles set on the element */ + --avatarSize: 50cqmin; /* Half of the smallest dimension of the tile */ + width: var(--avatarSize) !important; + height: var(--avatarSize) !important; + border-radius: 10000px !important; + } +} diff --git a/src/video-grid/VideoTile.tsx b/src/video-grid/VideoTile.tsx index d4185a5..703d3da 100644 --- a/src/video-grid/VideoTile.tsx +++ b/src/video-grid/VideoTile.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ComponentProps, forwardRef } from "react"; +import React from "react"; import { animated } from "@react-spring/web"; import classNames from "classnames"; import { useTranslation } from "react-i18next"; @@ -24,15 +24,18 @@ import { VideoTrack, useMediaTrack, } from "@livekit/components-react"; -import { RoomMember } from "matrix-js-sdk/src/models/room-member"; +import { + RoomMember, + RoomMemberEvent, +} from "matrix-js-sdk/src/models/room-member"; +import { Avatar } from "../Avatar"; import styles from "./VideoTile.module.css"; import { ReactComponent as MicIcon } from "../icons/Mic.svg"; import { ReactComponent as MicMutedIcon } from "../icons/MicMuted.svg"; -import { useRoomMemberName } from "./useRoomMemberName"; export interface ItemData { - member: RoomMember; + member?: RoomMember; sfuParticipant: LocalParticipant | RemoteParticipant; content: TileContent; } @@ -44,35 +47,36 @@ export enum TileContent { interface Props { data: ItemData; - className?: string; - showSpeakingIndicator: boolean; - style?: ComponentProps["style"]; + + // TODO: Refactor these props. targetWidth: number; targetHeight: number; - getAvatar: ( - roomMember: RoomMember, - width: number, - height: number - ) => JSX.Element; + className?: string; + style?: React.ComponentProps["style"]; } -export const VideoTile = forwardRef( - ( - { - data, - className, - showSpeakingIndicator, - style, - targetWidth, - targetHeight, - getAvatar, - }, - tileRef - ) => { +export const VideoTile = React.forwardRef( + ({ data, className, style, targetWidth, targetHeight }, tileRef) => { const { t } = useTranslation(); - const { content, sfuParticipant } = data; - const { rawDisplayName: name } = useRoomMemberName(data.member); + const { content, sfuParticipant, member } = data; + + // Handle display name changes. + const [displayName, setDisplayName] = React.useState("[👻]"); + React.useEffect(() => { + if (member) { + setDisplayName(member.rawDisplayName); + + const updateName = () => { + setDisplayName(member.rawDisplayName); + }; + + member!.on(RoomMemberEvent.Name, updateName); + return () => { + member!.removeListener(RoomMemberEvent.Name, updateName); + }; + } + }, [member]); const audioEl = React.useRef(null); const { isMuted: microphoneMuted } = useMediaTrack( @@ -92,7 +96,7 @@ export const VideoTile = forwardRef( ( {content === TileContent.UserMedia && !sfuParticipant.isCameraEnabled && ( <>
- {getAvatar(data.member, targetWidth, targetHeight)} + )} - {!false && - (content === TileContent.ScreenShare ? ( -
- {t("{{name}} is presenting", { name })} -
- ) : ( -
- {microphoneMuted ? : } - {name} - -
- ))} + {content == TileContent.ScreenShare ? ( +
+ {t("{{displayName}} is presenting", { displayName })} +
+ ) : ( +
+ {microphoneMuted ? : } + {displayName} + +
+ )} ({ - name: member.name, - rawDisplayName: member.rawDisplayName, - }); - - useEffect(() => { - function updateName() { - setState({ name: member.name, rawDisplayName: member.rawDisplayName }); - } - - updateName(); - - member.on(RoomMemberEvent.Name, updateName); - - return () => { - member.removeListener(RoomMemberEvent.Name, updateName); - }; - }, [member]); - - return state; -}