From ad53ed15f5e42fae128e275fc0d2b88a8ba056c7 Mon Sep 17 00:00:00 2001 From: hzrd149 Date: Sun, 24 Dec 2023 11:51:51 -0600 Subject: [PATCH] Add Simple Satellite CDN view --- .changeset/weak-shirts-help.md | 5 + src/app.tsx | 2 + src/helpers/satellite-cdn.ts | 85 ++++++++ src/views/tools/index.tsx | 11 + .../satellite-cdn/delete-file-button.tsx | 33 +++ src/views/tools/satellite-cdn/index.tsx | 194 ++++++++++++++++++ 6 files changed, 330 insertions(+) create mode 100644 .changeset/weak-shirts-help.md create mode 100644 src/helpers/satellite-cdn.ts create mode 100644 src/views/tools/satellite-cdn/delete-file-button.tsx create mode 100644 src/views/tools/satellite-cdn/index.tsx diff --git a/.changeset/weak-shirts-help.md b/.changeset/weak-shirts-help.md new file mode 100644 index 000000000..f635cfe34 --- /dev/null +++ b/.changeset/weak-shirts-help.md @@ -0,0 +1,5 @@ +--- +"nostrudel": minor +--- + +Add Simple Satellite CDN view diff --git a/src/app.tsx b/src/app.tsx index ae9079308..5326abd59 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -77,6 +77,7 @@ import LoginNostrConnectView from "./views/signin/nostr-connect"; import ThreadsNotificationsView from "./views/notifications/threads"; import DVMFeedView from "./views/dvm-feed/feed"; import TransformNoteView from "./views/tools/transform-note"; +import SatelliteCDNView from "./views/tools/satellite-cdn"; const UserTracksTab = lazy(() => import("./views/user/tracks")); const ToolsHomeView = lazy(() => import("./views/tools")); @@ -264,6 +265,7 @@ const router = createHashRouter([ { path: "network-dm-graph", element: }, { path: "dm-feed", element: }, { path: "transform/:id", element: }, + { path: "satellite-cdn", element: }, ], }, { diff --git a/src/helpers/satellite-cdn.ts b/src/helpers/satellite-cdn.ts new file mode 100644 index 000000000..ba37de226 --- /dev/null +++ b/src/helpers/satellite-cdn.ts @@ -0,0 +1,85 @@ +import dayjs from "dayjs"; +import { DraftNostrEvent, NostrEvent, Tag } from "../types/nostr-event"; + +const ROOT_URL = "https://api.satellite.earth/v1/media"; +type Signer = (draft: DraftNostrEvent) => Promise; + +export type SatelliteCDNUpload = { + created: number; + sha256: string; + name: string; + url: string; + infohash: string; + magnet: string; + size: number; + type: string; + nip94: Tag[]; +}; +export type SatelliteCDNFile = { + created: number; + magnet: string; + type: string; + name: string; + sha256: string; + size: number; + url: string; +}; +export type SatelliteCDNAccount = { + timeRemaining: number; + paidThrough: number; + transactions: { + order: NostrEvent; + receipt: NostrEvent; + payment: NostrEvent; + }[]; + storageTotal: number; + creditTotal: number; + usageTotal: number; + rateFiat: { + usd: number; + }; + exchangeFiat: { + usd: number; + }; + files: SatelliteCDNFile[]; +}; + +export function getAccountAuthToken(signEvent: Signer) { + return signEvent({ + created_at: dayjs().unix(), + kind: 22242, + content: "Authenticate User", + tags: [], + }); +} + +export async function getAccount(authToken: NostrEvent) { + return fetch(`${ROOT_URL}/account?auth=${encodeURIComponent(JSON.stringify(authToken))}`).then((res) => + res.json(), + ) as Promise; +} + +export async function deleteFile(sha256: string, signEvent: Signer) { + const draft: DraftNostrEvent = { + created_at: dayjs().unix(), + kind: 22242, + content: "Delete Item", + tags: [["x", sha256]], + }; + const signed = await signEvent(draft); + await fetch(`${ROOT_URL}/item?auth=${encodeURIComponent(JSON.stringify(signed))}`, { method: "DELETE" }); +} + +export async function uploadFile(file: File, signEvent: Signer) { + const draft: DraftNostrEvent = { + created_at: dayjs().unix(), + kind: 22242, + content: "Authorize Upload", + tags: [["name", file.name]], + }; + const signed = await signEvent(draft); + return (await fetch(`${ROOT_URL}/item?auth=${encodeURIComponent(JSON.stringify(signed))}`, { + method: "PUT", + body: file, + }).then((res) => res.json())) as Promise; +} diff --git a/src/views/tools/index.tsx b/src/views/tools/index.tsx index 15a1f3fb8..36c48791d 100644 --- a/src/views/tools/index.tsx +++ b/src/views/tools/index.tsx @@ -7,6 +7,7 @@ import ShieldOff from "../../components/icons/shield-off"; import HoverLinkOverlay from "../../components/hover-link-overlay"; import Users01 from "../../components/icons/users-01"; import Magnet from "../../components/icons/magnet"; +import Moon01 from "../../components/icons/moon-01"; function InternalLink({ to, @@ -56,6 +57,16 @@ export default function ToolsHomeView() { Torrents + + + + + + Satellite CDN + + + + User Network diff --git a/src/views/tools/satellite-cdn/delete-file-button.tsx b/src/views/tools/satellite-cdn/delete-file-button.tsx new file mode 100644 index 000000000..f12afe861 --- /dev/null +++ b/src/views/tools/satellite-cdn/delete-file-button.tsx @@ -0,0 +1,33 @@ +import { useCallback, useState } from "react"; +import { IconButton, useToast } from "@chakra-ui/react"; + +import { SatelliteCDNFile, deleteFile } from "../../../helpers/satellite-cdn"; +import { useSigningContext } from "../../../providers/signing-provider"; +import { TrashIcon } from "../../../components/icons"; + +export default function FileDeleteButton({ file }: { file: SatelliteCDNFile }) { + const toast = useToast(); + const { requestSignature } = useSigningContext(); + + const [loading, setLoading] = useState(false); + const handleClick = useCallback(async () => { + setLoading(true); + try { + await deleteFile(file.sha256, requestSignature); + } catch (e) { + if (e instanceof Error) toast({ description: e.message, status: "error" }); + } + setLoading(false); + }, [requestSignature, file]); + + return ( + } + aria-label="Delete File" + title="Delete File" + colorScheme="red" + onClick={handleClick} + isLoading={loading} + /> + ); +} diff --git a/src/views/tools/satellite-cdn/index.tsx b/src/views/tools/satellite-cdn/index.tsx new file mode 100644 index 000000000..9694056b7 --- /dev/null +++ b/src/views/tools/satellite-cdn/index.tsx @@ -0,0 +1,194 @@ +import { useCallback, useRef, useState } from "react"; +import { + Button, + ButtonGroup, + Flex, + Heading, + Image, + Input, + Link, + Table, + TableContainer, + Tbody, + Td, + Th, + Thead, + Tr, + useToast, +} from "@chakra-ui/react"; +import { useAsync } from "react-use"; + +import VerticalPageLayout from "../../../components/vertical-page-layout"; +import { useSigningContext } from "../../../providers/signing-provider"; +import { NostrEvent } from "../../../types/nostr-event"; +import { formatBytes } from "../../../helpers/number"; +import { CopyIconButton } from "../../../components/copy-icon-button"; +import Timestamp from "../../../components/timestamp"; +import { SatelliteCDNFile, getAccount, getAccountAuthToken, uploadFile } from "../../../helpers/satellite-cdn"; +import FileDeleteButton from "./delete-file-button"; +import { matchSorter } from "match-sorter"; + +function FileUploadButton() { + const toast = useToast(); + const { requestSignature } = useSigningContext(); + const inputRef = useRef(null); + + const [loading, setLoading] = useState(false); + const handleInputChange = useCallback( + async (file: File) => { + setLoading(true); + try { + const upload = await uploadFile(file, requestSignature); + } catch (e) { + if (e instanceof Error) toast({ description: e.message, status: "error" }); + } + setLoading(false); + }, + [requestSignature], + ); + + return ( + <> + { + if (e.target.files?.[0]) handleInputChange(e.target.files[0]); + }} + /> + + + ); +} + +function FileRow({ file }: { file: SatelliteCDNFile }) { + return ( + <> + + + + {file.name} + + + + {formatBytes(file.size)} + {file.type} + + + + + + + + + + + ); +} + +function FilesTable({ files }: { files: SatelliteCDNFile[] }) { + return ( + + + + + + + + + + + + {files.map((file) => ( + + ))} + +
NameSizeTypeCreated +
+
+ ); +} + +export default function SatelliteCDNView() { + const toast = useToast(); + const { requestSignature } = useSigningContext(); + + const [authToken, setAuthToken] = useState(); + + const { value: account, loading: isLoadingAccount } = useAsync( + async () => authToken && (await getAccount(authToken)), + [authToken], + ); + + const [loading, setLoading] = useState(false); + const handleAuthClick = useCallback(async () => { + setLoading(true); + try { + setAuthToken(await getAccountAuthToken(requestSignature)); + } catch (e) { + if (e instanceof Error) toast({ description: e.message, status: "error" }); + } + setLoading(false); + }, [requestSignature]); + + const [search, setSearch] = useState(""); + + const renderContent = () => { + if (!account) + return ( + + ); + + if (search) { + const filteredFiles = account.files.filter((f) => + f.name.toLocaleLowerCase().includes(search.toLocaleLowerCase().trim()), + ); + const sortedFiles = matchSorter(filteredFiles, search.toLocaleLowerCase().trim(), { keys: ["name"] }); + return ; + } else return ; + }; + + return ( + + + + Satellite CDN + + + + + + {account && ( + + setSearch(e.target.value)} + /> + + )} + {renderContent()} + + ); +}