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 && (
+
+
-
+
+
+
+
+
+ )}
+
+
+ {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;
+};