mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-20 13:01:07 +02:00
show corrections and unknown events under thread
This commit is contained in:
1
src/helpers/nostr/corrections.ts
Normal file
1
src/helpers/nostr/corrections.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const CORRECTION_EVENT_KIND = 1010;
|
@@ -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,
|
||||
);
|
||||
|
@@ -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()}
|
||||
|
15
src/views/thread/components/tabs/corrections.tsx
Normal file
15
src/views/thread/components/tabs/corrections.tsx
Normal 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>
|
||||
);
|
||||
}
|
@@ -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) => (
|
||||
|
15
src/views/thread/components/tabs/unknown.tsx
Normal file
15
src/views/thread/components/tabs/unknown.tsx
Normal 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>
|
||||
);
|
||||
}
|
@@ -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>
|
||||
);
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -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,
|
||||
|
@@ -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 (
|
||||
|
@@ -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);
|
||||
|
@@ -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);
|
||||
|
Reference in New Issue
Block a user