diff --git a/.changeset/modern-socks-tickle.md b/.changeset/modern-socks-tickle.md new file mode 100644 index 000000000..397d13a90 --- /dev/null +++ b/.changeset/modern-socks-tickle.md @@ -0,0 +1,5 @@ +--- +"nostrudel": minor +--- + +Add support for native android and ios sharing diff --git a/src/classes/multi-subscription.ts b/src/classes/multi-subscription.ts index f40488523..a81dc1b14 100644 --- a/src/classes/multi-subscription.ts +++ b/src/classes/multi-subscription.ts @@ -163,6 +163,10 @@ export default class MultiSubscription { } destroy() { + for (const [relay, sub] of this.subscriptions) { + sub.destroy(); + } + this.process.remove(); processManager.unregisterProcess(this.process); } diff --git a/src/classes/timeline-loader.ts b/src/classes/timeline-loader.ts index 547946855..bdbc60dbd 100644 --- a/src/classes/timeline-loader.ts +++ b/src/classes/timeline-loader.ts @@ -40,8 +40,8 @@ export default class TimelineLoader { private log: Debugger; private subscription: MultiSubscription; - private cacheChunkLoader: ChunkedRequest | null = null; - private chunkLoaders = new Map(); + private cacheLoader: ChunkedRequest | null = null; + private loaders = new Map(); constructor(name: string) { this.name = name; @@ -107,10 +107,10 @@ export default class TimelineLoader { // recreate all chunk loaders for (const relay of this.relays) { - const loader = this.chunkLoaders.get(relay.url); + const loader = this.loaders.get(relay.url); if (loader) { this.disconnectFromChunkLoader(loader); - this.chunkLoaders.delete(relay.url); + this.loaders.delete(relay.url); } const chunkLoader = new ChunkedRequest( @@ -118,7 +118,7 @@ export default class TimelineLoader { filters, this.log.extend(relay.url), ); - this.chunkLoaders.set(relay.url, chunkLoader); + this.loaders.set(relay.url, chunkLoader); this.connectToChunkLoader(chunkLoader); } @@ -126,10 +126,10 @@ export default class TimelineLoader { this.filters = filters; // recreate cache chunk loader - if (this.cacheChunkLoader) this.disconnectFromChunkLoader(this.cacheChunkLoader); + if (this.cacheLoader) this.disconnectFromChunkLoader(this.cacheLoader); if (localRelay) { - this.cacheChunkLoader = new ChunkedRequest(localRelay, this.filters, this.log.extend("cache-relay")); - this.connectToChunkLoader(this.cacheChunkLoader); + this.cacheLoader = new ChunkedRequest(localRelay, this.filters, this.log.extend("cache-relay")); + this.connectToChunkLoader(this.cacheLoader); } // update the live subscription query map and add limit @@ -141,20 +141,20 @@ export default class TimelineLoader { // remove chunk loaders for (const relay of newRelays) { - const loader = this.chunkLoaders.get(relay.url); + const loader = this.loaders.get(relay.url); if (!loader) continue; if (!this.relays.includes(relay)) { this.disconnectFromChunkLoader(loader); - this.chunkLoaders.delete(relay.url); + this.loaders.delete(relay.url); } } // create chunk loaders only if filters are set if (this.filters.length > 0) { for (const relay of newRelays) { - if (!this.chunkLoaders.has(relay.url)) { + if (!this.loaders.has(relay.url)) { const loader = new ChunkedRequest(relay, this.filters, this.log.extend(relay.url)); - this.chunkLoaders.set(relay.url, loader); + this.loaders.set(relay.url, loader); this.connectToChunkLoader(loader); } } @@ -177,9 +177,7 @@ export default class TimelineLoader { } private getAllLoaders() { - return this.cacheChunkLoader - ? [...this.chunkLoaders.values(), this.cacheChunkLoader] - : Array.from(this.chunkLoaders.values()); + return this.cacheLoader ? [...this.loaders.values(), this.cacheLoader] : Array.from(this.loaders.values()); } triggerChunkLoad() { @@ -256,8 +254,8 @@ export default class TimelineLoader { this.cursor = dayjs().unix(); const loaders = this.getAllLoaders(); for (const loader of loaders) this.disconnectFromChunkLoader(loader); - this.chunkLoaders.clear(); - this.cacheChunkLoader = null; + this.loaders.clear(); + this.cacheLoader = null; this.forgetEvents(); } @@ -267,8 +265,8 @@ export default class TimelineLoader { const loaders = this.getAllLoaders(); for (const loader of loaders) this.disconnectFromChunkLoader(loader); - this.chunkLoaders.clear(); - this.cacheChunkLoader = null; + this.loaders.clear(); + this.cacheLoader = null; this.subscription.destroy(); diff --git a/src/components/common-menu-items/copy-share-link.tsx b/src/components/common-menu-items/copy-share-link.tsx deleted file mode 100644 index bc902761b..000000000 --- a/src/components/common-menu-items/copy-share-link.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { MenuItem, useToast } from "@chakra-ui/react"; - -import { NostrEvent } from "../../types/nostr-event"; -import { getSharableEventAddress } from "../../helpers/nip19"; -import { ShareIcon } from "../icons"; - -export default function CopyShareLinkMenuItem({ event }: { event: NostrEvent }) { - const toast = useToast(); - const address = getSharableEventAddress(event); - - return ( - address && ( - { - const text = "https://njump.me/" + address; - if (navigator.clipboard) navigator.clipboard.writeText(text); - else toast({ description: text, isClosable: true, duration: null }); - }} - icon={} - > - Copy share link - - ) - ); -} diff --git a/src/components/common-menu-items/share-link.tsx b/src/components/common-menu-items/share-link.tsx new file mode 100644 index 000000000..f5ebc880b --- /dev/null +++ b/src/components/common-menu-items/share-link.tsx @@ -0,0 +1,48 @@ +import { MenuItem, useToast } from "@chakra-ui/react"; + +import { NostrEvent } from "../../types/nostr-event"; +import { getSharableEventAddress } from "../../helpers/nip19"; +import { ShareIcon } from "../icons"; +import { Signature } from "@noble/secp256k1"; +import { descriptors } from "chart.js/dist/core/core.defaults"; +import useUserMetadata from "../../hooks/use-user-metadata"; +import { useCallback } from "react"; +import { getDisplayName } from "../../helpers/nostr/user-metadata"; + +let urlShareFailed = false; + +export default function ShareLinkMenuItem({ event }: { event: NostrEvent }) { + const toast = useToast(); + const address = getSharableEventAddress(event); + const metadata = useUserMetadata(event.pubkey); + + const share = useCallback(async () => { + const data: ShareData = { + url: "https://njump.me/" + address, + title: event.tags.find((t) => t[0] === "title")?.[1] || "Nostr note by " + getDisplayName(metadata, event.pubkey), + }; + + if (event.content.length <= 256) data.text = event.content; + + try { + if (navigator.canShare?.(data)) { + await navigator.share(data); + } else { + if (navigator.clipboard) { + await navigator.clipboard.writeText(data.url!); + toast({ status: "success", description: "Copied" }); + } else toast({ description: data.url, isClosable: true, duration: null }); + } + } catch (err) { + if (err instanceof Error) toast({ status: "error", description: err.message }); + } + }, [metadata, event, toast]); + + return ( + address && ( + }> + Share Link + + ) + ); +} diff --git a/src/components/note/note-menu.tsx b/src/components/note/note-menu.tsx index 3c757a36d..71ace17cd 100644 --- a/src/components/note/note-menu.tsx +++ b/src/components/note/note-menu.tsx @@ -8,7 +8,7 @@ import { DotsMenuButton, MenuIconButtonProps } from "../dots-menu-button"; import NoteTranslationModal from "../../views/tools/transform-note/translation"; import Translate01 from "../icons/translate-01"; import PinNoteMenuItem from "../common-menu-items/pin-note"; -import CopyShareLinkMenuItem from "../common-menu-items/copy-share-link"; +import ShareLinkMenuItem from "../common-menu-items/share-link"; import OpenInAppMenuItem from "../common-menu-items/open-in-app"; import MuteUserMenuItem from "../common-menu-items/mute-user"; import DeleteEventMenuItem from "../common-menu-items/delete-event"; @@ -30,7 +30,7 @@ export default function NoteMenu({ event, ...props }: { event: NostrEvent } & Om <> - + diff --git a/src/services/replaceable-events.ts b/src/services/replaceable-events.ts index 3f4b847d5..ed457bf33 100644 --- a/src/services/replaceable-events.ts +++ b/src/services/replaceable-events.ts @@ -20,8 +20,6 @@ export type RequestOptions = { alwaysRequest?: boolean; /** ignore the cache on initial load */ ignoreCache?: boolean; - // TODO: figure out a clean way for useReplaceableEvent hook to "unset" or "unsubscribe" - // keepAlive?: boolean; }; export function getHumanReadableCoordinate(kind: number, pubkey: string, d?: string) { @@ -36,8 +34,8 @@ class ReplaceableEventsService { private subjects = new SuperMap>(() => new Subject()); cacheLoader: BatchKindLoader | null = null; - loaders = new SuperMap((relay) => { - const loader = new BatchKindLoader(relayPoolService.requestRelay(relay), this.log.extend(relay)); + loaders = new SuperMap((relay) => { + const loader = new BatchKindLoader(relay, this.log.extend(relay.url)); loader.events.onEvent.subscribe((e) => this.handleEvent(e)); this.process.addChild(loader.process); return loader; @@ -93,8 +91,14 @@ class ReplaceableEventsService { this.writeToCacheThrottle(); } - private requestEventFromRelays(relays: Iterable, kind: number, pubkey: string, d?: string) { + private requestEventFromRelays( + urls: Iterable, + kind: number, + pubkey: string, + d?: string, + ) { const cord = createCoordinate(kind, pubkey, d); + const relays = relayPoolService.getRelays(urls); const sub = this.subjects.get(cord); for (const relay of relays) this.loaders.get(relay).requestEvent(kind, pubkey, d); @@ -102,8 +106,15 @@ class ReplaceableEventsService { return sub; } - requestEvent(relays: Iterable, kind: number, pubkey: string, d?: string, opts: RequestOptions = {}) { + requestEvent( + urls: Iterable, + kind: number, + pubkey: string, + d?: string, + opts: RequestOptions = {}, + ) { const key = createCoordinate(kind, pubkey, d); + const relays = relayPoolService.getRelays(urls); const sub = this.subjects.get(key); if (!sub.value && this.cacheLoader) { diff --git a/src/views/community/components/community-post-menu.tsx b/src/views/community/components/community-post-menu.tsx index 547c6b7fc..1b9cd7835 100644 --- a/src/views/community/components/community-post-menu.tsx +++ b/src/views/community/components/community-post-menu.tsx @@ -4,7 +4,7 @@ import { nip19 } from "nostr-tools"; import { DotsMenuButton, MenuIconButtonProps } from "../../../components/dots-menu-button"; import { NostrEvent } from "../../../types/nostr-event"; import { CopyToClipboardIcon } from "../../../components/icons"; -import CopyShareLinkMenuItem from "../../../components/common-menu-items/copy-share-link"; +import ShareLinkMenuItem from "../../../components/common-menu-items/share-link"; import OpenInAppMenuItem from "../../../components/common-menu-items/open-in-app"; import DeleteEventMenuItem from "../../../components/common-menu-items/delete-event"; import DebugEventMenuItem from "../../../components/debug-modal/debug-event-menu-item"; @@ -20,7 +20,7 @@ export default function CommunityPostMenu({ <> - + { const text = nip19.noteEncode(event.id); diff --git a/src/views/torrents/components/torrent-comment-menu.tsx b/src/views/torrents/components/torrent-comment-menu.tsx index 76b2c1d97..bacf539d0 100644 --- a/src/views/torrents/components/torrent-comment-menu.tsx +++ b/src/views/torrents/components/torrent-comment-menu.tsx @@ -1,6 +1,6 @@ import { DotsMenuButton, MenuIconButtonProps } from "../../../components/dots-menu-button"; import OpenInAppMenuItem from "../../../components/common-menu-items/open-in-app"; -import CopyShareLinkMenuItem from "../../../components/common-menu-items/copy-share-link"; +import ShareLinkMenuItem from "../../../components/common-menu-items/share-link"; import CopyEmbedCodeMenuItem from "../../../components/common-menu-items/copy-embed-code"; import MuteUserMenuItem from "../../../components/common-menu-items/mute-user"; import DeleteEventMenuItem from "../../../components/common-menu-items/delete-event"; @@ -15,7 +15,7 @@ export default function TorrentCommentMenu({ <> - + diff --git a/src/views/tracks/components/track-menu.tsx b/src/views/tracks/components/track-menu.tsx index 12fb79a61..4b76dce3f 100644 --- a/src/views/tracks/components/track-menu.tsx +++ b/src/views/tracks/components/track-menu.tsx @@ -1,7 +1,7 @@ import { NostrEvent } from "../../../types/nostr-event"; import { DotsMenuButton, MenuIconButtonProps } from "../../../components/dots-menu-button"; import OpenInAppMenuItem from "../../../components/common-menu-items/open-in-app"; -import CopyShareLinkMenuItem from "../../../components/common-menu-items/copy-share-link"; +import ShareLinkMenuItem from "../../../components/common-menu-items/share-link"; import CopyEmbedCodeMenuItem from "../../../components/common-menu-items/copy-embed-code"; import MuteUserMenuItem from "../../../components/common-menu-items/mute-user"; import DebugEventMenuItem from "../../../components/debug-modal/debug-event-menu-item"; @@ -11,7 +11,7 @@ export default function TrackMenu({ track, ...props }: { track: NostrEvent } & O <> - + diff --git a/src/views/videos/components/video-menu.tsx b/src/views/videos/components/video-menu.tsx index 6d5920164..616c89ab9 100644 --- a/src/views/videos/components/video-menu.tsx +++ b/src/views/videos/components/video-menu.tsx @@ -1,6 +1,6 @@ import { DotsMenuButton, MenuIconButtonProps } from "../../../components/dots-menu-button"; import OpenInAppMenuItem from "../../../components/common-menu-items/open-in-app"; -import CopyShareLinkMenuItem from "../../../components/common-menu-items/copy-share-link"; +import ShareLinkMenuItem from "../../../components/common-menu-items/share-link"; import CopyEmbedCodeMenuItem from "../../../components/common-menu-items/copy-embed-code"; import MuteUserMenuItem from "../../../components/common-menu-items/mute-user"; import { NostrEvent } from "../../../types/nostr-event"; @@ -11,7 +11,7 @@ export default function VideoMenu({ video, ...props }: { video: NostrEvent } & O <> - + diff --git a/src/views/wiki/components/wiki-page-menu.tsx b/src/views/wiki/components/wiki-page-menu.tsx index a64da47ed..4e06b1dca 100644 --- a/src/views/wiki/components/wiki-page-menu.tsx +++ b/src/views/wiki/components/wiki-page-menu.tsx @@ -4,7 +4,7 @@ import { MenuItem } from "@chakra-ui/react"; import { DotsMenuButton, MenuIconButtonProps } from "../../../components/dots-menu-button"; import OpenInAppMenuItem from "../../../components/common-menu-items/open-in-app"; -import CopyShareLinkMenuItem from "../../../components/common-menu-items/copy-share-link"; +import ShareLinkMenuItem from "../../../components/common-menu-items/share-link"; import CopyEmbedCodeMenuItem from "../../../components/common-menu-items/copy-embed-code"; import DebugEventMenuItem from "../../../components/debug-modal/debug-event-menu-item"; import useCurrentAccount from "../../../hooks/use-current-account"; @@ -24,7 +24,7 @@ export default function WikiPageMenu({ page, ...props }: { page: NostrEvent } & )} - +