Skip to content

Commit 80879cc

Browse files
author
Evie Gauthier
committed
feat(threads): implement thread side panel with full functionality
Implements Matrix thread support with side panels, browser, and notification integration. Features: - Thread Drawer: Side panel for viewing and replying within threads - Full message rendering with replies, reactions, edits, and deletions - Emoji picker and sticker support - Automatic read receipt management - Thread Browser: Panel listing all room threads with search functionality - Thread Chips: Visual indicators on messages that start threads - Shows reply count with participant avatars - Click to open thread drawer - Unread Badge: Discord-style notification badge on thread icon showing unread count - Cross-device Sync: Threads created on other devices automatically appear - Initializes Thread objects from room history on mount - Auto-creates Thread objects when receiving new thread events - Notification Integration: Clicking inbox notifications for thread replies opens the thread Implementation: - Uses matrix-js-sdk Thread API (room.createThread(), room.getThreads()) - Filters thread replies from main timeline - Scans timeline on mount to initialize Thread objects for existing threads - Auto-creates Thread objects when receiving ThreadEvent.New - Auto-opens drawer when navigating to thread events from notifications - Tracks unread status using SDK notification APIs - Thread reply counts exclude root message, reactions, and edits - Proper state management with Jotai atoms per room - Quality check script added for pre-push validation Thread UX: - Thread button in header opens drawer/browser - Clicking thread chip on message opens drawer - Thread browser shows all threads with search - Jump button navigates to root message in timeline - Thread drawer scrollable for long content - Following indicator shows thread participants reading
1 parent 8216d54 commit 80879cc

29 files changed

Lines changed: 2853 additions & 126 deletions

.changeset/feat-threads.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
sable: minor
3+
---
4+
5+
Add thread support with side panel, browser, unread badges, and cross-device sync

.changeset/fix_call_preferences.md

Lines changed: 0 additions & 5 deletions
This file was deleted.

.github/dependabot.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ updates:
3636

3737
- package-ecosystem: npm
3838
cooldown:
39-
default-days: 7
39+
default-days: 1
4040
directory: /
4141
schedule:
4242
interval: daily

.github/workflows/cloudflare-web-preview.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ jobs:
6666
if [ "${{ github.event_name }}" = "pull_request" ]; then
6767
echo "alias=pr-${{ github.event.pull_request.number }}" >> "$GITHUB_OUTPUT"
6868
else
69-
branch="${GITHUB_REF_NAME}"
69+
branch="${{ github.ref_name }}"
7070
alias="$(echo "$branch" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-\|-$//g')"
7171
echo "alias=${alias}" >> "$GITHUB_OUTPUT"
7272
fi

.github/workflows/docker-publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ on:
44
push:
55
branches: [dev]
66
tags:
7-
- 'v*'
7+
- 'sable/v*'
88
pull_request:
99
paths:
1010
- 'Dockerfile'

.github/workflows/quality-checks.yml

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,11 @@ on:
44
pull_request:
55
push:
66
branches: [dev]
7-
merge_group:
87

98
jobs:
109
format:
1110
name: Format check
1211
runs-on: ubuntu-latest
13-
if: github.head_ref != 'release'
1412
permissions:
1513
contents: read
1614
steps:
@@ -28,7 +26,6 @@ jobs:
2826
lint:
2927
name: Lint
3028
runs-on: ubuntu-latest
31-
if: github.head_ref != 'release'
3229
permissions:
3330
contents: read
3431
steps:
@@ -46,7 +43,6 @@ jobs:
4643
typecheck:
4744
name: Typecheck
4845
runs-on: ubuntu-latest
49-
if: github.head_ref != 'release'
5046
permissions:
5147
contents: read
5248
steps:
@@ -64,7 +60,6 @@ jobs:
6460
knip:
6561
name: Knip
6662
runs-on: ubuntu-latest
67-
if: github.head_ref != 'release'
6863
permissions:
6964
contents: read
7065
steps:
@@ -78,20 +73,3 @@ jobs:
7873

7974
- name: Run Knip
8075
run: pnpm run knip
81-
82-
build:
83-
name: Build
84-
runs-on: ubuntu-latest
85-
if: github.head_ref != 'release'
86-
permissions:
87-
contents: read
88-
steps:
89-
- name: Checkout repository
90-
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
91-
with:
92-
persist-credentials: false
93-
94-
- name: Setup app and build
95-
uses: ./.github/actions/setup
96-
with:
97-
build: 'true'

.github/workflows/require-changeset.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,13 @@ name: Require Changeset
33
on:
44
pull_request:
55
types: [opened, synchronize, reopened, labeled, unlabeled]
6-
merge_group:
76
branches: [dev]
87

