mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-10 12:49:29 +02:00
Add Simple Satellite CDN view
This commit is contained in:
parent
f4ba4fddbc
commit
ad53ed15f5
5
.changeset/weak-shirts-help.md
Normal file
5
.changeset/weak-shirts-help.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Add Simple Satellite CDN view
|
@ -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: <NetworkDMGraphView /> },
|
||||
{ path: "dm-feed", element: <DMFeedView /> },
|
||||
{ path: "transform/:id", element: <TransformNoteView /> },
|
||||
{ path: "satellite-cdn", element: <SatelliteCDNView /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
85
src/helpers/satellite-cdn.ts
Normal file
85
src/helpers/satellite-cdn.ts
Normal file
@ -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<NostrEvent>;
|
||||
|
||||
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<SatelliteCDNAccount>;
|
||||
}
|
||||
|
||||
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<SatelliteCDNUpload>;
|
||||
}
|
@ -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() {
|
||||
<InternalLink to="/torrents" icon={Magnet}>
|
||||
Torrents
|
||||
</InternalLink>
|
||||
<Card as={LinkBox} alignItems="center" p="4" gap="4" minW="40">
|
||||
<Image src="https://satellite.earth/image.png" w="10" h="10" />
|
||||
<CardHeader p="0">
|
||||
<Heading size="md">
|
||||
<HoverLinkOverlay as={RouterLink} to="/tools/satellite-cdn">
|
||||
Satellite CDN
|
||||
</HoverLinkOverlay>
|
||||
</Heading>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<InternalLink to="/tools/network" icon={Users01}>
|
||||
User Network
|
||||
</InternalLink>
|
||||
|
33
src/views/tools/satellite-cdn/delete-file-button.tsx
Normal file
33
src/views/tools/satellite-cdn/delete-file-button.tsx
Normal file
@ -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 (
|
||||
<IconButton
|
||||
icon={<TrashIcon />}
|
||||
aria-label="Delete File"
|
||||
title="Delete File"
|
||||
colorScheme="red"
|
||||
onClick={handleClick}
|
||||
isLoading={loading}
|
||||
/>
|
||||
);
|
||||
}
|
194
src/views/tools/satellite-cdn/index.tsx
Normal file
194
src/views/tools/satellite-cdn/index.tsx
Normal file
@ -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<HTMLInputElement | null>(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 (
|
||||
<>
|
||||
<Input
|
||||
type="file"
|
||||
hidden
|
||||
ref={inputRef}
|
||||
onChange={(e) => {
|
||||
if (e.target.files?.[0]) handleInputChange(e.target.files[0]);
|
||||
}}
|
||||
/>
|
||||
<Button colorScheme="primary" onClick={() => inputRef.current?.click()} isLoading={loading}>
|
||||
Upload File
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function FileRow({ file }: { file: SatelliteCDNFile }) {
|
||||
return (
|
||||
<>
|
||||
<Tr>
|
||||
<Td>
|
||||
<Link isExternal href={file.url}>
|
||||
{file.name}
|
||||
</Link>
|
||||
<CopyIconButton text={file.url} aria-label="Copy URL" title="Copy URL" size="xs" variant="ghost" ml="2" />
|
||||
</Td>
|
||||
<Td isNumeric>{formatBytes(file.size)}</Td>
|
||||
<Td>{file.type}</Td>
|
||||
<Td>
|
||||
<Timestamp timestamp={file.created} />
|
||||
</Td>
|
||||
<Td isNumeric>
|
||||
<ButtonGroup size="sm" variant="ghost">
|
||||
<FileDeleteButton file={file} />
|
||||
</ButtonGroup>
|
||||
</Td>
|
||||
</Tr>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function FilesTable({ files }: { files: SatelliteCDNFile[] }) {
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table size="sm">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
<Th isNumeric>Size</Th>
|
||||
<Th>Type</Th>
|
||||
<Th>Created</Th>
|
||||
<Th isNumeric />
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{files.map((file) => (
|
||||
<FileRow key={file.sha256} file={file} />
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SatelliteCDNView() {
|
||||
const toast = useToast();
|
||||
const { requestSignature } = useSigningContext();
|
||||
|
||||
const [authToken, setAuthToken] = useState<NostrEvent>();
|
||||
|
||||
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 (
|
||||
<Button
|
||||
onClick={handleAuthClick}
|
||||
mx="auto"
|
||||
px="10"
|
||||
my="8"
|
||||
colorScheme="primary"
|
||||
isLoading={loading || isLoadingAccount}
|
||||
>
|
||||
Unlock Account
|
||||
</Button>
|
||||
);
|
||||
|
||||
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 <FilesTable files={sortedFiles} />;
|
||||
} else return <FilesTable files={account.files} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<VerticalPageLayout>
|
||||
<Flex gap="2" alignItems="center" wrap="wrap">
|
||||
<Image src="https://satellite.earth/image.png" w="12" />
|
||||
<Heading>Satellite CDN</Heading>
|
||||
<ButtonGroup ml="auto">
|
||||
<Button
|
||||
as={Link}
|
||||
href="https://github.com/lovvtide/satellite-web/blob/master/docs/cdn.md"
|
||||
isExternal
|
||||
variant="ghost"
|
||||
>
|
||||
View Docs
|
||||
</Button>
|
||||
<FileUploadButton />
|
||||
</ButtonGroup>
|
||||
</Flex>
|
||||
{account && (
|
||||
<Flex gap="2">
|
||||
<Input
|
||||
type="Search"
|
||||
placeholder="Search files"
|
||||
w={{ base: "full", md: "md" }}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
{renderContent()}
|
||||
</VerticalPageLayout>
|
||||
);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user