From a9c7edb09d8ecd219678eced1fb80a8978b2bbf5 Mon Sep 17 00:00:00 2001 From: Martti Malmi Date: Tue, 27 Feb 2024 17:01:39 +0200 Subject: [PATCH] split Thread into smaller files --- packages/app/src/Components/Event/Thread.tsx | 350 ------------------ .../src/Components/Event/Thread/Divider.tsx | 12 + .../src/Components/Event/Thread/Subthread.tsx | 50 +++ .../Components/Event/{ => Thread}/Thread.css | 0 .../src/Components/Event/Thread/Thread.tsx | 125 +++++++ .../Components/Event/Thread/ThreadNote.tsx | 59 +++ .../Components/Event/Thread/ThreadRoute.tsx | 17 + .../src/Components/Event/Thread/TierThree.tsx | 71 ++++ .../src/Components/Event/Thread/TierTwo.tsx | 38 ++ .../app/src/Components/Event/Thread/util.ts | 9 + .../Spotlight/SpotlightThreadModal.tsx | 2 +- packages/app/src/Pages/NostrLinkHandler.tsx | 2 +- packages/app/src/index.tsx | 2 +- 13 files changed, 384 insertions(+), 353 deletions(-) delete mode 100644 packages/app/src/Components/Event/Thread.tsx create mode 100644 packages/app/src/Components/Event/Thread/Divider.tsx create mode 100644 packages/app/src/Components/Event/Thread/Subthread.tsx rename packages/app/src/Components/Event/{ => Thread}/Thread.css (100%) create mode 100644 packages/app/src/Components/Event/Thread/Thread.tsx create mode 100644 packages/app/src/Components/Event/Thread/ThreadNote.tsx create mode 100644 packages/app/src/Components/Event/Thread/ThreadRoute.tsx create mode 100644 packages/app/src/Components/Event/Thread/TierThree.tsx create mode 100644 packages/app/src/Components/Event/Thread/TierTwo.tsx create mode 100644 packages/app/src/Components/Event/Thread/util.ts diff --git a/packages/app/src/Components/Event/Thread.tsx b/packages/app/src/Components/Event/Thread.tsx deleted file mode 100644 index e1914508..00000000 --- a/packages/app/src/Components/Event/Thread.tsx +++ /dev/null @@ -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 ( -
-
-
- ); -}; - -interface SubthreadProps { - isLastSubthread?: boolean; - active: u256; - notes: readonly TaggedNostrEvent[]; - chains: Map>; - 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 ( - -
0 ? "subthread-multi" : ""}`}> - - 5} - /> -
-
- {replies.length > 0 && ( - - )} -
- ); - }; - - return
{notes.map(renderSubthread)}
; -}; - -interface ThreadNoteProps extends Omit { - 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 ( - <> -
- - 5} - /> -
-
- {replies.length > 0 && ( - - - - )} - - ); -}; - -const TierTwo = ({ active, isLastSubthread, notes, chains, onNavigate }: SubthreadProps) => { - const [first, ...rest] = notes; - - return ( - <> - - - {rest.map((r: TaggedNostrEvent, idx: number) => { - const lastReply = idx === rest.length - 1; - return ( - - ); - })} - - ); -}; - -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 ( - <> -
- - -
-
- - {replies.length > 0 && ( - - )} - - {rest.map((r: TaggedNostrEvent, idx: number) => { - const lastReply = idx === rest.length - 1; - const lastNote = isLastSubthread && lastReply; - return ( -
- - 5} - /> -
-
- ); - })} - - ); -}; - -export function ThreadRoute({ id }: { id?: string }) { - const params = useParams(); - const resolvedId = id ?? params.id; - const link = parseNostrLink(resolvedId ?? "", NostrPrefix.Note); - - return ( - - - - ); -} - -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 ( - - ); - } else { - return Loading thread root.. ({thread.data?.length} notes loaded); - } - } - - function renderChain(from: u256): ReactNode { - if (!from || thread.chains.size === 0) { - return; - } - const replies = thread.chains.get(from); - if (replies && thread.current) { - return ; - } - } - - 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 && ( -
-

Chains

-
-            {JSON.stringify(
-              Object.fromEntries([...thread.chains.entries()].map(([k, v]) => [k, v.map(c => c.id)])),
-              undefined,
-              "  ",
-            )}
-          
-

Current

-
{JSON.stringify(thread.current)}
-

Root

-
{JSON.stringify(thread.root, undefined, "  ")}
-

Data

-
{JSON.stringify(thread.data, undefined, "  ")}
-

Reactions

-
{JSON.stringify(thread.reactions, undefined, "  ")}
-
- )} - {parent && ( -
- -
- )} -
- {thread.root && renderRoot(thread.root)} - {thread.root && renderChain(chainKey(thread.root))} -
- - ); -} - -function getReplies(from: u256, chains?: Map>): Array { - if (!from || !chains) { - return []; - } - const replies = chains.get(from); - return replies ? replies : []; -} diff --git a/packages/app/src/Components/Event/Thread/Divider.tsx b/packages/app/src/Components/Event/Thread/Divider.tsx new file mode 100644 index 00000000..ae0069e4 --- /dev/null +++ b/packages/app/src/Components/Event/Thread/Divider.tsx @@ -0,0 +1,12 @@ +interface DividerProps { + variant?: "regular" | "small"; +} + +export const Divider = ({ variant = "regular" }: DividerProps) => { + const className = variant === "small" ? "divider divider-small" : "divider"; + return ( +
+
+
+ ); +}; diff --git a/packages/app/src/Components/Event/Thread/Subthread.tsx b/packages/app/src/Components/Event/Thread/Subthread.tsx new file mode 100644 index 00000000..e8767bd4 --- /dev/null +++ b/packages/app/src/Components/Event/Thread/Subthread.tsx @@ -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>; + 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 ( + +
0 ? "subthread-multi" : ""}`}> + + 5} + /> +
+
+ {replies.length > 0 && ( + + )} +
+ ); + }; + + return
{notes.map(renderSubthread)}
; +}; diff --git a/packages/app/src/Components/Event/Thread.css b/packages/app/src/Components/Event/Thread/Thread.css similarity index 100% rename from packages/app/src/Components/Event/Thread.css rename to packages/app/src/Components/Event/Thread/Thread.css diff --git a/packages/app/src/Components/Event/Thread/Thread.tsx b/packages/app/src/Components/Event/Thread/Thread.tsx new file mode 100644 index 00000000..a3285a17 --- /dev/null +++ b/packages/app/src/Components/Event/Thread/Thread.tsx @@ -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 ( + + ); + } else { + return Loading thread root.. ({thread.data?.length} notes loaded); + } + } + + function renderChain(from: u256): ReactNode { + if (!from || thread.chains.size === 0) { + return; + } + const replies = thread.chains.get(from); + if (replies && thread.current) { + return ; + } + } + + 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 && ( +
+

Chains

+
+            {JSON.stringify(
+              Object.fromEntries([...thread.chains.entries()].map(([k, v]) => [k, v.map(c => c.id)])),
+              undefined,
+              "  ",
+            )}
+          
+

Current

+
{JSON.stringify(thread.current)}
+

Root

+
{JSON.stringify(thread.root, undefined, "  ")}
+

Data

+
{JSON.stringify(thread.data, undefined, "  ")}
+

Reactions

+
{JSON.stringify(thread.reactions, undefined, "  ")}
+
+ )} + {parent && ( +
+ +
+ )} +
+ {thread.root && renderRoot(thread.root)} + {thread.root && renderChain(chainKey(thread.root))} +
+ + ); +} diff --git a/packages/app/src/Components/Event/Thread/ThreadNote.tsx b/packages/app/src/Components/Event/Thread/ThreadNote.tsx new file mode 100644 index 00000000..da610943 --- /dev/null +++ b/packages/app/src/Components/Event/Thread/ThreadNote.tsx @@ -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 { + 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 ( + <> +
+ + 5} + /> +
+
+ {replies.length > 0 && ( + + + + )} + + ); +}; diff --git a/packages/app/src/Components/Event/Thread/ThreadRoute.tsx b/packages/app/src/Components/Event/Thread/ThreadRoute.tsx new file mode 100644 index 00000000..6f767024 --- /dev/null +++ b/packages/app/src/Components/Event/Thread/ThreadRoute.tsx @@ -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 ( + + + + ); +} diff --git a/packages/app/src/Components/Event/Thread/TierThree.tsx b/packages/app/src/Components/Event/Thread/TierThree.tsx new file mode 100644 index 00000000..ee9b6021 --- /dev/null +++ b/packages/app/src/Components/Event/Thread/TierThree.tsx @@ -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 ( + <> +
+ + +
+
+ + {replies.length > 0 && ( + + )} + + {rest.map((r: TaggedNostrEvent, idx: number) => { + const lastReply = idx === rest.length - 1; + const lastNote = isLastSubthread && lastReply; + return ( +
+ + 5} + /> +
+
+ ); + })} + + ); +}; diff --git a/packages/app/src/Components/Event/Thread/TierTwo.tsx b/packages/app/src/Components/Event/Thread/TierTwo.tsx new file mode 100644 index 00000000..5c71d900 --- /dev/null +++ b/packages/app/src/Components/Event/Thread/TierTwo.tsx @@ -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 ( + <> + + + {rest.map((r: TaggedNostrEvent, idx: number) => { + const lastReply = idx === rest.length - 1; + return ( + + ); + })} + + ); +}; diff --git a/packages/app/src/Components/Event/Thread/util.ts b/packages/app/src/Components/Event/Thread/util.ts new file mode 100644 index 00000000..9cb988d5 --- /dev/null +++ b/packages/app/src/Components/Event/Thread/util.ts @@ -0,0 +1,9 @@ +import { TaggedNostrEvent, u256 } from "@snort/system"; + +export function getReplies(from: u256, chains?: Map>): Array { + if (!from || !chains) { + return []; + } + const replies = chains.get(from); + return replies ? replies : []; +} diff --git a/packages/app/src/Components/Spotlight/SpotlightThreadModal.tsx b/packages/app/src/Components/Spotlight/SpotlightThreadModal.tsx index e9554988..528002ae 100644 --- a/packages/app/src/Components/Spotlight/SpotlightThreadModal.tsx +++ b/packages/app/src/Components/Spotlight/SpotlightThreadModal.tsx @@ -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"; diff --git a/packages/app/src/Pages/NostrLinkHandler.tsx b/packages/app/src/Pages/NostrLinkHandler.tsx index 378d2435..8a0ddbd3 100644 --- a/packages/app/src/Pages/NostrLinkHandler.tsx +++ b/packages/app/src/Pages/NostrLinkHandler.tsx @@ -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"; diff --git a/packages/app/src/index.tsx b/packages/app/src/index.tsx index 91adc74e..233d5c9e 100644 --- a/packages/app/src/index.tsx +++ b/packages/app/src/index.tsx @@ -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";