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 (
+
+
+
+
+ Name |
+ Size |
+ Type |
+ Created |
+ |
+
+
+
+ {files.map((file) => (
+
+ ))}
+
+
+
+ );
+}
+
+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()}
+
+ );
+}