Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 82 additions & 48 deletions src/app/features/room-nav/RoomNavItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import { useRoomTypingMember } from '$hooks/useRoomTypingMembers';
import { TypingIndicator } from '$components/typing-indicator';
import { stopPropagation } from '$utils/keyboard';
import { getMatrixToRoom } from '$plugins/matrix-to';
import { getCanonicalAliasOrRoomId, isRoomAlias } from '$utils/matrix';
import { getCanonicalAliasOrRoomId, isRoomAlias, mxcUrlToHttp } from '$utils/matrix';
import { getViaServers } from '$plugins/via-servers';
import { useMediaAuthentication } from '$hooks/useMediaAuthentication';
import { useSetting } from '$state/hooks/settings';
Expand Down Expand Up @@ -72,6 +72,11 @@ import { useAutoDiscoveryInfo } from '$hooks/useAutoDiscoveryInfo';
import { livekitSupport } from '$hooks/useLivekitSupport';
import { Presence, useUserPresence } from '$hooks/useUserPresence';
import { AvatarPresence, PresenceBadge } from '$components/presence';
import { useRoomLastMessagePreview } from '$hooks/useRoomLastMessagePreview';
import { useRoomLastMessage } from '$hooks/useRoomLastMessage';
import { useGroupDMMembers } from '$hooks/useGroupDMMembers';
import { UserAvatar } from '$components/user-avatar';
import * as css from './styles.css';
import { RoomNavUser } from './RoomNavUser';
import { SidebarUnreadBadge } from '$components/sidebar';

Expand Down Expand Up @@ -293,6 +298,9 @@ export function RoomNavItem({
);

const nicknames = useAtomValue(nicknamesAtom);
const isGroupDM = direct === true && room.getJoinedMemberCount() > 2;
// Keep hook call unconditional; pass undefined when not a group DM so the hook no-ops.
const groupMembers = useGroupDMMembers(mx, isGroupDM ? room : undefined, 3);
const dmUserId = direct ? room.getAvatarFallbackMember()?.userId : undefined;
const matrixRoomName = useRoomName(room);
const roomName = (dmUserId && nicknames[dmUserId]) || matrixRoomName;
Expand Down Expand Up @@ -420,55 +428,81 @@ export function RoomNavItem({
>
<NavItemContent style={hideTextStyling(hideText)}>
<Box as="span" grow="Yes" alignItems="Center" style={hideTextStyling(hideText)}>
<AvatarPresence
badge={
presence &&
presence.presence !== Presence.Offline && (
<PresenceBadge
presence={presence.presence}
size={hideText ? '300' : '200'}
/>
)
}
style={hideTextStyling(hideText)}
>
<Avatar
size={hideText ? undefined : '200'}
radii="400"
{isGroupDM && showAvatar && groupMembers.length > 1 ? (
// Group DM: triangle layout of mini avatars
<div className={css.GroupAvatarRow}>
{groupMembers.map((member) => {
const avatarSrc = member.avatarUrl
? (mxcUrlToHttp(mx, member.avatarUrl, useAuthentication, 32, 32, 'crop') ??
undefined)
: undefined;
return (
<Avatar key={member.userId} className={css.GroupAvatarMini}>
<UserAvatar
userId={member.userId}
src={avatarSrc}
alt={member.displayName ?? member.userId}
renderFallback={() => (
<Text as="span" size="T200">
{nameInitials(member.displayName ?? member.userId)}
</Text>
)}
/>
</Avatar>
);
})}
</div>
) : (
<AvatarPresence
badge={
presence &&
presence.presence !== Presence.Offline && (
<PresenceBadge
presence={presence.presence}
size={hideText ? '300' : '200'}
/>
)
}
style={hideTextStyling(hideText)}
>
{showAvatar ? (
<RoomAvatar
roomId={room.roomId}
src={
((!direct || customDMCards) &&
getRoomAvatarUrl(mx, room, 96, useAuthentication)) ||
getDirectRoomAvatarUrl(mx, room, 96, useAuthentication)
}
uniformIcons
alt={roomName}
renderFallback={() => (
<Text as="span" size="H6">
{nameInitials(roomName)}
</Text>
)}
/>
) : (
<RoomIcon
style={{
opacity:
unread || hasRoomUnread || isActiveCall
? config.opacity.P500
: config.opacity.P300,
}}
filled={selected || isActiveCall}
size="100"
joinRule={room.getJoinRule()}
roomType={room.getType()}
/>
)}
</Avatar>
</AvatarPresence>
<Avatar
size={hideText ? undefined : '200'}
radii="400"
style={hideTextStyling(hideText)}
>
{showAvatar ? (
<RoomAvatar
roomId={room.roomId}
src={
((!direct || customDMCards) &&
getRoomAvatarUrl(mx, room, 96, useAuthentication)) ||
getDirectRoomAvatarUrl(mx, room, 96, useAuthentication)
}
uniformIcons
alt={roomName}
renderFallback={() => (
<Text as="span" size="H6">
{nameInitials(roomName)}
</Text>
)}
/>
) : (
<RoomIcon
style={{
opacity:
unread || hasRoomUnread || isActiveCall
? config.opacity.P500
: config.opacity.P300,
}}
filled={selected || isActiveCall}
size="100"
joinRule={room.getJoinRule()}
roomType={room.getType()}
/>
)}
</Avatar>
</AvatarPresence>
)}
{unread && hideText && (
<SidebarUnreadBadge
highlight={unread.highlight > 0}
Expand Down
41 changes: 40 additions & 1 deletion src/app/features/room-nav/styles.css.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,48 @@
import { style } from '@vanilla-extract/css';
import { config } from 'folds';
import { color, config } from 'folds';

export const CategoryButton = style({
flexGrow: 1,
});
export const CategoryButtonIcon = style({
opacity: config.opacity.P400,
});

/**
* Group DM multi-avatar layout for the nav item's Avatar size="200" (24 px) slot.
* Three mini avatars are stacked in a triangle: top-centre, bottom-left, bottom-right.
*/
export const GroupAvatarRow = style({
position: 'relative',
// Match the Avatar size="200" footprint so layout is not disrupted.
width: '24px',
height: '24px',
flexShrink: 0,
});

export const GroupAvatarMini = style({
position: 'absolute',
width: '14px',
height: '14px',
border: `1.5px solid ${color.Surface.Container}`,
borderRadius: '50%',
overflow: 'hidden',
selectors: {
'&:nth-child(1)': {
top: '0',
left: '50%',
transform: 'translateX(-50%)',
zIndex: 3,
},
'&:nth-child(2)': {
bottom: '0',
left: '0',
zIndex: 2,
},
'&:nth-child(3)': {
bottom: '0',
right: '0',
zIndex: 1,
},
},
});
Loading