mirror of
https://github.com/hoornet/vega.git
synced 2026-05-13 14:08:36 -07:00
Fix thread reply UX: inline reply boxes below each note, scroll-to-parent
Reply boxes now open directly below the note you're replying to instead of scrolling to a top-level composer. "Replying to" link scrolls to the parent note when already visible in the thread instead of re-pushing the same thread.
This commit is contained in:
@@ -124,8 +124,16 @@ export function NoteCard({ event, focused, onReplyInThread }: NoteCardProps) {
|
|||||||
<button
|
<button
|
||||||
onClick={async (e) => {
|
onClick={async (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
// If already in thread view, try scrolling to parent first
|
||||||
|
if (currentView === "thread") {
|
||||||
|
const el = document.querySelector(`[data-note-id="${parentEventId}"]`);
|
||||||
|
if (el) {
|
||||||
|
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
const parent = await fetchNoteById(parentEventId);
|
const parent = await fetchNoteById(parentEventId);
|
||||||
if (parent) openThread(parent, currentView as "feed" | "profile");
|
if (parent) openThread(parent);
|
||||||
}}
|
}}
|
||||||
className="hover:text-accent transition-colors"
|
className="hover:text-accent transition-colors"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import { useState } from "react";
|
import { useState, useRef } from "react";
|
||||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
||||||
import type { ThreadNode as ThreadNodeType } from "../../lib/threadTree";
|
import type { ThreadNode as ThreadNodeType } from "../../lib/threadTree";
|
||||||
import { NoteCard } from "../feed/NoteCard";
|
import { NoteCard } from "../feed/NoteCard";
|
||||||
|
import { publishReply } from "../../lib/nostr";
|
||||||
|
import { useProfile } from "../../hooks/useProfile";
|
||||||
|
import { shortenPubkey } from "../../lib/utils";
|
||||||
|
import { EmojiPicker } from "../shared/EmojiPicker";
|
||||||
|
|
||||||
interface ThreadNodeProps {
|
interface ThreadNodeProps {
|
||||||
node: ThreadNodeType;
|
node: ThreadNodeType;
|
||||||
onReplyInThread: (event: NDKEvent) => void;
|
rootEvent: NDKEvent;
|
||||||
|
onReplyPublished: (reply: NDKEvent) => void;
|
||||||
focusedId?: string;
|
focusedId?: string;
|
||||||
mutedPubkeys: string[];
|
mutedPubkeys: string[];
|
||||||
contentMatchesMutedKeyword: (content: string) => boolean;
|
contentMatchesMutedKeyword: (content: string) => boolean;
|
||||||
@@ -14,8 +19,96 @@ interface ThreadNodeProps {
|
|||||||
const MAX_VISIBLE_CHILDREN = 3;
|
const MAX_VISIBLE_CHILDREN = 3;
|
||||||
const MAX_INDENT_DEPTH = 4;
|
const MAX_INDENT_DEPTH = 4;
|
||||||
|
|
||||||
export function ThreadNodeComponent({ node, onReplyInThread, focusedId, mutedPubkeys, contentMatchesMutedKeyword }: ThreadNodeProps) {
|
function InlineThreadReply({ replyTo, rootEvent, onPublished }: {
|
||||||
|
replyTo: NDKEvent;
|
||||||
|
rootEvent: NDKEvent;
|
||||||
|
onPublished: (reply: NDKEvent) => void;
|
||||||
|
}) {
|
||||||
|
const profile = useProfile(replyTo.pubkey);
|
||||||
|
const name = profile?.displayName || profile?.name || shortenPubkey(replyTo.pubkey);
|
||||||
|
const [text, setText] = useState("");
|
||||||
|
const [replying, setReplying] = useState(false);
|
||||||
|
const [sent, setSent] = useState(false);
|
||||||
|
const [showEmoji, setShowEmoji] = useState(false);
|
||||||
|
const ref = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!text.trim() || replying) return;
|
||||||
|
setReplying(true);
|
||||||
|
try {
|
||||||
|
const rootArg = replyTo.id !== rootEvent.id
|
||||||
|
? { id: rootEvent.id, pubkey: rootEvent.pubkey }
|
||||||
|
: undefined;
|
||||||
|
const reply = await publishReply(text.trim(), { id: replyTo.id, pubkey: replyTo.pubkey }, rootArg);
|
||||||
|
setText("");
|
||||||
|
setSent(true);
|
||||||
|
onPublished(reply);
|
||||||
|
setTimeout(() => setSent(false), 2000);
|
||||||
|
} finally {
|
||||||
|
setReplying(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) handleSubmit();
|
||||||
|
if (e.key === "Escape") ref.current?.blur();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border-l-2 border-accent/40 ml-3 pl-3 py-2">
|
||||||
|
<div className="text-text-dim text-[10px] mb-1">replying to <span className="text-accent">@{name}</span></div>
|
||||||
|
<textarea
|
||||||
|
ref={ref}
|
||||||
|
value={text}
|
||||||
|
onChange={(e) => setText(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder="Write a reply..."
|
||||||
|
rows={2}
|
||||||
|
className="w-full bg-transparent text-text text-[12px] placeholder:text-text-dim resize-none focus:outline-none"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<div className="flex items-center justify-end gap-2 mt-1">
|
||||||
|
<div className="relative">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowEmoji((v) => !v)}
|
||||||
|
title="Insert emoji"
|
||||||
|
className="text-text-dim hover:text-text text-[12px] transition-colors"
|
||||||
|
>
|
||||||
|
☺
|
||||||
|
</button>
|
||||||
|
{showEmoji && (
|
||||||
|
<EmojiPicker
|
||||||
|
onSelect={(emoji) => {
|
||||||
|
const ta = ref.current;
|
||||||
|
if (ta) {
|
||||||
|
const start = ta.selectionStart ?? text.length;
|
||||||
|
const end = ta.selectionEnd ?? text.length;
|
||||||
|
setText(text.slice(0, start) + emoji + text.slice(end));
|
||||||
|
setTimeout(() => { ta.selectionStart = ta.selectionEnd = start + emoji.length; ta.focus(); }, 0);
|
||||||
|
} else {
|
||||||
|
setText((t) => t + emoji);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onClose={() => setShowEmoji(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="text-text-dim text-[10px]">Ctrl+Enter</span>
|
||||||
|
<button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={!text.trim() || replying}
|
||||||
|
className="px-2 py-0.5 text-[10px] bg-accent hover:bg-accent-hover text-white transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{sent ? "replied ✓" : replying ? "posting..." : "reply"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ThreadNodeComponent({ node, rootEvent, onReplyPublished, focusedId, mutedPubkeys, contentMatchesMutedKeyword }: ThreadNodeProps) {
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
const [showReplyBox, setShowReplyBox] = useState(false);
|
||||||
|
|
||||||
// Filter out muted children
|
// Filter out muted children
|
||||||
const visibleChildren = node.children.filter(
|
const visibleChildren = node.children.filter(
|
||||||
@@ -38,14 +131,28 @@ export function ThreadNodeComponent({ node, onReplyInThread, focusedId, mutedPub
|
|||||||
<NoteCard
|
<NoteCard
|
||||||
event={node.event}
|
event={node.event}
|
||||||
focused={isFocused}
|
focused={isFocused}
|
||||||
onReplyInThread={onReplyInThread}
|
onReplyInThread={() => setShowReplyBox((v) => !v)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{showReplyBox && (
|
||||||
|
<div style={indent > 0 ? { marginLeft: "16px" } : undefined}>
|
||||||
|
<InlineThreadReply
|
||||||
|
replyTo={node.event}
|
||||||
|
rootEvent={rootEvent}
|
||||||
|
onPublished={(reply) => {
|
||||||
|
setShowReplyBox(false);
|
||||||
|
onReplyPublished(reply);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{shownChildren.map((child) => (
|
{shownChildren.map((child) => (
|
||||||
<ThreadNodeComponent
|
<ThreadNodeComponent
|
||||||
key={child.event.id}
|
key={child.event.id}
|
||||||
node={child}
|
node={child}
|
||||||
onReplyInThread={onReplyInThread}
|
rootEvent={rootEvent}
|
||||||
|
onReplyPublished={onReplyPublished}
|
||||||
focusedId={focusedId}
|
focusedId={focusedId}
|
||||||
mutedPubkeys={mutedPubkeys}
|
mutedPubkeys={mutedPubkeys}
|
||||||
contentMatchesMutedKeyword={contentMatchesMutedKeyword}
|
contentMatchesMutedKeyword={contentMatchesMutedKeyword}
|
||||||
|
|||||||
@@ -6,25 +6,11 @@ import { useMuteStore } from "../../stores/mute";
|
|||||||
import { fetchNoteById, fetchThreadEvents, fetchAncestors, publishReply, getNDK } from "../../lib/nostr";
|
import { fetchNoteById, fetchThreadEvents, fetchAncestors, publishReply, getNDK } from "../../lib/nostr";
|
||||||
import { buildThreadTree, getRootEventId } from "../../lib/threadTree";
|
import { buildThreadTree, getRootEventId } from "../../lib/threadTree";
|
||||||
import type { ThreadNode } from "../../lib/threadTree";
|
import type { ThreadNode } from "../../lib/threadTree";
|
||||||
import { useProfile } from "../../hooks/useProfile";
|
|
||||||
import { shortenPubkey } from "../../lib/utils";
|
|
||||||
import { AncestorChain } from "./AncestorChain";
|
import { AncestorChain } from "./AncestorChain";
|
||||||
import { ThreadNodeComponent } from "./ThreadNode";
|
import { ThreadNodeComponent } from "./ThreadNode";
|
||||||
import { NoteCard } from "../feed/NoteCard";
|
import { NoteCard } from "../feed/NoteCard";
|
||||||
import { EmojiPicker } from "../shared/EmojiPicker";
|
import { EmojiPicker } from "../shared/EmojiPicker";
|
||||||
|
|
||||||
function ReplyTargetBadge({ event, onClear }: { event: NDKEvent; onClear: () => void }) {
|
|
||||||
const profile = useProfile(event.pubkey);
|
|
||||||
const name = profile?.displayName || profile?.name || shortenPubkey(event.pubkey);
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-2 mb-1.5 text-[11px]">
|
|
||||||
<span className="text-text-dim">replying to</span>
|
|
||||||
<span className="text-accent font-medium">@{name}</span>
|
|
||||||
<button onClick={onClear} className="text-text-dim hover:text-text transition-colors">x</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ThreadView() {
|
export function ThreadView() {
|
||||||
const { selectedNote, goBack } = useUIStore();
|
const { selectedNote, goBack } = useUIStore();
|
||||||
const { loggedIn } = useUserStore();
|
const { loggedIn } = useUserStore();
|
||||||
@@ -36,7 +22,7 @@ export function ThreadView() {
|
|||||||
const [ancestors, setAncestors] = useState<NDKEvent[]>([]);
|
const [ancestors, setAncestors] = useState<NDKEvent[]>([]);
|
||||||
const [tree, setTree] = useState<ThreadNode | null>(null);
|
const [tree, setTree] = useState<ThreadNode | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [replyTarget, setReplyTarget] = useState<NDKEvent | null>(null);
|
const [showRootReply, setShowRootReply] = useState(false);
|
||||||
const [replyText, setReplyText] = useState("");
|
const [replyText, setReplyText] = useState("");
|
||||||
const [replying, setReplying] = useState(false);
|
const [replying, setReplying] = useState(false);
|
||||||
const [replySent, setReplySent] = useState(false);
|
const [replySent, setReplySent] = useState(false);
|
||||||
@@ -52,26 +38,21 @@ export function ThreadView() {
|
|||||||
setTree(null);
|
setTree(null);
|
||||||
setAncestors([]);
|
setAncestors([]);
|
||||||
setRootEvent(null);
|
setRootEvent(null);
|
||||||
setReplyTarget(null);
|
setShowRootReply(false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Determine root
|
|
||||||
const rootId = getRootEventId(focusedEvent);
|
const rootId = getRootEventId(focusedEvent);
|
||||||
let root: NDKEvent;
|
let root: NDKEvent;
|
||||||
|
|
||||||
if (!rootId || rootId === focusedEvent.id) {
|
if (!rootId || rootId === focusedEvent.id) {
|
||||||
// This IS the root
|
|
||||||
root = focusedEvent;
|
root = focusedEvent;
|
||||||
} else {
|
} else {
|
||||||
// Fetch the root event
|
|
||||||
const fetched = await fetchNoteById(rootId);
|
const fetched = await fetchNoteById(rootId);
|
||||||
if (fetched) {
|
if (fetched) {
|
||||||
root = fetched;
|
root = fetched;
|
||||||
// Fetch ancestors between root and focused
|
|
||||||
const anc = await fetchAncestors(focusedEvent);
|
const anc = await fetchAncestors(focusedEvent);
|
||||||
if (!cancelled) setAncestors(anc.filter((a) => a.id !== root.id));
|
if (!cancelled) setAncestors(anc.filter((a) => a.id !== root.id));
|
||||||
} else {
|
} else {
|
||||||
// Root not found, treat focused as root
|
|
||||||
root = focusedEvent;
|
root = focusedEvent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,11 +60,9 @@ export function ThreadView() {
|
|||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
setRootEvent(root);
|
setRootEvent(root);
|
||||||
|
|
||||||
// Fetch all thread events and build tree
|
|
||||||
const events = await fetchThreadEvents(root.id);
|
const events = await fetchThreadEvents(root.id);
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
|
|
||||||
// Include root in the event set
|
|
||||||
const allEvents = [root, ...events.filter((e) => e.id !== root.id)];
|
const allEvents = [root, ...events.filter((e) => e.id !== root.id)];
|
||||||
const built = buildThreadTree(root.id, allEvents);
|
const built = buildThreadTree(root.id, allEvents);
|
||||||
setTree(built);
|
setTree(built);
|
||||||
@@ -101,7 +80,6 @@ export function ThreadView() {
|
|||||||
// Scroll to focused note after tree renders (if not root)
|
// Scroll to focused note after tree renders (if not root)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loading && rootEvent && focusedEvent.id !== rootEvent.id) {
|
if (!loading && rootEvent && focusedEvent.id !== rootEvent.id) {
|
||||||
// Small delay to allow DOM to render
|
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
const el = document.querySelector(`[data-note-id="${focusedEvent.id}"]`);
|
const el = document.querySelector(`[data-note-id="${focusedEvent.id}"]`);
|
||||||
el?.scrollIntoView({ behavior: "smooth", block: "center" });
|
el?.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||||
@@ -110,39 +88,26 @@ export function ThreadView() {
|
|||||||
}
|
}
|
||||||
}, [loading, rootEvent?.id, focusedEvent.id]);
|
}, [loading, rootEvent?.id, focusedEvent.id]);
|
||||||
|
|
||||||
const handleReplyInThread = (event: NDKEvent) => {
|
// Called when any inline reply box publishes a reply
|
||||||
setReplyTarget(event);
|
const handleReplyPublished = (reply: NDKEvent) => {
|
||||||
setTimeout(() => replyRef.current?.focus(), 50);
|
if (tree && rootEvent) {
|
||||||
|
const allEvents = collectEvents(tree);
|
||||||
|
allEvents.push(reply);
|
||||||
|
const rebuilt = buildThreadTree(rootEvent.id, allEvents);
|
||||||
|
setTree(rebuilt);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const effectiveReplyTarget = replyTarget ?? rootEvent;
|
// Root-level reply (reply to the root note)
|
||||||
|
const handleRootReply = async () => {
|
||||||
const handleReply = async () => {
|
|
||||||
if (!replyText.trim() || replying || !rootEvent) return;
|
if (!replyText.trim() || replying || !rootEvent) return;
|
||||||
setReplying(true);
|
setReplying(true);
|
||||||
try {
|
try {
|
||||||
const target = effectiveReplyTarget ?? rootEvent;
|
const reply = await publishReply(replyText.trim(), { id: rootEvent.id, pubkey: rootEvent.pubkey });
|
||||||
const rootArg = target.id !== rootEvent.id
|
|
||||||
? { id: rootEvent.id, pubkey: rootEvent.pubkey }
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const replyEvent = await publishReply(
|
|
||||||
replyText.trim(),
|
|
||||||
{ id: target.id, pubkey: target.pubkey },
|
|
||||||
rootArg,
|
|
||||||
);
|
|
||||||
setReplyText("");
|
setReplyText("");
|
||||||
setReplySent(true);
|
setReplySent(true);
|
||||||
setReplyTarget(null);
|
setShowRootReply(false);
|
||||||
|
handleReplyPublished(reply);
|
||||||
// Optimistically insert into tree
|
|
||||||
if (tree) {
|
|
||||||
const allEvents = collectEvents(tree);
|
|
||||||
allEvents.push(replyEvent);
|
|
||||||
const rebuilt = buildThreadTree(rootEvent.id, allEvents);
|
|
||||||
setTree(rebuilt);
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => setReplySent(false), 2000);
|
setTimeout(() => setReplySent(false), 2000);
|
||||||
} finally {
|
} finally {
|
||||||
setReplying(false);
|
setReplying(false);
|
||||||
@@ -150,7 +115,7 @@ export function ThreadView() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) handleReply();
|
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) handleRootReply();
|
||||||
if (e.key === "Escape") replyRef.current?.blur();
|
if (e.key === "Escape") replyRef.current?.blur();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -185,21 +150,18 @@ export function ThreadView() {
|
|||||||
{/* Ancestors (when opening a deep reply) */}
|
{/* Ancestors (when opening a deep reply) */}
|
||||||
<AncestorChain ancestors={ancestors} />
|
<AncestorChain ancestors={ancestors} />
|
||||||
|
|
||||||
{/* Root note rendered via tree */}
|
{/* Root note */}
|
||||||
<div data-note-id={tree.event.id}>
|
<div data-note-id={tree.event.id}>
|
||||||
<NoteCard
|
<NoteCard
|
||||||
event={tree.event}
|
event={tree.event}
|
||||||
focused={tree.event.id === focusedEvent.id}
|
focused={tree.event.id === focusedEvent.id}
|
||||||
onReplyInThread={handleReplyInThread}
|
onReplyInThread={() => setShowRootReply((v) => !v)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Reply composer */}
|
{/* Root reply box (inline, right below root) */}
|
||||||
{loggedIn && !!getNDK().signer && (
|
{showRootReply && loggedIn && !!getNDK().signer && (
|
||||||
<div className="border-b border-border px-4 py-3">
|
<div className="border-b border-border border-l-2 border-l-accent/40 ml-3 px-3 py-2">
|
||||||
{replyTarget && replyTarget.id !== rootEvent.id && (
|
|
||||||
<ReplyTargetBadge event={replyTarget} onClear={() => setReplyTarget(null)} />
|
|
||||||
)}
|
|
||||||
<textarea
|
<textarea
|
||||||
ref={replyRef}
|
ref={replyRef}
|
||||||
value={replyText}
|
value={replyText}
|
||||||
@@ -207,7 +169,8 @@ export function ThreadView() {
|
|||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
placeholder="Write a reply..."
|
placeholder="Write a reply..."
|
||||||
rows={2}
|
rows={2}
|
||||||
className="w-full bg-transparent text-text text-[13px] placeholder:text-text-dim resize-none focus:outline-none"
|
className="w-full bg-transparent text-text text-[12px] placeholder:text-text-dim resize-none focus:outline-none"
|
||||||
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center justify-end gap-2 mt-1">
|
<div className="flex items-center justify-end gap-2 mt-1">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@@ -237,9 +200,9 @@ export function ThreadView() {
|
|||||||
</div>
|
</div>
|
||||||
<span className="text-text-dim text-[10px]">Ctrl+Enter</span>
|
<span className="text-text-dim text-[10px]">Ctrl+Enter</span>
|
||||||
<button
|
<button
|
||||||
onClick={handleReply}
|
onClick={handleRootReply}
|
||||||
disabled={!replyText.trim() || replying}
|
disabled={!replyText.trim() || replying}
|
||||||
className="px-3 py-1 text-[11px] bg-accent hover:bg-accent-hover text-white transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
className="px-2 py-0.5 text-[10px] bg-accent hover:bg-accent-hover text-white transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
{replySent ? "replied ✓" : replying ? "posting..." : "reply"}
|
{replySent ? "replied ✓" : replying ? "posting..." : "reply"}
|
||||||
</button>
|
</button>
|
||||||
@@ -248,7 +211,7 @@ export function ThreadView() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Thread tree (children of root) */}
|
{/* Thread tree (children of root) */}
|
||||||
{tree.children.length === 0 && (
|
{tree.children.length === 0 && !showRootReply && (
|
||||||
<div className="px-4 py-6 text-text-dim text-[12px] text-center">
|
<div className="px-4 py-6 text-text-dim text-[12px] text-center">
|
||||||
No replies yet.
|
No replies yet.
|
||||||
</div>
|
</div>
|
||||||
@@ -260,7 +223,8 @@ export function ThreadView() {
|
|||||||
<ThreadNodeComponent
|
<ThreadNodeComponent
|
||||||
key={child.event.id}
|
key={child.event.id}
|
||||||
node={child}
|
node={child}
|
||||||
onReplyInThread={handleReplyInThread}
|
rootEvent={rootEvent}
|
||||||
|
onReplyPublished={handleReplyPublished}
|
||||||
focusedId={focusedEvent.id}
|
focusedId={focusedEvent.id}
|
||||||
mutedPubkeys={mutedPubkeys}
|
mutedPubkeys={mutedPubkeys}
|
||||||
contentMatchesMutedKeyword={contentMatchesMutedKeyword}
|
contentMatchesMutedKeyword={contentMatchesMutedKeyword}
|
||||||
|
|||||||
Reference in New Issue
Block a user