mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-17 13:21:44 +01:00
Add option to mirror blobs when sharing notes
This commit is contained in:
parent
c88e2df5f0
commit
ab394aa6e3
5
.changeset/happy-penguins-check.md
Normal file
5
.changeset/happy-penguins-check.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Add option to mirror blobs when sharing notes
|
@ -142,7 +142,7 @@
|
||||
"@capacitor/core": "^6.2.0",
|
||||
"@capacitor/ios": "^6.2.0",
|
||||
"@capacitor/preferences": "^6.0.3",
|
||||
"@changesets/cli": "^2.27.11",
|
||||
"@changesets/cli": "^2.27.12",
|
||||
"@types/canvas-confetti": "^1.9.0",
|
||||
"@types/chroma-js": "^2.4.5",
|
||||
"@types/debug": "^4.1.12",
|
||||
|
928
pnpm-lock.yaml
generated
928
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,17 +0,0 @@
|
||||
import { memo } from "react";
|
||||
import { verifyEvent } from "nostr-tools";
|
||||
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import { CheckIcon, VerificationFailed } from "../icons";
|
||||
import useAppSettings from "../../hooks/use-user-app-settings";
|
||||
|
||||
function EventVerificationIcon({ event }: { event: NostrEvent }) {
|
||||
const { showSignatureVerification } = useAppSettings();
|
||||
if (!showSignatureVerification) return null;
|
||||
|
||||
if (!verifyEvent(event)) {
|
||||
return <VerificationFailed color="red.500" />;
|
||||
}
|
||||
return <CheckIcon color="green.500" />;
|
||||
}
|
||||
export default memo(EventVerificationIcon);
|
@ -5,17 +5,14 @@ import { Link as RouterLink, useNavigate } from "react-router-dom";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import UserAvatarLink from "../../user/user-avatar-link";
|
||||
import UserLink from "../../user/user-link";
|
||||
import EventVerificationIcon from "../../common-event/event-verification-icon";
|
||||
import { TrustProvider } from "../../../providers/local/trust-provider";
|
||||
import { NoteLink } from "../../note/note-link";
|
||||
import Timestamp from "../../timestamp";
|
||||
import { CompactNoteContent } from "../../compact-note-content";
|
||||
import HoverLinkOverlay from "../../hover-link-overlay";
|
||||
import { getSharableEventAddress } from "../../../services/relay-hints";
|
||||
import useAppSettings from "../../../hooks/use-user-app-settings";
|
||||
|
||||
export default function EmbeddedNote({ event, ...props }: Omit<CardProps, "children"> & { event: NostrEvent }) {
|
||||
const { showSignatureVerification } = useAppSettings();
|
||||
const navigate = useNavigate();
|
||||
const to = `/n/${getSharableEventAddress(event)}`;
|
||||
|
||||
@ -38,7 +35,6 @@ export default function EmbeddedNote({ event, ...props }: Omit<CardProps, "child
|
||||
</NoteLink>
|
||||
<HoverLinkOverlay as={RouterLink} to={to} onClick={handleClick} />
|
||||
<Spacer />
|
||||
{showSignatureVerification && <EventVerificationIcon event={event} />}
|
||||
</Flex>
|
||||
<CompactNoteContent px="2" event={event} maxLength={96} />
|
||||
</Card>
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { MouseEventHandler, useCallback } from "react";
|
||||
import { Card, CardProps, Flex, LinkBox, Spacer, Text } from "@chakra-ui/react";
|
||||
import { Link as RouterLink, useNavigate } from "react-router-dom";
|
||||
import { nip19 } from "nostr-tools";
|
||||
@ -5,7 +6,6 @@ import { nip19 } from "nostr-tools";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import UserAvatarLink from "../../user/user-avatar-link";
|
||||
import UserLink from "../../user/user-link";
|
||||
import EventVerificationIcon from "../../common-event/event-verification-icon";
|
||||
import { TrustProvider } from "../../../providers/local/trust-provider";
|
||||
import Timestamp from "../../timestamp";
|
||||
import { CompactNoteContent } from "../../compact-note-content";
|
||||
@ -13,15 +13,12 @@ import HoverLinkOverlay from "../../hover-link-overlay";
|
||||
import { getThreadReferences } from "../../../helpers/nostr/event";
|
||||
import useSingleEvent from "../../../hooks/use-single-event";
|
||||
import { getTorrentTitle } from "../../../helpers/nostr/torrents";
|
||||
import { MouseEventHandler, useCallback } from "react";
|
||||
import useAppSettings from "../../../hooks/use-user-app-settings";
|
||||
|
||||
export default function EmbeddedTorrentComment({
|
||||
comment,
|
||||
...props
|
||||
}: Omit<CardProps, "children"> & { comment: NostrEvent }) {
|
||||
const navigate = useNavigate();
|
||||
const { showSignatureVerification } = useAppSettings();
|
||||
const refs = getThreadReferences(comment);
|
||||
const torrent = useSingleEvent(refs.root?.e?.id, refs.root?.e?.relays);
|
||||
const linkToTorrent = refs.root?.e && `/torrents/${nip19.neventEncode(refs.root.e)}`;
|
||||
@ -45,7 +42,6 @@ export default function EmbeddedTorrentComment({
|
||||
{torrent ? getTorrentTitle(torrent) : "torrent"}
|
||||
</HoverLinkOverlay>
|
||||
<Spacer />
|
||||
{showSignatureVerification && <EventVerificationIcon event={comment} />}
|
||||
<Timestamp timestamp={comment.created_at} />
|
||||
</Flex>
|
||||
<CompactNoteContent px="2" event={comment} maxLength={96} />
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
Center,
|
||||
Checkbox,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
@ -9,12 +11,22 @@ import {
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
ModalProps,
|
||||
Spinner,
|
||||
Text,
|
||||
useToast,
|
||||
} from "@chakra-ui/react";
|
||||
import { NostrEvent } from "nostr-tools";
|
||||
import { useEventFactory } from "applesauce-react/hooks";
|
||||
import { getMediaAttachments } from "applesauce-core/helpers";
|
||||
import { getMediaAttachmentURLsFromContent } from "applesauce-content/helpers";
|
||||
import { BlossomClient } from "blossom-client-sdk";
|
||||
|
||||
import { usePublishEvent } from "../../../../providers/global/publish-provider";
|
||||
import { EmbedEvent } from "../../../embed-event";
|
||||
import useAppSettings from "../../../../hooks/use-user-app-settings";
|
||||
import useUsersMediaServers from "../../../../hooks/use-user-media-servers";
|
||||
import useCurrentAccount from "../../../../hooks/use-current-account";
|
||||
import { useSigningContext } from "../../../../providers/global/signing-provider";
|
||||
|
||||
export default function ShareModal({
|
||||
event,
|
||||
@ -22,17 +34,76 @@ export default function ShareModal({
|
||||
onClose,
|
||||
...props
|
||||
}: Omit<ModalProps, "children"> & { event: NostrEvent }) {
|
||||
const { mirrorBlobsOnShare } = useAppSettings();
|
||||
const account = useCurrentAccount();
|
||||
const publish = usePublishEvent();
|
||||
const factory = useEventFactory();
|
||||
const toast = useToast();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { requestSignature } = useSigningContext();
|
||||
const { servers } = useUsersMediaServers(account?.pubkey);
|
||||
const [mirror, setMirror] = useState(mirrorBlobsOnShare);
|
||||
const mediaAttachments = useMemo(() => {
|
||||
const attachments = getMediaAttachments(event)
|
||||
// filter out media attachments without hashes
|
||||
.filter((media) => !!media.sha256);
|
||||
|
||||
// extra media attachments from content
|
||||
const content = getMediaAttachmentURLsFromContent(event.content)
|
||||
// remove duplicates
|
||||
.filter((media) => !attachments.some((a) => a.sha256 === media.sha256));
|
||||
|
||||
return [...attachments, ...content];
|
||||
}, [event]);
|
||||
|
||||
const canMirror = servers.length > 0 && mediaAttachments.length > 0;
|
||||
|
||||
const [loading, setLoading] = useState("");
|
||||
const share = async () => {
|
||||
setLoading(true);
|
||||
if (mirror && canMirror) {
|
||||
try {
|
||||
setLoading("Requesting signature for mirroring...");
|
||||
const auth = await BlossomClient.createUploadAuth(
|
||||
requestSignature,
|
||||
mediaAttachments.filter((m) => !!m.sha256).map((m) => m.sha256!),
|
||||
);
|
||||
|
||||
setLoading("Mirror blobs...");
|
||||
for (const media of mediaAttachments) {
|
||||
// send mirror request to all servers
|
||||
await Promise.allSettled(
|
||||
servers.map((server) =>
|
||||
BlossomClient.mirrorBlob(
|
||||
server,
|
||||
{
|
||||
sha256: media.sha256!,
|
||||
url: media.url,
|
||||
// TODO: these are not needed and should be removed
|
||||
uploaded: 0,
|
||||
size: media.size ?? 0,
|
||||
},
|
||||
{ auth },
|
||||
).catch((err) => {
|
||||
// ignore errors from individual servers
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error)
|
||||
toast({ status: "error", title: `Failed to mirror media`, description: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
setLoading("Sharing...");
|
||||
const draft = await factory.share(event);
|
||||
|
||||
setLoading("Publishing...");
|
||||
await publish("Share", draft);
|
||||
setLoading("");
|
||||
|
||||
// close modal
|
||||
onClose();
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
@ -44,7 +115,26 @@ export default function ShareModal({
|
||||
</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody px="4" py="0">
|
||||
<EmbedEvent event={event} />
|
||||
{loading ? (
|
||||
<Center>
|
||||
<Spinner /> {loading}
|
||||
</Center>
|
||||
) : (
|
||||
<>
|
||||
<EmbedEvent event={event} />
|
||||
|
||||
{canMirror && (
|
||||
<>
|
||||
<Checkbox isChecked={mirror} onChange={() => setMirror(!mirror)} mt="4">
|
||||
Mirror media ({mediaAttachments.length}) to blossom servers ({servers.length})
|
||||
</Checkbox>
|
||||
<Text fontSize="sm" color="GrayText">
|
||||
Copy media to your blossom servers so it can be found later
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter px="4" py="4">
|
||||
@ -56,7 +146,7 @@ export default function ShareModal({
|
||||
variant="solid"
|
||||
onClick={() => share()}
|
||||
size="md"
|
||||
isLoading={loading}
|
||||
isLoading={!!loading}
|
||||
flexShrink={0}
|
||||
>
|
||||
Share
|
||||
|
@ -22,7 +22,6 @@ import NoteMenu from "../note-menu";
|
||||
import UserLink from "../../user/user-link";
|
||||
import EventZapButton from "../../zap/event-zap-button";
|
||||
import { ExpandProvider } from "../../../providers/local/expanded";
|
||||
import EventVerificationIcon from "../../common-event/event-verification-icon";
|
||||
import EventShareButton from "./components/event-share-button";
|
||||
import EventQuoteButton from "../event-quote-button";
|
||||
import { ReplyIcon } from "../../icons";
|
||||
@ -67,7 +66,7 @@ export function TimelineNote({
|
||||
...props
|
||||
}: TimelineNoteProps) {
|
||||
const account = useCurrentAccount();
|
||||
const { showReactions, showSignatureVerification } = useAppSettings();
|
||||
const { showReactions } = useAppSettings();
|
||||
const hideZapBubbles = useObservable(localSettings.hideZapBubbles);
|
||||
const replyForm = useDisclosure();
|
||||
|
||||
@ -98,7 +97,6 @@ export function TimelineNote({
|
||||
<POWIcon event={event} boxSize={5} />
|
||||
<NotePublishedUsing event={event} />
|
||||
<Flex grow={1} />
|
||||
{showSignatureVerification && <EventVerificationIcon event={event} />}
|
||||
</Flex>
|
||||
<NoteCommunityMetadata event={event} />
|
||||
{showReplyLine && <ReplyContext event={event} />}
|
||||
|
@ -4,7 +4,7 @@ import { kinds } from "nostr-tools";
|
||||
export const APP_SETTINGS_KIND = kinds.Application;
|
||||
export const APP_SETTING_IDENTIFIER = "nostrudel-settings";
|
||||
|
||||
export type AppSettingsV0 = {
|
||||
type AppSettingsV0 = {
|
||||
version: 0;
|
||||
colorMode: ColorModeWithSystem;
|
||||
defaultRelays: string[];
|
||||
@ -12,7 +12,6 @@ export type AppSettingsV0 = {
|
||||
autoShowMedia: boolean;
|
||||
proxyUserMedia: boolean;
|
||||
showReactions: boolean;
|
||||
/** @deprecated */
|
||||
showSignatureVerification: boolean;
|
||||
|
||||
autoPayWithWebLN: boolean;
|
||||
@ -26,36 +25,41 @@ export type AppSettingsV0 = {
|
||||
redditRedirect?: string;
|
||||
youtubeRedirect?: string;
|
||||
};
|
||||
export type AppSettingsV1 = Omit<AppSettingsV0, "version"> & {
|
||||
type AppSettingsV1 = Omit<AppSettingsV0, "version"> & {
|
||||
version: 1;
|
||||
mutedWords?: string;
|
||||
maxPageWidth: "none" | "sm" | "md" | "lg" | "xl" | "full";
|
||||
};
|
||||
export type AppSettingsV2 = Omit<AppSettingsV1, "version"> & { version: 2; theme: string };
|
||||
export type AppSettingsV3 = Omit<AppSettingsV2, "version"> & { version: 3; quickReactions: string[] };
|
||||
export type AppSettingsV4 = Omit<AppSettingsV3, "version"> & { version: 4; loadOpenGraphData: boolean };
|
||||
export type AppSettingsV5 = Omit<AppSettingsV4, "version"> & { version: 5; hideUsernames: boolean };
|
||||
export type AppSettingsV6 = Omit<AppSettingsV5, "version"> & { version: 6; noteDifficulty: number | null };
|
||||
export type AppSettingsV7 = Omit<AppSettingsV6, "version"> & { version: 7; autoDecryptDMs: boolean };
|
||||
export type AppSettingsV8 = Omit<AppSettingsV7, "version"> & {
|
||||
type AppSettingsV2 = Omit<AppSettingsV1, "version"> & { version: 2; theme: string };
|
||||
type AppSettingsV3 = Omit<AppSettingsV2, "version"> & { version: 3; quickReactions: string[] };
|
||||
type AppSettingsV4 = Omit<AppSettingsV3, "version"> & { version: 4; loadOpenGraphData: boolean };
|
||||
type AppSettingsV5 = Omit<AppSettingsV4, "version"> & { version: 5; hideUsernames: boolean };
|
||||
type AppSettingsV6 = Omit<AppSettingsV5, "version"> & { version: 6; noteDifficulty: number | null };
|
||||
type AppSettingsV7 = Omit<AppSettingsV6, "version"> & { version: 7; autoDecryptDMs: boolean };
|
||||
type AppSettingsV8 = Omit<AppSettingsV7, "version"> & {
|
||||
version: 8;
|
||||
mediaUploadService: "nostr.build" | "blossom";
|
||||
};
|
||||
export type AppSettingsV9 = Omit<AppSettingsV8, "version"> & { version: 9; removeEmojisInUsernames: boolean };
|
||||
type AppSettingsV9 = Omit<AppSettingsV8, "version"> & { version: 9; removeEmojisInUsernames: boolean };
|
||||
|
||||
export type AppSettingsV10 = Omit<AppSettingsV9, "version" | "defaultRelays"> & {
|
||||
type AppSettingsV10 = Omit<AppSettingsV9, "version" | "defaultRelays"> & {
|
||||
version: 10;
|
||||
showPubkeyColor: "none" | "avatar" | "underline";
|
||||
};
|
||||
|
||||
export type AppSettingsV11 = Omit<AppSettingsV10, "quickReactions" | "version"> & {
|
||||
type AppSettingsV11 = Omit<AppSettingsV10, "quickReactions" | "version"> & {
|
||||
version: 11;
|
||||
};
|
||||
|
||||
export type AppSettings = AppSettingsV11;
|
||||
type AppSettingsV12 = Omit<AppSettingsV11, "showSignatureVerification" | "version"> & {
|
||||
version: 12;
|
||||
mirrorBlobsOnShare: boolean;
|
||||
};
|
||||
|
||||
export type AppSettings = AppSettingsV12;
|
||||
|
||||
export const defaultSettings: AppSettings = {
|
||||
version: 11,
|
||||
version: 12,
|
||||
|
||||
// display
|
||||
theme: "default",
|
||||
@ -69,12 +73,11 @@ export const defaultSettings: AppSettings = {
|
||||
autoShowMedia: true,
|
||||
showContentWarning: true,
|
||||
loadOpenGraphData: true,
|
||||
/** @deprecated */
|
||||
showSignatureVerification: false,
|
||||
|
||||
// posting
|
||||
noteDifficulty: null,
|
||||
proxyUserMedia: false,
|
||||
mirrorBlobsOnShare: false,
|
||||
|
||||
// performance
|
||||
showReactions: true,
|
||||
|
@ -12,7 +12,6 @@ import {
|
||||
AlertIcon,
|
||||
AlertTitle,
|
||||
AlertDescription,
|
||||
Heading,
|
||||
Switch,
|
||||
} from "@chakra-ui/react";
|
||||
import { useObservable } from "applesauce-react/hooks";
|
||||
@ -20,7 +19,6 @@ import { useObservable } from "applesauce-react/hooks";
|
||||
import useUsersMediaServers from "../../../hooks/use-user-media-servers";
|
||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||
import useSettingsForm from "../use-settings-form";
|
||||
import VerticalPageLayout from "../../../components/vertical-page-layout";
|
||||
import localSettings from "../../../services/local-settings";
|
||||
import SimpleView from "../../../components/layout/presets/simple-view";
|
||||
|
||||
@ -118,6 +116,16 @@ export default function PostSettings() {
|
||||
client tags on events
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<Flex alignItems="center">
|
||||
<FormLabel htmlFor="mirrorBlobsOnShare" mb="0">
|
||||
Always mirror media
|
||||
</FormLabel>
|
||||
<Switch id="mirrorBlobsOnShare" {...register("mirrorBlobsOnShare")} />
|
||||
</Flex>
|
||||
<FormHelperText>Copy all media to your personal blossom servers when sharing notes</FormHelperText>
|
||||
</FormControl>
|
||||
</SimpleView>
|
||||
);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user