feat: add thread functionality to room features

- Enhance RoomInput to support sending messages as replies in threads by adding `threadRootId` prop.
- Update RoomTimeline to manage active thread state and handle opening threads.
- Implement ThreadView component to display thread messages and input.
- Create HomeThreadsCategory to list threads user has participated in.
- Introduce activeThreadIdAtomFamily to manage active thread state across rooms.
This commit is contained in:
2026-03-13 00:44:33 +11:00
parent 37f4297972
commit ced29a6571
9 changed files with 1039 additions and 44 deletions

View File

@@ -3,6 +3,7 @@ import { Box, Text, config } from 'folds';
import { EventType, Room } from 'matrix-js-sdk';
import { ReactEditor } from 'slate-react';
import { isKeyHotkey } from 'is-hotkey';
import { useAtomValue } from 'jotai';
import { useStateEvent } from '../../hooks/useStateEvent';
import { StateEvent } from '../../../types/matrix/room';
import { usePowerLevelsContext } from '../../hooks/usePowerLevels';
@@ -22,6 +23,8 @@ import { settingsAtom } from '../../state/settings';
import { useSetting } from '../../state/hooks/settings';
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
import { useRoomCreators } from '../../hooks/useRoomCreators';
import { activeThreadIdAtomFamily } from '../../state/activeThread';
import { ThreadView } from './ThreadView';
const FN_KEYS_REGEX = /^F\d+$/;
const shouldFocusMessageField = (evt: KeyboardEvent): boolean => {
@@ -74,6 +77,8 @@ export function RoomView({ room, eventId }: { room: Room; eventId?: string }) {
const permissions = useRoomPermissions(creators, powerLevels);
const canMessage = permissions.event(EventType.RoomMessage, mx.getSafeUserId());
const activeThreadId = useAtomValue(activeThreadIdAtomFamily(roomId));
useKeyDown(
window,
useCallback(
@@ -94,49 +99,55 @@ export function RoomView({ room, eventId }: { room: Room; eventId?: string }) {
return (
<Page ref={roomViewRef}>
<RoomViewHeader />
<Box grow="Yes" direction="Column">
<RoomTimeline
key={roomId}
room={room}
eventId={eventId}
roomInputRef={roomInputRef}
editor={editor}
/>
<RoomViewTyping room={room} />
</Box>
<Box shrink="No" direction="Column">
<div style={{ padding: `0 ${config.space.S400}` }}>
{tombstoneEvent ? (
<RoomTombstone
roomId={roomId}
body={tombstoneEvent.getContent().body}
replacementRoomId={tombstoneEvent.getContent().replacement_room}
{activeThreadId ? (
<ThreadView room={room} threadRootId={activeThreadId} />
) : (
<>
<Box grow="Yes" direction="Column">
<RoomTimeline
key={roomId}
room={room}
eventId={eventId}
roomInputRef={roomInputRef}
editor={editor}
/>
) : (
<>
{canMessage && (
<RoomInput
room={room}
editor={editor}
<RoomViewTyping room={room} />
</Box>
<Box shrink="No" direction="Column">
<div style={{ padding: `0 ${config.space.S400}` }}>
{tombstoneEvent ? (
<RoomTombstone
roomId={roomId}
fileDropContainerRef={roomViewRef}
ref={roomInputRef}
body={tombstoneEvent.getContent().body}
replacementRoomId={tombstoneEvent.getContent().replacement_room}
/>
) : (
<>
{canMessage && (
<RoomInput
room={room}
editor={editor}
roomId={roomId}
fileDropContainerRef={roomViewRef}
ref={roomInputRef}
/>
)}
{!canMessage && (
<RoomInputPlaceholder
style={{ padding: config.space.S200 }}
alignItems="Center"
justifyContent="Center"
>
<Text align="Center">You do not have permission to post in this room</Text>
</RoomInputPlaceholder>
)}
</>
)}
{!canMessage && (
<RoomInputPlaceholder
style={{ padding: config.space.S200 }}
alignItems="Center"
justifyContent="Center"
>
<Text align="Center">You do not have permission to post in this room</Text>
</RoomInputPlaceholder>
)}
</>
)}
</div>
{hideActivity ? <RoomViewFollowingPlaceholder /> : <RoomViewFollowing room={room} />}
</Box>
</div>
{hideActivity ? <RoomViewFollowingPlaceholder /> : <RoomViewFollowing room={room} />}
</Box>
</>
)}
</Page>
);
}