195 lines
6.7 KiB
TypeScript
195 lines
6.7 KiB
TypeScript
import React, { useEffect, useRef, useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { useAtomValue } from 'jotai';
|
|
import { Box, Button, Icon, Icons, Text, color, config, toRem } from 'folds';
|
|
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
|
import { factoryRoomIdByActivity } from '../../../utils/sort';
|
|
import { NavCategory, NavEmptyCenter, NavEmptyLayout } from '../../../components/nav';
|
|
import { getDirectCreatePath, getDirectRoomPath } from '../../pathUtils';
|
|
import { getCanonicalAliasOrRoomId } from '../../../utils/matrix';
|
|
import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom';
|
|
import { VirtualTile } from '../../../components/virtualizer';
|
|
import { DmStreamRow } from '../../../features/room-nav';
|
|
import { roomToUnreadAtom } from '../../../state/room/roomToUnread';
|
|
import { useNavToActivePathMapper } from '../../../hooks/useNavToActivePathMapper';
|
|
import { useDirectRooms } from './useDirectRooms';
|
|
import { PageNav, PageNavContent } from '../../../components/page';
|
|
import {
|
|
getRoomNotificationMode,
|
|
useRoomsNotificationPreferencesContext,
|
|
} from '../../../hooks/useRoomsNotificationPreferences';
|
|
import { DirectStreamHeader } from './DirectStreamHeader';
|
|
import { DirectNewChatRow } from './DirectNewChatRow';
|
|
import { DirectSelfRow } from './DirectSelfRow';
|
|
|
|
const MONO_FONT = '"JetBrains Mono Variable", ui-monospace, monospace';
|
|
|
|
function DirectEmpty() {
|
|
const { t } = useTranslation();
|
|
const navigate = useNavigate();
|
|
|
|
return (
|
|
<NavEmptyCenter>
|
|
<NavEmptyLayout
|
|
icon={<Icon size="600" src={Icons.Mention} />}
|
|
title={
|
|
<Text size="H5" align="Center">
|
|
{t('Direct.no_direct_messages')}
|
|
</Text>
|
|
}
|
|
content={
|
|
<Text size="T300" align="Center">
|
|
{t('Direct.no_direct_messages_desc')}
|
|
</Text>
|
|
}
|
|
options={
|
|
<Button variant="Primary" size="300" onClick={() => navigate(getDirectCreatePath())}>
|
|
<Text size="B300" truncate>
|
|
{t('Direct.start_first_chat')}
|
|
</Text>
|
|
</Button>
|
|
}
|
|
/>
|
|
</NavEmptyCenter>
|
|
);
|
|
}
|
|
|
|
function DirectFooterStatus() {
|
|
const { t } = useTranslation();
|
|
return (
|
|
<Box
|
|
alignItems="Center"
|
|
gap="200"
|
|
style={{
|
|
padding: `${toRem(8)} ${toRem(14)}`,
|
|
borderTop: `${config.borderWidth.B300} solid ${color.Surface.ContainerLine}`,
|
|
fontFamily: MONO_FONT,
|
|
fontSize: toRem(11),
|
|
color: color.Surface.OnContainer,
|
|
opacity: 0.45,
|
|
}}
|
|
>
|
|
<span
|
|
style={{
|
|
width: toRem(6),
|
|
height: toRem(6),
|
|
borderRadius: '50%',
|
|
background: color.Success.Main,
|
|
display: 'inline-block',
|
|
}}
|
|
aria-hidden
|
|
/>
|
|
<span>vojo.chat</span>
|
|
<Box grow="Yes" />
|
|
<span>{t('Direct.status_e2ee')}</span>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
export function Direct() {
|
|
const mx = useMatrixClient();
|
|
useNavToActivePathMapper('direct');
|
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
const directs = useDirectRooms();
|
|
const notificationPreferences = useRoomsNotificationPreferencesContext();
|
|
// roomToUnreadAtom only changes on read/unread transitions and ignores own
|
|
// events — covers incoming notifying messages but not own sends or muted
|
|
// incoming. Kept as a subscribe-only re-render trigger.
|
|
useAtomValue(roomToUnreadAtom);
|
|
|
|
// Activity tick: bump on every live timeline event in any DM room so list
|
|
// order, preview text and time labels refresh on own sends, muted incoming,
|
|
// reactions — anything roomToUnread misses. Uses the 'Room.timeline'
|
|
// literal (= RoomEvent.Timeline) to avoid named imports from matrix-js-sdk
|
|
// that the project's moduleResolution flags as TS2614 (see
|
|
// docs/known-tech-debt-lint/).
|
|
const [, setActivityTick] = useState(0);
|
|
useEffect(() => {
|
|
const directsSet = new Set(directs);
|
|
const handleTimeline = (
|
|
_ev: unknown,
|
|
room: { roomId: string } | undefined,
|
|
_start: unknown,
|
|
_removed: unknown,
|
|
data: { liveEvent?: boolean } | undefined
|
|
) => {
|
|
if (!room || !data?.liveEvent) return;
|
|
if (!directsSet.has(room.roomId)) return;
|
|
setActivityTick((n) => n + 1);
|
|
};
|
|
const emitter = mx as unknown as {
|
|
on: (e: string, h: typeof handleTimeline) => void;
|
|
removeListener: (e: string, h: typeof handleTimeline) => void;
|
|
};
|
|
emitter.on('Room.timeline', handleTimeline);
|
|
return () => {
|
|
emitter.removeListener('Room.timeline', handleTimeline);
|
|
};
|
|
}, [mx, directs]);
|
|
|
|
const selectedRoomId = useSelectedRoom();
|
|
const noRoomToDisplay = directs.length === 0;
|
|
|
|
// Sort each render — small list, getLastActiveTimestamp changes outside
|
|
// React's dep model so memoising would need a manual trigger anyway.
|
|
const sortedDirects = Array.from(directs).sort(factoryRoomIdByActivity(mx));
|
|
|
|
const virtualizer = useVirtualizer({
|
|
count: sortedDirects.length,
|
|
getScrollElement: () => scrollRef.current,
|
|
estimateSize: () => 68,
|
|
overscan: 10,
|
|
});
|
|
|
|
return (
|
|
<PageNav size="500">
|
|
<DirectStreamHeader />
|
|
{noRoomToDisplay ? (
|
|
<DirectEmpty />
|
|
) : (
|
|
<PageNavContent scrollRef={scrollRef}>
|
|
<Box direction="Column" gap="300">
|
|
<NavCategory>
|
|
<div
|
|
style={{
|
|
position: 'relative',
|
|
height: virtualizer.getTotalSize(),
|
|
}}
|
|
>
|
|
{virtualizer.getVirtualItems().map((vItem) => {
|
|
const roomId = sortedDirects[vItem.index];
|
|
const room = mx.getRoom(roomId);
|
|
if (!room) return null;
|
|
const selected = selectedRoomId === roomId;
|
|
|
|
return (
|
|
<VirtualTile
|
|
virtualItem={vItem}
|
|
key={vItem.index}
|
|
ref={virtualizer.measureElement}
|
|
>
|
|
<DmStreamRow
|
|
room={room}
|
|
selected={selected}
|
|
linkPath={getDirectRoomPath(getCanonicalAliasOrRoomId(mx, roomId))}
|
|
notificationMode={getRoomNotificationMode(
|
|
notificationPreferences,
|
|
room.roomId
|
|
)}
|
|
/>
|
|
</VirtualTile>
|
|
);
|
|
})}
|
|
</div>
|
|
</NavCategory>
|
|
</Box>
|
|
</PageNavContent>
|
|
)}
|
|
<DirectNewChatRow />
|
|
<DirectSelfRow />
|
|
<DirectFooterStatus />
|
|
</PageNav>
|
|
);
|
|
}
|