move thread helpers out to applesauce

This commit is contained in:
hzrd149 2024-09-30 13:24:55 -05:00
parent a0c9d91802
commit aa2f2104f0
25 changed files with 104 additions and 176 deletions

View File

@ -42,7 +42,7 @@
"@uiw/codemirror-theme-github": "^4.23.0",
"@uiw/react-codemirror": "^4.23.0",
"@webscopeio/react-textarea-autocomplete": "^4.9.2",
"applesauce-core": "^0.3.0",
"applesauce-core": "^0.4.0",
"bech32": "^2.0.0",
"blossom-client-sdk": "^0.7.0",
"blossom-drive-sdk": "^0.4.0",

10
pnpm-lock.yaml generated
View File

@ -91,8 +91,8 @@ importers:
specifier: ^4.9.2
version: 4.9.2(prop-types@15.8.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
applesauce-core:
specifier: ^0.3.0
version: 0.3.0(typescript@5.6.2)
specifier: ^0.4.0
version: 0.4.0(typescript@5.6.2)
bech32:
specifier: ^2.0.0
version: 2.0.0
@ -2280,8 +2280,8 @@ packages:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
applesauce-core@0.3.0:
resolution: {integrity: sha512-7lZCj1ei3lRdGM40umlQHe4rovP3HAeC3JdQg/YrSQgXRvfWM2s95jgMIY1KcfDmHo07CNctD3Z5xzdFlqr+Mw==}
applesauce-core@0.4.0:
resolution: {integrity: sha512-UvzmM+mh9D5g697NHJv6x9bzkXk64uaiFLiIJR5Yb+FpYYOsIRJZGT73fVmgVC8EAWkItZ9jYS25IUi4a0cgrg==}
argparse@1.0.10:
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
@ -7209,7 +7209,7 @@ snapshots:
dependencies:
color-convert: 2.0.1
applesauce-core@0.3.0(typescript@5.6.2):
applesauce-core@0.4.0(typescript@5.6.2):
dependencies:
'@types/zen-push': 0.1.4
debug: 4.3.7

View File

@ -19,7 +19,8 @@ import {
Text,
} from "@chakra-ui/react";
import { NostrEvent, kinds, nip19 } from "nostr-tools";
import { encodeDecodeResult } from "../../helpers/nip19";
import { encodeDecodeResult } from "applesauce-core/helpers";
import { ExternalLinkIcon } from "../icons";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import useSingleEvent from "../../hooks/use-single-event";

View File

@ -1,5 +1,6 @@
import { Box, ButtonGroup, Card, CardBody, CardHeader, CardProps, LinkBox, Text } from "@chakra-ui/react";
import { useMemo } from "react";
import { Box, ButtonGroup, Card, CardBody, CardHeader, CardProps, LinkBox, Text } from "@chakra-ui/react";
import { getPointerFromTag } from "applesauce-core/helpers";
import { NostrEvent } from "../../../types/nostr-event";
import UserLink from "../../user/user-link";
@ -10,7 +11,6 @@ import UserAvatar from "../../user/user-avatar";
import { LightningIcon } from "../../icons";
import { readablizeSats } from "../../../helpers/bolt11";
import ZapReceiptMenu from "../../zap/zap-receipt-menu";
import { getPointerFromTag } from "../../../helpers/nip19";
import { EmbedEventPointer } from "../index";
export default function EmbeddedZapRecept({ zap, ...props }: Omit<CardProps, "children"> & { zap: NostrEvent }) {

View File

@ -5,7 +5,6 @@ import {
ButtonGroup,
Heading,
Input,
Link,
Modal,
ModalBody,
ModalCloseButton,
@ -19,10 +18,10 @@ import {
} from "@chakra-ui/react";
import { nip19 } from "nostr-tools";
import { useSet } from "react-use";
import { encodeDecodeResult } from "applesauce-core/helpers";
import { ExternalLinkIcon, SearchIcon } from "./icons";
import UserLink from "./user/user-link";
import { encodeDecodeResult } from "../helpers/nip19";
import relayPoolService from "../services/relay-pool";
import { isValidRelayURL } from "../helpers/relay";

View File

@ -152,7 +152,7 @@ export function TimelineNote({
</ExpandProvider>
{replyForm.isOpen && (
<ReplyForm
item={{ event, replies: [], refs: getThreadReferences(event) }}
item={{ event, replies: new Set(), refs: getThreadReferences(event) }}
onCancel={replyForm.onClose}
onSubmitted={replyForm.onClose}
/>

View File

@ -22,6 +22,13 @@ export function safeDecode(str: string) {
} catch (e) {}
}
export function normalizeToHexPubkey(hex: string) {
if (isHexKey(hex)) return hex;
const decode = safeDecode(hex);
if (!decode) return null;
return getPubkeyFromDecodeResult(decode) ?? null;
}
export function getPubkeyFromDecodeResult(result?: nip19.DecodeResult) {
if (!result) return;
switch (result.type) {
@ -34,58 +41,3 @@ export function getPubkeyFromDecodeResult(result?: nip19.DecodeResult) {
return getPublicKey(result.data);
}
}
export function normalizeToHexPubkey(hex: string) {
if (isHexKey(hex)) return hex;
const decode = safeDecode(hex);
if (!decode) return null;
return getPubkeyFromDecodeResult(decode) ?? null;
}
export function encodeDecodeResult(result: nip19.DecodeResult) {
switch (result.type) {
case "naddr":
return nip19.naddrEncode(result.data);
case "nprofile":
return nip19.nprofileEncode(result.data);
case "nevent":
return nip19.neventEncode(result.data);
case "nrelay":
return nip19.nrelayEncode(result.data);
case "nsec":
return nip19.nsecEncode(result.data);
case "npub":
return nip19.npubEncode(result.data);
case "note":
return nip19.noteEncode(result.data);
}
}
export function getPointerFromTag(tag: Tag): nip19.DecodeResult | null {
switch (tag[0]) {
case "e": {
if (!tag[1]) return null;
const pointer: nip19.DecodeResult = { type: "nevent", data: { id: tag[1] } };
if (tag[2]) pointer.data.relays = [tag[2]];
return pointer;
}
case "a": {
const parsed = parseCoordinate(tag[1]);
if (!parsed?.identifier) return null;
const pointer: nip19.DecodeResult = {
type: "naddr",
data: { pubkey: parsed.pubkey, identifier: parsed.identifier, kind: parsed.kind },
};
if (tag[2]) pointer.data.relays = [tag[2]];
return pointer;
}
case "p": {
const [_, pubkey, relay] = tag;
if (!pubkey) return null;
return { type: "nprofile", data: { pubkey, relays: relay ? [relay] : undefined } };
}
}
return null;
}

View File

@ -1,7 +1,8 @@
import dayjs from "dayjs";
import { NostrEvent, isRTag } from "../../types/nostr-event";
import { DecodeResult } from "nostr-tools/nip19";
import { getPointerFromTag } from "../nip19";
import { getPointerFromTag } from "applesauce-core/helpers";
import { NostrEvent, isRTag } from "../../types/nostr-event";
export const GOAL_KIND = 9041;

View File

@ -1,10 +1,10 @@
import dayjs from "dayjs";
import { EventTemplate, NostrEvent, kinds, nip19 } from "nostr-tools";
import { getPointerFromTag } from "applesauce-core/helpers";
import { PTag, isATag, isDTag, isPTag, isRTag } from "../../types/nostr-event";
import { getEventCoordinate, replaceOrAddSimpleTag } from "./event";
import { getRelayVariations, safeRelayUrls } from "../relay";
import { getPointerFromTag } from "../nip19";
export const MUTE_LIST_KIND = kinds.Mutelist;
export const PIN_LIST_KIND = kinds.Pinlist;

View File

@ -1,23 +1,13 @@
import { NostrEvent } from "../types/nostr-event";
import { getNip10References, ThreadReferences } from "./nostr/threading";
import { ThreadItem } from "applesauce-core/queries";
import { sortByDate } from "./nostr/event";
export function countReplies(replies: ThreadItem[]): number {
return replies.reduce((c, item) => c + countReplies(item.replies), 0) + replies.length;
export function countReplies(replies: Set<ThreadItem> | ThreadItem[]): number {
return (
Array.from(replies).reduce((c, item) => c + countReplies(item.replies), 0) +
(Array.isArray(replies) ? replies.length : replies.size)
);
}
export type ThreadItem = {
/** underlying nostr event */
event: NostrEvent;
/** the thread root, according to this event */
root?: ThreadItem;
/** the parent event this is replying to */
replyingTo?: ThreadItem;
/** refs from nostr event */
refs: ThreadReferences;
/** direct child replies */
replies: ThreadItem[];
};
/** Returns an array of all pubkeys participating in the thread */
export function getThreadMembers(item: ThreadItem, omit?: string) {
const pubkeys = new Set<string>();
@ -25,41 +15,13 @@ export function getThreadMembers(item: ThreadItem, omit?: string) {
let next = item;
while (true) {
if (next.event.pubkey !== omit) pubkeys.add(next.event.pubkey);
if (!next.replyingTo) break;
else next = next.replyingTo;
if (!next.parent) break;
else next = next.parent;
}
return Array.from(pubkeys);
}
export function buildThread(events: NostrEvent[]) {
const idToChildren: Record<string, NostrEvent[]> = {};
const replies = new Map<string, ThreadItem>();
for (const event of events) {
const refs = getNip10References(event);
if (refs.reply?.e) {
idToChildren[refs.reply.e.id] = idToChildren[refs.reply.e.id] || [];
idToChildren[refs.reply.e.id].push(event);
}
replies.set(event.id, {
event,
refs,
replies: [],
});
}
for (const [id, reply] of replies) {
reply.root = reply.refs.root?.e ? replies.get(reply.refs.root.e.id) : undefined;
reply.replyingTo = reply.refs.reply?.e ? replies.get(reply.refs.reply.e.id) : undefined;
reply.replies = idToChildren[id]?.map((e) => replies.get(e.id) as ThreadItem) ?? [];
reply.replies.sort((a, b) => b.event.created_at - a.event.created_at);
}
return replies;
export function repliesByDate(item: ThreadItem) {
return Array.from(item.replies).sort((a, b) => sortByDate(a.event, b.event));
}

View File

@ -2,14 +2,19 @@ import { useState } from "react";
import { isStateful } from "applesauce-core/observable";
import { useIsomorphicLayoutEffect } from "react-use";
import Observable from "zen-observable";
import { useForceUpdate } from "@chakra-ui/react";
export function useObservable<T>(observable?: Observable<T>): T | undefined {
const forceUpdate = useForceUpdate();
const [value, update] = useState<T | undefined>(observable && isStateful(observable) ? observable.value : undefined);
useIsomorphicLayoutEffect(() => {
if (!observable) return;
const s = observable.subscribe(update);
const s = observable.subscribe((v) => {
update(v);
forceUpdate();
});
return () => s.unsubscribe();
}, [observable]);

View File

@ -1,7 +1,8 @@
import { encodeDecodeResult } from "applesauce-core/helpers";
import { EmbedEventPointer } from "../../../components/embed-event";
import { getGoalEventPointers, getGoalLinks } from "../../../helpers/nostr/goal";
import { NostrEvent } from "../../../types/nostr-event";
import { encodeDecodeResult } from "../../../helpers/nip19";
import OpenGraphCard from "../../../components/open-graph/open-graph-card";
export default function GoalContents({ goal }: { goal: NostrEvent }) {

View File

@ -2,6 +2,7 @@ import { useNavigate } from "react-router-dom";
import { kinds, nip19 } from "nostr-tools";
import type { DecodeResult } from "nostr-tools/nip19";
import { Box, Button, Flex, Heading, SimpleGrid, Spacer, Spinner, Text } from "@chakra-ui/react";
import { encodeDecodeResult } from "applesauce-core/helpers";
import UserLink from "../../../components/user/user-link";
import { ChevronLeftIcon } from "../../../components/icons";
@ -26,7 +27,6 @@ import ListFeedButton from "../components/list-feed-button";
import VerticalPageLayout from "../../../components/vertical-page-layout";
import { COMMUNITY_DEFINITION_KIND } from "../../../helpers/nostr/communities";
import { EmbedEvent, EmbedEventPointer } from "../../../components/embed-event";
import { encodeDecodeResult } from "../../../helpers/nip19";
import useSingleEvent from "../../../hooks/use-single-event";
import UserAvatarLink from "../../../components/user/user-avatar-link";
import useParamsAddressPointer from "../../../hooks/use-params-address-pointer";

View File

@ -2,8 +2,8 @@ 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 "applesauce-core/queries";
import { ThreadItem } from "../../../helpers/thread";
import PostZapsTab from "./tabs/zaps";
import ThreadPost from "./thread-post";
import useEventZaps from "../../../hooks/use-event-zaps";
@ -18,6 +18,7 @@ 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";
import { repliesByDate } from "../../../helpers/thread";
const HiddenScrollbar = styled(Flex)`
-ms-overflow-style: none; /* IE and Edge */
@ -50,7 +51,7 @@ export default function DetailsTabs({ post }: { post: ThreadItem }) {
const unknown = events.filter(
(e) =>
!post.replies.some((p) => p.event.id === e.id) &&
!Array.from(post.replies).some((p) => p.event.id === e.id) &&
e.kind !== kinds.ShortTextNote &&
e.kind !== kinds.Zap &&
!reactions.includes(e) &&
@ -64,7 +65,7 @@ export default function DetailsTabs({ post }: { post: ThreadItem }) {
case "replies":
return (
<Flex direction="column" gap="2" pl={{ base: 2, md: 4 }}>
{post.replies.map((child) => (
{repliesByDate(post).map((child) => (
<ThreadPost key={child.event.id} post={child} focusId={undefined} level={0} />
))}
</Flex>
@ -94,7 +95,7 @@ export default function DetailsTabs({ post }: { post: ThreadItem }) {
variant={selected === "replies" ? "solid" : "outline"}
onClick={() => setSelected("replies")}
>
Replies{post.replies.length > 0 ? ` (${post.replies.length})` : ""}
Replies{post.replies.size > 0 ? ` (${post.replies.size})` : ""}
</Button>
<Button
size="sm"

View File

@ -3,11 +3,12 @@ import { Box, Button, ButtonGroup, Flex, IconButton, VisuallyHiddenInput } from
import { useForm } from "react-hook-form";
import { useThrottle } from "react-use";
import { kinds } from "nostr-tools";
import { ThreadItem } from "applesauce-core/queries";
import dayjs from "dayjs";
import { NostrEvent } from "../../../types/nostr-event";
import { UserAvatarStack } from "../../../components/compact-user-stack";
import { ThreadItem, getThreadMembers } from "../../../helpers/thread";
import { getThreadMembers } from "../../../helpers/thread";
import {
addReplyTags,
createEmojiTags,

View File

@ -1,7 +1,7 @@
import { NostrEvent } from "nostr-tools";
import { Flex } from "@chakra-ui/react";
import { ThreadItem } from "applesauce-core/queries";
import { ThreadItem } from "../../../../helpers/thread";
import CorrectionCard from "../../../tools/corrections/correction-card";
export default function CorrectionsTab({ post, corrections }: { post: ThreadItem; corrections: NostrEvent[] }) {

View File

@ -1,7 +1,7 @@
import { NostrEvent } from "nostr-tools";
import { Flex } from "@chakra-ui/react";
import { ThreadItem } from "applesauce-core/queries";
import { ThreadItem } from "../../../../helpers/thread";
import { TimelineNote } from "../../../../components/note/timeline-note";
export default function PostQuotesTab({ post, quotes }: { post: ThreadItem; quotes: NostrEvent[] }) {

View File

@ -1,8 +1,8 @@
import { useMemo } from "react";
import { Box, Button, Divider, Flex, SimpleGrid, SimpleGridProps, useDisclosure } from "@chakra-ui/react";
import { NostrEvent } from "nostr-tools";
import { ThreadItem } from "applesauce-core/queries";
import { ThreadItem } from "../../../../helpers/thread";
import { groupReactions } from "../../../../helpers/nostr/reactions";
import ReactionIcon from "../../../../components/event-reactions/reaction-icon";
import UserLink from "../../../../components/user/user-link";

View File

@ -1,10 +1,10 @@
import { Flex, Text } from "@chakra-ui/react";
import { NostrEvent } from "nostr-tools";
import { ThreadItem } from "applesauce-core/queries";
import UserAvatarLink from "../../../../components/user/user-avatar-link";
import UserLink from "../../../../components/user/user-link";
import Timestamp from "../../../../components/timestamp";
import { ThreadItem } from "../../../../helpers/thread";
export default function PostRepostsTab({ post, reposts }: { post: ThreadItem; reposts: NostrEvent[] }) {
return (

View File

@ -1,7 +1,7 @@
import { NostrEvent } from "nostr-tools";
import { Flex } from "@chakra-ui/react";
import { ThreadItem } from "applesauce-core/queries";
import { ThreadItem } from "../../../../helpers/thread";
import { EmbedEvent } from "../../../../components/embed-event";
export default function UnknownTab({ post, events }: { post: ThreadItem; events: NostrEvent[] }) {

View File

@ -1,7 +1,7 @@
import { memo } from "react";
import { Box, ButtonGroup, Flex, Text } from "@chakra-ui/react";
import { ThreadItem } from "applesauce-core/queries";
import { ThreadItem } from "../../../../helpers/thread";
import { ParsedZap } from "../../../../helpers/nostr/zaps";
import UserAvatarLink from "../../../../components/user/user-avatar-link";
import UserLink from "../../../../components/user/user-link";

View File

@ -1,10 +1,11 @@
import { memo, useState } from "react";
import { Alert, AlertIcon, Button, ButtonGroup, Flex, IconButton, Link, Spacer, useDisclosure } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { ThreadItem } from "applesauce-core/queries";
import ReplyForm from "./reply-form";
import { ReplyIcon } from "../../../components/icons";
import { countReplies, ThreadItem } from "../../../helpers/thread";
import { countReplies, repliesByDate } from "../../../helpers/thread";
import { TrustProvider } from "../../../providers/local/trust-provider";
import useClientSideMuteFilter from "../../../hooks/use-client-side-mute-filter";
import UserAvatarLink from "../../../components/user/user-avatar-link";
@ -31,6 +32,7 @@ import DetailsTabs from "./details-tabs";
import useEventIntersectionRef from "../../../hooks/use-event-intersection-ref";
import relayHintService from "../../../services/event-relay-hint";
import NotePublishedUsing from "../../../components/note/note-published-using";
import { sortByDate } from "../../../helpers/nostr/event";
export type ThreadItemProps = {
post: ThreadItem;
@ -41,13 +43,13 @@ export type ThreadItemProps = {
function ThreadPost({ post, initShowReplies, focusId, level = -1 }: ThreadItemProps) {
const { showReactions } = useAppSettings();
const expanded = useDisclosure({ defaultIsOpen: initShowReplies ?? (level < 2 || post.replies.length <= 1) });
const expanded = useDisclosure({ defaultIsOpen: initShowReplies ?? (level < 2 || post.replies.size <= 1) });
const replyForm = useDisclosure();
const muteFilter = useClientSideMuteFilter();
const isFocused = level === -1;
const replies = post.replies.filter((r) => !muteFilter(r.event));
const replies = Array.from(post.replies).filter((r) => !muteFilter(r.event));
const numberOfReplies = countReplies(replies);
const isMuted = muteFilter(post.event);
@ -162,9 +164,9 @@ function ThreadPost({ post, initShowReplies, focusId, level = -1 }: ThreadItemPr
<DetailsTabs post={post} />
) : (
expanded.isOpen &&
post.replies.length > 0 && (
post.replies.size > 0 && (
<Flex direction="column" gap="2" pl={{ base: 2, md: 4 }}>
{post.replies.map((child) => (
{repliesByDate(post).map((child) => (
<ThreadPost key={child.event.id} post={child} focusId={focusId} level={level + 1} />
))}
</Flex>

View File

@ -1,12 +1,12 @@
import { ReactNode, useMemo } from "react";
import { ReactNode } from "react";
import { Card, Heading, Link, Spinner } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { Thread, ThreadQuery } from "applesauce-core/queries";
import { nip19 } from "nostr-tools";
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";
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import useThreadTimelineLoader from "../../hooks/use-thread-timeline-loader";
@ -18,6 +18,7 @@ import UserAvatarLink from "../../components/user/user-avatar-link";
import { ReplyIcon } from "../../components/icons";
import TimelineNote from "../../components/note/timeline-note";
import relayHintService from "../../services/event-relay-hint";
import { useStoreQuery } from "../../hooks/use-store-query";
function CollapsedReplies({
pointer,
@ -25,10 +26,10 @@ function CollapsedReplies({
root,
}: {
pointer: nip19.EventPointer;
thread: Map<string, ThreadItem>;
thread: Thread;
root: nip19.EventPointer;
}) {
const post = thread.get(pointer.id);
const post = thread.all.get(pointer.id);
if (!post) return <LoadingNostrLink link={{ type: "nevent", data: pointer }} />;
let reply: ReactNode = null;
@ -56,30 +57,29 @@ function ThreadPage({
rootPointer,
focusId,
}: {
thread: Map<string, ThreadItem>;
thread: Thread;
rootPointer: nip19.EventPointer;
focusId: string;
}) {
const isRoot = rootPointer.id === focusId;
const focusedPost = thread.get(focusId);
const rootPost = thread.get(rootPointer.id);
if (isRoot && rootPost) {
return <ThreadPost post={rootPost} initShowReplies focusId={focusId} />;
const focusedPost = thread.all.get(focusId);
if (isRoot && thread.root) {
return <ThreadPost post={thread.root} initShowReplies focusId={focusId} />;
}
if (!focusedPost) return null;
const parentPosts = [];
if (focusedPost.replyingTo) {
if (focusedPost.parent) {
let p = focusedPost;
while (p.replyingTo) {
parentPosts.unshift(p.replyingTo);
p = p.replyingTo;
while (p.parent) {
parentPosts.unshift(p.parent);
p = p.parent;
}
}
const grandparentPointer = focusedPost.replyingTo?.refs.reply?.e;
const grandparentPointer = focusedPost.parent?.refs.reply?.e;
return (
<>
@ -89,8 +89,8 @@ function ThreadPage({
{grandparentPointer && grandparentPointer.id !== rootPointer.id && (
<CollapsedReplies pointer={grandparentPointer} thread={thread} root={rootPointer} />
)}
{focusedPost.replyingTo ? (
<TimelineNote event={focusedPost.replyingTo.event} hideDrawerButton showReplyLine={false} />
{focusedPost.parent ? (
<TimelineNote event={focusedPost.parent.event} hideDrawerButton showReplyLine={false} />
) : (
focusedPost.refs.reply?.e && <LoadingNostrLink link={{ type: "nevent", data: focusedPost.refs.reply.e }} />
)}
@ -104,8 +104,8 @@ export default function ThreadView() {
const readRelays = useReadRelays(pointer.relays);
const focusedEvent = useSingleEvent(pointer.id, pointer.relays);
const { rootPointer, events, timeline } = useThreadTimelineLoader(focusedEvent, readRelays);
const thread = useMemo(() => buildThread(events), [events]);
const { rootPointer, timeline } = useThreadTimelineLoader(focusedEvent, readRelays);
const thread = useStoreQuery(ThreadQuery, rootPointer && [rootPointer]);
const callback = useTimelineCurserIntersectionCallback(timeline);
@ -120,7 +120,7 @@ export default function ThreadView() {
</>
)}
<IntersectionObserverProvider callback={callback}>
{focusedEvent && rootPointer && (
{thread && focusedEvent && rootPointer && (
<ThreadPage thread={thread} rootPointer={rootPointer} focusId={focusedEvent.id} />
)}
</IntersectionObserverProvider>

View File

@ -1,13 +1,4 @@
import { memo, useMemo, useState } from "react";
import { NostrEvent } from "../../../types/nostr-event";
import { TORRENT_COMMENT_KIND } from "../../../helpers/nostr/torrents";
import { useReadRelays } from "../../../hooks/use-client-relays";
import useThreadTimelineLoader from "../../../hooks/use-thread-timeline-loader";
import { ThreadItem, buildThread, countReplies } from "../../../helpers/thread";
import { useTimelineCurserIntersectionCallback } from "../../../hooks/use-timeline-cursor-intersection-callback";
import IntersectionObserverProvider from "../../../providers/local/intersection-observer";
import useAppSettings from "../../../hooks/use-app-settings";
import { memo, useState } from "react";
import {
Alert,
AlertIcon,
@ -19,6 +10,16 @@ import {
useBreakpointValue,
useDisclosure,
} from "@chakra-ui/react";
import { ThreadItem, ThreadQuery } from "applesauce-core/queries";
import { NostrEvent } from "../../../types/nostr-event";
import { TORRENT_COMMENT_KIND } from "../../../helpers/nostr/torrents";
import { useReadRelays } from "../../../hooks/use-client-relays";
import useThreadTimelineLoader from "../../../hooks/use-thread-timeline-loader";
import { countReplies, repliesByDate } from "../../../helpers/thread";
import { useTimelineCurserIntersectionCallback } from "../../../hooks/use-timeline-cursor-intersection-callback";
import IntersectionObserverProvider from "../../../providers/local/intersection-observer";
import useAppSettings from "../../../hooks/use-app-settings";
import useClientSideMuteFilter from "../../../hooks/use-client-side-mute-filter";
import UserAvatarLink from "../../../components/user/user-avatar-link";
import UserLink from "../../../components/user/user-link";
@ -35,15 +36,17 @@ import NoteReactions from "../../../components/note/timeline-note/components/not
import NoteZapButton from "../../../components/note/note-zap-button";
import { TextNoteContents } from "../../../components/note/timeline-note/text-note-contents";
import useEventIntersectionRef from "../../../hooks/use-event-intersection-ref";
import { useStoreQuery } from "../../../hooks/use-store-query";
import { sortByDate } from "../../../helpers/nostr/event";
export const ThreadPost = memo(({ post, level = -1 }: { post: ThreadItem; level?: number }) => {
const { showReactions } = useAppSettings();
const expanded = useDisclosure({ defaultIsOpen: level < 2 || post.replies.length <= 1 });
const expanded = useDisclosure({ defaultIsOpen: level < 2 || post.replies.size <= 1 });
const replyForm = useDisclosure();
const muteFilter = useClientSideMuteFilter();
const replies = post.replies.filter((r) => !muteFilter(r.event));
const replies = Array.from(post.replies).filter((r) => !muteFilter(r.event));
const numberOfReplies = countReplies(replies);
const isMuted = muteFilter(post.event);
@ -140,9 +143,9 @@ export const ThreadPost = memo(({ post, level = -1 }: { post: ThreadItem; level?
replyKind={TORRENT_COMMENT_KIND}
/>
)}
{post.replies.length > 0 && expanded.isOpen && (
{post.replies.size > 0 && expanded.isOpen && (
<Flex direction="column" gap="2" pl={{ base: 2, md: 4 }}>
{post.replies.map((child) => (
{repliesByDate(post).map((child) => (
<ThreadPost key={child.event.id} post={child} level={level + 1} />
))}
</Flex>
@ -153,16 +156,16 @@ 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 } = useThreadTimelineLoader(torrent, readRelays, [TORRENT_COMMENT_KIND]);
const thread = useMemo(() => buildThread(events), [events]);
const rootItem = thread.get(torrent.id);
const thread = useStoreQuery(ThreadQuery, [torrent.id]);
const callback = useTimelineCurserIntersectionCallback(timeline);
return (
<IntersectionObserverProvider callback={callback}>
{rootItem?.replies.map((item) => <ThreadPost key={item.event.id} post={item} level={0} />)}
{thread?.root &&
repliesByDate(thread.root).map((item) => <ThreadPost key={item.event.id} post={item} level={0} />)}
</IntersectionObserverProvider>
);
}

View File

@ -127,7 +127,7 @@ function TorrentDetailsPage({ torrent }: { torrent: NostrEvent }) {
</Flex>
{replyForm.isOpen && (
<ReplyForm
item={{ event: torrent, refs: getThreadReferences(torrent), replies: [] }}
item={{ event: torrent, refs: getThreadReferences(torrent), replies: new Set() }}
onCancel={replyForm.onClose}
onSubmitted={replyForm.onClose}
replyKind={TORRENT_COMMENT_KIND}