show corrections and unknown events under thread

This commit is contained in:
hzrd149
2024-05-03 09:12:37 -05:00
parent 35e3cedbe9
commit 3c57aca4a9
12 changed files with 135 additions and 58 deletions

View File

@@ -0,0 +1 @@
export const CORRECTION_EVENT_KIND = 1010;

View File

@@ -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<string>,
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,
);

View File

@@ -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 <PostReactionsTab post={post} reactions={reactions} />;
case "reposts":
return <PostRepostsTab post={post} />;
return <PostRepostsTab post={post} reposts={reposts} />;
case "zaps":
return <PostZapsTab post={post} zaps={zaps} />;
case "corrections":
return <CorrectionsTab post={post} corrections={corrections} />;
case "unknown":
return <UnknownTab post={post} events={unknown} />;
}
return null;
};
@@ -84,14 +104,6 @@ export default function DetailsTabs({ post }: { post: ThreadItem }) {
>
Quotes{quotes.length > 0 ? ` (${quotes.length})` : ""}
</Button>
<Button
size="sm"
flexShrink={0}
variant={selected === "reposts" ? "solid" : "outline"}
onClick={() => setSelected("reposts")}
>
Reposts{repostCount && repostCount > 0 ? ` (${repostCount})` : ""}
</Button>
<Button
size="sm"
flexShrink={0}
@@ -100,14 +112,43 @@ export default function DetailsTabs({ post }: { post: ThreadItem }) {
>
Zaps{zaps.length > 0 ? ` (${zaps.length})` : ""}
</Button>
<Button
size="sm"
flexShrink={0}
variant={selected === "reposts" ? "solid" : "outline"}
onClick={() => setSelected("reposts")}
>
Reposts{reposts.length && reposts.length > 0 ? ` (${reposts.length})` : ""}
</Button>
<Button
size="sm"
flexShrink={0}
variant={selected === "reactions" ? "solid" : "outline"}
onClick={() => setSelected("reactions")}
mr="auto"
>
Reactions{reactions.length > 0 ? ` (${reactions.length})` : ""}
</Button>
{corrections.length > 0 && (
<Button
size="sm"
flexShrink={0}
variant={selected === "corrections" ? "solid" : "outline"}
onClick={() => setSelected("corrections")}
>
Corrections ({corrections.length})
</Button>
)}
{unknown.length > 0 && (
<Button
size="sm"
flexShrink={0}
variant={selected === "unknown" ? "solid" : "outline"}
onClick={() => setSelected("unknown")}
>
Unknown Refs ({unknown.length})
</Button>
)}
</HiddenScrollbar>
{renderContent()}

View File

@@ -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 (
<Flex gap="2" direction="column">
{corrections.map((correction) => (
<CorrectionCard correction={correction} key={correction.id} initView="diff" />
))}
</Flex>
);
}

View File

@@ -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 (
<Flex direction="column" gap="2" px="2">
{reposts.map((repost) => (

View File

@@ -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 (
<Flex gap="2" direction="column">
{events.map((event) => (
<EmbedEvent event={event} key={event.id} />
))}
</Flex>
);
}

View File

@@ -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 (
<>
<TrustProvider event={zap.request}>
<Flex gap="2">
<UserAvatarLink pubkey={zap.request.pubkey} size="sm" />
<Flex direction="column" alignItems="center" minW="10">
<LightningIcon color="yellow.500" boxSize={5} />
<Text>{readablizeSats(zap.payment.amount / 1000)}</Text>
</Flex>
<UserAvatarLink pubkey={zap.request.pubkey} size="sm" ml="2" />
<Box>
<UserLink pubkey={zap.request.pubkey} fontWeight="bold" />
<Text>
<LightningIcon color="yellow.500" /> {readablizeSats(zap.payment.amount / 1000)}
</Text>
<Timestamp timestamp={zap.event.created_at} ml="2" />
<TextNoteContents event={zap.request} />
</Box>
<Timestamp timestamp={zap.event.created_at} />
</Flex>
{zap.request.content && <Text>{zap.request.content}</Text>}
</>
</TrustProvider>
);
});
export default function PostZapsTab({ post, zaps }: { post: ThreadItem; zaps: ParsedZap[] }) {
return (
<Flex px="2" direction="column" gap="2" mb="2">
{zaps.map((zap) => (
<ZapEvent key={zap.event.id} zap={zap} />
))}
{Array.from(zaps)
.sort((a, b) => (b.payment.amount ?? 0) - (a.payment.amount ?? 0))
.map((zap) => (
<ZapEvent key={zap.event.id} zap={zap} />
))}
</Flex>
);
}

View File

@@ -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);

View File

@@ -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,

View File

@@ -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 (

View File

@@ -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);

View File

@@ -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);