Add mailboxes view

This commit is contained in:
hzrd149 2024-01-22 16:55:45 +00:00
parent 238a3be17e
commit 588f38db35
21 changed files with 495 additions and 61 deletions

View File

@ -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: [

View File

@ -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),
);
}
}

View File

@ -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>();

View File

@ -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

View File

@ -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>

View 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}
/>
)} */}
</>
);
}

View File

@ -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"

View File

@ -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} />
</>
);

View File

@ -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 };

View File

@ -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[]);

View 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;
}

View 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),
};
}

View 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 };
}

View 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 : [];
}

View File

@ -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();
}

View File

@ -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;

View File

@ -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,

View 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>
);
}

View File

@ -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>
</>

View File

@ -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 />

View File

@ -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 />