From 45535a0dba8a1e48d62206eaec00f707c56a64de Mon Sep 17 00:00:00 2001 From: heaven Date: Sat, 30 May 2026 01:21:27 +0300 Subject: [PATCH] feat(direct): show a green online dot on 1:1 chat avatars in the direct list, matching the profile card indicator --- src/app/features/room-nav/DmStreamRow.tsx | 88 +++++++++++------------ src/app/features/room-nav/styles.css.ts | 27 +++++++ 2 files changed, 70 insertions(+), 45 deletions(-) diff --git a/src/app/features/room-nav/DmStreamRow.tsx b/src/app/features/room-nav/DmStreamRow.tsx index 6ec7cbf1..da0b1208 100644 --- a/src/app/features/room-nav/DmStreamRow.tsx +++ b/src/app/features/room-nav/DmStreamRow.tsx @@ -38,7 +38,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, guessDmRoomUserId, isRoomAlias } from '../../utils/matrix'; import { getViaServers } from '../../plugins/via-servers'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; import { useSetting } from '../../state/hooks/settings'; @@ -60,6 +60,7 @@ import { callChatAtom } from '../../state/callEmbed'; import { useCallPreferencesAtom } from '../../state/hooks/callPreferences'; import { useAutoDiscoveryInfo } from '../../hooks/useAutoDiscoveryInfo'; import { livekitSupport } from '../../hooks/useLivekitSupport'; +import { Presence, useUserPresence } from '../../hooks/useUserPresence'; import { timeHourMinute } from '../../utils/time'; import * as css from './styles.css'; @@ -295,6 +296,18 @@ export function DmStreamRow({ room, selected, notificationMode, linkPath }: DmSt const roomName = useRoomName(room); + // Peer online dot — mirrors the profile-card HeroOnlineDot. Only 1:1 rooms + // resolve a single peer; group rooms have no single presence to surface. + // `guessDmRoomUserId`'s last fallback is my own id, so drop that case — the + // empty string makes `useUserPresence` a no-op rather than reading my own + // presence as if I were the peer. + const myUserId = mx.getSafeUserId(); + const oneOnOne = isOneOnOneRoom(room); + const peerCandidate = oneOnOne ? guessDmRoomUserId(room, myUserId) : undefined; + const peerUserId = peerCandidate && peerCandidate !== myUserId ? peerCandidate : undefined; + const peerPresence = useUserPresence(peerUserId ?? ''); + const peerOnline = !!peerUserId && peerPresence?.presence === Presence.Online; + const preview = deriveRoomPreview(room); const previewText = preview.text; const previewTs = preview.ts; @@ -349,47 +362,34 @@ export function DmStreamRow({ room, selected, notificationMode, linkPath }: DmSt > - - - ( - - {nameInitials(roomName)} - - )} - /> - - + + + + ( + + {nameInitials(roomName)} + + )} + /> + + {peerOnline && } + + - {timeLabel && !optionsVisible && ( - {timeLabel} - )} + {timeLabel && !optionsVisible && {timeLabel}} diff --git a/src/app/features/room-nav/styles.css.ts b/src/app/features/room-nav/styles.css.ts index 2091719b..9c8183b6 100644 --- a/src/app/features/room-nav/styles.css.ts +++ b/src/app/features/room-nav/styles.css.ts @@ -25,6 +25,33 @@ export const DmRowInner = style({ padding: `${toRem(10)} 0`, }); +// Positioning context for the peer online dot — inline-flex so the wrapper +// hugs the 48px avatar and the absolutely-positioned dot anchors to its +// bottom-right corner. +export const DmRowAvatar = style({ + position: 'relative', + display: 'inline-flex', + flexShrink: 0, +}); + +// Peer online indicator — green dot inset at the avatar's bottom-right. Mirrors +// the profile-card `HeroOnlineDot` (user-profile/styles.css.ts): same +// `Success.Main` fill and a ring in `Background.Container` — which is exactly +// what the `NavItem variant="Background"` row paints at rest, so the dot punches +// out against the avatar circle. Sized for the smaller 48px list avatar (the +// profile card's 16px would read oversized here): the design bundle's +// `size * 0.28` ≈ 14px, scaled down 1.2× to ~11.7px so it sits quieter. +export const DmRowOnlineDot = style({ + position: 'absolute', + right: 0, + bottom: 0, + width: toRem(14 / 1.2), + height: toRem(14 / 1.2), + borderRadius: '50%', + backgroundColor: color.Success.Main, + boxShadow: `0 0 0 ${toRem(2.5)} ${color.Background.Container}`, +}); + // Locks the title block to its natural 2-line height so the name does // not jump vertically when the second row's content (preview / unread // badge / notification icon) collapses to 0px on hover. Bridged rooms