add support for web share

This commit is contained in:
hzrd149 2024-05-06 16:46:29 -05:00
parent fc1fa763b5
commit 92b950a0e6
12 changed files with 103 additions and 62 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Add support for native android and ios sharing

View File

@ -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);
}

View File

@ -40,8 +40,8 @@ export default class TimelineLoader {
private log: Debugger;
private subscription: MultiSubscription;
private cacheChunkLoader: ChunkedRequest | null = null;
private chunkLoaders = new Map<string, ChunkedRequest>();
private cacheLoader: ChunkedRequest | null = null;
private loaders = new Map<string, ChunkedRequest>();
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();

View File

@ -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 && (
<MenuItem
onClick={() => {
const text = "https://njump.me/" + address;
if (navigator.clipboard) navigator.clipboard.writeText(text);
else toast({ description: text, isClosable: true, duration: null });
}}
icon={<ShareIcon />}
>
Copy share link
</MenuItem>
)
);
}

View File

@ -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 && (
<MenuItem onClick={share} icon={<ShareIcon />}>
Share Link
</MenuItem>
)
);
}

View File

@ -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
<>
<DotsMenuButton {...props}>
<OpenInAppMenuItem event={event} />
<CopyShareLinkMenuItem event={event} />
<ShareLinkMenuItem event={event} />
<CopyEmbedCodeMenuItem event={event} />
<MuteUserMenuItem event={event} />
<DeleteEventMenuItem event={event} />

View File

@ -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<string, Subject<NostrEvent>>(() => new Subject<NostrEvent>());
cacheLoader: BatchKindLoader | null = null;
loaders = new SuperMap<string, BatchKindLoader>((relay) => {
const loader = new BatchKindLoader(relayPoolService.requestRelay(relay), this.log.extend(relay));
loaders = new SuperMap<AbstractRelay, BatchKindLoader>((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<string>, kind: number, pubkey: string, d?: string) {
private requestEventFromRelays(
urls: Iterable<string | URL | AbstractRelay>,
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<string>, kind: number, pubkey: string, d?: string, opts: RequestOptions = {}) {
requestEvent(
urls: Iterable<string | URL | AbstractRelay>,
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) {

View File

@ -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({
<>
<DotsMenuButton {...props}>
<OpenInAppMenuItem event={event} />
<CopyShareLinkMenuItem event={event} />
<ShareLinkMenuItem event={event} />
<MenuItem
onClick={() => {
const text = nip19.noteEncode(event.id);

View File

@ -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({
<>
<DotsMenuButton {...props}>
<OpenInAppMenuItem event={comment} />
<CopyShareLinkMenuItem event={comment} />
<ShareLinkMenuItem event={comment} />
<CopyEmbedCodeMenuItem event={comment} />
<MuteUserMenuItem event={comment} />
<DeleteEventMenuItem event={comment} />

View File

@ -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
<>
<DotsMenuButton {...props}>
<OpenInAppMenuItem event={track} />
<CopyShareLinkMenuItem event={track} />
<ShareLinkMenuItem event={track} />
<CopyEmbedCodeMenuItem event={track} />
<MuteUserMenuItem event={track} />

View File

@ -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
<>
<DotsMenuButton {...props}>
<OpenInAppMenuItem event={video} />
<CopyShareLinkMenuItem event={video} />
<ShareLinkMenuItem event={video} />
<CopyEmbedCodeMenuItem event={video} />
<MuteUserMenuItem event={video} />
<DebugEventMenuItem event={video} />

View File

@ -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 } &
</MenuItem>
)}
<CopyShareLinkMenuItem event={page} />
<ShareLinkMenuItem event={page} />
<CopyEmbedCodeMenuItem event={page} />
<DebugEventMenuItem event={page} />