(null);
@@ -39,10 +40,11 @@ export default function TorrentTableRow({ torrent }: { torrent: NostrEvent }) {
|
-
+ |
- } aria-label="Magnet URI" isExternal href={magnetLink} />
+ } aria-label="Magnet URI" isExternal href={magnetLink} />
+
|
diff --git a/src/views/torrents/index.tsx b/src/views/torrents/index.tsx
index ef28d95ac..7f1a742fc 100644
--- a/src/views/torrents/index.tsx
+++ b/src/views/torrents/index.tsx
@@ -1,5 +1,7 @@
-import { useCallback } from "react";
-import { Flex, Table, TableContainer, Tbody, Th, Thead, Tr } from "@chakra-ui/react";
+import { useCallback, useState } from "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 VerticalPageLayout from "../../components/vertical-page-layout";
@@ -14,6 +16,44 @@ import useSubject from "../../hooks/use-subject";
import TorrentTableRow from "./components/torrent-table-row";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
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 && (
+
+ There are many jurisdictions where Torrenting is illegal, You should probably not use your personal nostr
+ account.
+
+
+ )
+ );
+}
function TorrentsPage() {
const { filter, listId } = usePeopleListContext();
@@ -31,17 +71,24 @@ function TorrentsPage() {
`${listId}-torrents`,
relays,
{ ...filter, kinds: [TORRENT_KIND] },
- { eventFilter },
+ { eventFilter, enabled: !!filter },
);
const torrents = useSubject(timeline.timeline);
const callback = useTimelineCurserIntersectionCallback(timeline);
+ const account = useCurrentAccount();
+
return (
+ {!!account && }
+
+
diff --git a/src/views/torrents/new.tsx b/src/views/torrents/new.tsx
new file mode 100644
index 000000000..d79d9340d
--- /dev/null
+++ b/src/views/torrents/new.tsx
@@ -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 (
+
+
+
+
+ );
+}
+
+export default function NewTorrentView() {
+ const toast = useToast();
+ const navigate = useNavigate();
+ const { requestSignature } = useSigningContext();
+ const torrentFileInput = useRef(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;
+ const infoBuf = encode(torrent["info"]);
+ const info = torrent["info"] as {
+ files?: Array<{ length: number; path: Array }>;
+ 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) => (
+
+
+ {category.name}
+
+
+ {renderCategory(category, [category.tag])}
+
+
+ ))}
+ >
+ );
+ }
+ function renderCategory(a: Category, tags: Array): ReactNode {
+ return (
+ <>
+ {a.name}
+ {a.sub_category?.map((b) => renderCategory(b, [...tags, b.tag]))}
+ >
+ );
+ }
+
+ const descriptionInput = (
+
+ Description
+
+
+ );
+
+ return (
+
+ New Torrent
+
+
+ {
+ const file = e.target.files?.[0];
+ if (file) selectTorrentFile(file);
+ }}
+ />
+
+
+
+
+
+
+ Title
+
+
+
+ Info Hash
+
+
+ {smallLayout && descriptionInput}
+ Category
+ {renderCategories()}
+
+ {!smallLayout && (
+
+ {descriptionInput}
+
+ )}
+
+
+ {getValues().files.map((file, i) => (
+
+
+ setValue(
+ "files",
+ getValues().files.map((f, ii) => {
+ if (ii === i) {
+ return { ...f, name: e.target.value };
+ }
+ return f;
+ }),
+ )
+ }
+ />
+
+ setValue(
+ "files",
+ getValues().files.map((f, ii) => {
+ if (ii === i) {
+ return { ...f, size: parseInt(v) };
+ }
+ return f;
+ }),
+ )
+ }
+ >
+
+
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+
+ );
+}
diff --git a/src/views/torrents/torrent.tsx b/src/views/torrents/torrent.tsx
index e582799f6..9c479995d 100644
--- a/src/views/torrents/torrent.tsx
+++ b/src/views/torrents/torrent.tsx
@@ -13,6 +13,7 @@ import {
Tbody,
Td,
Text,
+ Textarea,
Th,
Thead,
Tr,
@@ -32,17 +33,22 @@ import { formatBytes } from "../../helpers/number";
import { NoteContents } from "../../components/note/text-note-contents";
import Timestamp from "../../components/timestamp";
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 }) {
const files = getTorrentFiles(torrent);
return (
-
-
-
- -
- {getTorrentTitle(torrent)}
+
+
+
+
+ -
+ {getTorrentTitle(torrent)}
+
+
Size: {formatBytes(getTorrentSize(torrent))}
@@ -59,6 +65,7 @@ function TorrentDetailsPage({ torrent }: { torrent: NostrEvent }) {
+
} href={getTorrentMagnetLink(torrent)} isExternal>
Download torrent
@@ -93,6 +100,11 @@ function TorrentDetailsPage({ torrent }: { torrent: NostrEvent }) {
+
+
+ Comments (Coming soon)
+
+
);
}
diff --git a/src/views/user/torrents.tsx b/src/views/user/torrents.tsx
index f0ec1baeb..a489bc7bb 100644
--- a/src/views/user/torrents.tsx
+++ b/src/views/user/torrents.tsx
@@ -1,5 +1,6 @@
import { Table, TableContainer, Tbody, Th, Thead, Tr } from "@chakra-ui/react";
import { useOutletContext } from "react-router-dom";
+import { useCallback } from "react";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
@@ -8,17 +9,24 @@ import useSubject from "../../hooks/use-subject";
import IntersectionObserverProvider from "../../providers/intersection-observer";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
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 { NostrEvent } from "../../types/nostr-event";
export default function UserTorrentsTab() {
const { pubkey } = useOutletContext() as { pubkey: string };
const contextRelays = useAdditionalRelayContext();
- const timeline = useTimelineLoader(`${pubkey}-torrents`, contextRelays, {
- authors: [pubkey],
- kinds: [TORRENT_KIND],
- });
+ const eventFilter = useCallback((t: NostrEvent) => validateTorrent(t), []);
+ const timeline = useTimelineLoader(
+ `${pubkey}-torrents`,
+ contextRelays,
+ {
+ authors: [pubkey],
+ kinds: [TORRENT_KIND],
+ },
+ { eventFilter },
+ );
const torrents = useSubject(timeline.timeline);
const callback = useTimelineCurserIntersectionCallback(timeline);
diff --git a/yarn.lock b/yarn.lock
index ffc000eb2..2fc2c44e2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2377,7 +2377,7 @@
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9"
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"
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39"
integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==