diff --git a/.changeset/small-walls-invent.md b/.changeset/small-walls-invent.md new file mode 100644 index 000000000..8ce8da869 --- /dev/null +++ b/.changeset/small-walls-invent.md @@ -0,0 +1,5 @@ +--- +"nostrudel": minor +--- + +Remove legacy satellite cdn view diff --git a/src/app.tsx b/src/app.tsx index bca1bad99..2a211b7e7 100644 --- a/src/app.tsx +++ b/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: ( + + + + ), children: [ { path: "", element: }, { path: "note", element: }, @@ -286,7 +290,7 @@ const router = createHashRouter([ { path: "relays", element: }, { path: "reports", element: }, { path: "muted-by", element: }, - { path: "dms", element: }, + { path: "dms", element: }, { path: "torrents", element: }, ], }, @@ -433,7 +437,6 @@ const router = createHashRouter([ { path: "network-dm-graph", element: }, { path: "dm-timeline", element: }, { path: "transform/:id", element: }, - { path: "satellite-cdn", element: }, { path: "unknown", element: }, { path: "console", element: }, { path: "corrections", element: }, diff --git a/src/helpers/satellite-cdn.ts b/src/helpers/satellite-cdn.ts deleted file mode 100644 index a9c953b5e..000000000 --- a/src/helpers/satellite-cdn.ts +++ /dev/null @@ -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; - -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/media/media-post.tsx b/src/views/media/media-post.tsx index fcaaeb003..aae2d02cd 100644 --- a/src/views/media/media-post.tsx +++ b/src/views/media/media-post.tsx @@ -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"; diff --git a/src/views/new/note/short-text-form.tsx b/src/views/new/note/short-text-form.tsx index 75e2b0982..391af07d0 100644 --- a/src/views/new/note/short-text-form.tsx +++ b/src/views/new/note/short-text-form.tsx @@ -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 ( - - - - ); - } - - if (miningTarget && draft) { - return ( - - setMiningTarget(0)} - onSkip={publishPost} - onComplete={publishPost} - /> - - ); - } - - const showAdvanced = - advanced.isOpen || formState.dirtyFields.difficulty || formState.dirtyFields.nsfw || formState.dirtyFields.split; - - // TODO: wrap this in a form + if (publishAction) { return ( - <> - - 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 && ( - - Preview: - - - - - - - - - )} - - - - - - - - - {formState.isDirty && ( - - )} - - - {showAdvanced && ( - - - - Post to community - - - - NSFW - {getValues().nsfw && ( - - )} - - - POW Difficulty ({getValues("difficulty")}) - setValue("difficulty", v, { shouldDirty: true, shouldTouch: true })} - min={0} - max={40} - step={1} - > - - - - - - - The number of leading 0's in the event id. see{" "} - - NIP-13 - - - - - - setValue("split", splits, { shouldDirty: true, shouldTouch: true })} - authorPubkey={account?.pubkey} - /> - - - )} - - - {!addClientTag && promptAddClientTag.isOpen && ( - - - - Enable{" "} - - NIP-89 - {" "} - client tags and let other users know what app you're using to write notes - - - - - - - )} - + + + ); - }; + } + if (miningTarget && draft) { + return ( + + setMiningTarget(0)} + onSkip={publishPost} + onComplete={publishPost} + /> + + ); + } + + const showAdvanced = + advanced.isOpen || formState.dirtyFields.difficulty || formState.dirtyFields.nsfw || formState.dirtyFields.split; + + // TODO: wrap this in a form return ( <> - {publishAction && } - {renderBody()} + + 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 && ( + + Preview: + + + + + + + + + )} + + + + + + + + + {formState.isDirty && ( + + )} + + + {showAdvanced && ( + + + + Post to community + + + + NSFW + {getValues().nsfw && ( + + )} + + + POW Difficulty ({getValues("difficulty")}) + setValue("difficulty", v, { shouldDirty: true, shouldTouch: true })} + min={0} + max={40} + step={1} + > + + + + + + + The number of leading 0's in the event id. see{" "} + + NIP-13 + + + + + + setValue("split", splits, { shouldDirty: true, shouldTouch: true })} + authorPubkey={account?.pubkey} + /> + + + )} + + + {!addClientTag && promptAddClientTag.isOpen && ( + + + + Enable{" "} + + NIP-89 + {" "} + client tags and let other users know what app you're using to write notes + + + + + + + )} ); } diff --git a/src/views/tools/satellite-cdn/delete-file-button.tsx b/src/views/tools/satellite-cdn/delete-file-button.tsx deleted file mode 100644 index ae82c5ea0..000000000 --- a/src/views/tools/satellite-cdn/delete-file-button.tsx +++ /dev/null @@ -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 ( - } - 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 deleted file mode 100644 index 45e08ce76..000000000 --- a/src/views/tools/satellite-cdn/index.tsx +++ /dev/null @@ -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(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} - - - - - - - } - aria-label="Download" - download={file.name} - isExternal - /> - } aria-label="Open Magnet" isExternal /> - - - - - - ); -} - -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?.toLowerCase().includes(search.toLowerCase().trim())); - const sortedFiles = matchSorter(filteredFiles, search.toLowerCase().trim(), { keys: ["name"] }); - return ; - } else return ; - }; - - return ( - - - - Satellite CDN - - - - - - {account && ( - - setSearch(e.target.value)} - /> - - )} - {renderContent()} - - ); -} diff --git a/src/views/tools/satellite-cdn/share-file-button.tsx b/src/views/tools/satellite-cdn/share-file-button.tsx deleted file mode 100644 index ffd2f5c08..000000000 --- a/src/views/tools/satellite-cdn/share-file-button.tsx +++ /dev/null @@ -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 & { - 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 ( - } - onClick={handleClick} - aria-label={ariaLabel || title} - title={title} - {...props} - /> - ); -} diff --git a/src/views/user/about/index.tsx b/src/views/user/about/index.tsx index 57a2fd351..df86fa6f2 100644 --- a/src/views/user/about/index.tsx +++ b/src/views/user/about/index.tsx @@ -218,8 +218,9 @@ export default function UserAboutTab() { + - Recent activity: + Recent events: @@ -253,8 +254,6 @@ export default function UserAboutTab() { Nostree page - - diff --git a/src/views/user/about/user-recent-events.tsx b/src/views/user/about/user-recent-events.tsx index d1d8b76a4..71ec2a4ad 100644 --- a/src/views/user/about/user-recent-events.tsx +++ b/src/views/user/about/user-recent-events.tsx @@ -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 }) { {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 }]) => ( ))} + {!all.isOpen && ( + + )} ); } diff --git a/src/views/user/dms.tsx b/src/views/user/dms.tsx deleted file mode 100644 index 0e7b52eef..000000000 --- a/src/views/user/dms.tsx +++ /dev/null @@ -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 ( - - - - Sent: - - - - ); - } else if (receiver === pubkey) { - return ( - - - - Received: - - - - ); - } else { - return ( - - - - Mentioned: - - - - ); - } -} - -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 ( - - - {dms?.map((dm) => )} - - - - ); -} diff --git a/src/views/user/messages.tsx b/src/views/user/messages.tsx new file mode 100644 index 000000000..b19ba4f8d --- /dev/null +++ b/src/views/user/messages.tsx @@ -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(messages[0]); + + return ( + + + + + + + + + + + + {messages.filter((dm) => getDMSender(dm) === self).length} + {messages.filter((dm) => getDMRecipient(dm) === self).length} + {messages.length} + + ); +} + +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(() => []); + 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 ( + + + + + + + + + + + + + + + {Array.from(byCounterParty).map(([user, messages]) => ( + + ))} + +
UserRecentSentReceivedTotal
+
+ +
+
+ ); +}