feat: P1 features — quick switcher, media gallery, DM previews, knock-to-join, syntax highlighting
P1-1: Quick room switcher (Ctrl+K/Cmd+K) — QuickSwitcher.tsx + ClientNonUIFeatures hotkey
P1-2: Media gallery drawer (images/videos/files) — MediaGallery.tsx + RoomViewHeader toggle
P1-4: DM last message preview + relative timestamp in RoomNavItem when direct=true
P1-7: Code syntax highlighting — TDS tokenizer (syntaxHighlight.ts), custom CSS theme
(.prism-tds-dark/.prism-tds-light), applied in react-custom-html-parser.tsx
P1-11: Knock-to-join — "Request to Join" in RoomIntro + Pending Requests in MembersDrawer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,9 @@ import {
|
||||
import { useFocusWithin, useHover } from 'react-aria';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import dayjs from 'dayjs';
|
||||
import isToday from 'dayjs/plugin/isToday';
|
||||
import isYesterday from 'dayjs/plugin/isYesterday';
|
||||
import { NavItem, NavItemContent, NavItemOptions, NavLink } from '../../components/nav';
|
||||
import { UnreadBadge, UnreadBadgeCenter } from '../../components/unread-badge';
|
||||
import { RoomAvatar, RoomIcon } from '../../components/room-avatar';
|
||||
@@ -67,8 +70,31 @@ import { callChatAtom } from '../../state/callEmbed';
|
||||
import { useCallPreferencesAtom } from '../../state/hooks/callPreferences';
|
||||
import { useAutoDiscoveryInfo } from '../../hooks/useAutoDiscoveryInfo';
|
||||
import { livekitSupport } from '../../hooks/useLivekitSupport';
|
||||
import { StateEvent } from '../../../types/matrix/room';
|
||||
import { MessageEvent, StateEvent } from '../../../types/matrix/room';
|
||||
import { webRTCSupported } from '../../utils/rtc';
|
||||
import { useRoomLatestRenderedEvent } from '../../hooks/useRoomLatestRenderedEvent';
|
||||
|
||||
dayjs.extend(isToday);
|
||||
dayjs.extend(isYesterday);
|
||||
|
||||
const PREVIEW_MAX_CHARS = 48;
|
||||
|
||||
function formatDmTimestamp(ts: number): string {
|
||||
const d = dayjs(ts);
|
||||
const now = dayjs();
|
||||
const diffMinutes = now.diff(d, 'minute');
|
||||
if (diffMinutes < 60) {
|
||||
return `${diffMinutes < 1 ? 0 : diffMinutes}m`;
|
||||
}
|
||||
const diffHours = now.diff(d, 'hour');
|
||||
if (diffHours < 24) {
|
||||
return `${diffHours}h`;
|
||||
}
|
||||
if (d.isYesterday()) {
|
||||
return 'Yesterday';
|
||||
}
|
||||
return d.format('D MMM');
|
||||
}
|
||||
|
||||
type RenameRoomDialogProps = {
|
||||
room: Room;
|
||||
@@ -419,6 +445,28 @@ function RoomNavItem_({
|
||||
const roomName = useLocalRoomName(room);
|
||||
const hasLocalName = useHasLocalRoomName(room.roomId);
|
||||
|
||||
const latestEvent = useRoomLatestRenderedEvent(room);
|
||||
const dmPreview = (() => {
|
||||
if (!direct || !latestEvent) return null;
|
||||
const type = latestEvent.getType();
|
||||
const ts = latestEvent.getTs();
|
||||
if (!ts) return null;
|
||||
// Skip pure membership events
|
||||
if (type === StateEvent.RoomMember) return null;
|
||||
let body: string;
|
||||
if (latestEvent.isEncrypted()) {
|
||||
body = 'Encrypted message';
|
||||
} else if (type === MessageEvent.Sticker) {
|
||||
body = 'Sticker';
|
||||
} else {
|
||||
const rawBody: unknown = latestEvent.getContent()?.body;
|
||||
body = typeof rawBody === 'string' ? rawBody.replace(/\s+/g, ' ').trim() : '';
|
||||
}
|
||||
if (!body) return null;
|
||||
const preview = body.length > PREVIEW_MAX_CHARS ? `${body.slice(0, PREVIEW_MAX_CHARS)}…` : body;
|
||||
return { preview, time: formatDmTimestamp(ts) };
|
||||
})();
|
||||
|
||||
const handleContextMenu: MouseEventHandler<HTMLElement> = (evt) => {
|
||||
evt.preventDefault();
|
||||
setMenuAnchor({
|
||||
@@ -510,26 +558,47 @@ function RoomNavItem_({
|
||||
/>
|
||||
)}
|
||||
</Avatar>
|
||||
<Box as="span" grow="Yes" alignItems="Center" gap="100">
|
||||
<Text priority={unread ? '500' : '300'} as="span" size="Inherit" truncate>
|
||||
{roomName}
|
||||
</Text>
|
||||
{hasLocalName && (
|
||||
<Icon
|
||||
size="50"
|
||||
src={Icons.Pencil}
|
||||
aria-label="Custom local name"
|
||||
style={{ opacity: config.opacity.P300, flexShrink: 0 }}
|
||||
/>
|
||||
)}
|
||||
{isFavorite && (
|
||||
<Icon
|
||||
size="50"
|
||||
src={Icons.Star}
|
||||
filled
|
||||
aria-label="Favorited"
|
||||
style={{ opacity: config.opacity.P300, flexShrink: 0 }}
|
||||
/>
|
||||
<Box as="span" direction="Column" grow="Yes" style={{ minWidth: 0 }}>
|
||||
<Box as="span" grow="Yes" alignItems="Center" gap="100">
|
||||
<Text priority={unread ? '500' : '300'} as="span" size="Inherit" truncate>
|
||||
{roomName}
|
||||
</Text>
|
||||
{hasLocalName && (
|
||||
<Icon
|
||||
size="50"
|
||||
src={Icons.Pencil}
|
||||
aria-label="Custom local name"
|
||||
style={{ opacity: config.opacity.P300, flexShrink: 0 }}
|
||||
/>
|
||||
)}
|
||||
{isFavorite && (
|
||||
<Icon
|
||||
size="50"
|
||||
src={Icons.Star}
|
||||
filled
|
||||
aria-label="Favorited"
|
||||
style={{ opacity: config.opacity.P300, flexShrink: 0 }}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
{dmPreview && (
|
||||
<Box as="span" alignItems="Center" gap="100" style={{ minWidth: 0 }}>
|
||||
<Text
|
||||
as="span"
|
||||
size="T200"
|
||||
truncate
|
||||
style={{ opacity: config.opacity.P300, flexGrow: 1 }}
|
||||
>
|
||||
{dmPreview.preview}
|
||||
</Text>
|
||||
<Text
|
||||
as="span"
|
||||
size="T200"
|
||||
style={{ opacity: config.opacity.P300, flexShrink: 0, whiteSpace: 'nowrap' }}
|
||||
>
|
||||
{dmPreview.time}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
{!optionsVisible && !unread && !selected && typingMember.length > 0 && (
|
||||
|
||||
Reference in New Issue
Block a user