feat(direct): show a green online dot on 1:1 chat avatars in the direct list, matching the profile card indicator
This commit is contained in:
parent
54f96112ff
commit
45535a0dba
2 changed files with 70 additions and 45 deletions
|
|
@ -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
|
|||
>
|
||||
<NavLink to={linkPath} onClick={room.isCallRoom() ? handleStartCall : undefined}>
|
||||
<NavItemContent>
|
||||
<Box
|
||||
as="span"
|
||||
grow="Yes"
|
||||
alignItems="Center"
|
||||
gap="300"
|
||||
className={css.DmRowInner}
|
||||
>
|
||||
<Avatar
|
||||
size="500"
|
||||
radii="500"
|
||||
style={{ width: toRem(48), height: toRem(48) }}
|
||||
>
|
||||
<RoomAvatar
|
||||
roomId={room.roomId}
|
||||
src={
|
||||
isOneOnOneRoom(room)
|
||||
? // 1:1 — fall back to the peer's avatar when the room
|
||||
// itself has no custom one. Mirrors RoomViewHeader's
|
||||
// `useRoomAvatar(room, isOneOnOne)` semantics.
|
||||
getDirectRoomAvatarUrl(mx, room, 96, useAuthentication)
|
||||
: // Group / non-1:1 — use the room avatar only. Without
|
||||
// this guard `getDirectRoomAvatarUrl` would surface the
|
||||
// first non-self member's avatar in groups without a
|
||||
// custom room avatar, making them look like 1:1 chats.
|
||||
getRoomAvatarUrl(mx, room, 96, useAuthentication)
|
||||
}
|
||||
alt={roomName}
|
||||
renderFallback={() => (
|
||||
<Text as="span" size="H5">
|
||||
{nameInitials(roomName)}
|
||||
</Text>
|
||||
)}
|
||||
/>
|
||||
</Avatar>
|
||||
<Box
|
||||
as="span"
|
||||
direction="Column"
|
||||
grow="Yes"
|
||||
gap="100"
|
||||
className={css.DmRowText}
|
||||
>
|
||||
<Box as="span" grow="Yes" alignItems="Center" gap="300" className={css.DmRowInner}>
|
||||
<span className={css.DmRowAvatar}>
|
||||
<Avatar size="500" radii="500" style={{ width: toRem(48), height: toRem(48) }}>
|
||||
<RoomAvatar
|
||||
roomId={room.roomId}
|
||||
src={
|
||||
oneOnOne
|
||||
? // 1:1 — fall back to the peer's avatar when the room
|
||||
// itself has no custom one. Mirrors RoomViewHeader's
|
||||
// `useRoomAvatar(room, isOneOnOne)` semantics.
|
||||
getDirectRoomAvatarUrl(mx, room, 96, useAuthentication)
|
||||
: // Group / non-1:1 — use the room avatar only. Without
|
||||
// this guard `getDirectRoomAvatarUrl` would surface the
|
||||
// first non-self member's avatar in groups without a
|
||||
// custom room avatar, making them look like 1:1 chats.
|
||||
getRoomAvatarUrl(mx, room, 96, useAuthentication)
|
||||
}
|
||||
alt={roomName}
|
||||
renderFallback={() => (
|
||||
<Text as="span" size="H5">
|
||||
{nameInitials(roomName)}
|
||||
</Text>
|
||||
)}
|
||||
/>
|
||||
</Avatar>
|
||||
{peerOnline && <span className={css.DmRowOnlineDot} aria-hidden />}
|
||||
</span>
|
||||
<Box as="span" direction="Column" grow="Yes" gap="100" className={css.DmRowText}>
|
||||
<Box as="span" alignItems="Baseline" gap="200">
|
||||
<Box as="span" grow="Yes" className={css.DmRowTextStretch}>
|
||||
<Text
|
||||
|
|
@ -402,9 +402,7 @@ export function DmStreamRow({ room, selected, notificationMode, linkPath }: DmSt
|
|||
{roomName}
|
||||
</Text>
|
||||
</Box>
|
||||
{timeLabel && !optionsVisible && (
|
||||
<span className={css.DmRowTime}>{timeLabel}</span>
|
||||
)}
|
||||
{timeLabel && !optionsVisible && <span className={css.DmRowTime}>{timeLabel}</span>}
|
||||
</Box>
|
||||
<Box as="span" alignItems="Center" gap="200">
|
||||
<Box as="span" grow="Yes" className={css.DmRowTextStretch}>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue