Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a3b15a0
feat: Add adaptive background pagination for message history
Mar 10, 2026
61d302b
Update changeset
Mar 10, 2026
f1ebfb8
refactor: Extract adaptive signal detection into shared utility
Mar 10, 2026
336fc71
feat(threads): implement thread side panel with full functionality
Mar 10, 2026
84b8f91
fix: Move import to top of file
Mar 10, 2026
fba2e91
fix: prevent ResizeObserver from scrolling to bottom during back pagi…
Mar 10, 2026
1d65bfa
chore: remove thread mockup feature
Mar 10, 2026
94c83f2
fix: remove thread mockup button from welcome page
Mar 10, 2026
b23cd2f
Merge remote-tracking branch 'upstream/dev' into dev
Mar 10, 2026
2a002f9
Merge branch 'dev' into feat/threads
Mar 10, 2026
b908e24
Merge feat/background-thread-discovery into feat/threads
Mar 10, 2026
00b5eb3
feat(threads): replace ThreadMessage with full-featured event renderer
Mar 12, 2026
dcfa9ea
fix(threads): add data-message-id attribute for hover effects
Mar 12, 2026
435f118
Fix thread drawer UI: hover highlighting, spacing, and input alignment
Mar 12, 2026
d667b99
Fix thread messages incorrectly showing reply indicators
Mar 12, 2026
f634ad8
Fix Reply button not working in thread drawer
Mar 12, 2026
6b2f636
refactor(threads): Match thread drawer UI to room timeline styling
Mar 12, 2026
01204b2
feat(threads): Improve thread drawer layout and following indicator
Mar 12, 2026
c65ba5a
fix(threads): fix reply/reactions, remove input border, keep thread r…
Mar 12, 2026
a9524d4
fix(threads): filter reactions and edits from thread reply count and …
Mar 12, 2026
59bdeb0
fix(threads): fix reactions rerender, auto-scroll, jump highlight, re…
Mar 12, 2026
3c7a977
Merge branch 'dev' into feat/threads
Just-Insane Mar 12, 2026
8a95556
fix: resolve linting and type errors in ThreadDrawer, RoomInput, and …
Mar 12, 2026
e75ae37
fix: resolve remaining lint and format errors in ThreadDrawer
Mar 12, 2026
e8f8225
fix(threads): prevent reply count from shifting and hide redundant th…
Mar 12, 2026
fc650fa
fix(lint): remove unused threadRootId variable in ThreadMessage
Mar 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/feat-background-pagination.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: minor
---

Add adaptive background pagination for improved message history availability.
5 changes: 5 additions & 0 deletions .changeset/feat-threads.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
sable: minor
---

Add thread support with side panel, browser, unread badges, and cross-device sync
18 changes: 10 additions & 8 deletions src/app/components/editor/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,6 @@ import { CustomElement } from './slate';
import * as css from './Editor.css';
import { toggleKeyboardShortcut } from './keyboard';

const initialValue: CustomElement[] = [
{
type: BlockType.Paragraph,
children: [{ text: '' }],
},
];

