diff --git a/src/helpers/nostr/corrections.ts b/src/helpers/nostr/corrections.ts new file mode 100644 index 000000000..c0750ef0b --- /dev/null +++ b/src/helpers/nostr/corrections.ts @@ -0,0 +1 @@ +export const CORRECTION_EVENT_KIND = 1010; diff --git a/src/hooks/use-thread-timeline-loader.ts b/src/hooks/use-thread-timeline-loader.ts index b08bab705..7e7067017 100644 --- a/src/hooks/use-thread-timeline-loader.ts +++ b/src/hooks/use-thread-timeline-loader.ts @@ -1,5 +1,5 @@ import { useEffect, useMemo } from "react"; -import { kinds } from "nostr-tools"; +import { kinds as eventKinds } from "nostr-tools"; import useSubject from "./use-subject"; import useSingleEvent from "./use-single-event"; @@ -12,21 +12,21 @@ import { unique } from "../helpers/array"; export default function useThreadTimelineLoader( focusedEvent: NostrEvent | undefined, relays: Iterable, - kind: number = kinds.ShortTextNote, + kinds?: number[], ) { const refs = focusedEvent && getThreadReferences(focusedEvent); const rootPointer = refs?.root?.e || (focusedEvent && { id: focusedEvent?.id }); const readRelays = unique([...relays, ...(rootPointer?.relays ?? [])]); - const timelineId = `${rootPointer?.id}-replies`; + const timelineId = `${rootPointer?.id}-thread`; const timeline = useTimelineLoader( timelineId, readRelays, rootPointer ? { "#e": [rootPointer.id], - kinds: [kind], + kinds: kinds ? (kinds.length > 0 ? kinds : undefined) : [eventKinds.ShortTextNote], } : undefined, ); diff --git a/src/views/thread/components/details-tabs.tsx b/src/views/thread/components/details-tabs.tsx index dd425f2c7..f849d22d9 100644 --- a/src/views/thread/components/details-tabs.tsx +++ b/src/views/thread/components/details-tabs.tsx @@ -1,22 +1,23 @@ -import { useState } from "react"; import { Button, Flex } from "@chakra-ui/react"; import { kinds } from "nostr-tools"; import { getEventUID } from "nostr-idb"; import styled from "@emotion/styled"; import { ThreadItem } from "../../../helpers/thread"; -import useEventCount from "../../../hooks/use-event-count"; import PostZapsTab from "./tabs/zaps"; -import { ThreadPost } from "./thread-post"; +import ThreadPost from "./thread-post"; import useEventZaps from "../../../hooks/use-event-zaps"; import PostReactionsTab from "./tabs/reactions"; -import useEventReactions from "../../../hooks/use-event-reactions"; import PostRepostsTab from "./tabs/reposts"; import PostQuotesTab from "./tabs/quotes"; import { useReadRelays } from "../../../hooks/use-client-relays"; import useTimelineLoader from "../../../hooks/use-timeline-loader"; import useSubject from "../../../hooks/use-subject"; import { getContentTagRefs } from "../../../helpers/nostr/event"; +import { CORRECTION_EVENT_KIND } from "../../../helpers/nostr/corrections"; +import CorrectionsTab from "./tabs/corrections"; +import useRouteStateValue from "../../../hooks/use-route-state-value"; +import UnknownTab from "./tabs/unknown"; const HiddenScrollbar = styled(Flex)` -ms-overflow-style: none; /* IE and Edge */ @@ -27,21 +28,36 @@ const HiddenScrollbar = styled(Flex)` `; export default function DetailsTabs({ post }: { post: ThreadItem }) { - const [selected, setSelected] = useState("replies"); - const repostCount = useEventCount({ "#e": [post.event.id], kinds: [kinds.Repost, kinds.GenericRepost] }); + const { value: selected, setValue: setSelected } = useRouteStateValue("tab", "replies"); const zaps = useEventZaps(getEventUID(post.event)); - const reactions = useEventReactions(getEventUID(post.event)) ?? []; const readRelays = useReadRelays(); - const timeline = useTimelineLoader(`${post.event.id}-quotes`, readRelays, { - kinds: [kinds.ShortTextNote], - "#e": [post.event.id], - }); + const timeline = useTimelineLoader(`${post.event.id}-thread-refs`, readRelays, { "#e": [post.event.id] }); const events = useSubject(timeline.timeline); + + const reactions = events.filter((e) => e.kind === kinds.Reaction); + const reposts = events.filter((e) => e.kind === kinds.Repost || e.kind === kinds.GenericRepost); const quotes = events.filter((e) => { - return getContentTagRefs(e.content, e.tags).some((t) => t[0] === "e" && t[1] === post.event.id); + return ( + e.kind === kinds.ShortTextNote && + getContentTagRefs(e.content, e.tags).some((t) => t[0] === "e" && t[1] === post.event.id) + ); }); + const corrections = events.filter((e) => { + return e.kind === CORRECTION_EVENT_KIND; + }); + + const unknown = events.filter( + (e) => + !post.replies.some((p) => p.event.id === e.id) && + e.kind !== kinds.ShortTextNote && + e.kind !== kinds.Zap && + !reactions.includes(e) && + !reposts.includes(e) && + !quotes.includes(e) && + !corrections.includes(e), + ); const renderContent = () => { switch (selected) { @@ -58,9 +74,13 @@ export default function DetailsTabs({ post }: { post: ThreadItem }) { case "reactions": return ; case "reposts": - return ; + return ; case "zaps": return ; + case "corrections": + return ; + case "unknown": + return ; } return null; }; @@ -84,14 +104,6 @@ export default function DetailsTabs({ post }: { post: ThreadItem }) { > Quotes{quotes.length > 0 ? ` (${quotes.length})` : ""} - + + {corrections.length > 0 && ( + + )} + {unknown.length > 0 && ( + + )} {renderContent()} diff --git a/src/views/thread/components/tabs/corrections.tsx b/src/views/thread/components/tabs/corrections.tsx new file mode 100644 index 000000000..b355af9b3 --- /dev/null +++ b/src/views/thread/components/tabs/corrections.tsx @@ -0,0 +1,15 @@ +import { NostrEvent } from "nostr-tools"; +import { Flex } from "@chakra-ui/react"; + +import { ThreadItem } from "../../../../helpers/thread"; +import CorrectionCard from "../../../tools/corrections/correction-card"; + +export default function CorrectionsTab({ post, corrections }: { post: ThreadItem; corrections: NostrEvent[] }) { + return ( + + {corrections.map((correction) => ( + + ))} + + ); +} diff --git a/src/views/thread/components/tabs/reposts.tsx b/src/views/thread/components/tabs/reposts.tsx index 8fd15899c..217d93a51 100644 --- a/src/views/thread/components/tabs/reposts.tsx +++ b/src/views/thread/components/tabs/reposts.tsx @@ -1,23 +1,12 @@ import { Flex, Text } from "@chakra-ui/react"; -import { kinds } from "nostr-tools"; +import { NostrEvent } from "nostr-tools"; import UserAvatarLink from "../../../../components/user/user-avatar-link"; import UserLink from "../../../../components/user/user-link"; -import useTimelineLoader from "../../../../hooks/use-timeline-loader"; -import { useReadRelays } from "../../../../hooks/use-client-relays"; -import useSubject from "../../../../hooks/use-subject"; import Timestamp from "../../../../components/timestamp"; import { ThreadItem } from "../../../../helpers/thread"; -export default function PostRepostsTab({ post }: { post: ThreadItem }) { - const readRelays = useReadRelays(); - const timeline = useTimelineLoader(`${post.event.id}-reposts`, readRelays, { - kinds: [kinds.Repost, kinds.GenericRepost], - "#e": [post.event.id], - }); - - const reposts = useSubject(timeline.timeline); - +export default function PostRepostsTab({ post, reposts }: { post: ThreadItem; reposts: NostrEvent[] }) { return ( {reposts.map((repost) => ( diff --git a/src/views/thread/components/tabs/unknown.tsx b/src/views/thread/components/tabs/unknown.tsx new file mode 100644 index 000000000..64444f916 --- /dev/null +++ b/src/views/thread/components/tabs/unknown.tsx @@ -0,0 +1,15 @@ +import { NostrEvent } from "nostr-tools"; +import { Flex } from "@chakra-ui/react"; + +import { ThreadItem } from "../../../../helpers/thread"; +import { EmbedEvent } from "../../../../components/embed-event"; + +export default function UnknownTab({ post, events }: { post: ThreadItem; events: NostrEvent[] }) { + return ( + + {events.map((event) => ( + + ))} + + ); +} diff --git a/src/views/thread/components/tabs/zaps.tsx b/src/views/thread/components/tabs/zaps.tsx index c19ae9e70..2aa66533a 100644 --- a/src/views/thread/components/tabs/zaps.tsx +++ b/src/views/thread/components/tabs/zaps.tsx @@ -8,33 +8,39 @@ import UserLink from "../../../../components/user/user-link"; import Timestamp from "../../../../components/timestamp"; import { LightningIcon } from "../../../../components/icons"; import { readablizeSats } from "../../../../helpers/bolt11"; +import TextNoteContents from "../../../../components/note/timeline-note/text-note-contents"; +import { TrustProvider } from "../../../../providers/local/trust"; const ZapEvent = memo(({ zap }: { zap: ParsedZap }) => { if (!zap.payment.amount) return null; return ( - <> + - + + + {readablizeSats(zap.payment.amount / 1000)} + + + - - {readablizeSats(zap.payment.amount / 1000)} - + + - - {zap.request.content && {zap.request.content}} - + ); }); export default function PostZapsTab({ post, zaps }: { post: ThreadItem; zaps: ParsedZap[] }) { return ( - {zaps.map((zap) => ( - - ))} + {Array.from(zaps) + .sort((a, b) => (b.payment.amount ?? 0) - (a.payment.amount ?? 0)) + .map((zap) => ( + + ))} ); } diff --git a/src/views/thread/components/thread-post.tsx b/src/views/thread/components/thread-post.tsx index ffac62375..750a5a0d9 100644 --- a/src/views/thread/components/thread-post.tsx +++ b/src/views/thread/components/thread-post.tsx @@ -38,7 +38,7 @@ export type ThreadItemProps = { level?: number; }; -export const ThreadPost = memo(({ post, initShowReplies, focusId, level = -1 }: ThreadItemProps) => { +function ThreadPost({ post, initShowReplies, focusId, level = -1 }: ThreadItemProps) { const { showReactions } = useAppSettings(); const expanded = useDisclosure({ defaultIsOpen: initShowReplies ?? (level < 2 || post.replies.length <= 1) }); const replyForm = useDisclosure(); @@ -163,4 +163,6 @@ export const ThreadPost = memo(({ post, initShowReplies, focusId, level = -1 }: )} ); -}); +} + +export default memo(ThreadPost); diff --git a/src/views/thread/index.tsx b/src/views/thread/index.tsx index b0bb2dff8..d02cfdce8 100644 --- a/src/views/thread/index.tsx +++ b/src/views/thread/index.tsx @@ -3,7 +3,7 @@ import { Card, Heading, Link, Spinner } from "@chakra-ui/react"; import { Link as RouterLink } from "react-router-dom"; import { nip19 } from "nostr-tools"; -import { ThreadPost } from "./components/thread-post"; +import ThreadPost from "./components/thread-post"; import VerticalPageLayout from "../../components/vertical-page-layout"; import { useReadRelays } from "../../hooks/use-client-relays"; import { ThreadItem, buildThread } from "../../helpers/thread"; @@ -18,6 +18,7 @@ import { getSharableEventAddress } from "../../helpers/nip19"; import UserAvatarLink from "../../components/user/user-avatar-link"; import { ReplyIcon } from "../../components/icons"; import TimelineNote from "../../components/note/timeline-note"; +import TimelineLoader from "../../classes/timeline-loader"; function CollapsedReplies({ pointer, diff --git a/src/views/tools/corrections/correction-card.tsx b/src/views/tools/corrections/correction-card.tsx index 447ec0c7e..b26c2f213 100644 --- a/src/views/tools/corrections/correction-card.tsx +++ b/src/views/tools/corrections/correction-card.tsx @@ -1,20 +1,26 @@ -import { Suspense, lazy, useMemo, useState } from "react"; +import { Suspense, useMemo, useState } from "react"; import { NostrEvent } from "nostr-tools"; -import { Button, ButtonGroup, Spinner, useColorMode } from "@chakra-ui/react"; +import { Button, ButtonGroup, Spinner } from "@chakra-ui/react"; import { isETag } from "../../../types/nostr-event"; import useSingleEvent from "../../../hooks/use-single-event"; import TimelineItem from "../../../components/timeline-page/generic-note-timeline/timeline-item"; import DiffViewer from "../../../components/diff/diff-viewer"; -export default function CorrectionCard({ correction }: { correction: NostrEvent }) { +export default function CorrectionCard({ + correction, + initView, +}: { + correction: NostrEvent; + initView?: "original" | "modified" | "diff"; +}) { const originalId = correction.tags.find(isETag)?.[1]; const original = useSingleEvent(originalId); // NOTE: produces an invalid event const modified = useMemo(() => original && { ...original, content: correction.content }, [correction, original]); - const [show, setShow] = useState("modified"); + const [show, setShow] = useState(initView || "modified"); const showEvent = show === "original" ? original : modified; return ( diff --git a/src/views/tools/corrections/index.tsx b/src/views/tools/corrections/index.tsx index 945f230f8..6ece18632 100644 --- a/src/views/tools/corrections/index.tsx +++ b/src/views/tools/corrections/index.tsx @@ -8,6 +8,7 @@ import PeopleListProvider, { usePeopleListContext } from "../../../providers/loc import BackButton from "../../../components/router/back-button"; import PeopleListSelection from "../../../components/people-list-selection/people-list-selection"; import CorrectionCard from "./correction-card"; +import { CORRECTION_EVENT_KIND } from "../../../helpers/nostr/corrections"; function CorrectionsPage() { const { listId, filter } = usePeopleListContext(); @@ -15,7 +16,7 @@ function CorrectionsPage() { const timeline = useTimelineLoader( `${listId}-corrections`, readRelays, - filter ? [{ kinds: [1010], ...filter }] : undefined, + filter ? [{ kinds: [CORRECTION_EVENT_KIND], ...filter }] : undefined, ); const corrections = useSubject(timeline.timeline); diff --git a/src/views/torrents/components/torrents-comments.tsx b/src/views/torrents/components/torrents-comments.tsx index 47d641bf2..ca890ac82 100644 --- a/src/views/torrents/components/torrents-comments.tsx +++ b/src/views/torrents/components/torrents-comments.tsx @@ -155,7 +155,7 @@ export const ThreadPost = memo(({ post, level = -1 }: { post: ThreadItem; level? export default function TorrentComments({ torrent }: { torrent: NostrEvent }) { const readRelays = useReadRelays(); - const { timeline, events } = useThreadTimelineLoader(torrent, readRelays, TORRENT_COMMENT_KIND); + const { timeline, events } = useThreadTimelineLoader(torrent, readRelays, [TORRENT_COMMENT_KIND]); const thread = useMemo(() => buildThread(events), [events]); const rootItem = thread.get(torrent.id);