mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-17 13:21:44 +01:00
fix new note form
This commit is contained in:
parent
e8b7ecafe3
commit
b5a7f76d9a
5
.changeset/small-walls-invent.md
Normal file
5
.changeset/small-walls-invent.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Remove legacy satellite cdn view
|
11
src/app.tsx
11
src/app.tsx
@ -50,7 +50,7 @@ import UserListsTab from "./views/user/lists";
|
||||
import UserGoalsTab from "./views/user/goals";
|
||||
import MutedByView from "./views/user/muted-by";
|
||||
import UserArticlesTab from "./views/user/articles";
|
||||
import UserDMsTab from "./views/user/dms";
|
||||
import UserMessagesTab from "./views/user/messages";
|
||||
const UserTorrentsTab = lazy(() => import("./views/user/torrents"));
|
||||
|
||||
import ListsHomeView from "./views/lists";
|
||||
@ -124,7 +124,6 @@ const EventConsoleView = lazy(() => import("./views/tools/event-console"));
|
||||
const EventPublisherView = lazy(() => import("./views/tools/event-publisher"));
|
||||
const DMTimelineView = lazy(() => import("./views/tools/dm-timeline"));
|
||||
const TransformNoteView = lazy(() => import("./views/tools/transform-note"));
|
||||
const SatelliteCDNView = lazy(() => import("./views/tools/satellite-cdn"));
|
||||
const CorrectionsFeedView = lazy(() => import("./views/tools/corrections"));
|
||||
const NoStrudelUsersView = lazy(() => import("./views/tools/nostrudel-users/index"));
|
||||
|
||||
@ -258,6 +257,11 @@ const router = createHashRouter([
|
||||
children: [
|
||||
{
|
||||
path: "new",
|
||||
element: (
|
||||
<RequireCurrentAccount>
|
||||
<Outlet />
|
||||
</RequireCurrentAccount>
|
||||
),
|
||||
children: [
|
||||
{ path: "", element: <NewView /> },
|
||||
{ path: "note", element: <NewNoteView /> },
|
||||
@ -286,7 +290,7 @@ const router = createHashRouter([
|
||||
{ path: "relays", element: <UserRelaysTab /> },
|
||||
{ path: "reports", element: <UserReportsTab /> },
|
||||
{ path: "muted-by", element: <MutedByView /> },
|
||||
{ path: "dms", element: <UserDMsTab /> },
|
||||
{ path: "dms", element: <UserMessagesTab /> },
|
||||
{ path: "torrents", element: <UserTorrentsTab /> },
|
||||
],
|
||||
},
|
||||
@ -433,7 +437,6 @@ const router = createHashRouter([
|
||||
{ path: "network-dm-graph", element: <NetworkDMGraphView /> },
|
||||
{ path: "dm-timeline", element: <DMTimelineView /> },
|
||||
{ path: "transform/:id", element: <TransformNoteView /> },
|
||||
{ path: "satellite-cdn", element: <SatelliteCDNView /> },
|
||||
{ path: "unknown", element: <UnknownTimelineView /> },
|
||||
{ path: "console", element: <EventConsoleView /> },
|
||||
{ path: "corrections", element: <CorrectionsFeedView /> },
|
||||
|
@ -1,85 +0,0 @@
|
||||
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>;
|
||||
}
|
@ -1,8 +1,5 @@
|
||||
import { Box, ButtonGroup, Flex, Heading, Spinner } from "@chakra-ui/react";
|
||||
import { ButtonGroup, Flex, Heading, Spinner } from "@chakra-ui/react";
|
||||
import { NostrEvent } from "nostr-tools";
|
||||
import { COMMENT_KIND } from "applesauce-core/helpers";
|
||||
import { useStoreQuery } from "applesauce-react/hooks";
|
||||
import { CommentsQuery } from "applesauce-core/queries";
|
||||
|
||||
import { useReadRelays } from "../../hooks/use-client-relays";
|
||||
import useParamsEventPointer from "../../hooks/use-params-event-pointer";
|
||||
@ -16,9 +13,6 @@ import { TrustProvider } from "../../providers/local/trust-provider";
|
||||
import DebugEventButton from "../../components/debug-modal/debug-event-button";
|
||||
import RepostButton from "../../components/note/timeline-note/components/repost-button";
|
||||
import QuoteEventButton from "../../components/note/quote-event-button";
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import { useBreakpointValue } from "../../providers/global/breakpoint-provider";
|
||||
import EventZapIconButton from "../../components/zap/event-zap-icon-button";
|
||||
import AddReactionButton from "../../components/note/timeline-note/components/add-reaction-button";
|
||||
|
@ -7,7 +7,6 @@ import {
|
||||
useDisclosure,
|
||||
Input,
|
||||
Switch,
|
||||
ModalProps,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
@ -16,7 +15,6 @@ import {
|
||||
SliderTrack,
|
||||
SliderFilledTrack,
|
||||
SliderThumb,
|
||||
ModalCloseButton,
|
||||
Alert,
|
||||
AlertIcon,
|
||||
ButtonGroup,
|
||||
@ -27,7 +25,7 @@ import { useForm } from "react-hook-form";
|
||||
import { UnsignedEvent } from "nostr-tools";
|
||||
import { useAsync, useThrottle } from "react-use";
|
||||
import { useEventFactory, useObservable } from "applesauce-react/hooks";
|
||||
import { Emoji, ZapSplit } from "applesauce-core/helpers";
|
||||
import { Emoji } from "applesauce-core/helpers";
|
||||
|
||||
import { useFinalizeDraft, usePublishEvent } from "../../../providers/global/publish-provider";
|
||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||
@ -148,163 +146,154 @@ export default function ShortTextNoteForm({
|
||||
|
||||
const canSubmit = getValues().content.length > 0;
|
||||
|
||||
const renderBody = () => {
|
||||
if (publishAction) {
|
||||
return (
|
||||
<Flex direction="column" gap="2">
|
||||
<PublishDetails pub={publishAction} />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
if (miningTarget && draft) {
|
||||
return (
|
||||
<Flex direction="column" gap="2">
|
||||
<MinePOW
|
||||
draft={draft}
|
||||
targetPOW={miningTarget}
|
||||
onCancel={() => setMiningTarget(0)}
|
||||
onSkip={publishPost}
|
||||
onComplete={publishPost}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
const showAdvanced =
|
||||
advanced.isOpen || formState.dirtyFields.difficulty || formState.dirtyFields.nsfw || formState.dirtyFields.split;
|
||||
|
||||
// TODO: wrap this in a form
|
||||
if (publishAction) {
|
||||
return (
|
||||
<>
|
||||
<Flex direction="column" gap="2">
|
||||
<MagicTextArea
|
||||
autoFocus
|
||||
mb="2"
|
||||
value={getValues().content}
|
||||
onChange={(e) => setValue("content", e.target.value, { shouldDirty: true, shouldTouch: true })}
|
||||
rows={8}
|
||||
isRequired
|
||||
instanceRef={(inst) => (textAreaRef.current = inst)}
|
||||
onPaste={onPaste}
|
||||
onKeyDown={(e) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") submit();
|
||||
}}
|
||||
/>
|
||||
{preview && preview.content.length > 0 && (
|
||||
<Box>
|
||||
<Heading size="sm">Preview:</Heading>
|
||||
<Box borderWidth={1} borderRadius="md" p="2">
|
||||
<ErrorBoundary>
|
||||
<TrustProvider trust>
|
||||
<TextNoteContents event={preview} />
|
||||
</TrustProvider>
|
||||
</ErrorBoundary>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
<Flex gap="2" alignItems="center" justifyContent="flex-end">
|
||||
<Flex mr="auto" gap="2">
|
||||
<InsertImageButton onUploaded={insertText} aria-label="Upload image" />
|
||||
<InsertGifButton onSelectURL={insertText} aria-label="Add gif" />
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex gap="2" alignItems="center" justifyContent="space-between">
|
||||
<Button
|
||||
variant="link"
|
||||
rightIcon={advanced.isOpen ? <ChevronUpIcon /> : <ChevronDownIcon />}
|
||||
onClick={advanced.onToggle}
|
||||
>
|
||||
More Options
|
||||
</Button>
|
||||
{formState.isDirty && (
|
||||
<Button variant="ghost" onClick={() => confirm("Clear draft?") && reset()} ms="auto">
|
||||
Clear
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
colorScheme="primary"
|
||||
type="submit"
|
||||
isLoading={formState.isSubmitting}
|
||||
onClick={submit}
|
||||
isDisabled={!canSubmit}
|
||||
>
|
||||
Post
|
||||
</Button>
|
||||
</Flex>
|
||||
{showAdvanced && (
|
||||
<Flex direction={{ base: "column", lg: "row" }} gap="4">
|
||||
<Flex direction="column" gap="2" flex={1}>
|
||||
<FormControl>
|
||||
<FormLabel>Post to community</FormLabel>
|
||||
<CommunitySelect {...register("community")} />
|
||||
</FormControl>
|
||||
<Flex gap="2" direction="column">
|
||||
<Switch {...register("nsfw")}>NSFW</Switch>
|
||||
{getValues().nsfw && (
|
||||
<Input {...register("nsfwReason", { required: true })} placeholder="Reason" isRequired />
|
||||
)}
|
||||
</Flex>
|
||||
<FormControl>
|
||||
<FormLabel>POW Difficulty ({getValues("difficulty")})</FormLabel>
|
||||
<Slider
|
||||
aria-label="difficulty"
|
||||
value={getValues("difficulty")}
|
||||
onChange={(v) => setValue("difficulty", v, { shouldDirty: true, shouldTouch: true })}
|
||||
min={0}
|
||||
max={40}
|
||||
step={1}
|
||||
>
|
||||
<SliderTrack>
|
||||
<SliderFilledTrack />
|
||||
</SliderTrack>
|
||||
<SliderThumb />
|
||||
</Slider>
|
||||
<FormHelperText>
|
||||
The number of leading 0's in the event id. see{" "}
|
||||
<Link href="https://github.com/nostr-protocol/nips/blob/master/13.md" isExternal>
|
||||
NIP-13
|
||||
</Link>
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
</Flex>
|
||||
<Flex direction="column" gap="2" flex={1}>
|
||||
<ZapSplitCreator
|
||||
splits={getValues().split}
|
||||
onChange={(splits) => setValue("split", splits, { shouldDirty: true, shouldTouch: true })}
|
||||
authorPubkey={account?.pubkey}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{!addClientTag && promptAddClientTag.isOpen && (
|
||||
<Alert status="info" whiteSpace="pre-wrap" flexDirection={{ base: "column", lg: "row" }}>
|
||||
<AlertIcon hideBelow="lg" />
|
||||
<Text>
|
||||
Enable{" "}
|
||||
<Link isExternal href="https://github.com/nostr-protocol/nips/blob/master/89.md#client-tag">
|
||||
NIP-89
|
||||
</Link>{" "}
|
||||
client tags and let other users know what app you're using to write notes
|
||||
</Text>
|
||||
<ButtonGroup ml="auto" size="sm" variant="ghost">
|
||||
<Button onClick={promptAddClientTag.onClose}>Close</Button>
|
||||
<Button colorScheme="primary" onClick={() => localSettings.addClientTag.next(true)}>
|
||||
Enable
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Alert>
|
||||
)}
|
||||
</>
|
||||
<Flex direction="column" gap="2">
|
||||
<PublishDetails pub={publishAction} />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
if (miningTarget && draft) {
|
||||
return (
|
||||
<Flex direction="column" gap="2">
|
||||
<MinePOW
|
||||
draft={draft}
|
||||
targetPOW={miningTarget}
|
||||
onCancel={() => setMiningTarget(0)}
|
||||
onSkip={publishPost}
|
||||
onComplete={publishPost}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
const showAdvanced =
|
||||
advanced.isOpen || formState.dirtyFields.difficulty || formState.dirtyFields.nsfw || formState.dirtyFields.split;
|
||||
|
||||
// TODO: wrap this in a form
|
||||
return (
|
||||
<>
|
||||
{publishAction && <ModalCloseButton />}
|
||||
{renderBody()}
|
||||
<Flex direction="column" gap="2">
|
||||
<MagicTextArea
|
||||
autoFocus
|
||||
mb="2"
|
||||
value={getValues().content}
|
||||
onChange={(e) => setValue("content", e.target.value, { shouldDirty: true, shouldTouch: true })}
|
||||
rows={8}
|
||||
isRequired
|
||||
instanceRef={(inst) => (textAreaRef.current = inst)}
|
||||
onPaste={onPaste}
|
||||
onKeyDown={(e) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") submit();
|
||||
}}
|
||||
/>
|
||||
{preview && preview.content.length > 0 && (
|
||||
<Box>
|
||||
<Heading size="sm">Preview:</Heading>
|
||||
<Box borderWidth={1} borderRadius="md" p="2">
|
||||
<ErrorBoundary>
|
||||
<TrustProvider trust>
|
||||
<TextNoteContents event={preview} />
|
||||
</TrustProvider>
|
||||
</ErrorBoundary>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
<Flex gap="2" alignItems="center" justifyContent="flex-end">
|
||||
<Flex mr="auto" gap="2">
|
||||
<InsertImageButton onUploaded={insertText} aria-label="Upload image" />
|
||||
<InsertGifButton onSelectURL={insertText} aria-label="Add gif" />
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex gap="2" alignItems="center" justifyContent="space-between">
|
||||
<Button
|
||||
variant="link"
|
||||
rightIcon={advanced.isOpen ? <ChevronUpIcon /> : <ChevronDownIcon />}
|
||||
onClick={advanced.onToggle}
|
||||
>
|
||||
More Options
|
||||
</Button>
|
||||
{formState.isDirty && (
|
||||
<Button variant="ghost" onClick={() => confirm("Clear draft?") && reset()} ms="auto">
|
||||
Clear
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
colorScheme="primary"
|
||||
type="submit"
|
||||
isLoading={formState.isSubmitting}
|
||||
onClick={submit}
|
||||
isDisabled={!canSubmit}
|
||||
>
|
||||
Post
|
||||
</Button>
|
||||
</Flex>
|
||||
{showAdvanced && (
|
||||
<Flex direction={{ base: "column", lg: "row" }} gap="4">
|
||||
<Flex direction="column" gap="2" flex={1}>
|
||||
<FormControl>
|
||||
<FormLabel>Post to community</FormLabel>
|
||||
<CommunitySelect {...register("community")} />
|
||||
</FormControl>
|
||||
<Flex gap="2" direction="column">
|
||||
<Switch {...register("nsfw")}>NSFW</Switch>
|
||||
{getValues().nsfw && (
|
||||
<Input {...register("nsfwReason", { required: true })} placeholder="Reason" isRequired />
|
||||
)}
|
||||
</Flex>
|
||||
<FormControl>
|
||||
<FormLabel>POW Difficulty ({getValues("difficulty")})</FormLabel>
|
||||
<Slider
|
||||
aria-label="difficulty"
|
||||
value={getValues("difficulty")}
|
||||
onChange={(v) => setValue("difficulty", v, { shouldDirty: true, shouldTouch: true })}
|
||||
min={0}
|
||||
max={40}
|
||||
step={1}
|
||||
>
|
||||
<SliderTrack>
|
||||
<SliderFilledTrack />
|
||||
</SliderTrack>
|
||||
<SliderThumb />
|
||||
</Slider>
|
||||
<FormHelperText>
|
||||
The number of leading 0's in the event id. see{" "}
|
||||
<Link href="https://github.com/nostr-protocol/nips/blob/master/13.md" isExternal>
|
||||
NIP-13
|
||||
</Link>
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
</Flex>
|
||||
<Flex direction="column" gap="2" flex={1}>
|
||||
<ZapSplitCreator
|
||||
splits={getValues().split}
|
||||
onChange={(splits) => setValue("split", splits, { shouldDirty: true, shouldTouch: true })}
|
||||
authorPubkey={account?.pubkey}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{!addClientTag && promptAddClientTag.isOpen && (
|
||||
<Alert status="info" whiteSpace="pre-wrap" flexDirection={{ base: "column", lg: "row" }}>
|
||||
<AlertIcon hideBelow="lg" />
|
||||
<Text>
|
||||
Enable{" "}
|
||||
<Link isExternal href="https://github.com/nostr-protocol/nips/blob/master/89.md#client-tag">
|
||||
NIP-89
|
||||
</Link>{" "}
|
||||
client tags and let other users know what app you're using to write notes
|
||||
</Text>
|
||||
<ButtonGroup ml="auto" size="sm" variant="ghost">
|
||||
<Button onClick={promptAddClientTag.onClose}>Close</Button>
|
||||
<Button colorScheme="primary" onClick={() => localSettings.addClientTag.next(true)}>
|
||||
Enable
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Alert>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,33 +0,0 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { IconButton, useToast } from "@chakra-ui/react";
|
||||
|
||||
import { SatelliteCDNFile, deleteFile } from "../../../helpers/satellite-cdn";
|
||||
import { useSigningContext } from "../../../providers/global/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}
|
||||
/>
|
||||
);
|
||||
}
|
@ -1,206 +0,0 @@
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Flex,
|
||||
Heading,
|
||||
IconButton,
|
||||
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/global/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";
|
||||
import ShareFileButton from "./share-file-button";
|
||||
import { DownloadIcon, TorrentIcon } from "../../../components/icons";
|
||||
|
||||
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 value={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">
|
||||
<ShareFileButton file={file} />
|
||||
<IconButton
|
||||
as={Link}
|
||||
href={file.url}
|
||||
icon={<DownloadIcon />}
|
||||
aria-label="Download"
|
||||
download={file.name}
|
||||
isExternal
|
||||
/>
|
||||
<IconButton as={Link} href={file.magnet} icon={<TorrentIcon />} aria-label="Open Magnet" isExternal />
|
||||
<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}
|
||||
autoFocus
|
||||
>
|
||||
Unlock Account
|
||||
</Button>
|
||||
);
|
||||
|
||||
if (search) {
|
||||
const filteredFiles = account.files.filter((f) => f.name?.toLowerCase().includes(search.toLowerCase().trim()));
|
||||
const sortedFiles = matchSorter(filteredFiles, search.toLowerCase().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>
|
||||
);
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
import { useContext } from "react";
|
||||
import { SatelliteCDNFile } from "../../../helpers/satellite-cdn";
|
||||
import { PostModalContext } from "../../../providers/route/post-modal-provider";
|
||||
import { ButtonProps, IconButton } from "@chakra-ui/react";
|
||||
import { QuoteEventIcon } from "../../../components/icons";
|
||||
|
||||
export type ShareFileButtonProps = Omit<ButtonProps, "children" | "onClick"> & {
|
||||
file: SatelliteCDNFile;
|
||||
};
|
||||
|
||||
export default function ShareFileButton({
|
||||
file,
|
||||
"aria-label": ariaLabel,
|
||||
title = "Share File",
|
||||
...props
|
||||
}: ShareFileButtonProps) {
|
||||
const { openModal } = useContext(PostModalContext);
|
||||
|
||||
const handleClick = () => {
|
||||
openModal({ cacheFormKey: null, initContent: "\n" + file.url });
|
||||
};
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
icon={<QuoteEventIcon />}
|
||||
onClick={handleClick}
|
||||
aria-label={ariaLabel || title}
|
||||
title={title}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
@ -218,8 +218,9 @@ export default function UserAboutTab() {
|
||||
</Flex>
|
||||
|
||||
<UserProfileBadges pubkey={pubkey} px="2" />
|
||||
<UserPinnedEvents pubkey={pubkey} />
|
||||
<Box px="2">
|
||||
<Heading size="md">Recent activity:</Heading>
|
||||
<Heading size="md">Recent events:</Heading>
|
||||
<UserRecentEvents pubkey={pubkey} />
|
||||
</Box>
|
||||
<UserStatsAccordion pubkey={pubkey} />
|
||||
@ -253,8 +254,6 @@ export default function UserAboutTab() {
|
||||
Nostree page
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
<UserPinnedEvents pubkey={pubkey} />
|
||||
<UserJoinedCommunities pubkey={pubkey} />
|
||||
<UserJoinedChanneled pubkey={pubkey} />
|
||||
|
||||
|
@ -184,9 +184,9 @@ export default function UserRecentEvents({ pubkey }: { pubkey: string }) {
|
||||
authors: [pubkey],
|
||||
limit: 100,
|
||||
});
|
||||
const all = useDisclosure();
|
||||
|
||||
// const recent = useStoreQuery(TimelineQuery, [{ authors: [pubkey], limit: 100 }]);
|
||||
const all = useDisclosure();
|
||||
|
||||
const byKind = recent?.reduce(
|
||||
(dir, event) => {
|
||||
@ -206,11 +206,16 @@ export default function UserRecentEvents({ pubkey }: { pubkey: string }) {
|
||||
<Flex gap="2" wrap="wrap">
|
||||
{byKind &&
|
||||
Object.entries(byKind)
|
||||
.filter(([_, { known }]) => (known ? known.hidden !== true : true))
|
||||
.filter(([_, { known }]) => (!!known || all.isOpen) && (known ? known.hidden !== true : true))
|
||||
.sort((a, b) => parseInt(a[0]) - parseInt(b[0]))
|
||||
.map(([kind, { events, known }]) => (
|
||||
<EventKindButton key={kind} kind={parseInt(kind)} events={events} pubkey={pubkey} known={known} />
|
||||
))}
|
||||
{!all.isOpen && (
|
||||
<Button variant="link" p="4" onClick={all.onOpen}>
|
||||
Show more ({Object.entries(byKind).filter(([_, { known }]) => !!known).length})
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
@ -1,81 +0,0 @@
|
||||
import { Flex, Text } from "@chakra-ui/react";
|
||||
import { kinds } from "nostr-tools";
|
||||
import { useOutletContext } from "react-router-dom";
|
||||
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import { useAdditionalRelayContext } from "../../providers/local/additional-relay-context";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
|
||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||
import TimelineActionAndStatus from "../../components/timeline/timeline-action-and-status";
|
||||
import { NostrEvent, isPTag } from "../../types/nostr-event";
|
||||
import UserAvatarLink from "../../components/user/user-avatar-link";
|
||||
import UserLink from "../../components/user/user-link";
|
||||
import ArrowRight from "../../components/icons/arrow-right";
|
||||
import { AtIcon } from "../../components/icons";
|
||||
import Timestamp from "../../components/timestamp";
|
||||
import ArrowLeft from "../../components/icons/arrow-left";
|
||||
import useEventIntersectionRef from "../../hooks/use-event-intersection-ref";
|
||||
|
||||
function DirectMessage({ dm, pubkey }: { dm: NostrEvent; pubkey: string }) {
|
||||
const sender = dm.pubkey;
|
||||
const receiver = dm.tags.find(isPTag)?.[1];
|
||||
|
||||
const ref = useEventIntersectionRef(dm);
|
||||
|
||||
if (sender === pubkey) {
|
||||
if (!receiver) return null;
|
||||
return (
|
||||
<Flex gap="2" alignItems="center" ref={ref}>
|
||||
<ArrowRight boxSize={6} />
|
||||
<Timestamp timestamp={dm.created_at} />
|
||||
<Text>Sent: </Text>
|
||||
<UserAvatarLink pubkey={receiver} size="sm" />
|
||||
<UserLink pubkey={receiver} fontWeight="bold" fontSize="lg" />
|
||||
</Flex>
|
||||
);
|
||||
} else if (receiver === pubkey) {
|
||||
return (
|
||||
<Flex gap="2" alignItems="center" ref={ref}>
|
||||
<ArrowLeft boxSize={6} />
|
||||
<Timestamp timestamp={dm.created_at} />
|
||||
<Text>Received: </Text>
|
||||
<UserAvatarLink pubkey={sender} size="sm" />
|
||||
<UserLink pubkey={sender} fontWeight="bold" fontSize="lg" />
|
||||
</Flex>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Flex gap="2" alignItems="center" ref={ref}>
|
||||
<AtIcon boxSize={6} />
|
||||
<Timestamp timestamp={dm.created_at} />
|
||||
<Text>Mentioned: </Text>
|
||||
<UserAvatarLink pubkey={pubkey} size="sm" />
|
||||
<UserLink pubkey={pubkey} fontWeight="bold" fontSize="lg" />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default function UserDMsTab() {
|
||||
const { pubkey } = useOutletContext() as { pubkey: string };
|
||||
const readRelays = useAdditionalRelayContext();
|
||||
|
||||
const { loader, timeline: dms } = useTimelineLoader(pubkey + "-articles", readRelays, [
|
||||
{
|
||||
authors: [pubkey],
|
||||
kinds: [kinds.EncryptedDirectMessage],
|
||||
},
|
||||
{ "#p": [pubkey], kinds: [kinds.EncryptedDirectMessage] },
|
||||
]);
|
||||
const callback = useTimelineCurserIntersectionCallback(loader);
|
||||
|
||||
return (
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<VerticalPageLayout>
|
||||
{dms?.map((dm) => <DirectMessage key={dm.id} dm={dm} pubkey={pubkey} />)}
|
||||
<TimelineActionAndStatus timeline={loader} />
|
||||
</VerticalPageLayout>
|
||||
</IntersectionObserverProvider>
|
||||
);
|
||||
}
|
88
src/views/user/messages.tsx
Normal file
88
src/views/user/messages.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
import { Flex, Table, TableContainer, Tbody, Td, Th, Thead, Tr } from "@chakra-ui/react";
|
||||
import { kinds } from "nostr-tools";
|
||||
import { useOutletContext } from "react-router-dom";
|
||||
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import { useAdditionalRelayContext } from "../../providers/local/additional-relay-context";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
|
||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||
import TimelineActionAndStatus from "../../components/timeline/timeline-action-and-status";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import UserAvatarLink from "../../components/user/user-avatar-link";
|
||||
import UserLink from "../../components/user/user-link";
|
||||
import Timestamp from "../../components/timestamp";
|
||||
import useEventIntersectionRef from "../../hooks/use-event-intersection-ref";
|
||||
import SuperMap from "../../classes/super-map";
|
||||
import { getDMRecipient, getDMSender } from "../../helpers/nostr/dms";
|
||||
import UserDnsIdentityIcon from "../../components/user/user-dns-identity-icon";
|
||||
|
||||
function DirectMessageRow({ messages, pubkey, self }: { messages: NostrEvent[]; pubkey: string; self: string }) {
|
||||
const ref = useEventIntersectionRef<HTMLTableRowElement>(messages[0]);
|
||||
|
||||
return (
|
||||
<Tr ref={ref}>
|
||||
<Td>
|
||||
<Flex gap="2" alignItems="center">
|
||||
<UserAvatarLink pubkey={pubkey} size="xs" />
|
||||
<UserLink pubkey={pubkey} />
|
||||
<UserDnsIdentityIcon pubkey={pubkey} />
|
||||
</Flex>
|
||||
</Td>
|
||||
<Td isNumeric>
|
||||
<Timestamp timestamp={messages[0].created_at} />
|
||||
</Td>
|
||||
<Td isNumeric>{messages.filter((dm) => getDMSender(dm) === self).length}</Td>
|
||||
<Td isNumeric>{messages.filter((dm) => getDMRecipient(dm) === self).length}</Td>
|
||||
<Td isNumeric>{messages.length}</Td>
|
||||
</Tr>
|
||||
);
|
||||
}
|
||||
|
||||
export default function UserMessagesTab() {
|
||||
const { pubkey } = useOutletContext() as { pubkey: string };
|
||||
const readRelays = useAdditionalRelayContext();
|
||||
|
||||
const { loader, timeline: messages } = useTimelineLoader(pubkey + "-articles", readRelays, [
|
||||
{
|
||||
authors: [pubkey],
|
||||
kinds: [kinds.EncryptedDirectMessage],
|
||||
},
|
||||
{ "#p": [pubkey], kinds: [kinds.EncryptedDirectMessage] },
|
||||
]);
|
||||
const callback = useTimelineCurserIntersectionCallback(loader);
|
||||
|
||||
const byCounterParty = new SuperMap<string, NostrEvent[]>(() => []);
|
||||
for (const message of messages) {
|
||||
const sender = getDMSender(message);
|
||||
const receiver = getDMRecipient(message);
|
||||
if (sender !== pubkey) byCounterParty.get(sender).push(message);
|
||||
else if (receiver !== pubkey) byCounterParty.get(receiver).push(message);
|
||||
}
|
||||
|
||||
return (
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<VerticalPageLayout>
|
||||
<TableContainer>
|
||||
<Table size="sm">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>User</Th>
|
||||
<Th isNumeric>Recent</Th>
|
||||
<Th isNumeric>Sent</Th>
|
||||
<Th isNumeric>Received</Th>
|
||||
<Th isNumeric>Total</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{Array.from(byCounterParty).map(([user, messages]) => (
|
||||
<DirectMessageRow key={user} self={pubkey} pubkey={user} messages={messages} />
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<TimelineActionAndStatus timeline={loader} />
|
||||
</VerticalPageLayout>
|
||||
</IntersectionObserverProvider>
|
||||
);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user