const withInline = (editor: Editor): Editor => {
const { isInline } = editor;

Expand Down Expand Up @@ -96,6 +89,15 @@ export const CustomEditor = forwardRef<HTMLDivElement, CustomEditorProps>(
},
ref
) => {
// Each <Slate> instance must receive its own fresh node objects.
// Sharing a module-level constant causes Slate's global NODE_TO_ELEMENT
// WeakMap to be overwritten when multiple editors are mounted at the same
// time (e.g. RoomInput + MessageEditor in the thread drawer), leading to
// "Unable to find the path for Slate node" crashes.
const [slateInitialValue] = useState<CustomElement[]>(() => [
{ type: BlockType.Paragraph, children: [{ text: '' }] },
]);

const renderElement = useCallback(
(props: RenderElementProps) => <RenderElement {...props} />,
[]
Expand Down Expand Up @@ -132,7 +134,7 @@ export const CustomEditor = forwardRef<HTMLDivElement, CustomEditorProps>(

return (
<div className={`${css.Editor} ${className || ''}`} ref={ref}>
<Slate editor={editor} initialValue={initialValue} onChange={onChange}>
<Slate editor={editor} initialValue={slateInitialValue} onChange={onChange}>
{top}
<Box alignItems="Start">
{before && (
Expand Down
110 changes: 107 additions & 3 deletions src/app/features/room/Room.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useCallback } from 'react';
import { useCallback, useEffect } from 'react';
import { Box, Line } from 'folds';
import { useParams } from 'react-router-dom';
import { isKeyHotkey } from 'is-hotkey';
import { useAtomValue } from 'jotai';
import { useAtom, useAtomValue } from 'jotai';
import { Direction } from '$types/matrix-sdk';
import { ScreenSize, useScreenSizeContext } from '$hooks/useScreenSize';
import { useSetting } from '$state/hooks/settings';
import { settingsAtom } from '$state/settings';
Expand All @@ -15,10 +16,15 @@
import { CallView } from '$features/call/CallView';
import { WidgetsDrawer } from '$features/widgets/WidgetsDrawer';
import { callChatAtom } from '$state/callEmbed';
import { roomIdToOpenThreadAtomFamily } from '$state/room/roomToOpenThread';
import { roomIdToThreadBrowserAtomFamily } from '$state/room/roomToThreadBrowser';
import { getBackgroundPaginationConfig } from '$utils/device-capabilities';
import { RoomViewHeader } from './RoomViewHeader';
import { MembersDrawer } from './MembersDrawer';
import { RoomView } from './RoomView';
import { CallChatView } from './CallChatView';
import { ThreadDrawer } from './ThreadDrawer';
import { ThreadBrowser } from './ThreadBrowser';

export function Room() {
const { eventId } = useParams();
Expand All @@ -32,6 +38,30 @@
const powerLevels = usePowerLevels(room);
const members = useRoomMembers(mx, room.roomId);
const chat = useAtomValue(callChatAtom);
const [openThreadId, setOpenThread] = useAtom(roomIdToOpenThreadAtomFamily(room.roomId));
const [threadBrowserOpen, setThreadBrowserOpen] = useAtom(
roomIdToThreadBrowserAtomFamily(room.roomId)
);

// If navigating to an event in a thread, open the thread drawer
useEffect(() => {
if (!eventId) return;

const event = room.findEventById(eventId);
if (!event) return;

const { threadRootId } = event;
if (threadRootId) {
// Ensure Thread object exists
if (!room.getThread(threadRootId)) {
const rootEvent = room.findEventById(threadRootId);
if (rootEvent) {
room.createThread(threadRootId, rootEvent, [], false);
}
}
setOpenThread(threadRootId);
}
}, [eventId, room, setOpenThread]);

useKeyDown(
window,
Expand All @@ -45,11 +75,39 @@
)
);

// Background pagination to load additional message history
useEffect(() => {
const config = getBackgroundPaginationConfig();
if (!config.enabled) return undefined;

let cancelled = false;
const timer = setTimeout(() => {
if (cancelled) return;

const timeline = room.getLiveTimeline();
const token = timeline.getPaginationToken(Direction.Backward);

if (token) {
mx.paginateEventTimeline(timeline, {
backwards: true,
limit: config.limit,
}).catch((err) => {
console.warn('Background pagination failed:', err);

Check warning on line 95 in src/app/features/room/Room.tsx

View workflow job for this annotation

GitHub Actions / Lint

Unexpected console statement
});
}
}, config.delayMs);

return () => {
cancelled = true;
clearTimeout(timer);
};
}, [room, mx]);

const callView = room.isCallRoom();

return (
<PowerLevelsContextProvider value={powerLevels}>
<Box grow="Yes">
<Box grow="Yes" style={{ position: 'relative' }}>
{callView && (screenSize === ScreenSize.Desktop || !chat) && (
<Box grow="Yes" direction="Column">
<RoomViewHeader callView />
Expand Down Expand Up @@ -87,6 +145,52 @@
<WidgetsDrawer key={`widgets-${room.roomId}`} room={room} />
</>
)}
{screenSize === ScreenSize.Desktop && openThreadId && (
<>
<Line variant="Background" direction="Vertical" size="300" />
<ThreadDrawer
key={`thread-${room.roomId}-${openThreadId}`}
room={room}
threadRootId={openThreadId}
onClose={() => setOpenThread(undefined)}
/>
</>
)}
{screenSize === ScreenSize.Desktop && threadBrowserOpen && !openThreadId && (
<>
<Line variant="Background" direction="Vertical" size="300" />
<ThreadBrowser
key={`thread-browser-${room.roomId}`}
room={room}
onOpenThread={(id) => {
setOpenThread(id);
setThreadBrowserOpen(false);
}}
onClose={() => setThreadBrowserOpen(false)}
/>
</>
)}
{screenSize !== ScreenSize.Desktop && openThreadId && (
<ThreadDrawer
key={`thread-${room.roomId}-${openThreadId}`}
room={room}
threadRootId={openThreadId}
onClose={() => setOpenThread(undefined)}
overlay
/>
)}
{screenSize !== ScreenSize.Desktop && threadBrowserOpen && !openThreadId && (
<ThreadBrowser
key={`thread-browser-${room.roomId}`}
room={room}
onOpenThread={(id) => {
setOpenThread(id);
setThreadBrowserOpen(false);
}}
onClose={() => setThreadBrowserOpen(false)}
overlay
/>
)}
</Box>
</PowerLevelsContextProvider>
);
Expand Down
Loading
Loading