From 3d6ab39bfe76c69a0217eec600ca37e59a3c7697 Mon Sep 17 00:00:00 2001 From: Jure <44338+hoornet@users.noreply.github.com> Date: Tue, 31 Mar 2026 08:35:37 +0200 Subject: [PATCH] Thread focus: auto-expand collapsed replies, debounced scroll, visible highlight; persist script filter --- src/components/feed/NoteCard.tsx | 2 +- src/components/thread/ThreadNode.tsx | 13 ++++++++++++- src/components/thread/ThreadView.tsx | 20 +++++++++++--------- src/stores/ui.ts | 12 ++++++++++-- 4 files changed, 34 insertions(+), 13 deletions(-) diff --git a/src/components/feed/NoteCard.tsx b/src/components/feed/NoteCard.tsx index c894f1c..1b640fc 100644 --- a/src/components/feed/NoteCard.tsx +++ b/src/components/feed/NoteCard.tsx @@ -55,7 +55,7 @@ export function NoteCard({ event, focused, onReplyInThread }: NoteCardProps) {
{ // Don't navigate if clicking on interactive elements const target = e.target as HTMLElement; diff --git a/src/components/thread/ThreadNode.tsx b/src/components/thread/ThreadNode.tsx index 778cc2c..deea5b2 100644 --- a/src/components/thread/ThreadNode.tsx +++ b/src/components/thread/ThreadNode.tsx @@ -108,8 +108,19 @@ function InlineThreadReply({ replyTo, rootEvent, onPublished }: { ); } +/** Check if any descendant of a node has the given event ID. */ +function subtreeContains(node: ThreadNodeType, id: string): boolean { + for (const child of node.children) { + if (child.event.id === id) return true; + if (subtreeContains(child, id)) return true; + } + return false; +} + export function ThreadNodeComponent({ node, rootEvent, onReplyPublished, focusedId, mutedPubkeys, contentMatchesMutedKeyword }: ThreadNodeProps) { - const [expanded, setExpanded] = useState(false); + // Auto-expand if the focused note is hidden inside a collapsed section + const focusedInChildren = focusedId ? subtreeContains(node, focusedId) : false; + const [expanded, setExpanded] = useState(focusedInChildren); const [showReplyBox, setShowReplyBox] = useState(false); // Filter out muted children diff --git a/src/components/thread/ThreadView.tsx b/src/components/thread/ThreadView.tsx index a7da0c7..73af15e 100644 --- a/src/components/thread/ThreadView.tsx +++ b/src/components/thread/ThreadView.tsx @@ -108,16 +108,18 @@ export function ThreadView() { return () => { cancelled = true; }; }, [focusedEvent.id, retryCount]); - // Scroll to focused note after tree renders (if not root) + // Scroll to focused note after tree fully loads. + // Use a short delay after each tree update; the last one wins. + const scrollTimer = useRef>(undefined); useEffect(() => { - if (!loading && rootEvent && focusedEvent.id !== rootEvent.id) { - const timer = setTimeout(() => { - const el = document.querySelector(`[data-note-id="${focusedEvent.id}"]`); - el?.scrollIntoView({ behavior: "smooth", block: "center" }); - }, 100); - return () => clearTimeout(timer); - } - }, [loading, rootEvent?.id, focusedEvent.id]); + if (focusedEvent.id === rootEvent?.id) return; + clearTimeout(scrollTimer.current); + scrollTimer.current = setTimeout(() => { + const el = document.querySelector(`[data-note-id="${focusedEvent.id}"]`); + el?.scrollIntoView({ behavior: "smooth", block: "center" }); + }, 400); + return () => clearTimeout(scrollTimer.current); + }, [tree, focusedEvent.id, rootEvent?.id]); // Called when any inline reply box publishes a reply const handleReplyPublished = (reply: NDKEvent) => { diff --git a/src/stores/ui.ts b/src/stores/ui.ts index 58e3d19..27f4af9 100644 --- a/src/stores/ui.ts +++ b/src/stores/ui.ts @@ -53,6 +53,7 @@ interface UIState { const SIDEBAR_KEY = "wrystr_sidebar_collapsed"; const FONT_SIZE_KEY = "wrystr_font_size"; const THEME_KEY = "wrystr_theme"; +const SCRIPT_FILTER_KEY = "wrystr_script_filter"; export const useUIStore = create((set, _get) => ({ currentView: "feed", @@ -69,7 +70,7 @@ export const useUIStore = create((set, _get) => ({ pendingHashtag: null, showHelp: false, showDebugPanel: false, - feedLanguageFilter: null, + feedLanguageFilter: localStorage.getItem(SCRIPT_FILTER_KEY) || null, followsTab: "followers", fontSize: parseInt(localStorage.getItem(FONT_SIZE_KEY) || "14", 10), themeId: localStorage.getItem(THEME_KEY) || "midnight", @@ -102,7 +103,14 @@ export const useUIStore = create((set, _get) => ({ } return { showHelp: false, currentView: "feed", selectedNote: null, viewStack: [] }; }), - setFeedLanguageFilter: (feedLanguageFilter) => set({ feedLanguageFilter }), + setFeedLanguageFilter: (feedLanguageFilter) => { + if (feedLanguageFilter) { + localStorage.setItem(SCRIPT_FILTER_KEY, feedLanguageFilter); + } else { + localStorage.removeItem(SCRIPT_FILTER_KEY); + } + set({ feedLanguageFilter }); + }, setFontSize: (fontSize) => { localStorage.setItem(FONT_SIZE_KEY, String(fontSize)); set({ fontSize });