diff --git a/package.json b/package.json index 5a5afe724..2e811ad57 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c39e2b1bf..7d26fec7a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/components/app-handler-modal/index.tsx b/src/components/app-handler-modal/index.tsx index 71dfe214b..2d65814b0 100644 --- a/src/components/app-handler-modal/index.tsx +++ b/src/components/app-handler-modal/index.tsx @@ -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"; diff --git a/src/components/embed-event/event-types/embedded-zap-receipt.tsx b/src/components/embed-event/event-types/embedded-zap-receipt.tsx index b68b2d7b8..a436a6fd1 100644 --- a/src/components/embed-event/event-types/embedded-zap-receipt.tsx +++ b/src/components/embed-event/event-types/embedded-zap-receipt.tsx @@ -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 & { zap: NostrEvent }) { diff --git a/src/components/loading-nostr-link.tsx b/src/components/loading-nostr-link.tsx index 9b0e2c1bb..497b1a216 100644 --- a/src/components/loading-nostr-link.tsx +++ b/src/components/loading-nostr-link.tsx @@ -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"; diff --git a/src/components/note/timeline-note/index.tsx b/src/components/note/timeline-note/index.tsx index 66f0e9afd..ff1d246dd 100644 --- a/src/components/note/timeline-note/index.tsx +++ b/src/components/note/timeline-note/index.tsx @@ -152,7 +152,7 @@ export function TimelineNote({ {replyForm.isOpen && ( diff --git a/src/helpers/nip19.ts b/src/helpers/nip19.ts index 05a3d408c..6e2d3bcc7 100644 --- a/src/helpers/nip19.ts +++ b/src/helpers/nip19.ts @@ -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; -} diff --git a/src/helpers/nostr/goal.ts b/src/helpers/nostr/goal.ts index 271319e42..c8f7cc063 100644 --- a/src/helpers/nostr/goal.ts +++ b/src/helpers/nostr/goal.ts @@ -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; diff --git a/src/helpers/nostr/lists.ts b/src/helpers/nostr/lists.ts index 9f8cbfb14..67287c7d1 100644 --- a/src/helpers/nostr/lists.ts +++ b/src/helpers/nostr/lists.ts @@ -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; diff --git a/src/helpers/thread.ts b/src/helpers/thread.ts index 9ebcd10f8..8fdb468a6 100644 --- a/src/helpers/thread.ts +++ b/src/helpers/thread.ts @@ -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[]): 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(); @@ -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 = {}; - - const replies = new Map(); - 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)); } diff --git a/src/hooks/use-observable.ts b/src/hooks/use-observable.ts index f50882ff7..0df23d18f 100644 --- a/src/hooks/use-observable.ts +++ b/src/hooks/use-observable.ts @@ -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(observable?: Observable): T | undefined { + const forceUpdate = useForceUpdate(); const [value, update] = useState(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]); diff --git a/src/views/goals/components/goal-contents.tsx b/src/views/goals/components/goal-contents.tsx index 15760f1bd..b9fa895c0 100644 --- a/src/views/goals/components/goal-contents.tsx +++ b/src/views/goals/components/goal-contents.tsx @@ -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 }) { diff --git a/src/views/lists/list/index.tsx b/src/views/lists/list/index.tsx index 072b1cf0f..8bbb1d0a0 100644 --- a/src/views/lists/list/index.tsx +++ b/src/views/lists/list/index.tsx @@ -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"; diff --git a/src/views/thread/components/details-tabs.tsx b/src/views/thread/components/details-tabs.tsx index f849d22d9..52ba3db68 100644 --- a/src/views/thread/components/details-tabs.tsx +++ b/src/views/thread/components/details-tabs.tsx @@ -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 ( - {post.replies.map((child) => ( + {repliesByDate(post).map((child) => ( ))} @@ -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})` : ""}