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:
@@ -10,6 +10,7 @@ import {
|
||||
Avatar,
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Chip,
|
||||
Header,
|
||||
Icon,
|
||||
@@ -29,6 +30,7 @@ import {
|
||||
import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import classNames from 'classnames';
|
||||
import { Membership } from '../../../types/matrix/room';
|
||||
|
||||
import * as css from './MembersDrawer.css';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
@@ -51,7 +53,11 @@ import { useRoomTypingMember } from '../../hooks/useRoomTypingMembers';
|
||||
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||
import { useMembershipFilter, useMembershipFilterMenu } from '../../hooks/useMemberFilter';
|
||||
import { useMemberPowerSort, useMemberSort, useMemberSortMenu } from '../../hooks/useMemberSort';
|
||||
import { useGetMemberPowerLevel, usePowerLevelsContext } from '../../hooks/usePowerLevels';
|
||||
import {
|
||||
readPowerLevel,
|
||||
useGetMemberPowerLevel,
|
||||
usePowerLevelsContext,
|
||||
} from '../../hooks/usePowerLevels';
|
||||
import { MembershipFilterMenu } from '../../components/MembershipFilterMenu';
|
||||
import { MemberSortMenu } from '../../components/MemberSortMenu';
|
||||
import { useOpenUserRoomProfile, useUserRoomProfileState } from '../../state/hooks/userRoomProfile';
|
||||
@@ -225,6 +231,15 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) {
|
||||
|
||||
const typingMembers = useRoomTypingMember(room.roomId);
|
||||
|
||||
const myUserId = mx.getUserId();
|
||||
const myPowerLevel = readPowerLevel.user(powerLevels, myUserId ?? undefined);
|
||||
const invitePowerLevel = readPowerLevel.action(powerLevels, 'invite');
|
||||
const canApproveKnock = myPowerLevel >= invitePowerLevel;
|
||||
const knockMembers = useMemo(
|
||||
() => (canApproveKnock ? room.getMembersWithMembership(Membership.Knock) : []),
|
||||
[room, canApproveKnock],
|
||||
);
|
||||
|
||||
const filteredMembers = useMemo(
|
||||
() => members.filter(membershipFilter.filterFn).sort(memberSort.sortFn).sort(memberPowerSort),
|
||||
[members, membershipFilter, memberSort, memberPowerSort],
|
||||
@@ -392,6 +407,78 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) {
|
||||
</IconButton>
|
||||
</ScrollTopContainer>
|
||||
|
||||
{knockMembers.length > 0 && (
|
||||
<Box direction="Column" gap="100">
|
||||
<Text
|
||||
style={{ padding: `${config.space.S100} ${config.space.S200}` }}
|
||||
size="L400"
|
||||
priority="300"
|
||||
>
|
||||
Pending Requests
|
||||
</Text>
|
||||
{knockMembers.map((knockMember) => {
|
||||
const knockName =
|
||||
getMemberDisplayName(room, knockMember.userId) ??
|
||||
getMxIdLocalPart(knockMember.userId) ??
|
||||
knockMember.userId;
|
||||
const knockAvatarMxc = knockMember.getMxcAvatarUrl();
|
||||
const knockAvatarUrl = knockAvatarMxc
|
||||
? mx.mxcUrlToHttp(
|
||||
knockAvatarMxc,
|
||||
100,
|
||||
100,
|
||||
'crop',
|
||||
undefined,
|
||||
false,
|
||||
useAuthentication,
|
||||
)
|
||||
: undefined;
|
||||
return (
|
||||
<Box
|
||||
key={knockMember.userId}
|
||||
alignItems="Center"
|
||||
gap="200"
|
||||
style={{ padding: `0 ${config.space.S200}` }}
|
||||
>
|
||||
<Avatar size="200">
|
||||
<UserAvatar
|
||||
userId={knockMember.userId}
|
||||
src={knockAvatarUrl ?? undefined}
|
||||
alt={knockName}
|
||||
renderFallback={() => <Icon size="50" src={Icons.User} filled />}
|
||||
/>
|
||||
</Avatar>
|
||||
<Box grow="Yes" direction="Column">
|
||||
<Text size="T400" truncate>
|
||||
{knockName}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box shrink="No" gap="100">
|
||||
<Button
|
||||
size="300"
|
||||
variant="Success"
|
||||
radii="300"
|
||||
fill="Soft"
|
||||
onClick={() => mx.invite(room.roomId, knockMember.userId)}
|
||||
>
|
||||
<Text size="B300">Approve</Text>
|
||||
</Button>
|
||||
<Button
|
||||
size="300"
|
||||
variant="Critical"
|
||||
radii="300"
|
||||
fill="Soft"
|
||||
onClick={() => mx.kick(room.roomId, knockMember.userId)}
|
||||
>
|
||||
<Text size="B300">Deny</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{!fetchingMembers && !result && processMembers.length === 0 && (
|
||||
<Text style={{ padding: config.space.S300 }} align="Center">
|
||||
{`No "${membershipFilter.name}" Members`}
|
||||
|
||||
Reference in New Issue
Block a user