mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-12 05:39:18 +02:00
Add Torrent create view
This commit is contained in:
parent
a2a920c4c7
commit
abce505a27
5
.changeset/angry-turkeys-glow.md
Normal file
5
.changeset/angry-turkeys-glow.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"nostrudel": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add Torrent create view
|
5
.changeset/fluffy-actors-breathe.md
Normal file
5
.changeset/fluffy-actors-breathe.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"nostrudel": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Change "Copy Share Link" to use njump.me
|
5
.changeset/itchy-bags-march.md
Normal file
5
.changeset/itchy-bags-march.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"nostrudel": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Replace "Copy Note Id" with "Copy Embed Code"
|
@ -23,6 +23,7 @@
|
|||||||
"@emotion/react": "^11.11.1",
|
"@emotion/react": "^11.11.1",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
"@getalby/bitcoin-connect-react": "^2.4.2",
|
"@getalby/bitcoin-connect-react": "^2.4.2",
|
||||||
|
"@noble/hashes": "^1.3.2",
|
||||||
"@webscopeio/react-textarea-autocomplete": "^4.9.2",
|
"@webscopeio/react-textarea-autocomplete": "^4.9.2",
|
||||||
"bech32": "^2.0.0",
|
"bech32": "^2.0.0",
|
||||||
"cheerio": "^1.0.0-rc.12",
|
"cheerio": "^1.0.0-rc.12",
|
||||||
|
@ -92,6 +92,7 @@ const MapView = lazy(() => import("./views/map"));
|
|||||||
const TorrentsView = lazy(() => import("./views/torrents"));
|
const TorrentsView = lazy(() => import("./views/torrents"));
|
||||||
const TorrentDetailsView = lazy(() => import("./views/torrents/torrent"));
|
const TorrentDetailsView = lazy(() => import("./views/torrents/torrent"));
|
||||||
const TorrentPreviewView = lazy(() => import("./views/torrents/preview"));
|
const TorrentPreviewView = lazy(() => import("./views/torrents/preview"));
|
||||||
|
const NewTorrentView = lazy(() => import("./views/torrents/new"));
|
||||||
|
|
||||||
const overrideReactTextareaAutocompleteStyles = css`
|
const overrideReactTextareaAutocompleteStyles = css`
|
||||||
.rta__autocomplete {
|
.rta__autocomplete {
|
||||||
@ -288,6 +289,7 @@ const router = createHashRouter([
|
|||||||
path: "torrents",
|
path: "torrents",
|
||||||
children: [
|
children: [
|
||||||
{ path: "", element: <TorrentsView /> },
|
{ path: "", element: <TorrentsView /> },
|
||||||
|
{ path: "new", element: <NewTorrentView /> },
|
||||||
{ path: ":id", element: <TorrentDetailsView /> },
|
{ path: ":id", element: <TorrentDetailsView /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
17
src/components/common-menu-items/copy-embed-code.tsx
Normal file
17
src/components/common-menu-items/copy-embed-code.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { MenuItem } from "@chakra-ui/react";
|
||||||
|
|
||||||
|
import { NostrEvent } from "../../types/nostr-event";
|
||||||
|
import { getSharableEventAddress } from "../../helpers/nip19";
|
||||||
|
import { CopyToClipboardIcon } from "../icons";
|
||||||
|
|
||||||
|
export default function CopyEmbedCodeMenuItem({ event }: { event: NostrEvent }) {
|
||||||
|
const address = getSharableEventAddress(event);
|
||||||
|
|
||||||
|
return (
|
||||||
|
address && (
|
||||||
|
<MenuItem onClick={() => window.navigator.clipboard.writeText("nostr:" + address)} icon={<CopyToClipboardIcon />}>
|
||||||
|
Copy Embed Code
|
||||||
|
</MenuItem>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
20
src/components/common-menu-items/copy-share-link.tsx
Normal file
20
src/components/common-menu-items/copy-share-link.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { MenuItem } 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 address = getSharableEventAddress(event);
|
||||||
|
|
||||||
|
return (
|
||||||
|
address && (
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => window.navigator.clipboard.writeText("https://njump.me/" + address)}
|
||||||
|
icon={<ShareIcon />}
|
||||||
|
>
|
||||||
|
Copy Share Link
|
||||||
|
</MenuItem>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
19
src/components/common-menu-items/delete-event.tsx
Normal file
19
src/components/common-menu-items/delete-event.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { MenuItem } from "@chakra-ui/react";
|
||||||
|
|
||||||
|
import { NostrEvent } from "../../types/nostr-event";
|
||||||
|
import { useDeleteEventContext } from "../../providers/delete-event-provider";
|
||||||
|
import useCurrentAccount from "../../hooks/use-current-account";
|
||||||
|
import { TrashIcon } from "../icons";
|
||||||
|
|
||||||
|
export default function DeleteEventMenuItem({ event, label }: { event: NostrEvent; label?: string }) {
|
||||||
|
const account = useCurrentAccount();
|
||||||
|
const { deleteEvent } = useDeleteEventContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
account?.pubkey === event.pubkey && (
|
||||||
|
<MenuItem icon={<TrashIcon />} color="red.500" onClick={() => deleteEvent(event)}>
|
||||||
|
{label ?? "Delete Note"}
|
||||||
|
</MenuItem>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
25
src/components/common-menu-items/mute-user.tsx
Normal file
25
src/components/common-menu-items/mute-user.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { MenuItem } from "@chakra-ui/react";
|
||||||
|
|
||||||
|
import { NostrEvent } from "../../types/nostr-event";
|
||||||
|
import useCurrentAccount from "../../hooks/use-current-account";
|
||||||
|
import { MuteIcon, UnmuteIcon } from "../icons";
|
||||||
|
import { useMuteModalContext } from "../../providers/mute-modal-provider";
|
||||||
|
import useUserMuteFunctions from "../../hooks/use-user-mute-functions";
|
||||||
|
|
||||||
|
export default function MuteUserMenuItem({ event }: { event: NostrEvent }) {
|
||||||
|
const account = useCurrentAccount();
|
||||||
|
const { isMuted, mute, unmute } = useUserMuteFunctions(event.pubkey);
|
||||||
|
const { openModal } = useMuteModalContext();
|
||||||
|
|
||||||
|
if (account?.pubkey !== event.pubkey) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
onClick={isMuted ? unmute : () => openModal(event.pubkey)}
|
||||||
|
icon={isMuted ? <UnmuteIcon /> : <MuteIcon />}
|
||||||
|
color="red.500"
|
||||||
|
>
|
||||||
|
{isMuted ? "Unmute User" : "Mute User"}
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
}
|
24
src/components/common-menu-items/open-in-app.tsx
Normal file
24
src/components/common-menu-items/open-in-app.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { Link, MenuItem } from "@chakra-ui/react";
|
||||||
|
|
||||||
|
import { NostrEvent } from "../../types/nostr-event";
|
||||||
|
import { buildAppSelectUrl } from "../../helpers/nostr/apps";
|
||||||
|
import { ExternalLinkIcon } from "../icons";
|
||||||
|
import { getSharableEventAddress } from "../../helpers/nip19";
|
||||||
|
|
||||||
|
export default function OpenInAppMenuItem({ event }: { event: NostrEvent }) {
|
||||||
|
const address = getSharableEventAddress(event);
|
||||||
|
|
||||||
|
return (
|
||||||
|
address && (
|
||||||
|
<MenuItem
|
||||||
|
as={Link}
|
||||||
|
href={buildAppSelectUrl(address)}
|
||||||
|
icon={<ExternalLinkIcon />}
|
||||||
|
isExternal
|
||||||
|
textDecoration="none !important"
|
||||||
|
>
|
||||||
|
View in app...
|
||||||
|
</MenuItem>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
52
src/components/common-menu-items/pin-note.tsx
Normal file
52
src/components/common-menu-items/pin-note.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import { MenuItem, useToast } from "@chakra-ui/react";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
import useCurrentAccount from "../../hooks/use-current-account";
|
||||||
|
import { useSigningContext } from "../../providers/signing-provider";
|
||||||
|
import useUserPinList from "../../hooks/use-user-pin-list";
|
||||||
|
import { DraftNostrEvent, NostrEvent, isETag } from "../../types/nostr-event";
|
||||||
|
import { PIN_LIST_KIND, listAddEvent, listRemoveEvent } from "../../helpers/nostr/lists";
|
||||||
|
import clientRelaysService from "../../services/client-relays";
|
||||||
|
import NostrPublishAction from "../../classes/nostr-publish-action";
|
||||||
|
import { PinIcon } from "../icons";
|
||||||
|
|
||||||
|
export default function PinNoteMenuItem({ event }: { event: NostrEvent }) {
|
||||||
|
const toast = useToast();
|
||||||
|
const account = useCurrentAccount();
|
||||||
|
const { requestSignature } = useSigningContext();
|
||||||
|
const { list } = useUserPinList(account?.pubkey);
|
||||||
|
|
||||||
|
const isPinned = list?.tags.some((t) => isETag(t) && t[1] === event.id) ?? false;
|
||||||
|
const label = isPinned ? "Unpin Note" : "Pin Note";
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const togglePin = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
let draft: DraftNostrEvent = {
|
||||||
|
kind: PIN_LIST_KIND,
|
||||||
|
created_at: dayjs().unix(),
|
||||||
|
content: list?.content ?? "",
|
||||||
|
tags: list?.tags ? Array.from(list.tags) : [],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isPinned) draft = listRemoveEvent(draft, event.id);
|
||||||
|
else draft = listAddEvent(draft, event.id);
|
||||||
|
|
||||||
|
const signed = await requestSignature(draft);
|
||||||
|
new NostrPublishAction(label, clientRelaysService.getWriteUrls(), signed);
|
||||||
|
setLoading(false);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) toast({ status: "error", description: e.message });
|
||||||
|
}
|
||||||
|
}, [list, isPinned]);
|
||||||
|
|
||||||
|
if (event.pubkey !== account?.pubkey) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItem onClick={togglePin} icon={<PinIcon />} isDisabled={loading || !account?.readonly}>
|
||||||
|
{label}
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
}
|
@ -61,6 +61,7 @@ import Download01 from "./icons/download-01";
|
|||||||
import Repeat01 from "./icons/repeat-01";
|
import Repeat01 from "./icons/repeat-01";
|
||||||
import ReverseLeft from "./icons/reverse-left";
|
import ReverseLeft from "./icons/reverse-left";
|
||||||
import Pin01 from "./icons/pin-01";
|
import Pin01 from "./icons/pin-01";
|
||||||
|
import Translate01 from "./icons/translate-01";
|
||||||
|
|
||||||
const defaultProps: IconProps = { boxSize: 4 };
|
const defaultProps: IconProps = { boxSize: 4 };
|
||||||
|
|
||||||
@ -90,6 +91,7 @@ export const ChevronRightIcon = ChevronRight;
|
|||||||
export const LightningIcon = Zap;
|
export const LightningIcon = Zap;
|
||||||
export const RelayIcon = Server04;
|
export const RelayIcon = Server04;
|
||||||
export const BroadcastEventIcon = Share07;
|
export const BroadcastEventIcon = Share07;
|
||||||
|
export const ShareIcon = Share07;
|
||||||
export const PinIcon = Pin01;
|
export const PinIcon = Pin01;
|
||||||
|
|
||||||
export const ExternalLinkIcon = Share04;
|
export const ExternalLinkIcon = Share04;
|
||||||
@ -229,3 +231,5 @@ export const GhostIcon = createIcon({
|
|||||||
export const ECashIcon = BankNote01;
|
export const ECashIcon = BankNote01;
|
||||||
export const WalletIcon = Wallet02;
|
export const WalletIcon = Wallet02;
|
||||||
export const DownloadIcon = Download01;
|
export const DownloadIcon = Download01;
|
||||||
|
|
||||||
|
export const TranslateIcon = Translate01;
|
||||||
|
@ -1,94 +1,30 @@
|
|||||||
import { useCallback, useState } from "react";
|
import { useCallback } from "react";
|
||||||
import { MenuItem, useDisclosure, useToast } from "@chakra-ui/react";
|
import { MenuItem, useDisclosure } from "@chakra-ui/react";
|
||||||
import { useCopyToClipboard } from "react-use";
|
|
||||||
import { nip19 } from "nostr-tools";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
|
|
||||||
import {
|
import { BroadcastEventIcon, CodeIcon } from "../icons";
|
||||||
BroadcastEventIcon,
|
import { NostrEvent } from "../../types/nostr-event";
|
||||||
CopyToClipboardIcon,
|
|
||||||
CodeIcon,
|
|
||||||
ExternalLinkIcon,
|
|
||||||
MuteIcon,
|
|
||||||
RepostIcon,
|
|
||||||
TrashIcon,
|
|
||||||
UnmuteIcon,
|
|
||||||
PinIcon,
|
|
||||||
} from "../icons";
|
|
||||||
import { getSharableEventAddress } from "../../helpers/nip19";
|
|
||||||
import { DraftNostrEvent, NostrEvent, isETag } from "../../types/nostr-event";
|
|
||||||
import { CustomMenuIconButton, MenuIconButtonProps } from "../menu-icon-button";
|
import { CustomMenuIconButton, MenuIconButtonProps } from "../menu-icon-button";
|
||||||
import NoteDebugModal from "../debug-modals/note-debug-modal";
|
import NoteDebugModal from "../debug-modals/note-debug-modal";
|
||||||
import useCurrentAccount from "../../hooks/use-current-account";
|
|
||||||
import { buildAppSelectUrl } from "../../helpers/nostr/apps";
|
|
||||||
import { useDeleteEventContext } from "../../providers/delete-event-provider";
|
|
||||||
import clientRelaysService from "../../services/client-relays";
|
import clientRelaysService from "../../services/client-relays";
|
||||||
import { handleEventFromRelay } from "../../services/event-relays";
|
import { handleEventFromRelay } from "../../services/event-relays";
|
||||||
import NostrPublishAction from "../../classes/nostr-publish-action";
|
import NostrPublishAction from "../../classes/nostr-publish-action";
|
||||||
import useUserMuteFunctions from "../../hooks/use-user-mute-functions";
|
|
||||||
import { useMuteModalContext } from "../../providers/mute-modal-provider";
|
|
||||||
import NoteTranslationModal from "../note-translation-modal";
|
import NoteTranslationModal from "../note-translation-modal";
|
||||||
import Translate01 from "../icons/translate-01";
|
import Translate01 from "../icons/translate-01";
|
||||||
import useUserPinList from "../../hooks/use-user-pin-list";
|
|
||||||
import { useSigningContext } from "../../providers/signing-provider";
|
|
||||||
import { PIN_LIST_KIND, listAddEvent, listRemoveEvent } from "../../helpers/nostr/lists";
|
|
||||||
import InfoCircle from "../icons/info-circle";
|
import InfoCircle from "../icons/info-circle";
|
||||||
|
import PinNoteMenuItem from "../common-menu-items/pin-note";
|
||||||
function PinNoteItem({ event }: { event: NostrEvent }) {
|
import CopyShareLinkMenuItem from "../common-menu-items/copy-share-link";
|
||||||
const toast = useToast();
|
import OpenInAppMenuItem from "../common-menu-items/open-in-app";
|
||||||
const account = useCurrentAccount();
|
import MuteUserMenuItem from "../common-menu-items/mute-user";
|
||||||
const { requestSignature } = useSigningContext();
|
import DeleteEventMenuItem from "../common-menu-items/delete-event";
|
||||||
const { list } = useUserPinList(account?.pubkey);
|
import CopyEmbedCodeMenuItem from "../common-menu-items/copy-embed-code";
|
||||||
|
|
||||||
const isPinned = list?.tags.some((t) => isETag(t) && t[1] === event.id) ?? false;
|
|
||||||
const label = isPinned ? "Unpin Note" : "Pin Note";
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const togglePin = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
let draft: DraftNostrEvent = {
|
|
||||||
kind: PIN_LIST_KIND,
|
|
||||||
created_at: dayjs().unix(),
|
|
||||||
content: list?.content ?? "",
|
|
||||||
tags: list?.tags ? Array.from(list.tags) : [],
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isPinned) draft = listRemoveEvent(draft, event.id);
|
|
||||||
else draft = listAddEvent(draft, event.id);
|
|
||||||
|
|
||||||
const signed = await requestSignature(draft);
|
|
||||||
new NostrPublishAction(label, clientRelaysService.getWriteUrls(), signed);
|
|
||||||
setLoading(false);
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Error) toast({ status: "error", description: e.message });
|
|
||||||
}
|
|
||||||
}, [list, isPinned]);
|
|
||||||
|
|
||||||
if (event.pubkey !== account?.pubkey) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MenuItem onClick={togglePin} icon={<PinIcon />} isDisabled={loading || account.readonly}>
|
|
||||||
{label}
|
|
||||||
</MenuItem>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function NoteMenu({
|
export default function NoteMenu({
|
||||||
event,
|
event,
|
||||||
detailsClick,
|
detailsClick,
|
||||||
...props
|
...props
|
||||||
}: { event: NostrEvent; detailsClick?: () => void } & Omit<MenuIconButtonProps, "children">) {
|
}: { event: NostrEvent; detailsClick?: () => void } & Omit<MenuIconButtonProps, "children">) {
|
||||||
const account = useCurrentAccount();
|
|
||||||
const debugModal = useDisclosure();
|
const debugModal = useDisclosure();
|
||||||
const translationsModal = useDisclosure();
|
const translationsModal = useDisclosure();
|
||||||
const { isMuted, mute, unmute } = useUserMuteFunctions(event.pubkey);
|
|
||||||
const { openModal } = useMuteModalContext();
|
|
||||||
|
|
||||||
const { deleteEvent } = useDeleteEventContext();
|
|
||||||
|
|
||||||
const [_clipboardState, copyToClipboard] = useCopyToClipboard();
|
|
||||||
const noteId = nip19.noteEncode(event.id);
|
|
||||||
|
|
||||||
const broadcast = useCallback(() => {
|
const broadcast = useCallback(() => {
|
||||||
const missingRelays = clientRelaysService.getWriteUrls();
|
const missingRelays = clientRelaysService.getWriteUrls();
|
||||||
@ -98,50 +34,26 @@ export default function NoteMenu({
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const address = getSharableEventAddress(event);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CustomMenuIconButton {...props}>
|
<CustomMenuIconButton {...props}>
|
||||||
|
<OpenInAppMenuItem event={event} />
|
||||||
|
<CopyShareLinkMenuItem event={event} />
|
||||||
|
<CopyEmbedCodeMenuItem event={event} />
|
||||||
|
<MuteUserMenuItem event={event} />
|
||||||
|
<DeleteEventMenuItem event={event} />
|
||||||
{detailsClick && (
|
{detailsClick && (
|
||||||
<MenuItem onClick={detailsClick} icon={<InfoCircle />}>
|
<MenuItem onClick={detailsClick} icon={<InfoCircle />}>
|
||||||
Details
|
Details
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
{address && (
|
|
||||||
<MenuItem onClick={() => window.open(buildAppSelectUrl(address), "_blank")} icon={<ExternalLinkIcon />}>
|
|
||||||
View in app...
|
|
||||||
</MenuItem>
|
|
||||||
)}
|
|
||||||
{account?.pubkey !== event.pubkey && (
|
|
||||||
<MenuItem
|
|
||||||
onClick={isMuted ? unmute : () => openModal(event.pubkey)}
|
|
||||||
icon={isMuted ? <UnmuteIcon /> : <MuteIcon />}
|
|
||||||
color="red.500"
|
|
||||||
>
|
|
||||||
{isMuted ? "Unmute User" : "Mute User"}
|
|
||||||
</MenuItem>
|
|
||||||
)}
|
|
||||||
<MenuItem onClick={() => copyToClipboard("nostr:" + address)} icon={<RepostIcon />}>
|
|
||||||
Copy Share Link
|
|
||||||
</MenuItem>
|
|
||||||
{noteId && (
|
|
||||||
<MenuItem onClick={() => copyToClipboard(noteId)} icon={<CopyToClipboardIcon />}>
|
|
||||||
Copy Note ID
|
|
||||||
</MenuItem>
|
|
||||||
)}
|
|
||||||
{account?.pubkey === event.pubkey && (
|
|
||||||
<MenuItem icon={<TrashIcon />} color="red.500" onClick={() => deleteEvent(event)}>
|
|
||||||
Delete Note
|
|
||||||
</MenuItem>
|
|
||||||
)}
|
|
||||||
<MenuItem onClick={translationsModal.onOpen} icon={<Translate01 />}>
|
<MenuItem onClick={translationsModal.onOpen} icon={<Translate01 />}>
|
||||||
Translations
|
Translations
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem onClick={broadcast} icon={<BroadcastEventIcon />}>
|
<MenuItem onClick={broadcast} icon={<BroadcastEventIcon />}>
|
||||||
Broadcast
|
Broadcast
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<PinNoteItem event={event} />
|
<PinNoteMenuItem event={event} />
|
||||||
<MenuItem onClick={debugModal.onOpen} icon={<CodeIcon />}>
|
<MenuItem onClick={debugModal.onOpen} icon={<CodeIcon />}>
|
||||||
View Raw
|
View Raw
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import { Flex, FlexProps } from "@chakra-ui/react";
|
import { ComponentWithAs, Flex, FlexProps } from "@chakra-ui/react";
|
||||||
|
|
||||||
export default function VerticalPageLayout({ children, ...props }: FlexProps) {
|
const VerticalPageLayout: ComponentWithAs<"div", FlexProps> = ({ children, ...props }: FlexProps) => {
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" pt="2" pb="12" gap="2" px="2" {...props}>
|
<Flex direction="column" pt="2" pb="12" gap="2" px="2" {...props}>
|
||||||
{children}
|
{children}
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default VerticalPageLayout;
|
||||||
|
@ -71,3 +71,98 @@ export function validateTorrent(torrent: NostrEvent) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Category = {
|
||||||
|
name: string;
|
||||||
|
tag: string;
|
||||||
|
sub_category?: Category[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const torrentCatagories: Category[] = [
|
||||||
|
{
|
||||||
|
name: "Video",
|
||||||
|
tag: "video",
|
||||||
|
sub_category: [
|
||||||
|
{
|
||||||
|
name: "Movies",
|
||||||
|
tag: "movie",
|
||||||
|
sub_category: [
|
||||||
|
{ name: "Movies DVDR", tag: "dvdr" },
|
||||||
|
{ name: "HD Movies", tag: "hd" },
|
||||||
|
{ name: "4k Movies", tag: "4k" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "TV",
|
||||||
|
tag: "tv",
|
||||||
|
sub_category: [
|
||||||
|
{ name: "HD TV", tag: "hd" },
|
||||||
|
{ name: "4k TV", tag: "4k" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Audio",
|
||||||
|
tag: "audio",
|
||||||
|
sub_category: [
|
||||||
|
{
|
||||||
|
name: "Music",
|
||||||
|
tag: "music",
|
||||||
|
sub_category: [{ name: "FLAC", tag: "flac" }],
|
||||||
|
},
|
||||||
|
{ name: "Audio Books", tag: "audio-book" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Applications",
|
||||||
|
tag: "application",
|
||||||
|
sub_category: [
|
||||||
|
{ name: "Windows", tag: "windows" },
|
||||||
|
{ name: "Mac", tag: "mac" },
|
||||||
|
{ name: "UNIX", tag: "unix" },
|
||||||
|
{ name: "iOS", tag: "ios" },
|
||||||
|
{ name: "Android", tag: "android" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Games",
|
||||||
|
tag: "game",
|
||||||
|
sub_category: [
|
||||||
|
{ name: "PC", tag: "pc" },
|
||||||
|
{ name: "Mac", tag: "mac" },
|
||||||
|
{ name: "PSx", tag: "psx" },
|
||||||
|
{ name: "XBOX", tag: "xbox" },
|
||||||
|
{ name: "Wii", tag: "wii" },
|
||||||
|
{ name: "iOS", tag: "ios" },
|
||||||
|
{ name: "Android", tag: "android" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Porn",
|
||||||
|
tag: "porn",
|
||||||
|
sub_category: [
|
||||||
|
{
|
||||||
|
name: "Movies",
|
||||||
|
tag: "movie",
|
||||||
|
sub_category: [
|
||||||
|
{ name: "Movies DVDR", tag: "dvdr" },
|
||||||
|
{ name: "HD Movies", tag: "hd" },
|
||||||
|
{ name: "4k Movies", tag: "4k" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ name: "Pictures", tag: "picture" },
|
||||||
|
{ name: "Games", tag: "game" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Other",
|
||||||
|
tag: "other",
|
||||||
|
sub_category: [
|
||||||
|
{ name: "Archives", tag: "archive" },
|
||||||
|
{ name: "E-Books", tag: "e-book" },
|
||||||
|
{ name: "Comics", tag: "comic" },
|
||||||
|
{ name: "Pictures", tag: "picture" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
160
src/lib/bencode/decode.ts
Normal file
160
src/lib/bencode/decode.ts
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
// copied from https://git.v0l.io/Kieran/dtan/src/branch/main/src/bencode/decode.ts
|
||||||
|
|
||||||
|
import { bytesToHex } from "@noble/hashes/utils";
|
||||||
|
|
||||||
|
const INTEGER_START = 0x69; // 'i'
|
||||||
|
const STRING_DELIM = 0x3a; // ':'
|
||||||
|
const DICTIONARY_START = 0x64; // 'd'
|
||||||
|
const LIST_START = 0x6c; // 'l'
|
||||||
|
const END_OF_TYPE = 0x65; // 'e'
|
||||||
|
|
||||||
|
export type BencodeValue = number | Uint8Array | BencodeValue[] | { [key: string]: BencodeValue };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* replaces parseInt(buffer.toString('ascii', start, end)).
|
||||||
|
* For strings with less then ~30 charachters, this is actually a lot faster.
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} buffer
|
||||||
|
* @param {Number} start
|
||||||
|
* @param {Number} end
|
||||||
|
* @return {Number} calculated number
|
||||||
|
*/
|
||||||
|
function getIntFromBuffer(buffer: Uint8Array, start: number, end: number) {
|
||||||
|
let sum = 0;
|
||||||
|
let sign = 1;
|
||||||
|
|
||||||
|
for (let i = start; i < end; i++) {
|
||||||
|
const num = buffer[i];
|
||||||
|
|
||||||
|
if (num < 58 && num >= 48) {
|
||||||
|
sum = sum * 10 + (num - 48);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i === start && num === 43) {
|
||||||
|
// +
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i === start && num === 45) {
|
||||||
|
// -
|
||||||
|
sign = -1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (num === 46) {
|
||||||
|
// .
|
||||||
|
// its a float. break here.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("not a number: buffer[" + i + "] = " + num);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sum * sign;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes bencoded data.
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} data
|
||||||
|
* @param {Number} start (optional)
|
||||||
|
* @param {Number} end (optional)
|
||||||
|
* @param {String} encoding (optional)
|
||||||
|
* @return {Object|Array|Uint8Array|String|Number}
|
||||||
|
*/
|
||||||
|
export function decode(data: Uint8Array, start?: number, end?: number, encoding?: string) {
|
||||||
|
const dec = {
|
||||||
|
position: 0,
|
||||||
|
bytes: 0,
|
||||||
|
encoding,
|
||||||
|
data: data.subarray(start, end),
|
||||||
|
} as Decode;
|
||||||
|
dec.bytes = dec.data.length;
|
||||||
|
return next(dec);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Decode {
|
||||||
|
bytes: number;
|
||||||
|
position: number;
|
||||||
|
data: Uint8Array;
|
||||||
|
encoding?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buffer(dec: Decode) {
|
||||||
|
let sep = find(dec, STRING_DELIM);
|
||||||
|
const length = getIntFromBuffer(dec.data, dec.position, sep);
|
||||||
|
const end = ++sep + length;
|
||||||
|
|
||||||
|
dec.position = end;
|
||||||
|
|
||||||
|
return dec.data.subarray(sep, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
function next(dec: Decode): BencodeValue {
|
||||||
|
switch (dec.data[dec.position]) {
|
||||||
|
case DICTIONARY_START:
|
||||||
|
return dictionary(dec);
|
||||||
|
case LIST_START:
|
||||||
|
return list(dec);
|
||||||
|
case INTEGER_START:
|
||||||
|
return integer(dec);
|
||||||
|
default:
|
||||||
|
return buffer(dec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function find(dec: Decode, chr: number) {
|
||||||
|
let i = dec.position;
|
||||||
|
const c = dec.data.length;
|
||||||
|
const d = dec.data;
|
||||||
|
|
||||||
|
while (i < c) {
|
||||||
|
if (d[i] === chr) return i;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Invalid data: Missing delimiter "' + String.fromCharCode(chr) + '" [0x' + chr.toString(16) + "]");
|
||||||
|
}
|
||||||
|
|
||||||
|
function dictionary(dec: Decode) {
|
||||||
|
dec.position++;
|
||||||
|
|
||||||
|
const dict = {} as Record<string, BencodeValue>;
|
||||||
|
|
||||||
|
while (dec.data[dec.position] !== END_OF_TYPE) {
|
||||||
|
const bf = buffer(dec);
|
||||||
|
let key = new TextDecoder().decode(bf);
|
||||||
|
if (key.includes("\uFFFD")) key = bytesToHex(bf);
|
||||||
|
dict[key] = next(dec);
|
||||||
|
}
|
||||||
|
|
||||||
|
dec.position++;
|
||||||
|
|
||||||
|
return dict;
|
||||||
|
}
|
||||||
|
|
||||||
|
function list(dec: Decode) {
|
||||||
|
dec.position++;
|
||||||
|
|
||||||
|
const lst = [] as Array<BencodeValue>;
|
||||||
|
|
||||||
|
while (dec.data[dec.position] !== END_OF_TYPE) {
|
||||||
|
lst.push(next(dec));
|
||||||
|
}
|
||||||
|
|
||||||
|
dec.position++;
|
||||||
|
|
||||||
|
return lst;
|
||||||
|
}
|
||||||
|
|
||||||
|
function integer(dec: Decode) {
|
||||||
|
const end = find(dec, END_OF_TYPE);
|
||||||
|
const number = getIntFromBuffer(dec.data, dec.position + 1, end);
|
||||||
|
|
||||||
|
dec.position += end + 1 - dec.position;
|
||||||
|
|
||||||
|
return number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default decode;
|
184
src/lib/bencode/encode.ts
Normal file
184
src/lib/bencode/encode.ts
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
// Copied from https://git.v0l.io/Kieran/dtan/src/branch/main/src/bencode/encode.ts
|
||||||
|
|
||||||
|
function getType(value: any) {
|
||||||
|
if (ArrayBuffer.isView(value)) return "arraybufferview";
|
||||||
|
if (Array.isArray(value)) return "array";
|
||||||
|
if (value instanceof Number) return "number";
|
||||||
|
if (value instanceof Boolean) return "boolean";
|
||||||
|
if (value instanceof Set) return "set";
|
||||||
|
if (value instanceof Map) return "map";
|
||||||
|
if (value instanceof String) return "string";
|
||||||
|
if (value instanceof ArrayBuffer) return "arraybuffer";
|
||||||
|
return typeof value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function text2arr(data: string) {
|
||||||
|
return new TextEncoder().encode(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function concat(arrays: Uint8Array[]): Uint8Array {
|
||||||
|
// Calculate the total length of all arrays
|
||||||
|
const totalLength = arrays.reduce((acc, value) => acc + value.length, 0);
|
||||||
|
|
||||||
|
// Create a new array with total length and fill it with elements of the arrays
|
||||||
|
const result = new Uint8Array(totalLength);
|
||||||
|
|
||||||
|
// Copy each array into the result
|
||||||
|
let length = 0;
|
||||||
|
for (const array of arrays) {
|
||||||
|
result.set(array, length);
|
||||||
|
length += array.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes data in bencode.
|
||||||
|
*
|
||||||
|
* @param {Uint8Array|Array|String|Object|Number|Boolean} data
|
||||||
|
* @return {Uint8Array}
|
||||||
|
*/
|
||||||
|
export function encode(data: any, outBuffer?: Uint8Array, offset?: number) {
|
||||||
|
const buffers = [] as Array<Uint8Array>;
|
||||||
|
let result = null;
|
||||||
|
|
||||||
|
encode._encode(buffers, data);
|
||||||
|
result = concat(buffers);
|
||||||
|
encode.bytes = result.length;
|
||||||
|
|
||||||
|
if (ArrayBuffer.isView(outBuffer)) {
|
||||||
|
outBuffer.set(result, offset);
|
||||||
|
return outBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
encode.bytes = -1;
|
||||||
|
encode._floatConversionDetected = false;
|
||||||
|
|
||||||
|
encode._encode = function (buffers: Array<Uint8Array>, data: any) {
|
||||||
|
if (data == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (getType(data)) {
|
||||||
|
case "object":
|
||||||
|
encode.dict(buffers, data);
|
||||||
|
break;
|
||||||
|
case "map":
|
||||||
|
encode.dictMap(buffers, data);
|
||||||
|
break;
|
||||||
|
case "array":
|
||||||
|
encode.list(buffers, data);
|
||||||
|
break;
|
||||||
|
case "set":
|
||||||
|
encode.listSet(buffers, data);
|
||||||
|
break;
|
||||||
|
case "string":
|
||||||
|
encode.string(buffers, data);
|
||||||
|
break;
|
||||||
|
case "number":
|
||||||
|
encode.number(buffers, data);
|
||||||
|
break;
|
||||||
|
case "boolean":
|
||||||
|
encode.number(buffers, data);
|
||||||
|
break;
|
||||||
|
case "arraybufferview":
|
||||||
|
encode.buffer(buffers, new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
|
||||||
|
break;
|
||||||
|
case "arraybuffer":
|
||||||
|
encode.buffer(buffers, new Uint8Array(data));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const buffE = new Uint8Array([0x65]);
|
||||||
|
const buffD = new Uint8Array([0x64]);
|
||||||
|
const buffL = new Uint8Array([0x6c]);
|
||||||
|
|
||||||
|
encode.buffer = function (buffers: Array<Uint8Array>, data: any) {
|
||||||
|
buffers.push(text2arr(data.length + ":"), data);
|
||||||
|
};
|
||||||
|
|
||||||
|
encode.string = function (buffers: Array<Uint8Array>, data: any) {
|
||||||
|
buffers.push(text2arr(text2arr(data).byteLength + ":" + data));
|
||||||
|
};
|
||||||
|
|
||||||
|
encode.number = function (buffers: Array<Uint8Array>, data: any) {
|
||||||
|
if (Number.isInteger(data)) return buffers.push(text2arr("i" + BigInt(data) + "e"));
|
||||||
|
|
||||||
|
const maxLo = 0x80000000;
|
||||||
|
const hi = (data / maxLo) << 0;
|
||||||
|
const lo = data % maxLo << 0;
|
||||||
|
const val = hi * maxLo + lo;
|
||||||
|
|
||||||
|
buffers.push(text2arr("i" + val + "e"));
|
||||||
|
|
||||||
|
if (val !== data && !encode._floatConversionDetected) {
|
||||||
|
encode._floatConversionDetected = true;
|
||||||
|
console.warn(
|
||||||
|
'WARNING: Possible data corruption detected with value "' + data + '":',
|
||||||
|
'Bencoding only defines support for integers, value was converted to "' + val + '"',
|
||||||
|
);
|
||||||
|
console.trace();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
encode.dict = function (buffers: Array<Uint8Array>, data: any) {
|
||||||
|
buffers.push(buffD);
|
||||||
|
|
||||||
|
let j = 0;
|
||||||
|
let k;
|
||||||
|
// fix for issue #13 - sorted dicts
|
||||||
|
const keys = Object.keys(data).sort();
|
||||||
|
const kl = keys.length;
|
||||||
|
|
||||||
|
for (; j < kl; j++) {
|
||||||
|
k = keys[j];
|
||||||
|
if (data[k] == null) continue;
|
||||||
|
encode.string(buffers, k);
|
||||||
|
encode._encode(buffers, data[k]);
|
||||||
|
}
|
||||||
|
|
||||||
|
buffers.push(buffE);
|
||||||
|
};
|
||||||
|
|
||||||
|
encode.dictMap = function (buffers: Array<Uint8Array>, data: any) {
|
||||||
|
buffers.push(buffD);
|
||||||
|
|
||||||
|
const keys = Array.from(data.keys()).sort();
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
if (data.get(key) == null) continue;
|
||||||
|
ArrayBuffer.isView(key) ? encode._encode(buffers, key) : encode.string(buffers, String(key));
|
||||||
|
encode._encode(buffers, data.get(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
buffers.push(buffE);
|
||||||
|
};
|
||||||
|
|
||||||
|
encode.list = function (buffers: Array<Uint8Array>, data: any) {
|
||||||
|
let i = 0;
|
||||||
|
const c = data.length;
|
||||||
|
buffers.push(buffL);
|
||||||
|
|
||||||
|
for (; i < c; i++) {
|
||||||
|
if (data[i] == null) continue;
|
||||||
|
encode._encode(buffers, data[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
buffers.push(buffE);
|
||||||
|
};
|
||||||
|
|
||||||
|
encode.listSet = function (buffers: Array<Uint8Array>, data: any) {
|
||||||
|
buffers.push(buffL);
|
||||||
|
|
||||||
|
for (const item of data) {
|
||||||
|
if (item == null) continue;
|
||||||
|
encode._encode(buffers, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
buffers.push(buffE);
|
||||||
|
};
|
2
src/lib/bencode/index.ts
Normal file
2
src/lib/bencode/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./encode";
|
||||||
|
export * from "./decode";
|
@ -49,6 +49,9 @@ class SigningService {
|
|||||||
|
|
||||||
const encrypted = await window.crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, encode.encode(secKey));
|
const encrypted = await window.crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, encode.encode(secKey));
|
||||||
|
|
||||||
|
// add key to cache
|
||||||
|
decryptedKeys.set(getPublicKey(secKey), secKey);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
secKey: encrypted,
|
secKey: encrypted,
|
||||||
iv,
|
iv,
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
import { MenuItem, useDisclosure } from "@chakra-ui/react";
|
import { MenuItem, useDisclosure } from "@chakra-ui/react";
|
||||||
import { useCopyToClipboard } from "react-use";
|
|
||||||
|
|
||||||
import { NostrEvent } from "../../../types/nostr-event";
|
import { NostrEvent } from "../../../types/nostr-event";
|
||||||
import { CustomMenuIconButton, MenuIconButtonProps } from "../../../components/menu-icon-button";
|
import { CustomMenuIconButton, MenuIconButtonProps } from "../../../components/menu-icon-button";
|
||||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||||
import NoteDebugModal from "../../../components/debug-modals/note-debug-modal";
|
import NoteDebugModal from "../../../components/debug-modals/note-debug-modal";
|
||||||
import { CodeIcon, ExternalLinkIcon, RepostIcon, TrashIcon } from "../../../components/icons";
|
import { CodeIcon, TrashIcon } from "../../../components/icons";
|
||||||
import { getSharableEventAddress } from "../../../helpers/nip19";
|
|
||||||
import { buildAppSelectUrl } from "../../../helpers/nostr/apps";
|
|
||||||
import { useDeleteEventContext } from "../../../providers/delete-event-provider";
|
import { useDeleteEventContext } from "../../../providers/delete-event-provider";
|
||||||
|
import OpenInAppMenuItem from "../../../components/common-menu-items/open-in-app";
|
||||||
|
import CopyEmbedCodeMenuItem from "../../../components/common-menu-items/copy-embed-code";
|
||||||
|
|
||||||
export default function BadgeMenu({ badge, ...props }: { badge: NostrEvent } & Omit<MenuIconButtonProps, "children">) {
|
export default function BadgeMenu({ badge, ...props }: { badge: NostrEvent } & Omit<MenuIconButtonProps, "children">) {
|
||||||
const account = useCurrentAccount();
|
const account = useCurrentAccount();
|
||||||
@ -16,23 +15,11 @@ export default function BadgeMenu({ badge, ...props }: { badge: NostrEvent } & O
|
|||||||
|
|
||||||
const { deleteEvent } = useDeleteEventContext();
|
const { deleteEvent } = useDeleteEventContext();
|
||||||
|
|
||||||
const [_clipboardState, copyToClipboard] = useCopyToClipboard();
|
|
||||||
|
|
||||||
const naddr = getSharableEventAddress(badge);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CustomMenuIconButton {...props}>
|
<CustomMenuIconButton {...props}>
|
||||||
{naddr && (
|
<OpenInAppMenuItem event={badge} />
|
||||||
<>
|
<CopyEmbedCodeMenuItem event={badge} />
|
||||||
<MenuItem onClick={() => window.open(buildAppSelectUrl(naddr), "_blank")} icon={<ExternalLinkIcon />}>
|
|
||||||
View in app...
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem onClick={() => copyToClipboard("nostr:" + naddr)} icon={<RepostIcon />}>
|
|
||||||
Copy Share Link
|
|
||||||
</MenuItem>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{account?.pubkey === badge.pubkey && (
|
{account?.pubkey === badge.pubkey && (
|
||||||
<MenuItem icon={<TrashIcon />} color="red.500" onClick={() => deleteEvent(badge)}>
|
<MenuItem icon={<TrashIcon />} color="red.500" onClick={() => deleteEvent(badge)}>
|
||||||
Delete Badge
|
Delete Badge
|
||||||
|
@ -61,7 +61,7 @@ export default function BadgesView() {
|
|||||||
// const account = useCurrentAccount();
|
// const account = useCurrentAccount();
|
||||||
// return account ? <BadgesPage /> : <Navigate to="/lists/browse" />;
|
// return account ? <BadgesPage /> : <Navigate to="/lists/browse" />;
|
||||||
return (
|
return (
|
||||||
<PeopleListProvider initList="global">
|
<PeopleListProvider>
|
||||||
<BadgesPage />
|
<BadgesPage />
|
||||||
</PeopleListProvider>
|
</PeopleListProvider>
|
||||||
);
|
);
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
import { MenuItem, useDisclosure } from "@chakra-ui/react";
|
import { MenuItem, useDisclosure } from "@chakra-ui/react";
|
||||||
import { useCopyToClipboard } from "react-use";
|
|
||||||
|
|
||||||
import { CustomMenuIconButton, MenuIconButtonProps } from "../../../components/menu-icon-button";
|
import { CustomMenuIconButton, MenuIconButtonProps } from "../../../components/menu-icon-button";
|
||||||
import { NostrEvent } from "../../../types/nostr-event";
|
import { NostrEvent } from "../../../types/nostr-event";
|
||||||
import { CodeIcon, ExternalLinkIcon, RepostIcon } from "../../../components/icons";
|
import { CodeIcon } from "../../../components/icons";
|
||||||
import NoteDebugModal from "../../../components/debug-modals/note-debug-modal";
|
import NoteDebugModal from "../../../components/debug-modals/note-debug-modal";
|
||||||
import { buildAppSelectUrl } from "../../../helpers/nostr/apps";
|
|
||||||
import { getSharableEventAddress } from "../../../helpers/nip19";
|
|
||||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||||
import PencilLine from "../../../components/icons/pencil-line";
|
import PencilLine from "../../../components/icons/pencil-line";
|
||||||
|
import OpenInAppMenuItem from "../../../components/common-menu-items/open-in-app";
|
||||||
|
import CopyEmbedCodeMenuItem from "../../../components/common-menu-items/copy-embed-code";
|
||||||
|
|
||||||
export default function CommunityMenu({
|
export default function CommunityMenu({
|
||||||
community,
|
community,
|
||||||
@ -17,26 +16,17 @@ export default function CommunityMenu({
|
|||||||
}: Omit<MenuIconButtonProps, "children"> & { community: NostrEvent; onEditClick?: () => void }) {
|
}: Omit<MenuIconButtonProps, "children"> & { community: NostrEvent; onEditClick?: () => void }) {
|
||||||
const account = useCurrentAccount();
|
const account = useCurrentAccount();
|
||||||
const debugModal = useDisclosure();
|
const debugModal = useDisclosure();
|
||||||
const [_clipboardState, copyToClipboard] = useCopyToClipboard();
|
|
||||||
|
|
||||||
const address = getSharableEventAddress(community);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CustomMenuIconButton {...props}>
|
<CustomMenuIconButton {...props}>
|
||||||
{address && (
|
<OpenInAppMenuItem event={community} />
|
||||||
<MenuItem onClick={() => window.open(buildAppSelectUrl(address), "_blank")} icon={<ExternalLinkIcon />}>
|
<CopyEmbedCodeMenuItem event={community} />
|
||||||
View in app...
|
|
||||||
</MenuItem>
|
|
||||||
)}
|
|
||||||
{account?.pubkey === community.pubkey && onEditClick && (
|
{account?.pubkey === community.pubkey && onEditClick && (
|
||||||
<MenuItem onClick={onEditClick} icon={<PencilLine />}>
|
<MenuItem onClick={onEditClick} icon={<PencilLine />}>
|
||||||
Edit Community
|
Edit Community
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
<MenuItem onClick={() => copyToClipboard("nostr:" + address)} icon={<RepostIcon />}>
|
|
||||||
Copy Share Link
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem onClick={debugModal.onOpen} icon={<CodeIcon />}>
|
<MenuItem onClick={debugModal.onOpen} icon={<CodeIcon />}>
|
||||||
View Raw
|
View Raw
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
@ -2,70 +2,32 @@ import { MenuItem, useDisclosure } from "@chakra-ui/react";
|
|||||||
import { nip19 } from "nostr-tools";
|
import { nip19 } from "nostr-tools";
|
||||||
|
|
||||||
import { CustomMenuIconButton, MenuIconButtonProps } from "../../../components/menu-icon-button";
|
import { CustomMenuIconButton, MenuIconButtonProps } from "../../../components/menu-icon-button";
|
||||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
|
||||||
import { NostrEvent } from "../../../types/nostr-event";
|
import { NostrEvent } from "../../../types/nostr-event";
|
||||||
import { useMuteModalContext } from "../../../providers/mute-modal-provider";
|
import { CodeIcon, CopyToClipboardIcon } from "../../../components/icons";
|
||||||
import useUserMuteFunctions from "../../../hooks/use-user-mute-functions";
|
|
||||||
import { useDeleteEventContext } from "../../../providers/delete-event-provider";
|
|
||||||
import { getSharableEventAddress } from "../../../helpers/nip19";
|
|
||||||
import {
|
|
||||||
CodeIcon,
|
|
||||||
CopyToClipboardIcon,
|
|
||||||
ExternalLinkIcon,
|
|
||||||
MuteIcon,
|
|
||||||
RepostIcon,
|
|
||||||
TrashIcon,
|
|
||||||
UnmuteIcon,
|
|
||||||
} from "../../../components/icons";
|
|
||||||
import { buildAppSelectUrl } from "../../../helpers/nostr/apps";
|
|
||||||
import CommunityPostDebugModal from "../../../components/debug-modals/community-post-debug-modal";
|
import CommunityPostDebugModal from "../../../components/debug-modals/community-post-debug-modal";
|
||||||
|
import CopyShareLinkMenuItem from "../../../components/common-menu-items/copy-share-link";
|
||||||
|
import OpenInAppMenuItem from "../../../components/common-menu-items/open-in-app";
|
||||||
|
import DeleteEventMenuItem from "../../../components/common-menu-items/delete-event";
|
||||||
|
|
||||||
export default function CommunityPostMenu({
|
export default function CommunityPostMenu({
|
||||||
event,
|
event,
|
||||||
approvals,
|
approvals,
|
||||||
...props
|
...props
|
||||||
}: Omit<MenuIconButtonProps, "children"> & { event: NostrEvent; approvals: NostrEvent[] }) {
|
}: Omit<MenuIconButtonProps, "children"> & { event: NostrEvent; approvals: NostrEvent[] }) {
|
||||||
const account = useCurrentAccount();
|
|
||||||
const debugModal = useDisclosure();
|
const debugModal = useDisclosure();
|
||||||
|
|
||||||
// const { isMuted, unmute } = useUserMuteFunctions(event.pubkey);
|
|
||||||
// const { openModal } = useMuteModalContext();
|
|
||||||
|
|
||||||
const { deleteEvent } = useDeleteEventContext();
|
|
||||||
|
|
||||||
const address = getSharableEventAddress(event);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CustomMenuIconButton {...props}>
|
<CustomMenuIconButton {...props}>
|
||||||
{address && (
|
<OpenInAppMenuItem event={event} />
|
||||||
<MenuItem onClick={() => window.open(buildAppSelectUrl(address), "_blank")} icon={<ExternalLinkIcon />}>
|
<CopyShareLinkMenuItem event={event} />
|
||||||
View in app...
|
|
||||||
</MenuItem>
|
|
||||||
)}
|
|
||||||
{/* {account?.pubkey !== event.pubkey && (
|
|
||||||
<MenuItem
|
|
||||||
onClick={isMuted ? unmute : () => openModal(event.pubkey)}
|
|
||||||
icon={isMuted ? <UnmuteIcon /> : <MuteIcon />}
|
|
||||||
color="red.500"
|
|
||||||
>
|
|
||||||
{isMuted ? "Unmute User" : "Mute User"}
|
|
||||||
</MenuItem>
|
|
||||||
)} */}
|
|
||||||
<MenuItem onClick={() => window.navigator.clipboard.writeText("nostr:" + address)} icon={<RepostIcon />}>
|
|
||||||
Copy Share Link
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => window.navigator.clipboard.writeText(nip19.noteEncode(event.id))}
|
onClick={() => window.navigator.clipboard.writeText(nip19.noteEncode(event.id))}
|
||||||
icon={<CopyToClipboardIcon />}
|
icon={<CopyToClipboardIcon />}
|
||||||
>
|
>
|
||||||
Copy Note ID
|
Copy Note ID
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{account?.pubkey === event.pubkey && (
|
<DeleteEventMenuItem event={event} label="Delete Post" />
|
||||||
<MenuItem icon={<TrashIcon />} color="red.500" onClick={() => deleteEvent(event)}>
|
|
||||||
Delete Note
|
|
||||||
</MenuItem>
|
|
||||||
)}
|
|
||||||
<MenuItem onClick={debugModal.onOpen} icon={<CodeIcon />}>
|
<MenuItem onClick={debugModal.onOpen} icon={<CodeIcon />}>
|
||||||
View Raw
|
View Raw
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
@ -1,46 +1,25 @@
|
|||||||
import { MenuItem, useDisclosure } from "@chakra-ui/react";
|
import { MenuItem, useDisclosure } from "@chakra-ui/react";
|
||||||
import { useCopyToClipboard } from "react-use";
|
|
||||||
|
|
||||||
import { NostrEvent } from "../../../types/nostr-event";
|
import { NostrEvent } from "../../../types/nostr-event";
|
||||||
import { CustomMenuIconButton, MenuIconButtonProps } from "../../../components/menu-icon-button";
|
import { CustomMenuIconButton, MenuIconButtonProps } from "../../../components/menu-icon-button";
|
||||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
|
||||||
import NoteDebugModal from "../../../components/debug-modals/note-debug-modal";
|
import NoteDebugModal from "../../../components/debug-modals/note-debug-modal";
|
||||||
import { CodeIcon, ExternalLinkIcon, RepostIcon, TrashIcon } from "../../../components/icons";
|
import { CodeIcon } from "../../../components/icons";
|
||||||
import { getSharableEventAddress } from "../../../helpers/nip19";
|
import OpenInAppMenuItem from "../../../components/common-menu-items/open-in-app";
|
||||||
import { buildAppSelectUrl } from "../../../helpers/nostr/apps";
|
import DeleteEventMenuItem from "../../../components/common-menu-items/delete-event";
|
||||||
import { useDeleteEventContext } from "../../../providers/delete-event-provider";
|
import CopyEmbedCodeMenuItem from "../../../components/common-menu-items/copy-embed-code";
|
||||||
|
|
||||||
export default function EmojiPackMenu({
|
export default function EmojiPackMenu({
|
||||||
pack,
|
pack,
|
||||||
...props
|
...props
|
||||||
}: { pack: NostrEvent } & Omit<MenuIconButtonProps, "children">) {
|
}: { pack: NostrEvent } & Omit<MenuIconButtonProps, "children">) {
|
||||||
const account = useCurrentAccount();
|
|
||||||
const infoModal = useDisclosure();
|
const infoModal = useDisclosure();
|
||||||
|
|
||||||
const { deleteEvent } = useDeleteEventContext();
|
|
||||||
|
|
||||||
const [_clipboardState, copyToClipboard] = useCopyToClipboard();
|
|
||||||
|
|
||||||
const naddr = getSharableEventAddress(pack);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CustomMenuIconButton {...props}>
|
<CustomMenuIconButton {...props}>
|
||||||
{naddr && (
|
<OpenInAppMenuItem event={pack} />
|
||||||
<>
|
<CopyEmbedCodeMenuItem event={pack} />
|
||||||
<MenuItem onClick={() => window.open(buildAppSelectUrl(naddr), "_blank")} icon={<ExternalLinkIcon />}>
|
<DeleteEventMenuItem event={pack} label="Delete Pack" />
|
||||||
View in app...
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem onClick={() => copyToClipboard("nostr:" + naddr)} icon={<RepostIcon />}>
|
|
||||||
Copy Share Link
|
|
||||||
</MenuItem>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{account?.pubkey === pack.pubkey && (
|
|
||||||
<MenuItem icon={<TrashIcon />} color="red.500" onClick={() => deleteEvent(pack)}>
|
|
||||||
Delete Pack
|
|
||||||
</MenuItem>
|
|
||||||
)}
|
|
||||||
<MenuItem onClick={infoModal.onOpen} icon={<CodeIcon />}>
|
<MenuItem onClick={infoModal.onOpen} icon={<CodeIcon />}>
|
||||||
View Raw
|
View Raw
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
@ -14,6 +14,7 @@ import { getEventUID } from "../../helpers/nostr/events";
|
|||||||
import { GOAL_KIND, getGoalClosedDate } from "../../helpers/nostr/goal";
|
import { GOAL_KIND, getGoalClosedDate } from "../../helpers/nostr/goal";
|
||||||
import { NostrEvent } from "../../types/nostr-event";
|
import { NostrEvent } from "../../types/nostr-event";
|
||||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||||
|
import { ErrorBoundary } from "../../components/error-boundary";
|
||||||
|
|
||||||
function GoalsBrowsePage() {
|
function GoalsBrowsePage() {
|
||||||
const { filter, listId } = usePeopleListContext();
|
const { filter, listId } = usePeopleListContext();
|
||||||
@ -50,7 +51,9 @@ function GoalsBrowsePage() {
|
|||||||
|
|
||||||
<SimpleGrid columns={{ base: 1, lg: 2 }} spacing="2">
|
<SimpleGrid columns={{ base: 1, lg: 2 }} spacing="2">
|
||||||
{goals.map((event) => (
|
{goals.map((event) => (
|
||||||
<GoalCard key={getEventUID(event)} goal={event} />
|
<ErrorBoundary key={getEventUID(event)}>
|
||||||
|
<GoalCard goal={event} />
|
||||||
|
</ErrorBoundary>
|
||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</VerticalPageLayout>
|
</VerticalPageLayout>
|
||||||
|
@ -1,42 +1,20 @@
|
|||||||
import { MenuItem, useDisclosure } from "@chakra-ui/react";
|
import { MenuItem, useDisclosure } from "@chakra-ui/react";
|
||||||
import { useCopyToClipboard } from "react-use";
|
|
||||||
|
|
||||||
import { NostrEvent } from "../../../types/nostr-event";
|
import { NostrEvent } from "../../../types/nostr-event";
|
||||||
import { CustomMenuIconButton, MenuIconButtonProps } from "../../../components/menu-icon-button";
|
import { CustomMenuIconButton, MenuIconButtonProps } from "../../../components/menu-icon-button";
|
||||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
|
||||||
import NoteDebugModal from "../../../components/debug-modals/note-debug-modal";
|
import NoteDebugModal from "../../../components/debug-modals/note-debug-modal";
|
||||||
import { CodeIcon, ExternalLinkIcon, RepostIcon, TrashIcon } from "../../../components/icons";
|
import { CodeIcon } from "../../../components/icons";
|
||||||
import { getSharableEventAddress } from "../../../helpers/nip19";
|
import OpenInAppMenuItem from "../../../components/common-menu-items/open-in-app";
|
||||||
import { buildAppSelectUrl } from "../../../helpers/nostr/apps";
|
import CopyEmbedCodeMenuItem from "../../../components/common-menu-items/copy-embed-code";
|
||||||
import { useDeleteEventContext } from "../../../providers/delete-event-provider";
|
|
||||||
|
|
||||||
export default function GoalMenu({ goal, ...props }: { goal: NostrEvent } & Omit<MenuIconButtonProps, "children">) {
|
export default function GoalMenu({ goal, ...props }: { goal: NostrEvent } & Omit<MenuIconButtonProps, "children">) {
|
||||||
// const account = useCurrentAccount();
|
|
||||||
const infoModal = useDisclosure();
|
const infoModal = useDisclosure();
|
||||||
|
|
||||||
// const { deleteEvent } = useDeleteEventContext();
|
|
||||||
const [_clipboardState, copyToClipboard] = useCopyToClipboard();
|
|
||||||
|
|
||||||
const nevent = getSharableEventAddress(goal);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CustomMenuIconButton {...props}>
|
<CustomMenuIconButton {...props}>
|
||||||
{nevent && (
|
<OpenInAppMenuItem event={goal} />
|
||||||
<>
|
<CopyEmbedCodeMenuItem event={goal} />
|
||||||
<MenuItem onClick={() => window.open(buildAppSelectUrl(nevent), "_blank")} icon={<ExternalLinkIcon />}>
|
|
||||||
View in app...
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem onClick={() => copyToClipboard("nostr:" + nevent)} icon={<RepostIcon />}>
|
|
||||||
Copy Share Link
|
|
||||||
</MenuItem>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{/* {account?.pubkey === goal.pubkey && (
|
|
||||||
<MenuItem icon={<TrashIcon />} color="red.500" onClick={() => deleteEvent(goal)}>
|
|
||||||
Delete Goal
|
|
||||||
</MenuItem>
|
|
||||||
)} */}
|
|
||||||
<MenuItem onClick={infoModal.onOpen} icon={<CodeIcon />}>
|
<MenuItem onClick={infoModal.onOpen} icon={<CodeIcon />}>
|
||||||
View Raw
|
View Raw
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
@ -7,6 +7,7 @@ import { NOTE_LIST_KIND, PEOPLE_LIST_KIND } from "../../helpers/nostr/lists";
|
|||||||
import { ErrorBoundary } from "../../components/error-boundary";
|
import { ErrorBoundary } from "../../components/error-boundary";
|
||||||
import { COMMUNITY_DEFINITION_KIND } from "../../helpers/nostr/communities";
|
import { COMMUNITY_DEFINITION_KIND } from "../../helpers/nostr/communities";
|
||||||
import { decode } from "ngeohash";
|
import { decode } from "ngeohash";
|
||||||
|
import { TORRENT_KIND } from "../../helpers/nostr/torrents";
|
||||||
|
|
||||||
function NostrLinkPage() {
|
function NostrLinkPage() {
|
||||||
const { link } = useParams() as { link?: string };
|
const { link } = useParams() as { link?: string };
|
||||||
@ -36,6 +37,7 @@ function NostrLinkPage() {
|
|||||||
if (decoded.data.kind === PEOPLE_LIST_KIND) return <Navigate to={`/lists/${cleanLink}`} replace />;
|
if (decoded.data.kind === PEOPLE_LIST_KIND) return <Navigate to={`/lists/${cleanLink}`} replace />;
|
||||||
if (decoded.data.kind === Kind.BadgeDefinition) return <Navigate to={`/badges/${cleanLink}`} replace />;
|
if (decoded.data.kind === Kind.BadgeDefinition) return <Navigate to={`/badges/${cleanLink}`} replace />;
|
||||||
if (decoded.data.kind === COMMUNITY_DEFINITION_KIND) return <Navigate to={`/c/${cleanLink}`} replace />;
|
if (decoded.data.kind === COMMUNITY_DEFINITION_KIND) return <Navigate to={`/c/${cleanLink}`} replace />;
|
||||||
|
if (decoded.data.kind === TORRENT_KIND) return <Navigate to={`/torrents/${cleanLink}`} replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,24 +1,17 @@
|
|||||||
import { Image, MenuItem, useDisclosure } from "@chakra-ui/react";
|
import { Image, MenuItem, useDisclosure } from "@chakra-ui/react";
|
||||||
import { useCopyToClipboard } from "react-use";
|
|
||||||
|
|
||||||
import { NostrEvent, isPTag } from "../../../types/nostr-event";
|
import { NostrEvent, isPTag } from "../../../types/nostr-event";
|
||||||
import { CustomMenuIconButton, MenuIconButtonProps } from "../../../components/menu-icon-button";
|
import { CustomMenuIconButton, MenuIconButtonProps } from "../../../components/menu-icon-button";
|
||||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
|
||||||
import NoteDebugModal from "../../../components/debug-modals/note-debug-modal";
|
import NoteDebugModal from "../../../components/debug-modals/note-debug-modal";
|
||||||
import { CodeIcon, ExternalLinkIcon, RepostIcon, TrashIcon } from "../../../components/icons";
|
import { CodeIcon } from "../../../components/icons";
|
||||||
import { getSharableEventAddress } from "../../../helpers/nip19";
|
import { getSharableEventAddress } from "../../../helpers/nip19";
|
||||||
import { buildAppSelectUrl } from "../../../helpers/nostr/apps";
|
import DeleteEventMenuItem from "../../../components/common-menu-items/delete-event";
|
||||||
import { useDeleteEventContext } from "../../../providers/delete-event-provider";
|
import OpenInAppMenuItem from "../../../components/common-menu-items/open-in-app";
|
||||||
import { isSpecialListKind } from "../../../helpers/nostr/lists";
|
import CopyEmbedCodeMenuItem from "../../../components/common-menu-items/copy-embed-code";
|
||||||
|
|
||||||
export default function ListMenu({ list, ...props }: { list: NostrEvent } & Omit<MenuIconButtonProps, "children">) {
|
export default function ListMenu({ list, ...props }: { list: NostrEvent } & Omit<MenuIconButtonProps, "children">) {
|
||||||
const account = useCurrentAccount();
|
|
||||||
const infoModal = useDisclosure();
|
const infoModal = useDisclosure();
|
||||||
|
|
||||||
const { deleteEvent } = useDeleteEventContext();
|
|
||||||
|
|
||||||
const [_clipboardState, copyToClipboard] = useCopyToClipboard();
|
|
||||||
|
|
||||||
const naddr = getSharableEventAddress(list);
|
const naddr = getSharableEventAddress(list);
|
||||||
|
|
||||||
const hasPeople = list.tags.some(isPTag);
|
const hasPeople = list.tags.some(isPTag);
|
||||||
@ -26,21 +19,9 @@ export default function ListMenu({ list, ...props }: { list: NostrEvent } & Omit
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CustomMenuIconButton {...props}>
|
<CustomMenuIconButton {...props}>
|
||||||
{naddr && (
|
<OpenInAppMenuItem event={list} />
|
||||||
<>
|
<CopyEmbedCodeMenuItem event={list} />
|
||||||
<MenuItem onClick={() => window.open(buildAppSelectUrl(naddr), "_blank")} icon={<ExternalLinkIcon />}>
|
<DeleteEventMenuItem event={list} label="Delete List" />
|
||||||
View in app...
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem onClick={() => copyToClipboard("nostr:" + naddr)} icon={<RepostIcon />}>
|
|
||||||
Copy Share Link
|
|
||||||
</MenuItem>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{account?.pubkey === list.pubkey && !isSpecialListKind(list.kind) && (
|
|
||||||
<MenuItem icon={<TrashIcon />} color="red.500" onClick={() => deleteEvent(list)}>
|
|
||||||
Delete List
|
|
||||||
</MenuItem>
|
|
||||||
)}
|
|
||||||
{hasPeople && (
|
{hasPeople && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={<Image w="4" h="4" src="https://www.makeprisms.com/favicon.ico" />}
|
icon={<Image w="4" h="4" src="https://www.makeprisms.com/favicon.ico" />}
|
||||||
|
@ -16,6 +16,7 @@ import Heart from "../../components/icons/heart";
|
|||||||
import UserAvatarLink from "../../components/user-avatar-link";
|
import UserAvatarLink from "../../components/user-avatar-link";
|
||||||
import { AtIcon, ChevronDownIcon, ChevronUpIcon, LightningIcon, ReplyIcon, RepostIcon } from "../../components/icons";
|
import { AtIcon, ChevronDownIcon, ChevronUpIcon, LightningIcon, ReplyIcon, RepostIcon } from "../../components/icons";
|
||||||
import useSingleEvent from "../../hooks/use-single-event";
|
import useSingleEvent from "../../hooks/use-single-event";
|
||||||
|
import { CompactNoteContent } from "../../components/compact-note-content";
|
||||||
|
|
||||||
const IconBox = ({ children }: PropsWithChildren) => (
|
const IconBox = ({ children }: PropsWithChildren) => (
|
||||||
<Box px="2" pb="2">
|
<Box px="2" pb="2">
|
||||||
@ -165,7 +166,6 @@ const ZapNotification = forwardRef<HTMLDivElement, { event: NostrEvent }>(({ eve
|
|||||||
</AvatarGroup>
|
</AvatarGroup>
|
||||||
<Text>{readablizeSats(zap.payment.amount / 1000)} sats</Text>
|
<Text>{readablizeSats(zap.payment.amount / 1000)} sats</Text>
|
||||||
<ExpandableToggleButton aria-label="Toggle event" ml="auto" toggle={expanded} />
|
<ExpandableToggleButton aria-label="Toggle event" ml="auto" toggle={expanded} />
|
||||||
{/* <Timestamp timestamp={event.created_at} ml="auto" /> */}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
{expanded.isOpen && eventJSX}
|
{expanded.isOpen && eventJSX}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
42
src/views/torrents/components/torrent-menu.tsx
Normal file
42
src/views/torrents/components/torrent-menu.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { MenuItem, useDisclosure } from "@chakra-ui/react";
|
||||||
|
|
||||||
|
import { CustomMenuIconButton, MenuIconButtonProps } from "../../../components/menu-icon-button";
|
||||||
|
import { NostrEvent } from "../../../types/nostr-event";
|
||||||
|
import { CodeIcon, TranslateIcon } from "../../../components/icons";
|
||||||
|
import DeleteEventMenuItem from "../../../components/common-menu-items/delete-event";
|
||||||
|
import NoteTranslationModal from "../../../components/note-translation-modal";
|
||||||
|
import NoteDebugModal from "../../../components/debug-modals/note-debug-modal";
|
||||||
|
import MuteUserMenuItem from "../../../components/common-menu-items/mute-user";
|
||||||
|
import OpenInAppMenuItem from "../../../components/common-menu-items/open-in-app";
|
||||||
|
import CopyEmbedCodeMenuItem from "../../../components/common-menu-items/copy-embed-code";
|
||||||
|
|
||||||
|
export default function TorrentMenu({
|
||||||
|
torrent,
|
||||||
|
...props
|
||||||
|
}: { torrent: NostrEvent } & Omit<MenuIconButtonProps, "children">) {
|
||||||
|
const debugModal = useDisclosure();
|
||||||
|
const translationsModal = useDisclosure();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CustomMenuIconButton {...props}>
|
||||||
|
<OpenInAppMenuItem event={torrent} />
|
||||||
|
<CopyEmbedCodeMenuItem event={torrent} />
|
||||||
|
<MuteUserMenuItem event={torrent} />
|
||||||
|
<DeleteEventMenuItem event={torrent} />
|
||||||
|
<MenuItem onClick={translationsModal.onOpen} icon={<TranslateIcon />}>
|
||||||
|
Translations
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={debugModal.onOpen} icon={<CodeIcon />}>
|
||||||
|
View Raw
|
||||||
|
</MenuItem>
|
||||||
|
</CustomMenuIconButton>
|
||||||
|
|
||||||
|
{debugModal.isOpen && (
|
||||||
|
<NoteDebugModal event={torrent} isOpen={debugModal.isOpen} onClose={debugModal.onClose} size="6xl" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{translationsModal.isOpen && <NoteTranslationModal isOpen onClose={translationsModal.onClose} note={torrent} />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -12,6 +12,7 @@ import { useRegisterIntersectionEntity } from "../../../providers/intersection-o
|
|||||||
import { getEventUID } from "../../../helpers/nostr/events";
|
import { getEventUID } from "../../../helpers/nostr/events";
|
||||||
import { formatBytes } from "../../../helpers/number";
|
import { formatBytes } from "../../../helpers/number";
|
||||||
import NoteZapButton from "../../../components/note/note-zap-button";
|
import NoteZapButton from "../../../components/note/note-zap-button";
|
||||||
|
import TorrentMenu from "./torrent-menu";
|
||||||
|
|
||||||
export default function TorrentTableRow({ torrent }: { torrent: NostrEvent }) {
|
export default function TorrentTableRow({ torrent }: { torrent: NostrEvent }) {
|
||||||
const ref = useRef<HTMLTableRowElement | null>(null);
|
const ref = useRef<HTMLTableRowElement | null>(null);
|
||||||
@ -39,10 +40,11 @@ export default function TorrentTableRow({ torrent }: { torrent: NostrEvent }) {
|
|||||||
<Td>
|
<Td>
|
||||||
<UserLink pubkey={torrent.pubkey} tab="torrents" />
|
<UserLink pubkey={torrent.pubkey} tab="torrents" />
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td isNumeric>
|
||||||
<ButtonGroup variant="ghost" size="xs">
|
<ButtonGroup variant="ghost" size="xs">
|
||||||
<IconButton as={Link} icon={<Magnet />} aria-label="Magnet URI" isExternal href={magnetLink} />
|
|
||||||
<NoteZapButton event={torrent} />
|
<NoteZapButton event={torrent} />
|
||||||
|
<IconButton as={Link} icon={<Magnet />} aria-label="Magnet URI" isExternal href={magnetLink} />
|
||||||
|
<TorrentMenu torrent={torrent} aria-label="More Options" ml="auto" />
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { Flex, Table, TableContainer, Tbody, Th, Thead, Tr } from "@chakra-ui/react";
|
import { Alert, Button, Flex, Spacer, Table, TableContainer, Tbody, Th, Thead, Tr, useToast } from "@chakra-ui/react";
|
||||||
|
import { Link as RouterLink, useNavigate } from "react-router-dom";
|
||||||
|
import { generatePrivateKey, getPublicKey } from "nostr-tools";
|
||||||
|
|
||||||
import PeopleListSelection from "../../components/people-list-selection/people-list-selection";
|
import PeopleListSelection from "../../components/people-list-selection/people-list-selection";
|
||||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||||
@ -14,6 +16,44 @@ import useSubject from "../../hooks/use-subject";
|
|||||||
import TorrentTableRow from "./components/torrent-table-row";
|
import TorrentTableRow from "./components/torrent-table-row";
|
||||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||||
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
||||||
|
import useCurrentAccount from "../../hooks/use-current-account";
|
||||||
|
import { useUserMetadata } from "../../hooks/use-user-metadata";
|
||||||
|
import accountService from "../../services/account";
|
||||||
|
import signingService from "../../services/signing";
|
||||||
|
|
||||||
|
function Warning() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const toast = useToast();
|
||||||
|
const account = useCurrentAccount()!;
|
||||||
|
const metadata = useUserMetadata(account.pubkey);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const createAnonAccount = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const secKey = generatePrivateKey();
|
||||||
|
const encrypted = await signingService.encryptSecKey(secKey);
|
||||||
|
const pubkey = getPublicKey(secKey);
|
||||||
|
accountService.addAccount({ ...encrypted, pubkey, readonly: false });
|
||||||
|
accountService.switchAccount(pubkey);
|
||||||
|
navigate("/relays");
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
!!metadata && (
|
||||||
|
<Alert status="warning" flexWrap="wrap">
|
||||||
|
There are many jurisdictions where Torrenting is illegal, You should probably not use your personal nostr
|
||||||
|
account.
|
||||||
|
<Button onClick={createAnonAccount} variant="link" ml="auto" isLoading={loading}>
|
||||||
|
Create anon account
|
||||||
|
</Button>
|
||||||
|
</Alert>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function TorrentsPage() {
|
function TorrentsPage() {
|
||||||
const { filter, listId } = usePeopleListContext();
|
const { filter, listId } = usePeopleListContext();
|
||||||
@ -31,17 +71,24 @@ function TorrentsPage() {
|
|||||||
`${listId}-torrents`,
|
`${listId}-torrents`,
|
||||||
relays,
|
relays,
|
||||||
{ ...filter, kinds: [TORRENT_KIND] },
|
{ ...filter, kinds: [TORRENT_KIND] },
|
||||||
{ eventFilter },
|
{ eventFilter, enabled: !!filter },
|
||||||
);
|
);
|
||||||
|
|
||||||
const torrents = useSubject(timeline.timeline);
|
const torrents = useSubject(timeline.timeline);
|
||||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||||
|
|
||||||
|
const account = useCurrentAccount();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VerticalPageLayout>
|
<VerticalPageLayout>
|
||||||
|
{!!account && <Warning />}
|
||||||
<Flex gap="2">
|
<Flex gap="2">
|
||||||
<RelaySelectionButton />
|
<RelaySelectionButton />
|
||||||
<PeopleListSelection />
|
<PeopleListSelection />
|
||||||
|
<Spacer />
|
||||||
|
<Button as={RouterLink} to="/torrents/new">
|
||||||
|
New Torrent
|
||||||
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
<IntersectionObserverProvider callback={callback}>
|
<IntersectionObserverProvider callback={callback}>
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
|
274
src/views/torrents/new.tsx
Normal file
274
src/views/torrents/new.tsx
Normal file
@ -0,0 +1,274 @@
|
|||||||
|
import { PropsWithChildren, ReactNode, useRef } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
ButtonGroup,
|
||||||
|
Flex,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
Heading,
|
||||||
|
Input,
|
||||||
|
NumberDecrementStepper,
|
||||||
|
NumberIncrementStepper,
|
||||||
|
NumberInput,
|
||||||
|
NumberInputField,
|
||||||
|
NumberInputStepper,
|
||||||
|
Textarea,
|
||||||
|
UseRadioProps,
|
||||||
|
VisuallyHiddenInput,
|
||||||
|
useRadio,
|
||||||
|
useRadioGroup,
|
||||||
|
useToast,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { bytesToHex } from "@noble/hashes/utils";
|
||||||
|
import { sha1 } from "@noble/hashes/sha1";
|
||||||
|
|
||||||
|
import { BencodeValue, decode, encode } from "../../lib/bencode";
|
||||||
|
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||||
|
import { Category, TORRENT_KIND, torrentCatagories } from "../../helpers/nostr/torrents";
|
||||||
|
import { useBreakpointValue } from "../../providers/breakpoint-provider";
|
||||||
|
import { DraftNostrEvent } from "../../types/nostr-event";
|
||||||
|
import { useSigningContext } from "../../providers/signing-provider";
|
||||||
|
import NostrPublishAction from "../../classes/nostr-publish-action";
|
||||||
|
import clientRelaysService from "../../services/client-relays";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { nip19 } from "nostr-tools";
|
||||||
|
|
||||||
|
function RadioCard(props: UseRadioProps & PropsWithChildren) {
|
||||||
|
const { getInputProps, getRadioProps } = useRadio(props);
|
||||||
|
|
||||||
|
const input = getInputProps();
|
||||||
|
const checkbox = getRadioProps();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box as="label">
|
||||||
|
<input {...input} />
|
||||||
|
<Button
|
||||||
|
as="div"
|
||||||
|
{...checkbox}
|
||||||
|
cursor="pointer"
|
||||||
|
variant="outline"
|
||||||
|
colorScheme={checkbox["data-checked"] !== undefined ? "primary" : undefined}
|
||||||
|
px="3"
|
||||||
|
py="1"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NewTorrentView() {
|
||||||
|
const toast = useToast();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { requestSignature } = useSigningContext();
|
||||||
|
const torrentFileInput = useRef<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
|
const smallLayout = useBreakpointValue({ base: true, lg: false });
|
||||||
|
const { getValues, watch, setValue, register, handleSubmit, formState } = useForm({
|
||||||
|
defaultValues: {
|
||||||
|
title: "",
|
||||||
|
description: "",
|
||||||
|
btih: "",
|
||||||
|
tags: [] as string[],
|
||||||
|
files: [] as {
|
||||||
|
name: string;
|
||||||
|
size: number;
|
||||||
|
}[],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectTorrentFile = async (file: File) => {
|
||||||
|
const buf = await file.arrayBuffer();
|
||||||
|
const torrent = decode(new Uint8Array(buf)) as Record<string, BencodeValue>;
|
||||||
|
const infoBuf = encode(torrent["info"]);
|
||||||
|
const info = torrent["info"] as {
|
||||||
|
files?: Array<{ length: number; path: Array<Uint8Array> }>;
|
||||||
|
length: number;
|
||||||
|
name: Uint8Array;
|
||||||
|
};
|
||||||
|
|
||||||
|
const dec = new TextDecoder();
|
||||||
|
setValue("title", dec.decode(info.name));
|
||||||
|
const comment = dec.decode(torrent["comment"] as Uint8Array | undefined) ?? "";
|
||||||
|
if (comment) setValue("description", comment);
|
||||||
|
setValue("btih", bytesToHex(sha1(infoBuf)));
|
||||||
|
setValue("tags", []);
|
||||||
|
const files = (info.files ?? [{ length: info.length, path: [info.name] }]).map((a) => ({
|
||||||
|
size: a.length,
|
||||||
|
name: a.path.map((b) => dec.decode(b)).join("/"),
|
||||||
|
}));
|
||||||
|
setValue("files", files);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = handleSubmit(async (values) => {
|
||||||
|
try {
|
||||||
|
const draft: DraftNostrEvent = {
|
||||||
|
kind: TORRENT_KIND,
|
||||||
|
content: values.description,
|
||||||
|
tags: [
|
||||||
|
["title", values.title],
|
||||||
|
["btih", values.btih],
|
||||||
|
...values.tags.map((v) => ["t", v]),
|
||||||
|
...values.files.map((f) => ["file", f.name, String(f.size)]),
|
||||||
|
],
|
||||||
|
created_at: dayjs().unix(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const signed = await requestSignature(draft);
|
||||||
|
new NostrPublishAction("Publish Torrent", clientRelaysService.getWriteUrls(), signed);
|
||||||
|
|
||||||
|
navigate(`/torrents/${nip19.noteEncode(signed.id)}`);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { getRootProps, getRadioProps } = useRadioGroup({
|
||||||
|
name: "category",
|
||||||
|
value: getValues().tags.join(","),
|
||||||
|
onChange: (v) => setValue("tags", v.split(","), { shouldDirty: true, shouldTouch: true }),
|
||||||
|
});
|
||||||
|
|
||||||
|
watch("tags");
|
||||||
|
watch("files");
|
||||||
|
function renderCategories() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{torrentCatagories.map((category) => (
|
||||||
|
<Box key={category.tag}>
|
||||||
|
<Heading size="sm" mt="2" mb="1">
|
||||||
|
{category.name}
|
||||||
|
</Heading>
|
||||||
|
<Flex gap="2" wrap="wrap">
|
||||||
|
{renderCategory(category, [category.tag])}
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function renderCategory(a: Category, tags: Array<string>): ReactNode {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<RadioCard {...getRadioProps({ value: tags.join(",") })}>{a.name}</RadioCard>
|
||||||
|
{a.sub_category?.map((b) => renderCategory(b, [...tags, b.tag]))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const descriptionInput = (
|
||||||
|
<FormControl isRequired>
|
||||||
|
<FormLabel>Description</FormLabel>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Description"
|
||||||
|
rows={smallLayout ? 10 : 25}
|
||||||
|
{...register("description", { required: true })}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VerticalPageLayout as="form" onSubmit={onSubmit}>
|
||||||
|
<Heading size="lg">New Torrent</Heading>
|
||||||
|
|
||||||
|
<ButtonGroup>
|
||||||
|
<VisuallyHiddenInput
|
||||||
|
type="file"
|
||||||
|
accept="application/x-bittorrent"
|
||||||
|
ref={torrentFileInput}
|
||||||
|
onChange={(e) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (file) selectTorrentFile(file);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button onClick={() => torrentFileInput.current?.click()}>Import Torrent file</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
|
||||||
|
<Flex gap="4">
|
||||||
|
<Flex gap="2" direction="column" w="full">
|
||||||
|
<FormControl isRequired>
|
||||||
|
<FormLabel>Title</FormLabel>
|
||||||
|
<Input type="text" {...register("title", { required: true })} />
|
||||||
|
</FormControl>
|
||||||
|
<FormControl isRequired>
|
||||||
|
<FormLabel>Info Hash</FormLabel>
|
||||||
|
<Input type="text" {...register("btih", { required: true })} placeholder="hex" />
|
||||||
|
</FormControl>
|
||||||
|
{smallLayout && descriptionInput}
|
||||||
|
<Heading size="md">Category</Heading>
|
||||||
|
<Box {...getRootProps()}>{renderCategories()}</Box>
|
||||||
|
</Flex>
|
||||||
|
{!smallLayout && (
|
||||||
|
<Flex gap="2" direction="column" w="full">
|
||||||
|
{descriptionInput}
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
<Flex direction="column" gap="2">
|
||||||
|
{getValues().files.map((file, i) => (
|
||||||
|
<Flex gap="2">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={file.name}
|
||||||
|
className="flex-1"
|
||||||
|
placeholder="collection1/IMG_00001.jpg"
|
||||||
|
onChange={(e) =>
|
||||||
|
setValue(
|
||||||
|
"files",
|
||||||
|
getValues().files.map((f, ii) => {
|
||||||
|
if (ii === i) {
|
||||||
|
return { ...f, name: e.target.value };
|
||||||
|
}
|
||||||
|
return f;
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<NumberInput
|
||||||
|
value={file.size}
|
||||||
|
min={0}
|
||||||
|
onChange={(v) =>
|
||||||
|
setValue(
|
||||||
|
"files",
|
||||||
|
getValues().files.map((f, ii) => {
|
||||||
|
if (ii === i) {
|
||||||
|
return { ...f, size: parseInt(v) };
|
||||||
|
}
|
||||||
|
return f;
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<NumberInputField />
|
||||||
|
<NumberInputStepper>
|
||||||
|
<NumberIncrementStepper />
|
||||||
|
<NumberDecrementStepper />
|
||||||
|
</NumberInputStepper>
|
||||||
|
</NumberInput>
|
||||||
|
<Button
|
||||||
|
flexShrink={0}
|
||||||
|
onClick={() =>
|
||||||
|
setValue(
|
||||||
|
"files",
|
||||||
|
getValues().files.filter((_, ii) => i !== ii),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
<Flex gap="2" justifyContent="flex-end">
|
||||||
|
<Button onClick={() => setValue("files", [...getValues().files, { name: "", size: 0 }])}>Add file info</Button>
|
||||||
|
<Button type="submit" isLoading={formState.isSubmitting} colorScheme="primary">
|
||||||
|
Publish
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</VerticalPageLayout>
|
||||||
|
);
|
||||||
|
}
|
@ -13,6 +13,7 @@ import {
|
|||||||
Tbody,
|
Tbody,
|
||||||
Td,
|
Td,
|
||||||
Text,
|
Text,
|
||||||
|
Textarea,
|
||||||
Th,
|
Th,
|
||||||
Thead,
|
Thead,
|
||||||
Tr,
|
Tr,
|
||||||
@ -32,17 +33,22 @@ import { formatBytes } from "../../helpers/number";
|
|||||||
import { NoteContents } from "../../components/note/text-note-contents";
|
import { NoteContents } from "../../components/note/text-note-contents";
|
||||||
import Timestamp from "../../components/timestamp";
|
import Timestamp from "../../components/timestamp";
|
||||||
import NoteZapButton from "../../components/note/note-zap-button";
|
import NoteZapButton from "../../components/note/note-zap-button";
|
||||||
|
import TorrentMenu from "./components/torrent-menu";
|
||||||
|
import { QuoteRepostButton } from "../../components/note/components/quote-repost-button";
|
||||||
|
|
||||||
function TorrentDetailsPage({ torrent }: { torrent: NostrEvent }) {
|
function TorrentDetailsPage({ torrent }: { torrent: NostrEvent }) {
|
||||||
const files = getTorrentFiles(torrent);
|
const files = getTorrentFiles(torrent);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VerticalPageLayout>
|
<VerticalPageLayout>
|
||||||
<Flex as={Heading} size="md" gap="2" alignItems="center" wrap="wrap">
|
<Flex gap="2" alignItems="center" wrap="wrap">
|
||||||
<UserAvatarLink pubkey={torrent.pubkey} size="md" />
|
<Flex as={Heading} size="md" gap="2" alignItems="center" wrap="wrap">
|
||||||
<UserLink pubkey={torrent.pubkey} fontWeight="bold" />
|
<UserAvatarLink pubkey={torrent.pubkey} size="md" />
|
||||||
<Text> - </Text>
|
<UserLink pubkey={torrent.pubkey} fontWeight="bold" />
|
||||||
<Text>{getTorrentTitle(torrent)}</Text>
|
<Text> - </Text>
|
||||||
|
<Text>{getTorrentTitle(torrent)}</Text>
|
||||||
|
</Flex>
|
||||||
|
<TorrentMenu torrent={torrent} ml="auto" aria-label="More Options" />
|
||||||
</Flex>
|
</Flex>
|
||||||
<Card p="2" display="flex" gap="2" flexDirection="column" alignItems="flex-start">
|
<Card p="2" display="flex" gap="2" flexDirection="column" alignItems="flex-start">
|
||||||
<Text>Size: {formatBytes(getTorrentSize(torrent))}</Text>
|
<Text>Size: {formatBytes(getTorrentSize(torrent))}</Text>
|
||||||
@ -59,6 +65,7 @@ function TorrentDetailsPage({ torrent }: { torrent: NostrEvent }) {
|
|||||||
</Flex>
|
</Flex>
|
||||||
<ButtonGroup variant="ghost" size="sm">
|
<ButtonGroup variant="ghost" size="sm">
|
||||||
<NoteZapButton event={torrent} />
|
<NoteZapButton event={torrent} />
|
||||||
|
<QuoteRepostButton event={torrent} />
|
||||||
<Button as={Link} leftIcon={<Magnet boxSize={5} />} href={getTorrentMagnetLink(torrent)} isExternal>
|
<Button as={Link} leftIcon={<Magnet boxSize={5} />} href={getTorrentMagnetLink(torrent)} isExternal>
|
||||||
Download torrent
|
Download torrent
|
||||||
</Button>
|
</Button>
|
||||||
@ -93,6 +100,11 @@ function TorrentDetailsPage({ torrent }: { torrent: NostrEvent }) {
|
|||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Heading size="sm" mt="2">
|
||||||
|
Comments (Coming soon)
|
||||||
|
</Heading>
|
||||||
|
<Textarea placeholder="Coming soon" isDisabled />
|
||||||
</VerticalPageLayout>
|
</VerticalPageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Table, TableContainer, Tbody, Th, Thead, Tr } from "@chakra-ui/react";
|
import { Table, TableContainer, Tbody, Th, Thead, Tr } from "@chakra-ui/react";
|
||||||
import { useOutletContext } from "react-router-dom";
|
import { useOutletContext } from "react-router-dom";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
|
||||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||||
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
||||||
@ -8,17 +9,24 @@ import useSubject from "../../hooks/use-subject";
|
|||||||
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
||||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||||
import { TORRENT_KIND } from "../../helpers/nostr/torrents";
|
import { TORRENT_KIND, validateTorrent } from "../../helpers/nostr/torrents";
|
||||||
import TorrentTableRow from "../torrents/components/torrent-table-row";
|
import TorrentTableRow from "../torrents/components/torrent-table-row";
|
||||||
|
import { NostrEvent } from "../../types/nostr-event";
|
||||||
|
|
||||||
export default function UserTorrentsTab() {
|
export default function UserTorrentsTab() {
|
||||||
const { pubkey } = useOutletContext() as { pubkey: string };
|
const { pubkey } = useOutletContext() as { pubkey: string };
|
||||||
const contextRelays = useAdditionalRelayContext();
|
const contextRelays = useAdditionalRelayContext();
|
||||||
|
|
||||||
const timeline = useTimelineLoader(`${pubkey}-torrents`, contextRelays, {
|
const eventFilter = useCallback((t: NostrEvent) => validateTorrent(t), []);
|
||||||
authors: [pubkey],
|
const timeline = useTimelineLoader(
|
||||||
kinds: [TORRENT_KIND],
|
`${pubkey}-torrents`,
|
||||||
});
|
contextRelays,
|
||||||
|
{
|
||||||
|
authors: [pubkey],
|
||||||
|
kinds: [TORRENT_KIND],
|
||||||
|
},
|
||||||
|
{ eventFilter },
|
||||||
|
);
|
||||||
|
|
||||||
const torrents = useSubject(timeline.timeline);
|
const torrents = useSubject(timeline.timeline);
|
||||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||||
|
@ -2377,7 +2377,7 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9"
|
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9"
|
||||||
integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==
|
integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==
|
||||||
|
|
||||||
"@noble/hashes@1.3.2", "@noble/hashes@~1.3.0", "@noble/hashes@~1.3.1", "@noble/hashes@~1.3.2":
|
"@noble/hashes@1.3.2", "@noble/hashes@^1.3.2", "@noble/hashes@~1.3.0", "@noble/hashes@~1.3.1", "@noble/hashes@~1.3.2":
|
||||||
version "1.3.2"
|
version "1.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39"
|
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39"
|
||||||
integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==
|
integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==
|
||||||
|
Loading…
x
Reference in New Issue
Block a user