From 79fab78c71d8dc51dd1d6b4ab28347a406d3bec3 Mon Sep 17 00:00:00 2001 From: Gimle Larpes Date: Sun, 29 Jun 2025 14:21:22 +0200 Subject: [PATCH] changes to RoomNavItem, RoomNavUser and add useCallMembers --- src/app/features/room-nav/RoomNavItem.tsx | 293 ++++++++++++---------- src/app/features/room-nav/RoomNavUser.tsx | 172 ++++++++++--- src/app/hooks/useCallMembers.ts | 81 ++++++ 3 files changed, 388 insertions(+), 158 deletions(-) create mode 100644 src/app/hooks/useCallMembers.ts diff --git a/src/app/features/room-nav/RoomNavItem.tsx b/src/app/features/room-nav/RoomNavItem.tsx index ff6ffe05..565e49a9 100644 --- a/src/app/features/room-nav/RoomNavItem.tsx +++ b/src/app/features/room-nav/RoomNavItem.tsx @@ -22,7 +22,7 @@ import { import { useFocusWithin, useHover } from 'react-aria'; import FocusTrap from 'focus-trap-react'; import { useParams } from 'react-router-dom'; -import { NavItem, NavItemContent, NavItemOptions, NavLink } from '../../components/nav'; +import { NavButton, NavItem, NavItemContent, NavItemOptions, NavLink } from '../../components/nav'; import { UnreadBadge, UnreadBadgeCenter } from '../../components/unread-badge'; import { RoomAvatar, RoomIcon } from '../../components/room-avatar'; import { getDirectRoomAvatarUrl, getRoomAvatarUrl } from '../../utils/room'; @@ -53,8 +53,10 @@ import { } from '../../hooks/useRoomsNotificationPreferences'; import { RoomNotificationModeSwitcher } from '../../components/RoomNotificationSwitcher'; import { useCallState } from '../../pages/client/call/CallProvider'; +import { useCallMembers } from '../../hooks/useCallMembers'; import { useRoomNavigate } from '../../hooks/useRoomNavigate'; import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize'; +import { RoomNavUser } from './RoomNavUser'; type RoomNavItemMenuProps = { room: Room; @@ -237,6 +239,7 @@ export function RoomNavItem({ (receipt) => receipt.userId !== mx.getUserId() ); const isActiveCall = isCallActive && activeCallRoomId === room.roomId; + const callMembers = useCallMembers(mx, room.roomId); const { navigateRoom } = useRoomNavigate(); const { roomIdOrAlias: viewedRoomId } = useParams(); const screenSize = useScreenSizeContext(); @@ -293,142 +296,176 @@ export function RoomNavItem({ }; const optionsVisible = hover || !!menuAnchor; + const ariaLabel = [ + room.name, + room.isCallRoom() + ? [ + 'Call Room', + isActiveCall && 'Currently in Call', + callMembers.length && `${callMembers.length} in Call`, + ] + : 'Text Room', + unread?.total && `${unread.total} Messages`, + ] + .flat() + .filter(Boolean) + .join(', '); return ( - - - - - {showAvatar ? ( - ( - - {nameInitials(room.name)} - + + + + + + + {showAvatar ? ( + ( + + {nameInitials(room.name)} + + )} + /> + ) : ( + )} - /> - ) : ( - - )} - - - - {room.name} - - - {!optionsVisible && !unread && !selected && typingMember.length > 0 && ( - - - - )} - {!optionsVisible && unread && ( - - 0} count={unread.total} /> - - )} - {!optionsVisible && notificationMode !== RoomNotificationMode.Unset && ( - - )} - - - {optionsVisible && ( - - setMenuAnchor(undefined), - clickOutsideDeactivates: true, - isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown', - isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp', - escapeDeactivates: stopPropagation, - }} - > - setMenuAnchor(undefined)} - notificationMode={notificationMode} + + + + {room.name} + + + {!optionsVisible && !unread && !selected && typingMember.length > 0 && ( + + + + )} + {!optionsVisible && unread && ( + + 0} count={unread.total} /> + + )} + {!optionsVisible && notificationMode !== RoomNotificationMode.Unset && ( + - - } - > - {room.isCallRoom() && ( - + + {optionsVisible && ( + + - Open Chat - + align={menuAnchor?.width === 0 ? 'Start' : 'End'} + content={ + setMenuAnchor(undefined), + clickOutsideDeactivates: true, + isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown', + isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp', + escapeDeactivates: stopPropagation, + }} + > + setMenuAnchor(undefined)} + notificationMode={notificationMode} + /> + } > - {(triggerRef) => ( - + Open Chat + + } > - - - - + {(triggerRef) => ( + + + + + + )} + )} - - )} - - - - - + + + + + + )} + + + {room.isCallRoom() && ( + + {callMembers.map((userId) => ( + + ))} + )} - + ); } diff --git a/src/app/features/room-nav/RoomNavUser.tsx b/src/app/features/room-nav/RoomNavUser.tsx index f72df92e..bd65ef78 100644 --- a/src/app/features/room-nav/RoomNavUser.tsx +++ b/src/app/features/room-nav/RoomNavUser.tsx @@ -1,51 +1,163 @@ -import { Avatar, Box, Icon, Icons, Text } from 'folds'; -import React from 'react'; +import { + Avatar, + Box, + config, + Icon, + IconButton, + Icons, + Text, + Tooltip, + TooltipProvider, +} from 'folds'; +import React, { useState } from 'react'; import { Room } from 'matrix-js-sdk'; -import { NavItem, NavItemContent } from '../../components/nav'; +import { useFocusWithin, useHover } from 'react-aria'; +import { NavItem, NavItemContent, NavItemOptions } from '../../components/nav'; import { UserAvatar } from '../../components/user-avatar'; import { useMatrixClient } from '../../hooks/useMatrixClient'; -import { useRoomMembers } from '../../hooks/useRoomMembers'; +import { useCallState } from '../../pages/client/call/CallProvider'; import { getMxIdLocalPart } from '../../utils/matrix'; -import { getMemberDisplayName } from '../../utils/room'; +import { getMemberAvatarMxc, getMemberDisplayName } from '../../utils/room'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; +import { openProfileViewer } from '../../../client/action/navigation'; type RoomNavUserProps = { room: Room; - space: Room; - sender: string; + userId: string; }; -export function RoomNavUser({ room, space, sender }: RoomNavUserProps) { +export function RoomNavUser({ room, userId }: RoomNavUserProps) { const mx = useMatrixClient(); - const members = useRoomMembers(mx, space.roomId); const useAuthentication = useMediaAuthentication(); - - const member = members.find((roomMember) => roomMember.userId === sender); - const avatarMxcUrl = member?.getMxcAvatarUrl(); + const [navUserExpanded, setNavUserExpanded] = useState(false); + const [hover, setHover] = useState(false); + const { hoverProps } = useHover({ onHoverChange: setHover }); + const { focusWithinProps } = useFocusWithin({ + onFocusWithinChange: (isFocused) => { + setHover(isFocused); + if (!isFocused) setNavUserExpanded(false); + }, + }); + const { isCallActive, activeCallRoomId } = useCallState(); + const isActiveCall = isCallActive && activeCallRoomId === room.roomId; + const avatarMxcUrl = getMemberAvatarMxc(room, userId); const avatarUrl = avatarMxcUrl ? mx.mxcUrlToHttp(avatarMxcUrl, 32, 32, 'crop', undefined, false, useAuthentication) : undefined; - const getName = - getMemberDisplayName(room, member?.userId ?? '') ?? - getMxIdLocalPart(member?.userId ?? '') ?? - member?.userId; + const getName = getMemberDisplayName(room, userId) ?? getMxIdLocalPart(userId) ?? userId; + const isCallParticipant = isActiveCall && userId !== mx.getUserId(); + + const handleNavUserClick = () => { + if (isCallParticipant) { + setNavUserExpanded((prev) => !prev); + } + }; + + const handleClickUser = () => { + openProfileViewer(userId, room.roomId); + }; + + // PLACEHOLDER + const [userMuted, setUserMuted] = useState(false); + const handleToggleMute = () => { + setUserMuted(!userMuted); + }; + + const optionsVisible = (hover || userMuted || navUserExpanded) && isCallParticipant && false; // Disable until individual volume control and mute have been added + const ariaLabel = isCallParticipant + ? `Call Participant: ${getName}${userMuted ? ', Muted' : ''}` + : getName; return ( - - - - - } - /> - - - {getName} - + + + + + + } + /> + + + {getName} + + + {navUserExpanded && ( + + {/* Slider here, when implemented into folds */} + ---- THIS IS A SLIDER --- + + )} + {optionsVisible && ( + + + {userMuted ? 'Unmute' : 'Mute'} + + } + > + {(triggerRef) => ( + + + + )} + + {navUserExpanded && ( + + View Profile + + } + > + {(triggerRef) => ( + + + + )} + + )} + + )} ); } diff --git a/src/app/hooks/useCallMembers.ts b/src/app/hooks/useCallMembers.ts new file mode 100644 index 00000000..6e2e184c --- /dev/null +++ b/src/app/hooks/useCallMembers.ts @@ -0,0 +1,81 @@ +/*import { MatrixClient } from 'matrix-js-sdk'; +import { + MatrixRTCSession, + MatrixRTCSessionEvent, +} from 'matrix-js-sdk/lib/matrixrtc/MatrixRTCSession'; +import { CallMembership } from 'matrix-js-sdk/lib/matrixrtc/CallMembership'; +import { useEffect, useState } from 'react'; + +export const useCallMembers = ( + mx: MatrixClient, + mxr: MatrixRTCSession, + roomId: string +): CallMembership[] => { + const [memberships, setMemberships] = useState([]); + + useEffect(() => { + const room = mx.getRoom(roomId); + + const updateMemberships = () => { + if (!room?.isCallRoom()) return; + setMemberships(MatrixRTCSession.callMembershipsForRoom(room)); + //setMemberships(mxr.memberships); + //console.log('MEMBERSHIPS:'); + //console.log(memberships); + }; + + updateMemberships(); + + mxr.on(MatrixRTCSessionEvent.MembershipsChanged, updateMemberships); + return () => { + mxr.removeListener(MatrixRTCSessionEvent.MembershipsChanged, updateMemberships); + }; + }, [mx, mxr, roomId]); + + return memberships; +};*/ + +// TEMPORARY +import { MatrixClient, MatrixEvent, RoomStateEvent } from 'matrix-js-sdk'; +import { useEffect, useMemo, useState } from 'react'; +import { getStateEvents } from '../utils/room'; +import { StateEvent } from '../../types/matrix/room'; + +export const useCallMembers = (mx: MatrixClient, roomId: string): string[] => { + const [events, setEvents] = useState([]); + + useEffect(() => { + const room = mx.getRoom(roomId); + + const updateEvents = (event?: MatrixEvent) => { + if (!room?.isCallRoom() || (event && event.getRoomId() !== roomId)) return; + setEvents(getStateEvents(room, StateEvent.GroupCallMemberPrefix)); + }; + + updateEvents(); + + mx.on(RoomStateEvent.Events, updateEvents); + return () => { + mx.removeListener(RoomStateEvent.Events, updateEvents); + }; + }, [mx, roomId]); + + const participants = useMemo( + () => + events + .filter((ev) => { + const content = ev.getContent(); + return ( + content && + ev.getSender() && + content.expires && + ev.getTs() + content.expires > Date.now() + ); + }) + /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ + .map((ev) => ev.getSender()!), + [events] + ); + + return participants; +};