split Thread into smaller files
This commit is contained in:
parent
a5532b23f3
commit
a9c7edb09d
@ -1,350 +0,0 @@
|
||||
import "./Thread.css";
|
||||
|
||||
import { EventExt, NostrPrefix, parseNostrLink, TaggedNostrEvent, u256 } from "@snort/system";
|
||||
import classNames from "classnames";
|
||||
import { Fragment, ReactNode, useCallback, useContext, useMemo, useState } from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
import BackButton from "@/Components/Button/BackButton";
|
||||
import Collapsed from "@/Components/Collapsed";
|
||||
import Note from "@/Components/Event/EventComponent";
|
||||
import NoteGhost from "@/Components/Event/Note/NoteGhost";
|
||||
import { chainKey } from "@/Utils/Thread/ChainKey";
|
||||
import { ThreadContext } from "@/Utils/Thread/ThreadContext";
|
||||
import { ThreadContextWrapper } from "@/Utils/Thread/ThreadContextWrapper";
|
||||
|
||||
import messages from "../messages";
|
||||
|
||||
interface DividerProps {
|
||||
variant?: "regular" | "small";
|
||||
}
|
||||
|
||||
const Divider = ({ variant = "regular" }: DividerProps) => {
|
||||
const className = variant === "small" ? "divider divider-small" : "divider";
|
||||
return (
|
||||
<div className="divider-container">
|
||||
<div className={className}></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface SubthreadProps {
|
||||
isLastSubthread?: boolean;
|
||||
active: u256;
|
||||
notes: readonly TaggedNostrEvent[];
|
||||
chains: Map<u256, Array<TaggedNostrEvent>>;
|
||||
onNavigate: (e: TaggedNostrEvent) => void;
|
||||
}
|
||||
|
||||
const Subthread = ({ active, notes, chains, onNavigate }: SubthreadProps) => {
|
||||
const renderSubthread = (a: TaggedNostrEvent, idx: number) => {
|
||||
const isLastSubthread = idx === notes.length - 1;
|
||||
const replies = getReplies(a.id, chains);
|
||||
return (
|
||||
<Fragment key={a.id}>
|
||||
<div className={`subthread-container ${replies.length > 0 ? "subthread-multi" : ""}`}>
|
||||
<Divider />
|
||||
<Note
|
||||
highlight={active === a.id}
|
||||
className={`thread-note ${isLastSubthread && replies.length === 0 ? "is-last-note" : ""}`}
|
||||
data={a}
|
||||
key={a.id}
|
||||
onClick={onNavigate}
|
||||
threadChains={chains}
|
||||
waitUntilInView={idx > 5}
|
||||
/>
|
||||
<div className="line-container"></div>
|
||||
</div>
|
||||
{replies.length > 0 && (
|
||||
<TierTwo
|
||||
active={active}
|
||||
isLastSubthread={isLastSubthread}
|
||||
notes={replies}
|
||||
chains={chains}
|
||||
onNavigate={onNavigate}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
return <div className="subthread">{notes.map(renderSubthread)}</div>;
|
||||
};
|
||||
|
||||
interface ThreadNoteProps extends Omit<SubthreadProps, "notes"> {
|
||||
note: TaggedNostrEvent;
|
||||
isLast: boolean;
|
||||
idx: number;
|
||||
}
|
||||
|
||||
const ThreadNote = ({ active, note, isLast, isLastSubthread, chains, onNavigate, idx }: ThreadNoteProps) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const replies = getReplies(note.id, chains);
|
||||
const activeInReplies = replies.map(r => r.id).includes(active);
|
||||
const [collapsed, setCollapsed] = useState(!activeInReplies);
|
||||
const hasMultipleNotes = replies.length > 1;
|
||||
const isLastVisibleNote = isLastSubthread && isLast && !hasMultipleNotes;
|
||||
const className = classNames(
|
||||
"subthread-container",
|
||||
isLast && collapsed ? "subthread-last" : "subthread-multi subthread-mid",
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<div className={className}>
|
||||
<Divider variant="small" />
|
||||
<Note
|
||||
highlight={active === note.id}
|
||||
className={classNames("thread-note", { "is-last-note": isLastVisibleNote })}
|
||||
data={note}
|
||||
key={note.id}
|
||||
onClick={onNavigate}
|
||||
threadChains={chains}
|
||||
waitUntilInView={idx > 5}
|
||||
/>
|
||||
<div className="line-container"></div>
|
||||
</div>
|
||||
{replies.length > 0 && (
|
||||
<Collapsed text={formatMessage(messages.ShowReplies)} collapsed={collapsed} setCollapsed={setCollapsed}>
|
||||
<TierThree
|
||||
active={active}
|
||||
isLastSubthread={isLastSubthread}
|
||||
notes={replies}
|
||||
chains={chains}
|
||||
onNavigate={onNavigate}
|
||||
/>
|
||||
</Collapsed>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const TierTwo = ({ active, isLastSubthread, notes, chains, onNavigate }: SubthreadProps) => {
|
||||
const [first, ...rest] = notes;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ThreadNote
|
||||
active={active}
|
||||
onNavigate={onNavigate}
|
||||
note={first}
|
||||
chains={chains}
|
||||
isLastSubthread={isLastSubthread}
|
||||
isLast={rest.length === 0}
|
||||
idx={0}
|
||||
/>
|
||||
|
||||
{rest.map((r: TaggedNostrEvent, idx: number) => {
|
||||
const lastReply = idx === rest.length - 1;
|
||||
return (
|
||||
<ThreadNote
|
||||
key={r.id}
|
||||
active={active}
|
||||
onNavigate={onNavigate}
|
||||
note={r}
|
||||
chains={chains}
|
||||
isLastSubthread={isLastSubthread}
|
||||
isLast={lastReply}
|
||||
idx={idx}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const TierThree = ({ active, isLastSubthread, notes, chains, onNavigate }: SubthreadProps) => {
|
||||
const [first, ...rest] = notes;
|
||||
const replies = getReplies(first.id, chains);
|
||||
const hasMultipleNotes = rest.length > 0 || replies.length > 0;
|
||||
const isLast = replies.length === 0 && rest.length === 0;
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={classNames("subthread-container", {
|
||||
"subthread-multi": hasMultipleNotes,
|
||||
"subthread-last": isLast,
|
||||
"subthread-mid": !isLast,
|
||||
})}>
|
||||
<Divider variant="small" />
|
||||
<Note
|
||||
highlight={active === first.id}
|
||||
className={classNames("thread-note", { "is-last-note": isLastSubthread && isLast })}
|
||||
data={first}
|
||||
key={first.id}
|
||||
threadChains={chains}
|
||||
waitUntilInView={true}
|
||||
/>
|
||||
<div className="line-container"></div>
|
||||
</div>
|
||||
|
||||
{replies.length > 0 && (
|
||||
<TierThree
|
||||
active={active}
|
||||
isLastSubthread={isLastSubthread}
|
||||
notes={replies}
|
||||
chains={chains}
|
||||
onNavigate={onNavigate}
|
||||
/>
|
||||
)}
|
||||
|
||||
{rest.map((r: TaggedNostrEvent, idx: number) => {
|
||||
const lastReply = idx === rest.length - 1;
|
||||
const lastNote = isLastSubthread && lastReply;
|
||||
return (
|
||||
<div
|
||||
key={r.id}
|
||||
className={classNames("subthread-container", {
|
||||
"subthread-multi": !lastReply,
|
||||
"subthread-last": !lastReply,
|
||||
"subthread-mid": lastReply,
|
||||
})}>
|
||||
<Divider variant="small" />
|
||||
<Note
|
||||
className={classNames("thread-note", { "is-last-note": lastNote })}
|
||||
highlight={active === r.id}
|
||||
data={r}
|
||||
key={r.id}
|
||||
onClick={onNavigate}
|
||||
threadChains={chains}
|
||||
waitUntilInView={idx > 5}
|
||||
/>
|
||||
<div className="line-container"></div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export function ThreadRoute({ id }: { id?: string }) {
|
||||
const params = useParams();
|
||||
const resolvedId = id ?? params.id;
|
||||
const link = parseNostrLink(resolvedId ?? "", NostrPrefix.Note);
|
||||
|
||||
return (
|
||||
<ThreadContextWrapper link={link}>
|
||||
<Thread />
|
||||
</ThreadContextWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export function Thread(props: { onBack?: () => void; disableSpotlight?: boolean }) {
|
||||
const thread = useContext(ThreadContext);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const isSingleNote = thread.chains?.size === 1 && [thread.chains.values].every(v => v.length === 0);
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const rootOptions = useMemo(
|
||||
() => ({ showReactionsLink: true, showMediaSpotlight: !props.disableSpotlight, isRoot: true }),
|
||||
[props.disableSpotlight],
|
||||
);
|
||||
|
||||
const navigateThread = useCallback(
|
||||
(e: TaggedNostrEvent) => {
|
||||
thread.setCurrent(e.id);
|
||||
// navigate(`/${NostrLink.fromEvent(e).encode()}`, { replace: true });
|
||||
},
|
||||
[thread],
|
||||
);
|
||||
|
||||
const parent = useMemo(() => {
|
||||
if (thread.root) {
|
||||
const currentThread = EventExt.extractThread(thread.root);
|
||||
return (
|
||||
currentThread?.replyTo?.value ??
|
||||
currentThread?.root?.value ??
|
||||
(currentThread?.root?.key === "a" && currentThread.root?.value)
|
||||
);
|
||||
}
|
||||
}, [thread.root]);
|
||||
|
||||
function renderRoot(note: TaggedNostrEvent) {
|
||||
const className = `thread-root${isSingleNote ? " thread-root-single" : ""}`;
|
||||
if (note) {
|
||||
return (
|
||||
<Note
|
||||
className={className}
|
||||
key={note.id}
|
||||
data={note}
|
||||
options={rootOptions}
|
||||
onClick={navigateThread}
|
||||
threadChains={thread.chains}
|
||||
waitUntilInView={false}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return <NoteGhost className={className}>Loading thread root.. ({thread.data?.length} notes loaded)</NoteGhost>;
|
||||
}
|
||||
}
|
||||
|
||||
function renderChain(from: u256): ReactNode {
|
||||
if (!from || thread.chains.size === 0) {
|
||||
return;
|
||||
}
|
||||
const replies = thread.chains.get(from);
|
||||
if (replies && thread.current) {
|
||||
return <Subthread active={thread.current} notes={replies} chains={thread.chains} onNavigate={navigateThread} />;
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
if (parent) {
|
||||
thread.setCurrent(parent);
|
||||
} else if (props.onBack) {
|
||||
props.onBack();
|
||||
} else {
|
||||
navigate(-1);
|
||||
}
|
||||
}
|
||||
|
||||
const parentText = formatMessage({
|
||||
defaultMessage: "Parent",
|
||||
id: "ADmfQT",
|
||||
description: "Link to parent note in thread",
|
||||
});
|
||||
|
||||
const debug = window.location.search.includes("debug=true");
|
||||
return (
|
||||
<>
|
||||
{debug && (
|
||||
<div className="main-content p xs">
|
||||
<h1>Chains</h1>
|
||||
<pre>
|
||||
{JSON.stringify(
|
||||
Object.fromEntries([...thread.chains.entries()].map(([k, v]) => [k, v.map(c => c.id)])),
|
||||
undefined,
|
||||
" ",
|
||||
)}
|
||||
</pre>
|
||||
<h1>Current</h1>
|
||||
<pre>{JSON.stringify(thread.current)}</pre>
|
||||
<h1>Root</h1>
|
||||
<pre>{JSON.stringify(thread.root, undefined, " ")}</pre>
|
||||
<h1>Data</h1>
|
||||
<pre>{JSON.stringify(thread.data, undefined, " ")}</pre>
|
||||
<h1>Reactions</h1>
|
||||
<pre>{JSON.stringify(thread.reactions, undefined, " ")}</pre>
|
||||
</div>
|
||||
)}
|
||||
{parent && (
|
||||
<div className="main-content p">
|
||||
<BackButton onClick={goBack} text={parentText} />
|
||||
</div>
|
||||
)}
|
||||
<div className="main-content">
|
||||
{thread.root && renderRoot(thread.root)}
|
||||
{thread.root && renderChain(chainKey(thread.root))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function getReplies(from: u256, chains?: Map<u256, Array<TaggedNostrEvent>>): Array<TaggedNostrEvent> {
|
||||
if (!from || !chains) {
|
||||
return [];
|
||||
}
|
||||
const replies = chains.get(from);
|
||||
return replies ? replies : [];
|
||||
}
|
12
packages/app/src/Components/Event/Thread/Divider.tsx
Normal file
12
packages/app/src/Components/Event/Thread/Divider.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
interface DividerProps {
|
||||
variant?: "regular" | "small";
|
||||
}
|
||||
|
||||
export const Divider = ({ variant = "regular" }: DividerProps) => {
|
||||
const className = variant === "small" ? "divider divider-small" : "divider";
|
||||
return (
|
||||
<div className="divider-container">
|
||||
<div className={className}></div>
|
||||
</div>
|
||||
);
|
||||
};
|
50
packages/app/src/Components/Event/Thread/Subthread.tsx
Normal file
50
packages/app/src/Components/Event/Thread/Subthread.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
import { TaggedNostrEvent, u256 } from "@snort/system";
|
||||
import { Fragment } from "react";
|
||||
|
||||
import Note from "@/Components/Event/EventComponent";
|
||||
import { Divider } from "@/Components/Event/Thread/Divider";
|
||||
import { TierTwo } from "@/Components/Event/Thread/TierTwo";
|
||||
import { getReplies } from "@/Components/Event/Thread/util";
|
||||
|
||||
export interface SubthreadProps {
|
||||
isLastSubthread?: boolean;
|
||||
active: u256;
|
||||
notes: readonly TaggedNostrEvent[];
|
||||
chains: Map<u256, Array<TaggedNostrEvent>>;
|
||||
onNavigate: (e: TaggedNostrEvent) => void;
|
||||
}
|
||||
|
||||
export const Subthread = ({ active, notes, chains, onNavigate }: SubthreadProps) => {
|
||||
const renderSubthread = (a: TaggedNostrEvent, idx: number) => {
|
||||
const isLastSubthread = idx === notes.length - 1;
|
||||
const replies = getReplies(a.id, chains);
|
||||
return (
|
||||
<Fragment key={a.id}>
|
||||
<div className={`subthread-container ${replies.length > 0 ? "subthread-multi" : ""}`}>
|
||||
<Divider />
|
||||
<Note
|
||||
highlight={active === a.id}
|
||||
className={`thread-note ${isLastSubthread && replies.length === 0 ? "is-last-note" : ""}`}
|
||||
data={a}
|
||||
key={a.id}
|
||||
onClick={onNavigate}
|
||||
threadChains={chains}
|
||||
waitUntilInView={idx > 5}
|
||||
/>
|
||||
<div className="line-container"></div>
|
||||
</div>
|
||||
{replies.length > 0 && (
|
||||
<TierTwo
|
||||
active={active}
|
||||
isLastSubthread={isLastSubthread}
|
||||
notes={replies}
|
||||
chains={chains}
|
||||
onNavigate={onNavigate}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
return <div className="subthread">{notes.map(renderSubthread)}</div>;
|
||||
};
|
125
packages/app/src/Components/Event/Thread/Thread.tsx
Normal file
125
packages/app/src/Components/Event/Thread/Thread.tsx
Normal file
@ -0,0 +1,125 @@
|
||||
import "./Thread.css";
|
||||
|
||||
import { EventExt, TaggedNostrEvent, u256 } from "@snort/system";
|
||||
import { ReactNode, useCallback, useContext, useMemo } from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import BackButton from "@/Components/Button/BackButton";
|
||||
import Note from "@/Components/Event/EventComponent";
|
||||
import NoteGhost from "@/Components/Event/Note/NoteGhost";
|
||||
import { Subthread } from "@/Components/Event/Thread/Subthread";
|
||||
import { chainKey } from "@/Utils/Thread/ChainKey";
|
||||
import { ThreadContext } from "@/Utils/Thread/ThreadContext";
|
||||
|
||||
export function Thread(props: { onBack?: () => void; disableSpotlight?: boolean }) {
|
||||
const thread = useContext(ThreadContext);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const isSingleNote = thread.chains?.size === 1 && [thread.chains.values].every(v => v.length === 0);
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const rootOptions = useMemo(
|
||||
() => ({ showReactionsLink: true, showMediaSpotlight: !props.disableSpotlight, isRoot: true }),
|
||||
[props.disableSpotlight],
|
||||
);
|
||||
|
||||
const navigateThread = useCallback(
|
||||
(e: TaggedNostrEvent) => {
|
||||
thread.setCurrent(e.id);
|
||||
// navigate(`/${NostrLink.fromEvent(e).encode()}`, { replace: true });
|
||||
},
|
||||
[thread],
|
||||
);
|
||||
|
||||
const parent = useMemo(() => {
|
||||
if (thread.root) {
|
||||
const currentThread = EventExt.extractThread(thread.root);
|
||||
return (
|
||||
currentThread?.replyTo?.value ??
|
||||
currentThread?.root?.value ??
|
||||
(currentThread?.root?.key === "a" && currentThread.root?.value)
|
||||
);
|
||||
}
|
||||
}, [thread.root]);
|
||||
|
||||
function renderRoot(note: TaggedNostrEvent) {
|
||||
const className = `thread-root${isSingleNote ? " thread-root-single" : ""}`;
|
||||
if (note) {
|
||||
return (
|
||||
<Note
|
||||
className={className}
|
||||
key={note.id}
|
||||
data={note}
|
||||
options={rootOptions}
|
||||
onClick={navigateThread}
|
||||
threadChains={thread.chains}
|
||||
waitUntilInView={false}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return <NoteGhost className={className}>Loading thread root.. ({thread.data?.length} notes loaded)</NoteGhost>;
|
||||
}
|
||||
}
|
||||
|
||||
function renderChain(from: u256): ReactNode {
|
||||
if (!from || thread.chains.size === 0) {
|
||||
return;
|
||||
}
|
||||
const replies = thread.chains.get(from);
|
||||
if (replies && thread.current) {
|
||||
return <Subthread active={thread.current} notes={replies} chains={thread.chains} onNavigate={navigateThread} />;
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
if (parent) {
|
||||
thread.setCurrent(parent);
|
||||
} else if (props.onBack) {
|
||||
props.onBack();
|
||||
} else {
|
||||
navigate(-1);
|
||||
}
|
||||
}
|
||||
|
||||
const parentText = formatMessage({
|
||||
defaultMessage: "Parent",
|
||||
id: "ADmfQT",
|
||||
description: "Link to parent note in thread",
|
||||
});
|
||||
|
||||
const debug = window.location.search.includes("debug=true");
|
||||
return (
|
||||
<>
|
||||
{debug && (
|
||||
<div className="main-content p xs">
|
||||
<h1>Chains</h1>
|
||||
<pre>
|
||||
{JSON.stringify(
|
||||
Object.fromEntries([...thread.chains.entries()].map(([k, v]) => [k, v.map(c => c.id)])),
|
||||
undefined,
|
||||
" ",
|
||||
)}
|
||||
</pre>
|
||||
<h1>Current</h1>
|
||||
<pre>{JSON.stringify(thread.current)}</pre>
|
||||
<h1>Root</h1>
|
||||
<pre>{JSON.stringify(thread.root, undefined, " ")}</pre>
|
||||
<h1>Data</h1>
|
||||
<pre>{JSON.stringify(thread.data, undefined, " ")}</pre>
|
||||
<h1>Reactions</h1>
|
||||
<pre>{JSON.stringify(thread.reactions, undefined, " ")}</pre>
|
||||
</div>
|
||||
)}
|
||||
{parent && (
|
||||
<div className="main-content p">
|
||||
<BackButton onClick={goBack} text={parentText} />
|
||||
</div>
|
||||
)}
|
||||
<div className="main-content">
|
||||
{thread.root && renderRoot(thread.root)}
|
||||
{thread.root && renderChain(chainKey(thread.root))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
59
packages/app/src/Components/Event/Thread/ThreadNote.tsx
Normal file
59
packages/app/src/Components/Event/Thread/ThreadNote.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import { TaggedNostrEvent } from "@snort/system";
|
||||
import classNames from "classnames";
|
||||
import { useState } from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import Collapsed from "@/Components/Collapsed";
|
||||
import Note from "@/Components/Event/EventComponent";
|
||||
import { Divider } from "@/Components/Event/Thread/Divider";
|
||||
import { SubthreadProps } from "@/Components/Event/Thread/Subthread";
|
||||
import { TierThree } from "@/Components/Event/Thread/TierThree";
|
||||
import { getReplies } from "@/Components/Event/Thread/util";
|
||||
import messages from "@/Components/messages";
|
||||
|
||||
interface ThreadNoteProps extends Omit<SubthreadProps, "notes"> {
|
||||
note: TaggedNostrEvent;
|
||||
isLast: boolean;
|
||||
idx: number;
|
||||
}
|
||||
|
||||
export const ThreadNote = ({ active, note, isLast, isLastSubthread, chains, onNavigate, idx }: ThreadNoteProps) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const replies = getReplies(note.id, chains);
|
||||
const activeInReplies = replies.map(r => r.id).includes(active);
|
||||
const [collapsed, setCollapsed] = useState(!activeInReplies);
|
||||
const hasMultipleNotes = replies.length > 1;
|
||||
const isLastVisibleNote = isLastSubthread && isLast && !hasMultipleNotes;
|
||||
const className = classNames(
|
||||
"subthread-container",
|
||||
isLast && collapsed ? "subthread-last" : "subthread-multi subthread-mid",
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<div className={className}>
|
||||
<Divider variant="small" />
|
||||
<Note
|
||||
highlight={active === note.id}
|
||||
className={classNames("thread-note", { "is-last-note": isLastVisibleNote })}
|
||||
data={note}
|
||||
key={note.id}
|
||||
onClick={onNavigate}
|
||||
threadChains={chains}
|
||||
waitUntilInView={idx > 5}
|
||||
/>
|
||||
<div className="line-container"></div>
|
||||
</div>
|
||||
{replies.length > 0 && (
|
||||
<Collapsed text={formatMessage(messages.ShowReplies)} collapsed={collapsed} setCollapsed={setCollapsed}>
|
||||
<TierThree
|
||||
active={active}
|
||||
isLastSubthread={isLastSubthread}
|
||||
notes={replies}
|
||||
chains={chains}
|
||||
onNavigate={onNavigate}
|
||||
/>
|
||||
</Collapsed>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
17
packages/app/src/Components/Event/Thread/ThreadRoute.tsx
Normal file
17
packages/app/src/Components/Event/Thread/ThreadRoute.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { NostrPrefix, parseNostrLink } from "@snort/system";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
import { Thread } from "@/Components/Event/Thread/Thread";
|
||||
import { ThreadContextWrapper } from "@/Utils/Thread/ThreadContextWrapper";
|
||||
|
||||
export function ThreadRoute({ id }: { id?: string }) {
|
||||
const params = useParams();
|
||||
const resolvedId = id ?? params.id;
|
||||
const link = parseNostrLink(resolvedId ?? "", NostrPrefix.Note);
|
||||
|
||||
return (
|
||||
<ThreadContextWrapper link={link}>
|
||||
<Thread />
|
||||
</ThreadContextWrapper>
|
||||
);
|
||||
}
|
71
packages/app/src/Components/Event/Thread/TierThree.tsx
Normal file
71
packages/app/src/Components/Event/Thread/TierThree.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
import { TaggedNostrEvent } from "@snort/system";
|
||||
import classNames from "classnames";
|
||||
|
||||
import Note from "@/Components/Event/EventComponent";
|
||||
import { Divider } from "@/Components/Event/Thread/Divider";
|
||||
import { SubthreadProps } from "@/Components/Event/Thread/Subthread";
|
||||
import { getReplies } from "@/Components/Event/Thread/util";
|
||||
|
||||
export const TierThree = ({ active, isLastSubthread, notes, chains, onNavigate }: SubthreadProps) => {
|
||||
const [first, ...rest] = notes;
|
||||
const replies = getReplies(first.id, chains);
|
||||
const hasMultipleNotes = rest.length > 0 || replies.length > 0;
|
||||
const isLast = replies.length === 0 && rest.length === 0;
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={classNames("subthread-container", {
|
||||
"subthread-multi": hasMultipleNotes,
|
||||
"subthread-last": isLast,
|
||||
"subthread-mid": !isLast,
|
||||
})}>
|
||||
<Divider variant="small" />
|
||||
<Note
|
||||
highlight={active === first.id}
|
||||
className={classNames("thread-note", { "is-last-note": isLastSubthread && isLast })}
|
||||
data={first}
|
||||
key={first.id}
|
||||
threadChains={chains}
|
||||
waitUntilInView={true}
|
||||
/>
|
||||
<div className="line-container"></div>
|
||||
</div>
|
||||
|
||||
{replies.length > 0 && (
|
||||
<TierThree
|
||||
active={active}
|
||||
isLastSubthread={isLastSubthread}
|
||||
notes={replies}
|
||||
chains={chains}
|
||||
onNavigate={onNavigate}
|
||||
/>
|
||||
)}
|
||||
|
||||
{rest.map((r: TaggedNostrEvent, idx: number) => {
|
||||
const lastReply = idx === rest.length - 1;
|
||||
const lastNote = isLastSubthread && lastReply;
|
||||
return (
|
||||
<div
|
||||
key={r.id}
|
||||
className={classNames("subthread-container", {
|
||||
"subthread-multi": !lastReply,
|
||||
"subthread-last": !lastReply,
|
||||
"subthread-mid": lastReply,
|
||||
})}>
|
||||
<Divider variant="small" />
|
||||
<Note
|
||||
className={classNames("thread-note", { "is-last-note": lastNote })}
|
||||
highlight={active === r.id}
|
||||
data={r}
|
||||
key={r.id}
|
||||
onClick={onNavigate}
|
||||
threadChains={chains}
|
||||
waitUntilInView={idx > 5}
|
||||
/>
|
||||
<div className="line-container"></div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
38
packages/app/src/Components/Event/Thread/TierTwo.tsx
Normal file
38
packages/app/src/Components/Event/Thread/TierTwo.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import { TaggedNostrEvent } from "@snort/system";
|
||||
|
||||
import { SubthreadProps } from "@/Components/Event/Thread/Subthread";
|
||||
import { ThreadNote } from "@/Components/Event/Thread/ThreadNote";
|
||||
|
||||
export const TierTwo = ({ active, isLastSubthread, notes, chains, onNavigate }: SubthreadProps) => {
|
||||
const [first, ...rest] = notes;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ThreadNote
|
||||
active={active}
|
||||
onNavigate={onNavigate}
|
||||
note={first}
|
||||
chains={chains}
|
||||
isLastSubthread={isLastSubthread}
|
||||
isLast={rest.length === 0}
|
||||
idx={0}
|
||||
/>
|
||||
|
||||
{rest.map((r: TaggedNostrEvent, idx: number) => {
|
||||
const lastReply = idx === rest.length - 1;
|
||||
return (
|
||||
<ThreadNote
|
||||
key={r.id}
|
||||
active={active}
|
||||
onNavigate={onNavigate}
|
||||
note={r}
|
||||
chains={chains}
|
||||
isLastSubthread={isLastSubthread}
|
||||
isLast={lastReply}
|
||||
idx={idx}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
9
packages/app/src/Components/Event/Thread/util.ts
Normal file
9
packages/app/src/Components/Event/Thread/util.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { TaggedNostrEvent, u256 } from "@snort/system";
|
||||
|
||||
export function getReplies(from: u256, chains?: Map<u256, Array<TaggedNostrEvent>>): Array<TaggedNostrEvent> {
|
||||
if (!from || !chains) {
|
||||
return [];
|
||||
}
|
||||
const replies = chains.get(from);
|
||||
return replies ? replies : [];
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||
|
||||
import { Thread } from "@/Components/Event/Thread";
|
||||
import { Thread } from "@/Components/Event/Thread/Thread";
|
||||
import Modal from "@/Components/Modal/Modal";
|
||||
import { SpotlightMedia } from "@/Components/Spotlight/SpotlightMedia";
|
||||
import getEventMedia from "@/Utils/getEventMedia";
|
||||
|
@ -4,7 +4,7 @@ import React, { useEffect, useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { useLocation, useParams } from "react-router-dom";
|
||||
|
||||
import { ThreadRoute } from "@/Components/Event/Thread";
|
||||
import { ThreadRoute } from "@/Components/Event/Thread/ThreadRoute";
|
||||
import { GenericFeed } from "@/Components/Feed/Generic";
|
||||
import Spinner from "@/Components/Icons/Spinner";
|
||||
import ProfilePage from "@/Pages/Profile/ProfilePage";
|
||||
|
@ -8,7 +8,7 @@ import * as ReactDOM from "react-dom/client";
|
||||
import { createBrowserRouter, RouteObject, RouterProvider } from "react-router-dom";
|
||||
|
||||
import { initRelayWorker, preload, UserCache } from "@/Cache";
|
||||
import { ThreadRoute } from "@/Components/Event/Thread";
|
||||
import { ThreadRoute } from "@/Components/Event/Thread/ThreadRoute";
|
||||
import { IntlProvider } from "@/Components/IntlProvider/IntlProvider";
|
||||
import { db } from "@/Db";
|
||||
import { addCachedMetadataToFuzzySearch } from "@/Db/FuzzySearch";
|
||||
|
Loading…
x
Reference in New Issue
Block a user