98
permissions: {}
109

1110
jobs:
1211
require-changeset:
1312
runs-on: ubuntu-latest
14-
if: github.head_ref != 'release' && github.event_name != 'merge_group'
1513
permissions:
1614
contents: read
1715
pull-requests: write

scripts/check-quality.sh

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#!/usr/bin/env bash
2+
set -e
3+
4+
echo "Running quality checks..."
5+
echo ""
6+
7+
echo "1/3 Checking formatting..."
8+
pnpm run fmt:check
9+
10+
echo ""
11+
echo "2/3 Running linter..."
12+
pnpm run lint
13+
14+
echo ""
15+
echo "3/3 Running type checker..."
16+
pnpm run typecheck
17+
18+
echo ""
19+
echo "✅ All quality checks passed!"

src/app/components/editor/Editor.tsx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,6 @@ import { CustomElement } from './slate';
2626
import * as css from './Editor.css';
2727
import { toggleKeyboardShortcut } from './keyboard';
2828

29-
const initialValue: CustomElement[] = [
30-
{
31-
type: BlockType.Paragraph,
32-
children: [{ text: '' }],
33-
},
34-
];
35-
3629
const withInline = (editor: Editor): Editor => {
3730
const { isInline } = editor;
3831

@@ -96,6 +89,15 @@ export const CustomEditor = forwardRef<HTMLDivElement, CustomEditorProps>(
9689
},
9790
ref
9891
) => {
92+
// Each <Slate> instance must receive its own fresh node objects.
93+
// Sharing a module-level constant causes Slate's global NODE_TO_ELEMENT
94+
// WeakMap to be overwritten when multiple editors are mounted at the same
95+
// time (e.g. RoomInput + MessageEditor in the thread drawer), leading to
96+
// "Unable to find the path for Slate node" crashes.
97+
const [slateInitialValue] = useState<CustomElement[]>(() => [
98+
{ type: BlockType.Paragraph, children: [{ text: '' }] },
99+
]);
100+
99101
const renderElement = useCallback(
100102
(props: RenderElementProps) => <RenderElement {...props} />,
101103
[]
@@ -132,7 +134,7 @@ export const CustomEditor = forwardRef<HTMLDivElement, CustomEditorProps>(
132134

133135
return (
134136
<div className={`${css.Editor} ${className || ''}`} ref={ref}>
135-
<Slate editor={editor} initialValue={initialValue} onChange={onChange}>
137+
<Slate editor={editor} initialValue={slateInitialValue} onChange={onChange}>
136138
{top}
137139
<Box alignItems="Start">
138140
{before && (

src/app/features/call/CallControls.tsx

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { MouseEventHandler, useCallback, useEffect, useRef, useState } from 'react';
1+
import { MouseEventHandler, useCallback, useRef, useState } from 'react';
22
import {
33
Box,
44
Button,
@@ -15,13 +15,7 @@ import {
1515
toRem,
1616
} from 'folds';
1717
import FocusTrap from 'focus-trap-react';
18-
import { SequenceCard } from '$components/sequence-card';
19-
import { CallEmbed, useCallControlState } from '$plugins/call';
20-
import { stopPropagation } from '$utils/keyboard';
21-
import { AsyncStatus, useAsyncCallback } from '$hooks/useAsyncCallback';
22-
import { useRoom } from '$hooks/useRoom';
23-
import { ScreenSize, useScreenSizeContext } from '$hooks/useScreenSize';
24-
import { useCallPreferences } from '$state/hooks/callPreferences';
18+
import { SequenceCard } from '../../components/sequence-card';
2519
import * as css from './styles.css';
2620
import {
2721
ChatButton,
@@ -31,6 +25,11 @@ import {
3125
SoundButton,
3226
VideoButton,
3327
} from './Controls';
28+
import { CallEmbed, useCallControlState } from '../../plugins/call';
29+
import { stopPropagation } from '../../utils/keyboard';
30+
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
31+
import { useRoom } from '../../hooks/useRoom';
32+
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
3433

3534
type CallControlsProps = {
3635
callEmbed: CallEmbed;
@@ -46,12 +45,6 @@ export function CallControls({ callEmbed }: CallControlsProps) {
4645
callEmbed.control
4746
);
4847

49-
const { setPreferences } = useCallPreferences();
50-
51-
useEffect(() => {
52-
setPreferences({ microphone, video, sound });
53-
}, [microphone, video, sound, setPreferences]);
54-
5548
const [cords, setCords] = useState<RectCords>();
5649

5750
const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {

0 commit comments

Comments
 (0)