mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-17 21:31:43 +01:00
move thread helpers out to applesauce
This commit is contained in:
parent
a0c9d91802
commit
aa2f2104f0
@ -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
10
pnpm-lock.yaml
generated
@ -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
|
||||
|
@ -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";
|
||||
|
@ -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 }) {
|
||||
|
@ -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";
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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]);
|
||||
|
||||
|
@ -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 }) {
|
||||
|
@ -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";
|
||||
|
@ -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"
|
||||
|
@ -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,
|
||||
|
@ -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[] }) {
|
||||
|
@ -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[] }) {
|
||||
|
@ -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";
|
||||
|
@ -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 (
|
||||
|
@ -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[] }) {
|
||||
|
@ -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";
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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}
|
||||
|
Loading…
x
Reference in New Issue
Block a user