mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-29 11:12:12 +01:00
Add mailboxes view
This commit is contained in:
parent
238a3be17e
commit
588f38db35
@ -84,6 +84,7 @@ import LaunchpadView from "./views/launchpad";
|
||||
import VideosView from "./views/videos";
|
||||
import VideoDetailsView from "./views/videos/video";
|
||||
import BookmarksView from "./views/bookmarks";
|
||||
import MailboxesView from "./views/mailboxes";
|
||||
const TracksView = lazy(() => import("./views/tracks"));
|
||||
const UserTracksTab = lazy(() => import("./views/user/tracks"));
|
||||
const UserVideosTab = lazy(() => import("./views/user/videos"));
|
||||
@ -268,6 +269,10 @@ const router = createHashRouter([
|
||||
{ path: "", element: <NotificationsView /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "mailboxes",
|
||||
children: [{ path: "", element: <MailboxesView /> }],
|
||||
},
|
||||
{
|
||||
path: "videos",
|
||||
children: [
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { safeRelayUrl, safeRelayUrls } from "../helpers/relay";
|
||||
import { getRelaysFromMailbox } from "../helpers/nostr/mailbox";
|
||||
import { safeRelayUrl } from "../helpers/relay";
|
||||
import relayPoolService from "../services/relay-pool";
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import { RelayMode } from "./relay";
|
||||
@ -32,16 +33,10 @@ export default class RelaySet extends Set<string> {
|
||||
}
|
||||
|
||||
static fromNIP65Event(event: NostrEvent, mode: RelayMode = RelayMode.ALL) {
|
||||
const set = new RelaySet();
|
||||
for (const tag of event.tags) {
|
||||
if (tag[0] === "r" && tag[1]) {
|
||||
const url = safeRelayUrl(tag[1]);
|
||||
if (!url) continue;
|
||||
if (tag[2] === "write" && mode & RelayMode.WRITE) set.add(url);
|
||||
else if (tag[2] === "read" && mode & RelayMode.READ) set.add(url);
|
||||
else set.add(url);
|
||||
}
|
||||
}
|
||||
return set;
|
||||
return new RelaySet(
|
||||
getRelaysFromMailbox(event)
|
||||
.filter((r) => r.mode & mode)
|
||||
.map((r) => r.url),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import dayjs from "dayjs";
|
||||
import { Debugger } from "debug";
|
||||
import { Filter } from "nostr-tools";
|
||||
import { Filter, matchFilters } from "nostr-tools";
|
||||
import _throttle from "lodash.throttle";
|
||||
|
||||
import { NostrEvent, isATag, isETag } from "../types/nostr-event";
|
||||
@ -79,6 +79,7 @@ export class RelayBlockLoader {
|
||||
}
|
||||
|
||||
private handleEvent(event: NostrEvent) {
|
||||
if (!matchFilters(Array.isArray(this.filter) ? this.filter : [this.filter], event)) return;
|
||||
return this.events.addEvent(event);
|
||||
}
|
||||
|
||||
@ -117,7 +118,6 @@ export default class TimelineLoader {
|
||||
name: string;
|
||||
private log: Debugger;
|
||||
private subscription: NostrMultiSubscription;
|
||||
private cacheSubscription?: Subscription;
|
||||
|
||||
private blockLoaders = new Map<string, RelayBlockLoader>();
|
||||
|
||||
|
@ -66,6 +66,7 @@ import MessageChatSquare from "./icons/message-chat-square";
|
||||
import Package from "./icons/package";
|
||||
import Magnet from "./icons/magnet";
|
||||
import Recording02 from "./icons/recording-02";
|
||||
import Upload01 from "./icons/upload-01";
|
||||
|
||||
const defaultProps: IconProps = { boxSize: 4 };
|
||||
|
||||
@ -240,3 +241,6 @@ export const ThreadIcon = MessageChatSquare;
|
||||
export const ThingsIcon = Package;
|
||||
export const TorrentIcon = Magnet;
|
||||
export const TrackIcon = Recording02;
|
||||
|
||||
export const InboxIcon = Download01
|
||||
export const OutboxIcon = Upload01
|
||||
|
@ -26,6 +26,7 @@ import Package from "../icons/package";
|
||||
import Rocket02 from "../icons/rocket-02";
|
||||
import { useBreakpointValue } from "../../providers/global/breakpoint-provider";
|
||||
import KeyboardShortcut from "../keyboard-shortcut";
|
||||
import Mail02 from "../icons/mail-02";
|
||||
|
||||
export default function NavItems() {
|
||||
const location = useLocation();
|
||||
@ -53,6 +54,7 @@ export default function NavItems() {
|
||||
else if (location.pathname.startsWith("/lists")) active = "lists";
|
||||
else if (location.pathname.startsWith("/communities")) active = "communities";
|
||||
else if (location.pathname.startsWith("/channels")) active = "channels";
|
||||
else if (location.pathname.startsWith("/mailboxes")) active = "mailboxes";
|
||||
else if (location.pathname.startsWith("/c/")) active = "communities";
|
||||
else if (location.pathname.startsWith("/goals")) active = "goals";
|
||||
else if (location.pathname.startsWith("/badges")) active = "badges";
|
||||
@ -159,6 +161,15 @@ export default function NavItems() {
|
||||
>
|
||||
Relays
|
||||
</Button>
|
||||
<Button
|
||||
as={RouterLink}
|
||||
to="/mailboxes"
|
||||
leftIcon={<Mail02 boxSize={6} />}
|
||||
colorScheme={active === "mailboxes" ? "primary" : undefined}
|
||||
{...buttonProps}
|
||||
>
|
||||
Mailboxes
|
||||
</Button>
|
||||
<Text position="relative" py="2" color="GrayText">
|
||||
Other Stuff
|
||||
</Text>
|
||||
|
120
src/components/relay-list-button.tsx
Normal file
120
src/components/relay-list-button.tsx
Normal file
@ -0,0 +1,120 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
IconButton,
|
||||
IconButtonProps,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuDivider,
|
||||
MenuItem,
|
||||
MenuItemOption,
|
||||
MenuList,
|
||||
MenuOptionGroup,
|
||||
useDisclosure,
|
||||
useToast,
|
||||
} from "@chakra-ui/react";
|
||||
import { isRTag } from "../types/nostr-event";
|
||||
import useCurrentAccount from "../hooks/use-current-account";
|
||||
import { useSigningContext } from "../providers/global/signing-provider";
|
||||
import useUserRelaySets from "../hooks/use-user-relay-sets";
|
||||
import { useWriteRelays } from "../hooks/use-client-relays";
|
||||
import { useUserOutbox } from "../hooks/use-user-mailboxes";
|
||||
import { getEventCoordinate } from "../helpers/nostr/events";
|
||||
import { getListName } from "../helpers/nostr/lists";
|
||||
import { relayListAddRelay, relayListRemoveRelay } from "../helpers/nostr/relay-list";
|
||||
import NostrPublishAction from "../classes/nostr-publish-action";
|
||||
import replaceableEventLoaderService from "../services/replaceable-event-requester";
|
||||
import { AddIcon, CheckIcon, ChevronDownIcon, DownloadIcon, InboxIcon, OutboxIcon, PlusCircleIcon } from "./icons";
|
||||
|
||||
export default function RelayListButton({ relay, ...props }: { relay: string } & Omit<IconButtonProps, "icon">) {
|
||||
const toast = useToast();
|
||||
const newListModal = useDisclosure();
|
||||
const account = useCurrentAccount();
|
||||
const { requestSignature } = useSigningContext();
|
||||
const writeRelays = useWriteRelays(useUserOutbox(account?.pubkey));
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
|
||||
const sets = useUserRelaySets(account?.pubkey);
|
||||
|
||||
const inSets = sets.filter((set) => set.tags.some((t) => isRTag(t) && t[1] === relay));
|
||||
|
||||
const handleChange = useCallback(
|
||||
async (cords: string | string[]) => {
|
||||
if (!Array.isArray(cords)) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const addToSet = sets.find((set) => !inSets.includes(set) && cords.includes(getEventCoordinate(set)));
|
||||
const removeFromList = sets.find((set) => inSets.includes(set) && !cords.includes(getEventCoordinate(set)));
|
||||
|
||||
if (addToSet) {
|
||||
const draft = relayListAddRelay(addToSet, relay);
|
||||
const signed = await requestSignature(draft);
|
||||
new NostrPublishAction("Add to list", writeRelays, signed);
|
||||
replaceableEventLoaderService.handleEvent(signed);
|
||||
} else if (removeFromList) {
|
||||
const draft = relayListRemoveRelay(removeFromList, relay);
|
||||
const signed = await requestSignature(draft);
|
||||
new NostrPublishAction("Remove from list", writeRelays, signed);
|
||||
replaceableEventLoaderService.handleEvent(signed);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||
}
|
||||
setLoading(false);
|
||||
},
|
||||
[sets, relay, writeRelays, requestSignature],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu isLazy closeOnSelect={false}>
|
||||
<MenuButton
|
||||
as={Button}
|
||||
icon={inSets.length > 0 ? <CheckIcon /> : <AddIcon />}
|
||||
isDisabled={account?.readonly ?? true}
|
||||
rightIcon={<ChevronDownIcon />}
|
||||
{...props}
|
||||
>
|
||||
Add to set
|
||||
</MenuButton>
|
||||
<MenuList minWidth="240px">
|
||||
<MenuItem icon={<InboxIcon />}>Inbox</MenuItem>
|
||||
<MenuItem icon={<OutboxIcon />}>Outbox</MenuItem>
|
||||
{sets.length > 0 && (
|
||||
<MenuOptionGroup
|
||||
type="checkbox"
|
||||
value={inSets.map((list) => getEventCoordinate(list))}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{sets.map((list) => (
|
||||
<MenuItemOption
|
||||
key={getEventCoordinate(list)}
|
||||
value={getEventCoordinate(list)}
|
||||
isDisabled={account?.readonly || isLoading}
|
||||
isTruncated
|
||||
maxW="90vw"
|
||||
>
|
||||
{getListName(list)}
|
||||
</MenuItemOption>
|
||||
))}
|
||||
</MenuOptionGroup>
|
||||
)}
|
||||
<MenuDivider />
|
||||
<MenuItem icon={<PlusCircleIcon />} onClick={newListModal.onOpen}>
|
||||
New relay set
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
{/* {newListModal.isOpen && (
|
||||
<NewListModal
|
||||
onClose={newListModal.onClose}
|
||||
isOpen
|
||||
onCreated={newListModal.onClose}
|
||||
initKind={NOTE_LIST_KIND}
|
||||
allowSelectKind={false}
|
||||
/>
|
||||
)} */}
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
Button,
|
||||
Drawer,
|
||||
@ -10,20 +11,20 @@ import {
|
||||
Flex,
|
||||
Heading,
|
||||
IconButton,
|
||||
Switch,
|
||||
Link,
|
||||
Text,
|
||||
} from "@chakra-ui/react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { CloseIcon } from "@chakra-ui/icons";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
|
||||
import { useReadRelays, useWriteRelays } from "../../hooks/use-client-relays";
|
||||
import { useMemo } from "react";
|
||||
import relayPoolService from "../../services/relay-pool";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import { RelayUrlInput } from "../relay-url-input";
|
||||
import { useForm } from "react-hook-form";
|
||||
import clientRelaysService from "../../services/client-relays";
|
||||
import { RelayMode } from "../../classes/relay";
|
||||
import RelaySet from "../../classes/relay-set";
|
||||
import { CloseIcon } from "@chakra-ui/icons";
|
||||
import Circle from "../icons/circle";
|
||||
import { safeRelayUrl } from "../../helpers/relay";
|
||||
import UploadCloud01 from "../icons/upload-cloud-01";
|
||||
import { RelayFavicon } from "../relay-favicon";
|
||||
@ -54,10 +55,11 @@ function RelayControl({ url }: { url: string }) {
|
||||
return (
|
||||
<Flex gap="2" alignItems="center">
|
||||
<RelayFavicon relay={url} size="xs" outline="2px solid" outlineColor={color} />
|
||||
<Text fontFamily="monospace" fontSize="md" flexGrow={1} isTruncated title={url}>
|
||||
<Link as={RouterLink} to={`/r/${encodeURIComponent(url)}`}>
|
||||
{url}
|
||||
</Text>
|
||||
</Link>
|
||||
<IconButton
|
||||
ml="auto"
|
||||
aria-label="Toggle Write"
|
||||
icon={<UploadCloud01 />}
|
||||
size="sm"
|
||||
|
@ -1,18 +1,20 @@
|
||||
import { Button, ButtonProps, useDisclosure } from "@chakra-ui/react";
|
||||
import { ButtonProps, IconButton, useDisclosure } from "@chakra-ui/react";
|
||||
|
||||
import { RelayIcon } from "../icons";
|
||||
import { useRelaySelectionContext } from "../../providers/local/relay-selection-provider";
|
||||
import RelayManagementDrawer from "../relay-management-drawer";
|
||||
|
||||
export default function RelaySelectionButton({ ...props }: ButtonProps) {
|
||||
const relaysModal = useDisclosure();
|
||||
const { setSelected, relays } = useRelaySelectionContext();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button leftIcon={<RelayIcon />} onClick={relaysModal.onOpen} {...props}>
|
||||
{relays.length} {relays.length === 1 ? "Relay" : "Relays"}
|
||||
</Button>
|
||||
<IconButton
|
||||
icon={<RelayIcon />}
|
||||
onClick={relaysModal.onOpen}
|
||||
aria-label="Relays"
|
||||
title="Relays"
|
||||
variant="ghost"
|
||||
{...props}
|
||||
/>
|
||||
<RelayManagementDrawer isOpen={relaysModal.isOpen} onClose={relaysModal.onClose} />
|
||||
</>
|
||||
);
|
||||
|
@ -7,6 +7,7 @@ import { safeJson } from "../parse";
|
||||
import { safeDecode } from "../nip19";
|
||||
import { getEventUID } from "nostr-idb";
|
||||
import { safeRelayUrls } from "../relay";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
export function truncatedId(str: string, keep = 6) {
|
||||
if (str.length < keep * 2 + 3) return str;
|
||||
@ -269,4 +270,13 @@ export function sortByDate(a: NostrEvent, b: NostrEvent) {
|
||||
return b.created_at - a.created_at;
|
||||
}
|
||||
|
||||
export function cloneEvent(event?: NostrEvent): DraftNostrEvent {
|
||||
return {
|
||||
kind: kinds.RelayList,
|
||||
created_at: dayjs().unix(),
|
||||
content: event?.content || "",
|
||||
tags: event?.tags ? Array.from(event.tags) : [],
|
||||
};
|
||||
}
|
||||
|
||||
export { getEventUID };
|
||||
|
@ -66,6 +66,7 @@ export function getEventPointersFromList(event: NostrEvent | DraftNostrEvent): n
|
||||
export function getReferencesFromList(event: NostrEvent | DraftNostrEvent) {
|
||||
return event.tags.filter(isRTag).map((t) => ({ url: t[1], petname: t[2] }));
|
||||
}
|
||||
/** @deprecated */
|
||||
export function getRelaysFromList(event: NostrEvent | DraftNostrEvent) {
|
||||
if (event.kind === kinds.RelayList) return safeRelayUrls(event.tags.filter(isRTag).map((t) => t[1]));
|
||||
else return safeRelayUrls(event.tags.filter((t) => t[0] === "relay" && t[1]).map((t) => t[1]) as string[]);
|
||||
|
68
src/helpers/nostr/mailbox.ts
Normal file
68
src/helpers/nostr/mailbox.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { RelayMode } from "../../classes/relay";
|
||||
import { DraftNostrEvent, NostrEvent, RTag, Tag, isRTag } from "../../types/nostr-event";
|
||||
import { safeRelayUrl } from "../relay";
|
||||
import { cloneEvent } from "./events";
|
||||
|
||||
/** fixes or removes any bad r tags */
|
||||
export function cleanRTags(tags: Tag[]) {
|
||||
let newTags: Tag[] = [];
|
||||
for (const tag of tags) {
|
||||
if (tag[0] === "r") {
|
||||
if (!tag[1]) continue;
|
||||
const url = safeRelayUrl(tag[1]);
|
||||
if (url) newTags.push(tag[2] ? ["r", url, tag[2]] : ["r", url]);
|
||||
} else newTags.push(tag);
|
||||
}
|
||||
return newTags;
|
||||
}
|
||||
|
||||
export function parseRTag(tag: RTag): { url: string; mode: RelayMode } {
|
||||
const url = tag[1];
|
||||
const mode = tag[2] === "write" ? RelayMode.WRITE : tag[2] === "read" ? RelayMode.READ : RelayMode.ALL;
|
||||
return { url, mode };
|
||||
}
|
||||
export function createRelayTag(url: string, mode: RelayMode) {
|
||||
switch (mode) {
|
||||
case RelayMode.WRITE:
|
||||
return ["r", url, "write"];
|
||||
case RelayMode.READ:
|
||||
return ["r", url, "read"];
|
||||
default:
|
||||
case RelayMode.ALL:
|
||||
return ["r", url];
|
||||
}
|
||||
}
|
||||
|
||||
export function getRelaysFromMailbox(list: NostrEvent | DraftNostrEvent): { url: string; mode: RelayMode }[] {
|
||||
return cleanRTags(list.tags).filter(isRTag).map(parseRTag);
|
||||
}
|
||||
|
||||
export function addRelayModeToMailbox(list: NostrEvent | undefined, relay: string, mode: RelayMode): DraftNostrEvent {
|
||||
let draft = cloneEvent(list);
|
||||
draft.tags = cleanRTags(draft.tags);
|
||||
|
||||
const existing = draft.tags.find((t) => t[0] === "r" && t[1] === relay) as RTag;
|
||||
if (existing) {
|
||||
const p = parseRTag(existing);
|
||||
draft.tags = draft.tags.map((t) => (t === existing ? createRelayTag(p.url, p.mode | mode) : t));
|
||||
} else draft.tags.push(createRelayTag(relay, mode));
|
||||
return draft;
|
||||
}
|
||||
export function removeRelayModeFromMailbox(
|
||||
list: NostrEvent | undefined,
|
||||
relay: string,
|
||||
mode: RelayMode,
|
||||
): DraftNostrEvent {
|
||||
let draft = cloneEvent(list);
|
||||
draft.tags = cleanRTags(draft.tags);
|
||||
|
||||
const existing = draft.tags.find((t) => t[0] === "r" && t[1] === relay) as RTag;
|
||||
if (existing) {
|
||||
const p = parseRTag(existing);
|
||||
if (p.mode & mode) {
|
||||
if ((p.mode & ~mode) === RelayMode.NONE) draft.tags = draft.tags.filter((t) => t !== existing);
|
||||
else draft.tags = draft.tags.map((t) => (t === existing ? createRelayTag(p.url, p.mode & ~mode) : t));
|
||||
}
|
||||
}
|
||||
return draft;
|
||||
}
|
20
src/helpers/nostr/relay-list.ts
Normal file
20
src/helpers/nostr/relay-list.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import dayjs from "dayjs";
|
||||
import { NostrEvent, isRTag } from "../../types/nostr-event";
|
||||
|
||||
export function relayListAddRelay(list: NostrEvent, relay: string) {
|
||||
if (list.tags.some((t) => isRTag(t) && t[1] === relay)) throw new Error("relay already in list");
|
||||
return {
|
||||
created_at: dayjs().unix(),
|
||||
kind: list.kind,
|
||||
content: list.content,
|
||||
tags: [...list.tags, ["r", relay]],
|
||||
};
|
||||
}
|
||||
export function relayListRemoveRelay(list: NostrEvent, relay: string) {
|
||||
return {
|
||||
created_at: dayjs().unix(),
|
||||
kind: list.kind,
|
||||
content: list.content,
|
||||
tags: list.tags.filter((t) => isRTag(t) && t[1] !== relay),
|
||||
};
|
||||
}
|
47
src/hooks/use-relay-mailbox-actions.ts
Normal file
47
src/hooks/use-relay-mailbox-actions.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { useCallback } from "react";
|
||||
import { useToast } from "@chakra-ui/react";
|
||||
|
||||
import NostrPublishAction from "../classes/nostr-publish-action";
|
||||
import { RelayMode } from "../classes/relay";
|
||||
import { useSigningContext } from "../providers/global/signing-provider";
|
||||
import clientRelaysService from "../services/client-relays";
|
||||
import replaceableEventLoaderService from "../services/replaceable-event-requester";
|
||||
import useCurrentAccount from "./use-current-account";
|
||||
import useUserMailboxes from "./use-user-mailboxes";
|
||||
import { addRelayModeToMailbox, removeRelayModeFromMailbox } from "../helpers/nostr/mailbox";
|
||||
|
||||
export default function useRelayMailboxActions(relay: string) {
|
||||
const toast = useToast();
|
||||
const account = useCurrentAccount();
|
||||
const { requestSignature } = useSigningContext();
|
||||
const { event, inbox, outbox } = useUserMailboxes(account?.pubkey, { alwaysRequest: true }) || {};
|
||||
|
||||
const addMode = useCallback(
|
||||
async (mode: RelayMode) => {
|
||||
try {
|
||||
let draft = addRelayModeToMailbox(event ?? undefined, relay, mode);
|
||||
const signed = await requestSignature(draft);
|
||||
new NostrPublishAction("Mute", clientRelaysService.outbox.urls, signed);
|
||||
replaceableEventLoaderService.handleEvent(signed);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||
}
|
||||
},
|
||||
[requestSignature, event],
|
||||
);
|
||||
const removeMode = useCallback(
|
||||
async (mode: RelayMode) => {
|
||||
try {
|
||||
let draft = removeRelayModeFromMailbox(event ?? undefined, relay, mode);
|
||||
const signed = await requestSignature(draft);
|
||||
new NostrPublishAction("Mute", clientRelaysService.outbox.urls, signed);
|
||||
replaceableEventLoaderService.handleEvent(signed);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||
}
|
||||
},
|
||||
[requestSignature, event],
|
||||
);
|
||||
|
||||
return { inbox, outbox, addMode, removeMode };
|
||||
}
|
26
src/hooks/use-user-relay-sets.ts
Normal file
26
src/hooks/use-user-relay-sets.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { useCallback } from "react";
|
||||
|
||||
import { useReadRelays } from "./use-client-relays";
|
||||
import useSubject from "./use-subject";
|
||||
import useTimelineLoader from "./use-timeline-loader";
|
||||
import { NostrEvent, isRTag } from "../types/nostr-event";
|
||||
import { kinds } from "nostr-tools";
|
||||
|
||||
export default function useUserRelaySets(pubkey?: string, additionalRelays?: Iterable<string>) {
|
||||
const readRelays = useReadRelays(additionalRelays);
|
||||
const eventFilter = useCallback((event: NostrEvent) => event.tags.some(isRTag), []);
|
||||
const timeline = useTimelineLoader(
|
||||
`${pubkey}-relay-sets`,
|
||||
readRelays,
|
||||
pubkey
|
||||
? {
|
||||
authors: pubkey ? [pubkey] : [],
|
||||
kinds: [kinds.Relaysets],
|
||||
}
|
||||
: undefined,
|
||||
{ eventFilter },
|
||||
);
|
||||
|
||||
const lists = useSubject(timeline.timeline);
|
||||
return pubkey ? lists : [];
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
import { useMemo } from "react";
|
||||
|
||||
import userMailboxesService from "../services/user-mailboxes";
|
||||
import useSubject from "./use-subject";
|
||||
import { useReadRelays } from "./use-client-relays";
|
||||
import { RequestOptions } from "../services/replaceable-event-requester";
|
||||
import { COMMON_CONTACT_RELAY } from "../const";
|
||||
import RelaySet from "../classes/relay-set";
|
||||
|
||||
/** @deprecated */
|
||||
export function useUserRelays(pubkey: string, additionalRelays: Iterable<string> = [], opts: RequestOptions = {}) {
|
||||
const readRelays = useReadRelays([...additionalRelays, COMMON_CONTACT_RELAY]);
|
||||
const subject = useMemo(
|
||||
() => userMailboxesService.requestMailboxes(pubkey, readRelays, opts),
|
||||
[pubkey, readRelays.urls.join("|")],
|
||||
);
|
||||
const userRelays = useSubject(subject);
|
||||
|
||||
return userRelays?.relays || new RelaySet();
|
||||
}
|
@ -50,14 +50,12 @@ class ClientRelayService {
|
||||
localStorage.setItem("write-relays", this.writeRelays.value.urls.join(","));
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
get outbox() {
|
||||
const account = accountService.current.value;
|
||||
if (account) return userMailboxesService.getMailboxes(account.pubkey).value?.outbox ?? this.writeRelays.value;
|
||||
return this.writeRelays.value;
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
get inbox() {
|
||||
const account = accountService.current.value;
|
||||
if (account) return userMailboxesService.getMailboxes(account.pubkey).value?.inbox ?? this.readRelays.value;
|
||||
|
@ -10,6 +10,7 @@ import { RelayMode } from "../classes/relay";
|
||||
|
||||
export type UserMailboxes = {
|
||||
pubkey: string;
|
||||
event: NostrEvent | null;
|
||||
relays: RelaySet;
|
||||
inbox: RelaySet;
|
||||
outbox: RelaySet;
|
||||
@ -19,6 +20,7 @@ export type UserMailboxes = {
|
||||
function nip65ToUserMailboxes(event: NostrEvent): UserMailboxes {
|
||||
return {
|
||||
pubkey: event.pubkey,
|
||||
event,
|
||||
relays: RelaySet.fromNIP65Event(event),
|
||||
inbox: RelaySet.fromNIP65Event(event, RelayMode.READ),
|
||||
outbox: RelaySet.fromNIP65Event(event, RelayMode.WRITE),
|
||||
@ -43,6 +45,7 @@ class UserMailboxesService {
|
||||
if (contacts.relays.size > 0 && !value) {
|
||||
next({
|
||||
pubkey: contacts.pubkey,
|
||||
event: null,
|
||||
relays: contacts.relays,
|
||||
inbox: contacts.inbox,
|
||||
outbox: contacts.outbox,
|
||||
|
149
src/views/mailboxes/index.tsx
Normal file
149
src/views/mailboxes/index.tsx
Normal file
@ -0,0 +1,149 @@
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardBody,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
Flex,
|
||||
Heading,
|
||||
IconButton,
|
||||
Link,
|
||||
Text,
|
||||
useToast,
|
||||
} from "@chakra-ui/react";
|
||||
import { CloseIcon } from "@chakra-ui/icons";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
|
||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||
import RequireCurrentAccount from "../../providers/route/require-current-account";
|
||||
import useUserMailboxes from "../../hooks/use-user-mailboxes";
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
import { InboxIcon } from "../../components/icons";
|
||||
import { RelayUrlInput } from "../../components/relay-url-input";
|
||||
import { RelayFavicon } from "../../components/relay-favicon";
|
||||
import { RelayMode } from "../../classes/relay";
|
||||
import { useCallback } from "react";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import { addRelayModeToMailbox, removeRelayModeFromMailbox } from "../../helpers/nostr/mailbox";
|
||||
import useAsyncErrorHandler from "../../hooks/use-async-error-handler";
|
||||
import NostrPublishAction from "../../classes/nostr-publish-action";
|
||||
import clientRelaysService from "../../services/client-relays";
|
||||
import { useSigningContext } from "../../providers/global/signing-provider";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { safeRelayUrl } from "../../helpers/relay";
|
||||
import replaceableEventLoaderService from "../../services/replaceable-event-requester";
|
||||
|
||||
function RelayLine({ relay, mode, list }: { relay: string; mode: RelayMode; list?: NostrEvent }) {
|
||||
const { requestSignature } = useSigningContext();
|
||||
const remove = useAsyncErrorHandler(async () => {
|
||||
const draft = removeRelayModeFromMailbox(list, relay, mode);
|
||||
const signed = await requestSignature(draft);
|
||||
new NostrPublishAction("Remove relay", clientRelaysService.outbox.urls, signed);
|
||||
}, [relay, mode, list, requestSignature]);
|
||||
|
||||
return (
|
||||
<Flex key={relay} gap="2" alignItems="center">
|
||||
<RelayFavicon relay={relay} size="xs" />
|
||||
<Link as={RouterLink} to={`/r/${encodeURIComponent(relay)}`}>
|
||||
{relay}
|
||||
</Link>
|
||||
<IconButton
|
||||
aria-label="Remove Relay"
|
||||
icon={<CloseIcon />}
|
||||
size="xs"
|
||||
ml="auto"
|
||||
colorScheme="red"
|
||||
variant="ghost"
|
||||
onClick={remove}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
function AddRelayForm({ onSubmit }: { onSubmit: (url: string) => void }) {
|
||||
const { register, handleSubmit, reset } = useForm({
|
||||
defaultValues: {
|
||||
url: "",
|
||||
},
|
||||
});
|
||||
|
||||
const submit = handleSubmit(async (values) => {
|
||||
const url = safeRelayUrl(values.url);
|
||||
if (!url) return;
|
||||
await onSubmit(url);
|
||||
reset();
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex as="form" display="flex" gap="2" onSubmit={submit} flex={1}>
|
||||
<RelayUrlInput {...register("url")} placeholder="wss://relay.example.com" size="sm" borderRadius="md" />
|
||||
<Button type="submit" colorScheme="primary" size="sm">
|
||||
Add
|
||||
</Button>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
function MailboxesPage() {
|
||||
const toast = useToast();
|
||||
const account = useCurrentAccount()!;
|
||||
const { inbox, outbox, event } = useUserMailboxes(account.pubkey) || {};
|
||||
|
||||
const { requestSignature } = useSigningContext();
|
||||
const addRelay = useCallback(
|
||||
async (relay: string, mode: RelayMode) => {
|
||||
try {
|
||||
const draft = addRelayModeToMailbox(event ?? undefined, relay, mode);
|
||||
const signed = await requestSignature(draft);
|
||||
replaceableEventLoaderService.handleEvent(signed);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) toast({ status: "error", description: e.message });
|
||||
}
|
||||
},
|
||||
[event, requestSignature],
|
||||
);
|
||||
|
||||
return (
|
||||
<VerticalPageLayout>
|
||||
<Heading>Mailboxes</Heading>
|
||||
<Card maxW="lg">
|
||||
<CardHeader p="4" pb="2" display="flex" gap="2" alignItems="center">
|
||||
<InboxIcon boxSize={5} />
|
||||
<Heading size="md">Inbox</Heading>
|
||||
</CardHeader>
|
||||
<CardBody px="4" py="0" display="flex" flexDirection="column" gap="2">
|
||||
<Text fontStyle="italic">Other users will send DMs and notes to these relays to notify you</Text>
|
||||
{inbox?.urls
|
||||
.sort()
|
||||
.map((url) => <RelayLine key={url} relay={url} mode={RelayMode.READ} list={event ?? undefined} />)}
|
||||
</CardBody>
|
||||
<CardFooter display="flex" gap="2" p="4">
|
||||
<AddRelayForm onSubmit={(r) => addRelay(r, RelayMode.READ)} />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Card maxW="lg">
|
||||
<CardHeader p="4" pb="2" display="flex" gap="2" alignItems="center">
|
||||
<InboxIcon boxSize={5} />
|
||||
<Heading size="md">Outbox</Heading>
|
||||
</CardHeader>
|
||||
<CardBody px="4" py="0" display="flex" flexDirection="column" gap="1">
|
||||
<Text fontStyle="italic">Always publish to these relays so your followers can find your notes</Text>
|
||||
{outbox?.urls
|
||||
.sort()
|
||||
.map((url) => <RelayLine key={url} relay={url} mode={RelayMode.WRITE} list={event ?? undefined} />)}
|
||||
</CardBody>
|
||||
<CardFooter display="flex" gap="2" p="4">
|
||||
<AddRelayForm onSubmit={(r) => addRelay(r, RelayMode.WRITE)} />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</VerticalPageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default function MailboxesView() {
|
||||
return (
|
||||
<RequireCurrentAccount>
|
||||
<MailboxesPage />
|
||||
</RequireCurrentAccount>
|
||||
);
|
||||
}
|
@ -169,7 +169,7 @@ export default function RelayCard({ url, ...props }: { url: string } & Omit<Card
|
||||
{/* <RelayJoinAction url={url} size="sm" /> */}
|
||||
{/* <RelayModeAction url={url} /> */}
|
||||
|
||||
<RelayShareButton relay={url} ml="auto" size="sm" />
|
||||
{/* <RelayShareButton relay={url} ml="auto" size="sm" /> */}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</>
|
||||
|
@ -27,6 +27,7 @@ import { RelayFavicon } from "../../../components/relay-favicon";
|
||||
import VerticalPageLayout from "../../../components/vertical-page-layout";
|
||||
import { safeRelayUrl } from "../../../helpers/relay";
|
||||
import RelayUsersTab from "./relay-users";
|
||||
import RelayListButton from "../../../components/relay-list-button";
|
||||
const RelayDetailsTab = lazy(() => import("./relay-details"));
|
||||
|
||||
function RelayPage({ relay }: { relay: string }) {
|
||||
@ -55,15 +56,7 @@ function RelayPage({ relay }: { relay: string }) {
|
||||
</Heading>
|
||||
<ButtonGroup size={["sm", "md"]}>
|
||||
<RelayDebugButton url={relay} ml="auto" />
|
||||
<Button
|
||||
as="a"
|
||||
href={`https://nostr.watch/relay/${new URL(relay).host}`}
|
||||
target="_blank"
|
||||
rightIcon={<ExternalLinkIcon />}
|
||||
>
|
||||
More info
|
||||
</Button>
|
||||
{/* <RelayJoinAction url={relay} /> */}
|
||||
{/* <RelayListButton relay={relay} aria-label="Add to set" /> */}
|
||||
</ButtonGroup>
|
||||
</Flex>
|
||||
<RelayMetadata url={relay} extended />
|
||||
|
@ -36,7 +36,7 @@ export default function SettingsView() {
|
||||
return (
|
||||
<VerticalPageLayout as="form" onSubmit={saveSettings}>
|
||||
<FormProvider {...form}>
|
||||
<Accordion defaultIndex={[0]} allowMultiple>
|
||||
<Accordion defaultIndex={[]} allowMultiple>
|
||||
<DisplaySettings />
|
||||
<PostSettings />
|
||||
<PerformanceSettings />
|
||||
|
Loading…
x
Reference in New Issue
Block a user