overhaul core relay code

This commit is contained in:
hzrd149 2024-01-20 11:40:11 +00:00
parent 078073bbed
commit 91f4c7c92e
100 changed files with 551 additions and 529 deletions
.changeset
src
classes
components
helpers
hooks
providers
services
views

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Overhaul core relay code

@ -23,9 +23,9 @@ export default class NostrPublishAction {
private remaining = new Set<Relay>();
constructor(label: string, relays: string[], event: NostrEvent, timeout: number = 5000) {
constructor(label: string, relays: Iterable<string>, event: NostrEvent, timeout: number = 5000) {
this.label = label;
this.relays = relays;
this.relays = Array.from(relays);
this.event = event;
for (const url of relays) {

@ -21,9 +21,9 @@ export default class NostrRequest {
onComplete = createDefer<void>();
seenEvents = new Set<string>();
constructor(relayUrls: string[], timeout?: number) {
constructor(relayUrls: Iterable<string>, timeout?: number) {
this.id = nanoid();
this.relays = new Set(relayUrls.map((url) => relayPoolService.requestRelay(url)));
this.relays = new Set(Array.from(relayUrls).map((url) => relayPoolService.requestRelay(url)));
for (const relay of this.relays) {
relay.onEOSE.subscribe(this.handleEOSE, this);

47
src/classes/relay-set.ts Normal file

@ -0,0 +1,47 @@
import { safeRelayUrl, safeRelayUrls } from "../helpers/relay";
import relayPoolService from "../services/relay-pool";
import { NostrEvent } from "../types/nostr-event";
import { RelayMode } from "./relay";
export default class RelaySet extends Set<string> {
get urls() {
return Array.from(this);
}
getRelays() {
return this.urls.map((url) => relayPoolService.requestRelay(url, false));
}
clone() {
return new RelaySet(this);
}
merge(src: Iterable<string>): this {
for (const url of src) this.add(url);
return this;
}
static from(...sources: (Iterable<string> | undefined)[]) {
const set = new RelaySet();
for (const src of sources) {
if (!src) continue;
for (const url of src) {
const safe = safeRelayUrl(url);
if (safe) set.add(safe);
}
}
return set;
}
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;
}
}

@ -24,7 +24,6 @@ export type IncomingEOSE = {
subId: string;
relay: Relay;
};
// NIP-20
export type IncomingCommandResult = {
type: "OK";
eventId: string;
@ -39,7 +38,6 @@ export enum RelayMode {
WRITE = 2,
ALL = 1 | 2,
}
export type RelayConfig = { url: string; mode: RelayMode };
const CONNECTION_TIMEOUT = 1000 * 30;
@ -53,7 +51,6 @@ export default class Relay {
onEOSE = new Subject<IncomingEOSE>(undefined, false);
onCommandResult = new Subject<IncomingCommandResult>(undefined, false);
ws?: WebSocket;
mode: RelayMode = RelayMode.ALL;
private connectionTimer?: () => void;
private ejectTimer?: () => void;
@ -61,9 +58,8 @@ export default class Relay {
private subscriptionResTimer = new Map<string, () => void>();
private queue: NostrOutgoingMessage[] = [];
constructor(url: string, mode: RelayMode = RelayMode.ALL) {
constructor(url: string) {
this.url = url;
this.mode = mode;
}
open() {
@ -112,16 +108,14 @@ export default class Relay {
this.ws.onmessage = this.handleMessage.bind(this);
}
send(json: NostrOutgoingMessage) {
if (this.mode & RelayMode.WRITE) {
if (this.connected) {
this.ws?.send(JSON.stringify(json));
if (this.connected) {
this.ws?.send(JSON.stringify(json));
// record start time
if (json[0] === "REQ" || json[0] === "COUNT") {
this.startSubResTimer(json[1]);
}
} else this.queue.push(json);
}
// record start time
if (json[0] === "REQ" || json[0] === "COUNT") {
this.startSubResTimer(json[1]);
}
} else this.queue.push(json);
}
close() {
this.ws?.close();
@ -172,8 +166,6 @@ export default class Relay {
// skip empty events
if (!event.data) return;
if (!(this.mode & RelayMode.READ)) return;
try {
const data: RawIncomingNostrEvent = JSON.parse(event.data);
const type = data[0];

@ -1,6 +1,7 @@
import dayjs from "dayjs";
import { Debugger } from "debug";
import { Filter } from "nostr-tools";
import _throttle from "lodash.throttle";
import { NostrEvent, isATag, isETag } from "../types/nostr-event";
import { NostrRequestFilter, RelayQueryMap } from "../types/nostr-query";
@ -129,13 +130,14 @@ export default class TimelineLoader {
this.subscription.onEvent.subscribe(this.handleEvent, this);
// update the timeline when there are new events
this.events.onEvent.subscribe(this.updateTimeline, this);
this.events.onDelete.subscribe(this.updateTimeline, this);
this.events.onClear.subscribe(this.updateTimeline, this);
this.events.onEvent.subscribe(this.throttleUpdateTimeline, this);
this.events.onDelete.subscribe(this.throttleUpdateTimeline, this);
this.events.onClear.subscribe(this.throttleUpdateTimeline, this);
deleteEventService.stream.subscribe(this.handleDeleteEvent, this);
}
private throttleUpdateTimeline = _throttle(this.updateTimeline, 10);
private updateTimeline() {
if (this.eventFilter) {
const filter = this.eventFilter;

@ -35,7 +35,7 @@ export default function PinNoteMenuItem({ event }: { event: NostrEvent }) {
else draft = listAddEvent(draft, event.id);
const signed = await requestSignature(draft);
new NostrPublishAction(label, clientRelaysService.getWriteUrls(), signed);
new NostrPublishAction(label, clientRelaysService.outbox.urls, signed);
setLoading(false);
} catch (e) {
if (e instanceof Error) toast({ status: "error", description: e.message });

@ -16,7 +16,7 @@ export default function NoteDebugModal({ event, ...props }: { event: NostrEvent
const [loading, setLoading] = useState(false);
const broadcast = useCallback(() => {
setLoading(true);
const relays = clientRelaysService.getWriteUrls();
const relays = clientRelaysService.outbox.urls;
const pub = new NostrPublishAction("Broadcast", relays, event, 5000);
pub.onComplete.then(() => setLoading(false));
}, []);

@ -1,11 +1,10 @@
import { Suspense, lazy } from "react";
import type { DecodeResult } from "nostr-tools/lib/types/nip19";
import { Button, CardProps, Spinner } from "@chakra-ui/react";
import { kinds, nip19 } from "nostr-tools";
import { CardProps, Spinner } from "@chakra-ui/react";
import { kinds } from "nostr-tools";
import EmbeddedNote from "./event-types/embedded-note";
import useSingleEvent from "../../hooks/use-single-event";
import { NoteLink } from "../note-link";
import { NostrEvent } from "../../types/nostr-event";
import { STREAM_CHAT_MESSAGE_KIND, STREAM_KIND } from "../../helpers/nostr/stream";
import { GOAL_KIND } from "../../helpers/nostr/goal";

@ -1,4 +1,4 @@
import { Link, Text, Tooltip } from "@chakra-ui/react";
import { Link, Tooltip } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { EmbedableContent, embedJSX } from "../../helpers/embeds";

@ -24,7 +24,7 @@ export function useAddReaction(event: NostrEvent, grouped: ReactionGroup[]) {
const signed = await requestSignature(draft);
if (signed) {
const writeRelays = clientRelaysService.getWriteUrls();
const writeRelays = clientRelaysService.outbox.urls;
new NostrPublishAction("Reaction", writeRelays, signed);
eventReactionsService.handleEvent(signed);
}

@ -15,11 +15,10 @@ import { DraftNostrEvent, NostrEvent, isDTag } from "../../types/nostr-event";
import clientRelaysService from "../../services/client-relays";
import { getZapSplits } from "../../helpers/nostr/zaps";
import { unique } from "../../helpers/array";
import { RelayMode } from "../../classes/relay";
import relayScoreboardService from "../../services/relay-scoreboard";
import { getEventCoordinate, isReplaceable } from "../../helpers/nostr/events";
import { EmbedProps } from "../embed-event";
import userRelaysService from "../../services/user-relays";
import userMailboxesService from "../../services/user-mailboxes";
import InputStep from "./input-step";
import lnurlMetadataService from "../../services/lnurl-metadata";
import userMetadataService from "../../services/user-metadata";
@ -62,15 +61,10 @@ async function getPayRequestForPubkey(
}
const userInbox = relayScoreboardService
.getRankedRelays(
userRelaysService
.getRelays(pubkey)
.value?.relays.filter((r) => r.mode & RelayMode.READ)
.map((r) => r.url) ?? [],
)
.getRankedRelays(userMailboxesService.getMailboxes(pubkey).value?.inbox)
.slice(0, 4);
const eventRelays = event ? relayHintService.getEventRelayHints(event, 4) : [];
const outbox = relayScoreboardService.getRankedRelays(clientRelaysService.getWriteUrls()).slice(0, 4);
const outbox = relayScoreboardService.getRankedRelays(clientRelaysService.outbox.urls).slice(0, 4);
const additional = relayScoreboardService.getRankedRelays(additionalRelays);
// create zap request

@ -41,7 +41,7 @@ export default function AddReactionButton({
const signed = await requestSignature(draft);
if (signed) {
const writeRelays = clientRelaysService.getWriteUrls();
const writeRelays = clientRelaysService.outbox.urls;
new NostrPublishAction("Reaction", writeRelays, signed);
eventReactionsService.handleEvent(signed);
setPopover.off();

@ -48,7 +48,7 @@ export default function BookmarkButton({ event, ...props }: { event: NostrEvent
async (cords: string | string[]) => {
if (!Array.isArray(cords)) return;
const writeRelays = clientRelaysService.getWriteUrls();
const writeRelays = clientRelaysService.outbox.urls;
setLoading(true);
try {

@ -67,7 +67,7 @@ export default function RepostModal({
]);
}
const signed = await requestSignature(draftRepost);
const pub = new NostrPublishAction("Repost", clientRelaysService.getWriteUrls(), signed);
const pub = new NostrPublishAction("Repost", clientRelaysService.outbox.urls, signed);
await pub.onComplete;
onClose();
} catch (e) {

@ -30,7 +30,7 @@ export default function NoteMenu({
const translationsModal = useDisclosure();
const broadcast = useCallback(() => {
const missingRelays = clientRelaysService.getWriteUrls();
const missingRelays = clientRelaysService.outbox.urls;
const pub = new NostrPublishAction("Broadcast", missingRelays, event, 5000);
pub.onResult.subscribe((result) => {
if (result.status) handleEventFromRelay(result.relay, event);

@ -28,7 +28,7 @@ export default function NoteZapButton({ event, allowComment, showEventPreview, .
const onZapped = () => {
onClose();
eventZapsService.requestZaps(getEventUID(event), clientRelaysService.getReadUrls(), true);
eventZapsService.requestZaps(getEventUID(event), clientRelaysService.inbox.urls, true);
};
const total = totalZaps(zaps);

@ -78,7 +78,7 @@ export default function RelaySelectionModal({
/>
<CheckboxGroup value={newSelected} onChange={(urls) => setSelected(urls.map(String))}>
<Flex direction="column" gap="2" mb="2">
{relays.map((url) => (
{relays.urls.map((url) => (
<Checkbox key={url} value={url}>
<RelayFavicon relay={url} size="xs" /> {url}
</Checkbox>
@ -87,7 +87,7 @@ export default function RelaySelectionModal({
</CheckboxGroup>
<ButtonGroup>
<Button onClick={() => setSelected(relays)} size="sm">
<Button onClick={() => setSelected(Array.from(relays))} size="sm">
All
</Button>
<Button onClick={() => setSelected([])} size="sm">

@ -51,7 +51,7 @@ function UsersLists({ pubkey }: { pubkey: string }) {
async (cords: string | string[]) => {
if (!Array.isArray(cords)) return;
const writeRelays = clientRelaysService.getWriteUrls();
const writeRelays = clientRelaysService.outbox.urls;
setLoading(true);
try {
@ -127,13 +127,13 @@ export const UserFollowButton = ({ pubkey, showLists, ...props }: UserFollowButt
const handleFollow = useAsyncErrorHandler(async () => {
const draft = listAddPerson(contacts || createEmptyContactList(), pubkey);
const signed = await requestSignature(draft);
const pub = new NostrPublishAction("Follow", clientRelaysService.getWriteUrls(), signed);
const pub = new NostrPublishAction("Follow", clientRelaysService.outbox.urls, signed);
replaceableEventLoaderService.handleEvent(signed);
}, [contacts, requestSignature]);
const handleUnfollow = useAsyncErrorHandler(async () => {
const draft = listRemovePerson(contacts || createEmptyContactList(), pubkey);
const signed = await requestSignature(draft);
const pub = new NostrPublishAction("Unfollow", clientRelaysService.getWriteUrls(), signed);
const pub = new NostrPublishAction("Unfollow", clientRelaysService.outbox.urls, signed);
replaceableEventLoaderService.handleEvent(signed);
}, [contacts, requestSignature]);

@ -3,6 +3,7 @@ import { getPublicKey, nip19 } from "nostr-tools";
import { NostrEvent, Tag, isATag, isDTag, isETag, isPTag } from "../types/nostr-event";
import { isReplaceable } from "./nostr/events";
import relayHintService from "../services/event-relay-hint";
import { safeRelayUrls } from "./relay";
export function isHex(str?: string) {
if (str?.match(/^[0-9a-f]+$/i)) return true;
@ -15,7 +16,10 @@ export function isHexKey(key?: string) {
export function safeDecode(str: string) {
try {
return nip19.decode(str);
const result = nip19.decode(str);
if ((result.type === "nevent" || result.type === "nprofile" || result.type === "naddr") && result.data.relays)
result.data.relays = safeRelayUrls(result.data.relays);
return result;
} catch (e) {}
}

@ -1,12 +1,12 @@
import { kinds, validateEvent } from "nostr-tools";
import { ATag, DraftNostrEvent, ETag, isATag, isDTag, isETag, NostrEvent, RTag, Tag } from "../../types/nostr-event";
import { RelayConfig, RelayMode } from "../../classes/relay";
import { ATag, DraftNostrEvent, ETag, isATag, isDTag, isETag, NostrEvent, Tag } from "../../types/nostr-event";
import { getMatchNostrLink } from "../regexp";
import { AddressPointer, EventPointer } from "nostr-tools/lib/types/nip19";
import { safeJson } from "../parse";
import { safeDecode } from "../nip19";
import { getEventUID } from "nostr-idb";
import { safeRelayUrls } from "../relay";
export function truncatedId(str: string, keep = 6) {
if (str.length < keep * 2 + 3) return str;
@ -33,20 +33,34 @@ export function pointerMatchEvent(event: NostrEvent, pointer: AddressPointer | E
return false;
}
const isReplySymbol = Symbol("isReply");
export function isReply(event: NostrEvent | DraftNostrEvent) {
// @ts-ignore
if (event[isReplySymbol] !== undefined) return event[isReplySymbol] as boolean;
if (event.kind === kinds.Repost || event.kind === kinds.GenericRepost) return false;
// TODO: update this to only look for a "root" or "reply" tag
return !!getThreadReferences(event).reply;
const isReply = !!getThreadReferences(event).reply;
// @ts-ignore
event[isReplySymbol] = isReply;
return isReply;
}
export function isMentionedInContent(event: NostrEvent | DraftNostrEvent, pubkey: string) {
return filterTagsByContentRefs(event.content, event.tags).some((t) => t[1] === pubkey);
}
const isRepostSymbol = Symbol("isRepost");
export function isRepost(event: NostrEvent | DraftNostrEvent) {
// @ts-ignore
if (event[isRepostSymbol] !== undefined) return event[isRepostSymbol] as boolean;
if (event.kind === kinds.Repost || event.kind === kinds.GenericRepost) return true;
const match = event.content.match(getMatchNostrLink());
return match && match[0].length === event.content.length;
const isRepost = !!match && match[0].length === event.content.length;
// @ts-ignore
event[isRepostSymbol] = isRepost;
return isRepost;
}
/**
@ -172,17 +186,6 @@ export function getThreadReferences(event: NostrEvent | DraftNostrEvent) {
};
}
export function parseRTag(tag: RTag): RelayConfig {
switch (tag[2]) {
case "write":
return { url: tag[1], mode: RelayMode.WRITE };
case "read":
return { url: tag[1], mode: RelayMode.READ };
default:
return { url: tag[1], mode: RelayMode.ALL };
}
}
export function getEventCoordinate(event: NostrEvent) {
const d = event.tags.find(isDTag)?.[1];
return d ? `${event.kind}:${event.pubkey}:${d}` : `${event.kind}:${event.pubkey}`;
@ -197,11 +200,11 @@ export function getEventAddressPointer(event: NostrEvent): AddressPointer {
}
export function eTagToEventPointer(tag: ETag): EventPointer {
return { id: tag[1], relays: tag[2] ? [tag[2]] : [] };
return { id: tag[1], relays: tag[2] ? safeRelayUrls([tag[2]]) : [] };
}
export function aTagToAddressPointer(tag: ATag): AddressPointer {
const cord = parseCoordinate(tag[1], true, false);
if (tag[2]) cord.relays = [tag[2]];
if (tag[2]) cord.relays = safeRelayUrls([tag[2]]);
return cord;
}
export function addressPointerToATag(pointer: AddressPointer): ATag {

@ -26,7 +26,7 @@ export function mapQueryMap(queryMap: RelayQueryMap, fn: (filter: NostrRequestFi
return newMap;
}
export function createSimpleQueryMap(relays: string[], filter: NostrRequestFilter) {
export function createSimpleQueryMap(relays: Iterable<string>, filter: NostrRequestFilter) {
const map: RelayQueryMap = {};
for (const relay of relays) map[relay] = filter;
return map;

@ -1,7 +1,6 @@
import { SimpleRelay, SubscriptionOptions } from "nostr-idb";
import { Filter } from "nostr-tools";
import { RelayConfig } from "../classes/relay";
import { NostrQuery, NostrRequestFilter } from "../types/nostr-query";
import { NostrEvent } from "../types/nostr-event";
@ -55,18 +54,6 @@ export function safeRelayUrls(urls: string[]): string[] {
return urls.map(safeRelayUrl).filter(Boolean) as string[];
}
export function normalizeRelayConfigs(relays: RelayConfig[]) {
const seen: string[] = [];
return relays.reduce((newArr, r) => {
const safeUrl = safeRelayUrl(r.url);
if (safeUrl && !seen.includes(safeUrl)) {
seen.push(safeUrl);
newArr.push({ ...r, url: safeUrl });
}
return newArr;
}, [] as RelayConfig[]);
}
export function splitNostrFilterByPubkeys(
filter: NostrRequestFilter,
relayPubkeyMap: Record<string, string[]>,

@ -4,20 +4,40 @@ import { useToast } from "@chakra-ui/react";
import appSettings, { replaceSettings } from "../services/settings/app-settings";
import useSubject from "./use-subject";
import { AppSettings } from "../services/settings/migrations";
import useCurrentAccount from "./use-current-account";
import accountService from "../services/account";
import userAppSettings from "../services/settings/user-app-settings";
import { useSigningContext } from "../providers/global/signing-provider";
import NostrPublishAction from "../classes/nostr-publish-action";
import clientRelaysService from "../services/client-relays";
export default function useAppSettings() {
const account = useCurrentAccount();
const settings = useSubject(appSettings);
const { requestSignature } = useSigningContext();
const toast = useToast();
const updateSettings = useCallback(
(newSettings: Partial<AppSettings>) => {
async (newSettings: Partial<AppSettings>) => {
try {
if (!account) return;
const full: AppSettings = { ...settings, ...newSettings };
if (account.readonly) {
accountService.updateAccountLocalSettings(account.pubkey, full);
appSettings.next(full);
} else {
const draft = userAppSettings.buildAppSettingsEvent(full);
const signed = await requestSignature(draft);
userAppSettings.receiveEvent(signed);
new NostrPublishAction("Update Settings", clientRelaysService.outbox.urls, signed);
}
return replaceSettings({ ...settings, ...newSettings });
} catch (e) {
if (e instanceof Error) toast({ description: e.message, status: "error" });
}
},
[settings],
[settings, account, requestSignature],
);
return {

@ -8,14 +8,14 @@ import useSingleEvent from "./use-single-event";
export default function useChannelMetadata(
channelId: string | undefined,
relays: string[] = [],
relays: Iterable<string> = [],
opts: RequestOptions = {},
) {
const channel = useSingleEvent(channelId);
const sub = useMemo(() => {
if (!channelId) return;
return channelMetadataService.requestMetadata(relays, channelId, opts);
}, [channelId, relays.join("|"), opts?.alwaysRequest, opts?.ignoreCache]);
}, [channelId, Array.from(relays).join("|"), opts?.alwaysRequest, opts?.ignoreCache]);
const event = useSubject(sub);
const baseMetadata = useMemo(() => channel && safeParseChannelMetadata(channel), [channel]);

@ -1,28 +1,13 @@
import { unique } from "../helpers/array";
import clientRelaysService from "../services/client-relays";
import { RelayMode } from "../classes/relay";
import useSubject from "./use-subject";
export function useClientRelays(mode: RelayMode = RelayMode.ALL) {
const relays = useSubject(clientRelaysService.relays) ?? [];
return relays.filter((r) => r.mode & mode);
export function useReadRelayUrls(additional?: Iterable<string>) {
const set = useSubject(clientRelaysService.readRelays);
if (additional) return set.clone().merge(additional);
return set;
}
export function useReadRelayUrls(additional: string[] = []) {
const relays = useClientRelays(RelayMode.READ);
const urls = relays.map((r) => r.url);
if (additional.length > 0) {
return unique([...urls, ...additional]);
}
return urls;
}
export function useWriteRelayUrls(additional: string[] = []) {
const relays = useClientRelays(RelayMode.WRITE);
const urls = relays.map((r) => r.url);
if (additional) {
return unique([...urls, ...additional]);
}
return urls;
export function useWriteRelayUrls(additional?: Iterable<string>) {
const set = useSubject(clientRelaysService.writeRelays);
if (additional) return set.clone().merge(additional);
return set;
}

@ -28,7 +28,7 @@ export default function useEventBookmarkActions(event: NostrEvent) {
: eventPointers.some((p) => p.id === event.id);
const removeBookmark = useCallback(async () => {
const writeRelays = clientRelaysService.getWriteUrls();
const writeRelays = clientRelaysService.outbox.urls;
setLoading(true);
try {
@ -53,7 +53,7 @@ export default function useEventBookmarkActions(event: NostrEvent) {
}, [event, requestSignature, bookmarkList, isBookmarked]);
const addBookmark = useCallback(async () => {
const writeRelays = clientRelaysService.getWriteUrls();
const writeRelays = clientRelaysService.outbox.urls;
setLoading(true);
try {

@ -3,12 +3,12 @@ import eventReactionsService from "../services/event-reactions";
import { useReadRelayUrls } from "./use-client-relays";
import useSubject from "./use-subject";
export default function useEventReactions(eventId: string, additionalRelays: string[] = [], alwaysRequest = true) {
export default function useEventReactions(eventId: string, additionalRelays?: Iterable<string>, alwaysRequest = true) {
const relays = useReadRelayUrls(additionalRelays);
const subject = useMemo(
() => eventReactionsService.requestReactions(eventId, relays, alwaysRequest),
[eventId, relays.join("|"), alwaysRequest],
[eventId, relays.urls.join("|"), alwaysRequest],
);
return useSubject(subject);

@ -5,12 +5,12 @@ import { useReadRelayUrls } from "./use-client-relays";
import useSubject from "./use-subject";
import { parseZapEvent } from "../helpers/nostr/zaps";
export default function useEventZaps(eventUID: string, additionalRelays: string[] = [], alwaysRequest = true) {
const relays = useReadRelayUrls(additionalRelays);
export default function useEventZaps(eventUID: string, additionalRelays?: Iterable<string>, alwaysRequest = true) {
const readRelays = useReadRelayUrls(additionalRelays);
const subject = useMemo(
() => eventZapsService.requestZaps(eventUID, relays, alwaysRequest),
[eventUID, relays.join("|"), alwaysRequest],
() => eventZapsService.requestZaps(eventUID, readRelays, alwaysRequest),
[eventUID, readRelays.urls.join("|"), alwaysRequest],
);
const events = useSubject(subject) || [];

@ -4,17 +4,21 @@ import { useReadRelayUrls } from "./use-client-relays";
import { NostrEvent } from "../types/nostr-event";
import Subject from "../classes/subject";
export default function useEventsReactions(eventIds: string[], additionalRelays: string[] = [], alwaysRequest = true) {
const relays = useReadRelayUrls(additionalRelays);
export default function useEventsReactions(
eventIds: string[],
additionalRelays?: Iterable<string>,
alwaysRequest = true,
) {
const readRelays = useReadRelayUrls(additionalRelays);
// get subjects
const subjects = useMemo(() => {
const dir: Record<string, Subject<NostrEvent[]>> = {};
for (const eventId of eventIds) {
dir[eventId] = eventReactionsService.requestReactions(eventId, relays, alwaysRequest);
dir[eventId] = eventReactionsService.requestReactions(eventId, readRelays, alwaysRequest);
}
return dir;
}, [eventIds, relays.join("|"), alwaysRequest]);
}, [eventIds, readRelays.urls.join("|"), alwaysRequest]);
// get values out of subjects
const reactions: Record<string, NostrEvent[]> = {};

@ -7,7 +7,7 @@ export const FAVORITE_LISTS_IDENTIFIER = "nostrudel-favorite-lists";
export default function useFavoriteEmojiPacks(
pubkey?: string,
additionalRelays: string[] = [],
additionalRelays?: Iterable<string>,
opts: RequestOptions = {},
) {
const account = useCurrentAccount();

@ -7,7 +7,7 @@ import useSubject from "./use-subject";
export default function useReplaceableEvent(
cord: string | CustomAddressPointer | undefined,
additionalRelays: string[] = [],
additionalRelays?: Iterable<string>,
opts: RequestOptions = {},
) {
const readRelays = useReadRelayUrls(additionalRelays);
@ -21,7 +21,7 @@ export default function useReplaceableEvent(
parsed.identifier,
opts,
);
}, [cord, readRelays.join("|"), opts?.alwaysRequest, opts?.ignoreCache]);
}, [cord, readRelays.urls.join("|"), opts?.alwaysRequest, opts?.ignoreCache]);
return useSubject(sub);
}

@ -9,7 +9,7 @@ import useSubjects from "./use-subjects";
export default function useReplaceableEvents(
coordinates: string[] | CustomAddressPointer[] | undefined,
additionalRelays: string[] = [],
additionalRelays?: Iterable<string>,
opts: RequestOptions = {},
) {
const readRelays = useReadRelayUrls(additionalRelays);
@ -30,7 +30,7 @@ export default function useReplaceableEvents(
);
}
return subs;
}, [coordinates, readRelays.join("|")]);
}, [coordinates, readRelays.urls.join("|")]);
return useSubjects(subs);
}

@ -1,18 +1,16 @@
import { useMemo } from "react";
import { nip19 } from "nostr-tools";
import { RelayMode } from "../classes/relay";
import relayScoreboardService from "../services/relay-scoreboard";
import { useUserRelays } from "./use-user-relays";
import useUserMailboxes from "./use-user-mailboxes";
/** @deprecated */
export function useSharableProfileId(pubkey: string, relayCount = 2) {
const userRelays = useUserRelays(pubkey);
const mailboxes = useUserMailboxes(pubkey);
return useMemo(() => {
const writeUrls = userRelays.filter((r) => r.mode & RelayMode.WRITE).map((r) => r.url);
const ranked = relayScoreboardService.getRankedRelays(writeUrls);
const ranked = relayScoreboardService.getRankedRelays(mailboxes?.outbox.urls);
const onlyTwo = ranked.slice(0, relayCount);
return onlyTwo.length > 0 ? nip19.nprofileEncode({ pubkey, relays: onlyTwo }) : nip19.npubEncode(pubkey);
}, [userRelays]);
}, [mailboxes]);
}

@ -3,11 +3,11 @@ import { useReadRelayUrls } from "./use-client-relays";
import { useMemo } from "react";
import useSubject from "./use-subject";
export default function useSingleEvent(id?: string, additionalRelays: string[] = []) {
export default function useSingleEvent(id?: string, additionalRelays?: Iterable<string>) {
const readRelays = useReadRelayUrls(additionalRelays);
const subject = useMemo(() => {
if (id) return singleEventService.requestEvent(id, readRelays);
}, [id, readRelays.join("|")]);
}, [id, readRelays.urls.join("|")]);
return useSubject(subject);
}

@ -4,11 +4,11 @@ import singleEventService from "../services/single-event";
import { useReadRelayUrls } from "./use-client-relays";
import useSubjects from "./use-subjects";
export default function useSingleEvents(ids?: string[], additionalRelays: string[] = []) {
export default function useSingleEvents(ids?: string[], additionalRelays?: Iterable<string>) {
const readRelays = useReadRelayUrls(additionalRelays);
const subjects = useMemo(() => {
return ids?.map((id) => singleEventService.requestEvent(id, readRelays)) ?? [];
}, [ids, readRelays.join("|")]);
}, [ids, readRelays.urls.join("|")]);
return useSubjects(subjects);
}

@ -9,19 +9,19 @@ import useSingleEvent from "./use-single-event";
export default function useStreamGoal(stream: ParsedStream) {
const [goal, setGoal] = useState<NostrEvent>();
const relays = useReadRelayUrls(stream.relays);
const readRelays = useReadRelayUrls(stream.relays);
const streamGoal = useSingleEvent(stream.goal);
useEffect(() => {
if (!stream.goal) {
const request = new NostrRequest(relays);
const request = new NostrRequest(readRelays);
request.onEvent.subscribe((event) => {
setGoal(event);
});
request.start({ "#a": [getATag(stream)], kinds: [GOAL_KIND] });
}
}, [stream.identifier, stream.goal, relays.join("|")]);
}, [stream.identifier, stream.goal, readRelays.urls.join("|")]);
return streamGoal || goal;
}

@ -11,7 +11,7 @@ import { unique } from "../helpers/array";
export default function useThreadTimelineLoader(
focusedEvent: NostrEvent | undefined,
relays: string[],
relays: Iterable<string>,
kind: number = kinds.ShortTextNote,
) {
const refs = focusedEvent && getThreadReferences(focusedEvent);

@ -17,7 +17,7 @@ type Options = {
export default function useTimelineLoader(
key: string,
relays: string[],
relays: Iterable<string>,
query: NostrRequestFilter | undefined,
opts?: Options,
) {
@ -28,7 +28,7 @@ export default function useTimelineLoader(
timeline.setQueryMap(createSimpleQueryMap(relays, query));
timeline.open();
} else timeline.close();
}, [timeline, JSON.stringify(query), relays.join("|")]);
}, [timeline, JSON.stringify(query), Array.from(relays).join("|")]);
useEffect(() => {
timeline.setEventFilter(opts?.eventFilter);

@ -4,7 +4,7 @@ import { RequestOptions } from "../services/replaceable-event-requester";
import useCurrentAccount from "./use-current-account";
import useReplaceableEvent from "./use-replaceable-event";
export default function useUserCommunitiesList(pubkey?: string, relays: string[] = [], opts?: RequestOptions) {
export default function useUserCommunitiesList(pubkey?: string, relays?: Iterable<string>, opts?: RequestOptions) {
const account = useCurrentAccount();
const key = pubkey ?? account?.pubkey;

@ -4,7 +4,7 @@ import { RequestOptions } from "../services/replaceable-event-requester";
export default function useUserContactList(
pubkey?: string,
additionalRelays: string[] = [],
additionalRelays?: Iterable<string>,
opts: RequestOptions = {},
) {
return useReplaceableEvent(pubkey && { kind: kinds.Contacts, pubkey }, additionalRelays, opts);

@ -6,7 +6,7 @@ import useSubject from "./use-subject";
import useTimelineLoader from "./use-timeline-loader";
import { NostrEvent } from "../types/nostr-event";
export default function useUserLists(pubkey?: string, additionalRelays: string[] = []) {
export default function useUserLists(pubkey?: string, additionalRelays?: Iterable<string>) {
const readRelays = useReadRelayUrls(additionalRelays);
const eventFilter = useCallback((event: NostrEvent) => {
return !isJunkList(event);

@ -0,0 +1,18 @@
import RelaySet from "../classes/relay-set";
import { RequestOptions } from "../services/replaceable-event-requester";
import userMailboxesService from "../services/user-mailboxes";
import { useReadRelayUrls } from "./use-client-relays";
import useSubject from "./use-subject";
export default function useUserMailboxes(pubkey: string, opts?: RequestOptions) {
const readRelays = useReadRelayUrls();
const sub = userMailboxesService.requestMailboxes(pubkey, readRelays, opts);
const value = useSubject(sub);
return value;
}
export function useUserInbox(pubkey: string, opts?: RequestOptions) {
return useUserMailboxes(pubkey, opts)?.inbox ?? new RelaySet();
}
export function useUserOutbox(pubkey: string, opts?: RequestOptions) {
return useUserMailboxes(pubkey, opts)?.outbox ?? new RelaySet();
}

@ -5,7 +5,7 @@ import useSubject from "./use-subject";
import { RequestOptions } from "../services/replaceable-event-requester";
import { COMMON_CONTACT_RELAY } from "../const";
export function useUserMetadata(pubkey: string, additionalRelays: string[] = [], opts: RequestOptions = {}) {
export function useUserMetadata(pubkey: string, additionalRelays: Iterable<string> = [], opts: RequestOptions = {}) {
const relays = useReadRelayUrls([...additionalRelays, COMMON_CONTACT_RELAY]);
const subject = useMemo(() => userMetadataService.requestMetadata(pubkey, relays, opts), [pubkey, relays]);

@ -27,7 +27,7 @@ export default function useUserMuteActions(pubkey: string) {
draft = pruneExpiredPubkeys(draft);
const signed = await requestSignature(draft);
new NostrPublishAction("Mute", clientRelaysService.getWriteUrls(), signed);
new NostrPublishAction("Mute", clientRelaysService.outbox.urls, signed);
replaceableEventLoaderService.handleEvent(signed);
}, [requestSignature, muteList]);
const unmute = useAsyncErrorHandler(async () => {
@ -35,7 +35,7 @@ export default function useUserMuteActions(pubkey: string) {
draft = pruneExpiredPubkeys(draft);
const signed = await requestSignature(draft);
new NostrPublishAction("Unmute", clientRelaysService.getWriteUrls(), signed);
new NostrPublishAction("Unmute", clientRelaysService.outbox.urls, signed);
replaceableEventLoaderService.handleEvent(signed);
}, [requestSignature, muteList]);

@ -2,6 +2,10 @@ import useReplaceableEvent from "./use-replaceable-event";
import { MUTE_LIST_KIND } from "../helpers/nostr/lists";
import { RequestOptions } from "../services/replaceable-event-requester";
export default function useUserMuteList(pubkey?: string, additionalRelays: string[] = [], opts: RequestOptions = {}) {
export default function useUserMuteList(
pubkey?: string,
additionalRelays?: Iterable<string>,
opts: RequestOptions = {},
) {
return useReplaceableEvent(pubkey && { kind: MUTE_LIST_KIND, pubkey }, additionalRelays, opts);
}

@ -5,7 +5,11 @@ import { PEOPLE_LIST_KIND, getPubkeysFromList } from "../helpers/nostr/lists";
import useUserMuteList from "./use-user-mute-list";
import { RequestOptions } from "../services/replaceable-event-requester";
export default function useUserMuteLists(pubkey?: string, additionalRelays: string[] = [], opts: RequestOptions = {}) {
export default function useUserMuteLists(
pubkey?: string,
additionalRelays?: Iterable<string>,
opts: RequestOptions = {},
) {
const muteList = useUserMuteList(pubkey, additionalRelays, opts);
const altMuteList = useReplaceableEvent(
pubkey && { kind: PEOPLE_LIST_KIND, pubkey, identifier: "mute" },

@ -9,7 +9,7 @@ import useSubjects from "./use-subjects";
import userMetadataService from "../services/user-metadata";
import { Kind0ParsedContent } from "../helpers/user-metadata";
export function useUsersMetadata(pubkeys: string[], additionalRelays: string[] = []) {
export function useUsersMetadata(pubkeys: string[], additionalRelays?: Iterable<string>) {
const readRelays = useReadRelayUrls(additionalRelays);
const metadataSubjects = useMemo(() => {
return pubkeys.map((pubkey) => userMetadataService.requestMetadata(pubkey, readRelays));
@ -27,7 +27,7 @@ export function useUsersMetadata(pubkeys: string[], additionalRelays: string[] =
return metadataDir;
}
export default function useUserNetwork(pubkey: string, additionalRelays: string[] = []) {
export default function useUserNetwork(pubkey: string, additionalRelays?: Iterable<string>) {
const readRelays = useReadRelayUrls(additionalRelays);
const contacts = useUserContactList(pubkey);
const contactsPubkeys = contacts ? getPubkeysFromList(contacts) : [];
@ -36,7 +36,7 @@ export default function useUserNetwork(pubkey: string, additionalRelays: string[
return contactsPubkeys.map((person) =>
replaceableEventLoaderService.requestEvent(readRelays, kinds.Contacts, person.pubkey),
);
}, [contactsPubkeys, readRelays.join("|")]);
}, [contactsPubkeys, readRelays.urls.join("|")]);
const lists = useSubjects(subjects);
const metadata = useUsersMetadata(lists.map((list) => list.pubkey).concat(pubkey));
@ -44,7 +44,7 @@ export default function useUserNetwork(pubkey: string, additionalRelays: string[
return { lists, contacts, metadata };
}
export function useNetworkConnectionCount(pubkey: string, additionalRelays: string[] = []) {
export function useNetworkConnectionCount(pubkey: string, additionalRelays?: Iterable<string>) {
const { lists, contacts } = useUserNetwork(pubkey, additionalRelays);
const contactsPubkeys = contacts ? getPubkeysFromList(contacts) : [];

@ -7,7 +7,7 @@ import useSingleEvents from "./use-single-events";
import { getEventCoordinate } from "../helpers/nostr/events";
import { NostrEvent } from "../types/nostr-event";
export default function useUserProfileBadges(pubkey: string, additionalRelays: string[] = []) {
export default function useUserProfileBadges(pubkey: string, additionalRelays?: Iterable<string>) {
const profileBadgesEvent = useReplaceableEvent(
{
pubkey,

@ -1,17 +1,20 @@
import { useMemo } from "react";
import userRelaysService from "../services/user-relays";
import userMailboxesService from "../services/user-mailboxes";
import useSubject from "./use-subject";
import { useReadRelayUrls } from "./use-client-relays";
import { RequestOptions } from "../services/replaceable-event-requester";
import { COMMON_CONTACT_RELAY } from "../const";
import RelaySet from "../classes/relay-set";
export function useUserRelays(pubkey: string, additionalRelays: string[] = [], opts: RequestOptions = {}) {
/** @deprecated */
export function useUserRelays(pubkey: string, additionalRelays: Iterable<string> = [], opts: RequestOptions = {}) {
const readRelays = useReadRelayUrls([...additionalRelays, COMMON_CONTACT_RELAY]);
const subject = useMemo(
() => userRelaysService.requestRelays(pubkey, readRelays, opts),
[pubkey, readRelays.join("|")],
() => userMailboxesService.requestMailboxes(pubkey, readRelays, opts),
[pubkey, readRelays.urls.join("|")],
);
const userRelays = useSubject(subject);
return userRelays?.relays ?? [];
return userRelays?.relays || new RelaySet();
}

@ -22,8 +22,8 @@ export function useRelaySelectionRelays() {
}
export type RelaySelectionProviderProps = PropsWithChildren & {
overrideDefault?: string[];
additionalDefaults?: string[];
overrideDefault?: Iterable<string>;
additionalDefaults?: Iterable<string>;
};
export default function RelaySelectionProvider({
@ -34,13 +34,13 @@ export default function RelaySelectionProvider({
const navigate = useNavigate();
const location = useLocation();
const userReadRelays = useReadRelayUrls();
const readRelays = useReadRelayUrls();
const relays = useMemo(() => {
if (location.state?.relays) return location.state.relays;
if (overrideDefault) return overrideDefault;
if (additionalDefaults) return unique([...userReadRelays, ...additionalDefaults]);
return userReadRelays;
}, [location.state?.relays, overrideDefault, userReadRelays.join("|"), additionalDefaults]);
if (location.state?.relays) return location.state.relays as string[];
if (overrideDefault) return Array.from(overrideDefault);
if (additionalDefaults) return unique([...readRelays, ...additionalDefaults]);
return readRelays.urls;
}, [location.state?.relays, overrideDefault, readRelays.urls.join("|"), additionalDefaults]);
const setSelected = useCallback(
(relays: string[]) => {

@ -27,7 +27,6 @@ import useCurrentAccount from "../../hooks/use-current-account";
import signingService from "../../services/signing";
import createDefer, { Deferred } from "../../classes/deferred";
import useEventRelays from "../../hooks/use-event-relays";
import { useWriteRelayUrls } from "../../hooks/use-client-relays";
import { RelayFavicon } from "../../components/relay-favicon";
import { ExternalLinkIcon } from "../../components/icons";
import { getEventCoordinate, getEventUID, isReplaceable } from "../../helpers/nostr/events";
@ -35,6 +34,7 @@ import NostrPublishAction from "../../classes/nostr-publish-action";
import { Tag } from "../../types/nostr-event";
import deleteEventService from "../../services/delete-events";
import { EmbedEvent } from "../../components/embed-event";
import { useWriteRelayUrls } from "../../hooks/use-client-relays";
type DeleteEventContextType = {
isLoading: boolean;
@ -86,7 +86,7 @@ export default function DeleteEventProvider({ children }: PropsWithChildren) {
created_at: dayjs().unix(),
};
const signed = await signingService.requestSignature(draft, account);
const pub = new NostrPublishAction("Delete", writeRelays, signed);
new NostrPublishAction("Delete", writeRelays, signed);
deleteEventService.handleEvent(signed);
defer?.resolve();
} catch (e) {
@ -137,7 +137,7 @@ export default function DeleteEventProvider({ children }: PropsWithChildren) {
</AccordionButton>
<AccordionPanel>
<Flex wrap="wrap" gap="2" py="2">
{writeRelays.map((url) => (
{writeRelays.urls.map((url) => (
<Box alignItems="center" key={url} px="2" borderRadius="lg" display="flex" borderWidth="1px">
<RelayFavicon relay={url} size="2xs" mr="2" />
<Text isTruncated>{url}</Text>

@ -69,7 +69,7 @@ function MuteModal({ pubkey, onClose, ...props }: Omit<ModalProps, "children"> &
draft = muteListAddPubkey(draft, pubkey, expiration);
const signed = await requestSignature(draft);
new NostrPublishAction("Mute", clientRelaysService.getWriteUrls(), signed);
new NostrPublishAction("Mute", clientRelaysService.outbox.urls, signed);
replaceableEventLoaderService.handleEvent(signed);
onClose();
} catch (e) {
@ -139,7 +139,7 @@ function UnmuteHandler() {
draft = pruneExpiredPubkeys(draft);
const signed = await requestSignature(draft);
new NostrPublishAction("Unmute", clientRelaysService.getWriteUrls(), signed);
new NostrPublishAction("Unmute", clientRelaysService.outbox.urls, signed);
replaceableEventLoaderService.handleEvent(signed);
return true;
} catch (e) {
@ -186,7 +186,7 @@ function UnmuteModal({ onClose }: Omit<ModalProps, "children">) {
draft = pruneExpiredPubkeys(draft);
const signed = await requestSignature(draft);
new NostrPublishAction("Unmute", clientRelaysService.getWriteUrls(), signed);
new NostrPublishAction("Unmute", clientRelaysService.outbox.urls, signed);
replaceableEventLoaderService.handleEvent(signed);
onClose();
} catch (e) {
@ -204,7 +204,7 @@ function UnmuteModal({ onClose }: Omit<ModalProps, "children">) {
}
const signed = await requestSignature(draft);
new NostrPublishAction("Extend mute", clientRelaysService.getWriteUrls(), signed);
new NostrPublishAction("Extend mute", clientRelaysService.outbox.urls, signed);
replaceableEventLoaderService.handleEvent(signed);
onClose();
} catch (e) {
@ -219,7 +219,7 @@ function UnmuteModal({ onClose }: Omit<ModalProps, "children">) {
draft = muteListRemovePubkey(draft, pubkey);
const signed = await requestSignature(draft);
new NostrPublishAction("Unmute", clientRelaysService.getWriteUrls(), signed);
new NostrPublishAction("Unmute", clientRelaysService.outbox.urls, signed);
replaceableEventLoaderService.handleEvent(signed);
} catch (e) {
if (e instanceof Error) toast({ description: e.message, status: "error" });
@ -233,7 +233,7 @@ function UnmuteModal({ onClose }: Omit<ModalProps, "children">) {
draft = muteListAddPubkey(draft, pubkey, expiration);
const signed = await requestSignature(draft);
new NostrPublishAction("Extend mute", clientRelaysService.getWriteUrls(), signed);
new NostrPublishAction("Extend mute", clientRelaysService.outbox.urls, signed);
replaceableEventLoaderService.handleEvent(signed);
} catch (e) {
if (e instanceof Error) toast({ description: e.message, status: "error" });

@ -228,7 +228,7 @@ class ChannelMetadataService {
await transaction.commit();
}
private requestChannelMetadataFromRelays(relays: string[], channelId: string) {
private requestChannelMetadataFromRelays(relays: Iterable<string>, channelId: string) {
const sub = this.metadata.get(channelId);
const relayUrls = Array.from(relays);
@ -249,7 +249,7 @@ class ChannelMetadataService {
return sub;
}
requestMetadata(relays: string[], channelId: string, opts: RequestOptions = {}) {
requestMetadata(relays: Iterable<string>, channelId: string, opts: RequestOptions = {}) {
const sub = this.metadata.get(channelId);
if (!sub.value) {

@ -1,36 +1,24 @@
import dayjs from "dayjs";
import { unique } from "../helpers/array";
import { DraftNostrEvent, RTag } from "../types/nostr-event";
import accountService from "./account";
import { RelayConfig, RelayMode } from "../classes/relay";
import userRelaysService, { ParsedUserRelays } from "./user-relays";
import { Connection, PersistentSubject, Subject } from "../classes/subject";
import signingService from "./signing";
import { RelayMode } from "../classes/relay";
import userMailboxesService, { UserMailboxes } from "./user-mailboxes";
import { PersistentSubject, Subject } from "../classes/subject";
import { logger } from "../helpers/debug";
import NostrPublishAction from "../classes/nostr-publish-action";
import { COMMON_CONTACT_RELAY } from "../const";
import appSettings from "./settings/app-settings";
import RelaySet from "../classes/relay-set";
import { safeUrl } from "../helpers/parse";
export type RelayDirectory = Record<string, { read: boolean; write: boolean }>;
const DEFAULT_RELAYS = [
{ url: "wss://relay.damus.io", mode: RelayMode.READ },
{ url: "wss://nostr.wine", mode: RelayMode.READ },
{ url: "wss://relay.snort.social", mode: RelayMode.READ },
{ url: "wss://nos.lol", mode: RelayMode.READ },
{ url: "wss://purplerelay.com", mode: RelayMode.READ },
];
const userRelaysToRelayConfig: Connection<ParsedUserRelays, RelayConfig[], RelayConfig[] | undefined> = (
userRelays,
next,
) => next(userRelays.relays);
// const userRelaysToRelayConfig: Connection<ParsedUserRelays, RelayConfig[], RelayConfig[] | undefined> = (
// userRelays,
// next,
// ) => next(userRelays.relays);
class ClientRelayService {
// bootstrapRelays = new Set<string>();
relays = new PersistentSubject<RelayConfig[]>([]);
writeRelays = new PersistentSubject<RelayConfig[]>([]);
readRelays = new PersistentSubject<RelayConfig[]>([]);
readRelays = new PersistentSubject(new RelaySet());
writeRelays = new PersistentSubject(new RelaySet());
// outbox = new PersistentSubject(new RelaySet());
// inbox = new PersistentSubject(new RelaySet());
log = logger.extend("ClientRelays");
@ -38,127 +26,96 @@ class ClientRelayService {
accountService.loading.subscribe(this.handleAccountChange, this);
accountService.current.subscribe(this.handleAccountChange, this);
appSettings.subscribe(this.handleSettingsChange, this);
// set the read and write relays
this.relays.subscribe((relays) => {
this.log("Got new relay list", relays);
this.writeRelays.next(relays.filter((r) => r.mode & RelayMode.WRITE));
this.readRelays.next(relays.filter((r) => r.mode & RelayMode.READ));
});
// this.relays.subscribe((relays) => {
// this.log("Got new relay list", relays);
// this.outbox.next(relays.filter((r) => r.mode & RelayMode.WRITE));
// this.inbox.next(relays.filter((r) => r.mode & RelayMode.READ));
// });
}
private userRequestRelaySubject: Subject<ParsedUserRelays> | undefined;
addRelay(url: string, mode: RelayMode) {
if (mode & RelayMode.WRITE) this.writeRelays.next(this.writeRelays.value.clone().add(url));
if (mode & RelayMode.READ) this.readRelays.next(this.readRelays.value.clone().add(url));
}
removeRelay(url: string, mode: RelayMode) {
if (mode & RelayMode.WRITE) {
const next = this.writeRelays.value.clone();
next.delete(url);
this.writeRelays.next(next);
}
if (mode & RelayMode.READ) {
const next = this.readRelays.value.clone();
next.delete(url);
this.readRelays.next(next);
}
}
private handleSettingsChange() {
this.readRelays.next(RelaySet.from(appSettings.value.defaultRelays));
this.writeRelays.next(new RelaySet());
}
private userRelaySub: Subject<UserMailboxes> | undefined;
private handleAccountChange() {
// skip if account is loading
if (accountService.loading.value) return;
// disconnect the relay list subject
if (this.userRequestRelaySubject) {
this.relays.disconnect(this.userRequestRelaySubject);
this.userRequestRelaySubject = undefined;
}
// if (this.userRelaySub) {
// this.relays.disconnect(this.userRelaySub);
// this.userRelaySub.unsubscribe(this.handleUserRelays, this);
// this.userRelaySub = undefined;
// }
const account = accountService.current.value;
if (!account) {
this.log("No account, using default relays");
this.relays.next(DEFAULT_RELAYS);
return;
}
// clear relays
this.relays.next([]);
if (!account) return;
// connect the relay subject with the account relay subject
this.userRequestRelaySubject = userRelaysService.getRelays(account.pubkey);
this.relays.connectWithHandler(this.userRequestRelaySubject, userRelaysToRelayConfig);
// this.userRelaySub = userMailboxesService.requestMailboxes(account.pubkey, [COMMON_CONTACT_RELAY]);
// this.userRelaySub.subscribe(this.handleUserRelays, this);
// this.relays.connectWithHandler(this.userRelaySub, userRelaysToRelayConfig);
// load the relays from cache
if (!userRelaysService.getRelays(account.pubkey).value) {
this.log("Load users relay list from cache");
userRelaysService.loadFromCache(account.pubkey).then(() => {
if (this.relays.value.length === 0) {
const bootstrapRelays = account.relays ?? [COMMON_CONTACT_RELAY];
// if (!userRelaysService.getRelays(account.pubkey).value) {
// this.log("Load users relay list from cache");
// userRelaysService.loadFromCache(account.pubkey).then(() => {
// if (this.relays.value.length === 0) {
// const bootstrapRelays = account.relays ?? [COMMON_CONTACT_RELAY];
this.log("Loading relay list from bootstrap relays", bootstrapRelays);
userRelaysService.requestRelays(account.pubkey, bootstrapRelays, { alwaysRequest: true });
}
});
}
// this.log("Loading relay list from bootstrap relays", bootstrapRelays);
// userRelaysService.requestRelays(account.pubkey, bootstrapRelays, { alwaysRequest: true });
// }
// });
// }
// double check for new relay notes
setTimeout(() => {
if (this.relays.value.length === 0) return;
// setTimeout(() => {
// if (this.relays.value.length === 0) return;
this.log("Requesting latest relay list from relays");
userRelaysService.requestRelays(account.pubkey, this.getWriteUrls(), { alwaysRequest: true });
}, 5000);
// this.log("Requesting latest relay list from relays");
// userRelaysService.requestRelays(account.pubkey, this.getOutboxURLs(), { alwaysRequest: true });
// }, 5000);
}
/** @deprecated */
async addRelay(url: string, mode: RelayMode) {
this.log(`Adding ${url} relay`);
if (!this.relays.value.some((r) => r.url === url)) {
const newRelays = [...this.relays.value, { url, mode }];
await this.postUpdatedRelays(newRelays);
}
// private handleUserRelays(userRelays: UserMailboxes) {
// if (userRelays.pubkey === accountService.current.value?.pubkey) {
// this.inbox.next(userRelays.mailboxes.filter(RelayMode.READ));
// this.outbox.next(userRelays.mailboxes.filter(RelayMode.WRITE));
// }
// }
get outbox() {
const account = accountService.current.value;
if (account) return userMailboxesService.getMailboxes(account.pubkey).value?.outbox ?? this.writeRelays.value;
return this.writeRelays.value;
}
/** @deprecated */
async updateRelay(url: string, mode: RelayMode) {
this.log(`Updating ${url} relay`);
if (this.relays.value.some((r) => r.url === url)) {
const newRelays = this.relays.value.map((r) => (r.url === url ? { url, mode } : r));
await this.postUpdatedRelays(newRelays);
}
}
/** @deprecated */
async removeRelay(url: string) {
this.log(`Removing ${url} relay`);
if (this.relays.value.some((r) => r.url === url)) {
const newRelays = this.relays.value.filter((r) => r.url !== url);
await this.postUpdatedRelays(newRelays);
}
}
/** @deprecated */
async postUpdatedRelays(newRelays: RelayConfig[]) {
const rTags: RTag[] = newRelays.map((r) => {
switch (r.mode) {
case RelayMode.READ:
return ["r", r.url, "read"];
case RelayMode.WRITE:
return ["r", r.url, "write"];
case RelayMode.ALL:
default:
return ["r", r.url];
}
});
const draft: DraftNostrEvent = {
kind: 10002,
tags: rTags,
content: "",
created_at: dayjs().unix(),
};
const newRelayUrls = newRelays.filter((r) => r.mode & RelayMode.WRITE).map((r) => r.url);
const oldRelayUrls = this.relays.value.filter((r) => r.mode & RelayMode.WRITE).map((r) => r.url);
const writeUrls = unique([...oldRelayUrls, ...newRelayUrls, COMMON_CONTACT_RELAY]);
const current = accountService.current.value;
if (!current) throw new Error("no account");
const signed = await signingService.requestSignature(draft, current);
const pub = new NostrPublishAction("Update Relays", writeUrls, signed);
// pass new event to the user relay service
userRelaysService.receiveEvent(signed);
await pub.onComplete;
}
getWriteUrls() {
return this.relays.value?.filter((r) => r.mode & RelayMode.WRITE).map((r) => r.url);
}
getReadUrls() {
return this.relays.value?.filter((r) => r.mode & RelayMode.READ).map((r) => r.url);
get inbox() {
const account = accountService.current.value;
if (account) return userMailboxesService.getMailboxes(account.pubkey).value?.inbox ?? this.readRelays.value;
return this.readRelays.value;
}
}

@ -12,7 +12,7 @@ class EventReactionsService {
subjects = new SuperMap<eventId, Subject<NostrEvent[]>>(() => new Subject<NostrEvent[]>([]));
pending = new SuperMap<eventId, Set<relay>>(() => new Set());
requestReactions(eventId: string, relays: relay[], alwaysRequest = true) {
requestReactions(eventId: string, relays: Iterable<string>, alwaysRequest = true) {
const subject = this.subjects.get(eventId);
if (!subject.value || alwaysRequest) {

@ -14,7 +14,7 @@ class EventZapsService {
subjects = new SuperMap<eventUID, Subject<NostrEvent[]>>(() => new Subject<NostrEvent[]>([]));
pending = new SuperMap<eventUID, Set<relay>>(() => new Set());
requestZaps(eventUID: eventUID, relays: relay[], alwaysRequest = true) {
requestZaps(eventUID: eventUID, relays: Iterable<string>, alwaysRequest = true) {
const subject = this.subjects.get(eventUID);
if (!subject.value || alwaysRequest) {

@ -201,8 +201,8 @@ class RelayScoreboardService {
return score;
}
getRankedRelays(customRelays?: string[]) {
const relays = customRelays ?? this.getRelays();
getRankedRelays(urls?: Iterable<string>) {
const relays = (urls && Array.from(urls)) ?? this.getRelays();
const relayScores = new Map<string, number>();
for (const relay of relays) {

@ -243,7 +243,7 @@ class ReplaceableEventLoaderService {
this.writeToCacheThrottle();
}
private requestEventFromRelays(relays: string[], kind: number, pubkey: string, d?: string) {
private requestEventFromRelays(relays: Iterable<string>, kind: number, pubkey: string, d?: string) {
const cord = createCoordinate(kind, pubkey, d);
const sub = this.events.get(cord);
@ -261,7 +261,7 @@ class ReplaceableEventLoaderService {
return sub;
}
requestEvent(relays: string[], kind: number, pubkey: string, d?: string, opts: RequestOptions = {}) {
requestEvent(relays: Iterable<string>, kind: number, pubkey: string, d?: string, opts: RequestOptions = {}) {
const cord = createCoordinate(kind, pubkey, d);
const sub = this.events.get(cord);

@ -25,7 +25,7 @@ export async function replaceSettings(newSettings: AppSettings) {
const draft = userAppSettings.buildAppSettingsEvent(newSettings);
const signed = await signingService.requestSignature(draft, account);
userAppSettings.receiveEvent(signed);
new NostrPublishAction("Update Settings", clientRelaysService.getWriteUrls(), signed);
new NostrPublishAction("Update Settings", clientRelaysService.outbox.urls, signed);
}
}
@ -44,20 +44,20 @@ accountService.current.subscribe(() => {
log("Loaded user settings from local storage");
}
const subject = userAppSettings.requestAppSettings(account.pubkey, clientRelaysService.getReadUrls(), {
const subject = userAppSettings.requestAppSettings(account.pubkey, clientRelaysService.inbox.urls, {
alwaysRequest: true,
});
appSettings.next(defaultSettings);
appSettings.connect(subject);
});
clientRelaysService.relays.subscribe(() => {
// relays changed, look for settings again
const account = accountService.current.value;
// clientRelaysService.relays.subscribe(() => {
// // relays changed, look for settings again
// const account = accountService.current.value;
if (account) {
userAppSettings.requestAppSettings(account.pubkey, clientRelaysService.getReadUrls(), { alwaysRequest: true });
}
});
// if (account) {
// userAppSettings.requestAppSettings(account.pubkey, clientRelaysService.getInboxURLs(), { alwaysRequest: true });
// }
// });
export default appSettings;

@ -5,6 +5,7 @@ import { safeJson } from "../../helpers/parse";
export type AppSettingsV0 = {
version: 0;
colorMode: ColorModeWithSystem;
defaultRelays: string[];
blurImages: boolean;
autoShowMedia: boolean;
proxyUserMedia: boolean;
@ -32,6 +33,7 @@ export type AppSettingsV3 = Omit<AppSettingsV2, "version"> & { version: 3; quick
export type AppSettingsV4 = Omit<AppSettingsV3, "version"> & { version: 4; loadOpenGraphData: boolean };
export type AppSettingsV5 = Omit<AppSettingsV4, "version"> & { version: 5; hideUsernames: boolean };
export type AppSettingsV6 = Omit<AppSettingsV5, "version"> & { version: 6; noteDifficulty: number | null };
export type AppSettingsV7 = Omit<AppSettingsV6, "version"> & { version: 7; defaultRelays: string[] };
export function isV0(settings: { version: number }): settings is AppSettingsV0 {
return settings.version === undefined || settings.version === 0;
@ -61,9 +63,9 @@ export const defaultSettings: AppSettings = {
version: 6,
theme: "default",
colorMode: "system",
defaultRelays: ["wss://relay.damus.io", "wss://nostr.wine", "wss://nos.lol", "wss://welcome.nostr.wine"],
maxPageWidth: "none",
blurImages: true,
// nostr:nevent1qqsxvkjgpc6zhydj4rxjpl0frev7hmgynruq027mujdgy2hwjypaqfspzpmhxue69uhkummnw3ezuamfdejszythwden5te0dehhxarjw4jjucm0d5sfntd0
hideUsernames: false,
autoShowMedia: true,
proxyUserMedia: false,

@ -5,7 +5,7 @@ import Subject from "../classes/subject";
import SuperMap from "../classes/super-map";
import { NostrEvent } from "../types/nostr-event";
import { localRelay } from "./local-relay";
import { relayRequest, safeRelayUrls } from "../helpers/relay";
import { relayRequest } from "../helpers/relay";
import { logger } from "../helpers/debug";
const RELAY_REQUEST_BATCH_TIME = 500;
@ -15,12 +15,11 @@ class SingleEventService {
pending = new Map<string, string[]>();
log = logger.extend("SingleEvent");
requestEvent(id: string, relays: string[]) {
requestEvent(id: string, relays: Iterable<string>) {
const subject = this.cache.get(id);
if (subject.value) return subject;
relays = safeRelayUrls(relays);
this.pending.set(id, this.pending.get(id)?.concat(relays) ?? relays);
this.pending.set(id, this.pending.get(id)?.concat(Array.from(relays)) ?? Array.from(relays));
this.batchRequestsThrottle();
return subject;

@ -4,32 +4,35 @@ import { isPTag, NostrEvent } from "../types/nostr-event";
import { safeJson } from "../helpers/parse";
import SuperMap from "../classes/super-map";
import Subject from "../classes/subject";
import { RelayConfig, RelayMode } from "../classes/relay";
import { normalizeRelayConfigs } from "../helpers/relay";
import replaceableEventLoaderService, { RequestOptions } from "./replaceable-event-requester";
import RelaySet from "../classes/relay-set";
export type UserContacts = {
pubkey: string;
relays: RelayConfig[];
relays: RelaySet;
inbox: RelaySet;
outbox: RelaySet;
contacts: string[];
contactRelay: Record<string, string | undefined>;
created_at: number;
};
type RelayJson = Record<string, { read: boolean; write: boolean }>;
function relayJsonToRelayConfig(relayJson: RelayJson): RelayConfig[] {
try {
return Array.from(Object.entries(relayJson)).map(([url, opts]) => ({
url,
mode: (opts.write ? RelayMode.WRITE : 0) | (opts.read ? RelayMode.READ : 0),
}));
} catch (e) {}
return [];
function relayJsonToMailboxes(relayJson: RelayJson) {
const relays = new RelaySet();
const inbox = new RelaySet();
const outbox = new RelaySet();
for (const [url, opts] of Object.entries(relayJson)) {
relays.add(url);
if (opts.write) outbox.add(url);
if (opts.read) inbox.add(url);
}
return { relays, inbox, outbox };
}
function parseContacts(event: NostrEvent): UserContacts {
const relayJson = safeJson(event.content, {}) as RelayJson;
const relays = normalizeRelayConfigs(relayJsonToRelayConfig(relayJson));
const { relays, inbox, outbox } = relayJsonToMailboxes(relayJson);
const pubkeys = event.tags.filter(isPTag).map((tag) => tag[1]);
const contactRelay = event.tags.filter(isPTag).reduce(
@ -45,6 +48,8 @@ function parseContacts(event: NostrEvent): UserContacts {
return {
pubkey: event.pubkey,
relays,
inbox,
outbox,
contacts: pubkeys,
contactRelay,
created_at: event.created_at,
@ -56,7 +61,7 @@ class UserContactsService {
getSubject(pubkey: string) {
return this.subjects.get(pubkey);
}
requestContacts(pubkey: string, relays: string[], opts?: RequestOptions) {
requestContacts(pubkey: string, relays: Iterable<string>, opts?: RequestOptions) {
const sub = this.subjects.get(pubkey);
const requestSub = replaceableEventLoaderService.requestEvent(relays, kinds.Contacts, pubkey, undefined, opts);

@ -1,44 +1,53 @@
import { kinds } from "nostr-tools";
import { isRTag, NostrEvent } from "../types/nostr-event";
import { RelayConfig } from "../classes/relay";
import { parseRTag } from "../helpers/nostr/events";
import { NostrEvent } from "../types/nostr-event";
import SuperMap from "../classes/super-map";
import Subject from "../classes/subject";
import { normalizeRelayConfigs } from "../helpers/relay";
import userContactsService from "./user-contacts";
import replaceableEventLoaderService, { createCoordinate, RequestOptions } from "./replaceable-event-requester";
import RelaySet from "../classes/relay-set";
import { RelayMode } from "../classes/relay";
export type ParsedUserRelays = {
export type UserMailboxes = {
pubkey: string;
relays: RelayConfig[];
relays: RelaySet;
inbox: RelaySet;
outbox: RelaySet;
created_at: number;
};
function parseRelaysEvent(event: NostrEvent): ParsedUserRelays {
function nip65ToUserMailboxes(event: NostrEvent): UserMailboxes {
return {
pubkey: event.pubkey,
relays: normalizeRelayConfigs(event.tags.filter(isRTag).map(parseRTag)),
relays: RelaySet.fromNIP65Event(event),
inbox: RelaySet.fromNIP65Event(event, RelayMode.READ),
outbox: RelaySet.fromNIP65Event(event, RelayMode.WRITE),
created_at: event.created_at,
};
}
class UserRelaysService {
private subjects = new SuperMap<string, Subject<ParsedUserRelays>>(() => new Subject<ParsedUserRelays>());
getRelays(pubkey: string) {
class UserMailboxesService {
private subjects = new SuperMap<string, Subject<UserMailboxes>>(() => new Subject<UserMailboxes>());
getMailboxes(pubkey: string) {
return this.subjects.get(pubkey);
}
requestRelays(pubkey: string, relays: string[], opts: RequestOptions = {}) {
requestMailboxes(pubkey: string, relays: Iterable<string>, opts: RequestOptions = {}) {
const sub = this.subjects.get(pubkey);
const requestSub = replaceableEventLoaderService.requestEvent(relays, kinds.RelayList, pubkey, undefined, opts);
sub.connectWithHandler(requestSub, (event, next) => next(parseRelaysEvent(event)));
sub.connectWithHandler(requestSub, (event, next) => next(nip65ToUserMailboxes(event)));
// also fetch the relays from the users contacts
const contactsSub = userContactsService.requestContacts(pubkey, relays, opts);
sub.connectWithHandler(contactsSub, (contacts, next, value) => {
// NOTE: only use relays from contact list if the user dose not have a NIP-65 relay list
if (contacts.relays.length > 0 && !value) {
next({ pubkey: contacts.pubkey, relays: contacts.relays, created_at: contacts.created_at });
if (contacts.relays.size > 0 && !value) {
next({
pubkey: contacts.pubkey,
relays: contacts.relays,
inbox: contacts.inbox,
outbox: contacts.outbox,
created_at: contacts.created_at,
});
}
});
@ -52,7 +61,7 @@ class UserRelaysService {
await replaceableEventLoaderService.loadFromCache(createCoordinate(kinds.RelayList, pubkey));
const requestSub = replaceableEventLoaderService.getEvent(kinds.RelayList, pubkey);
sub.connectWithHandler(requestSub, (event, next) => next(parseRelaysEvent(event)));
sub.connectWithHandler(requestSub, (event, next) => next(nip65ToUserMailboxes(event)));
}
receiveEvent(event: NostrEvent) {
@ -60,11 +69,11 @@ class UserRelaysService {
}
}
const userRelaysService = new UserRelaysService();
const userMailboxesService = new UserMailboxesService();
if (import.meta.env.DEV) {
// @ts-ignore
window.userRelaysService = userRelaysService;
window.userMailboxesService = userMailboxesService;
}
export default userRelaysService;
export default userMailboxesService;

@ -24,7 +24,7 @@ class UserMetadataService {
getSubject(pubkey: string) {
return this.parsedSubjects.get(pubkey);
}
requestMetadata(pubkey: string, relays: string[], opts: RequestOptions = {}) {
requestMetadata(pubkey: string, relays: Iterable<string>, opts: RequestOptions = {}) {
const sub = this.parsedSubjects.get(pubkey);
const requestSub = replaceableEventLoaderService.requestEvent(relays, kinds.Metadata, pubkey, undefined, opts);
sub.connectWithHandler(requestSub, (event, next) => next(parseKind0Event(event)));

@ -39,11 +39,7 @@ export default function ChannelJoinButton({
const signed = await requestSignature(draft);
new NostrPublishAction(
isSubscribed ? "Leave Channel" : "Join Channel",
clientRelaysService.getWriteUrls(),
signed,
);
new NostrPublishAction(isSubscribed ? "Leave Channel" : "Join Channel", clientRelaysService.outbox.urls, signed);
} catch (e) {
if (e instanceof Error) toast({ description: e.message, status: "error" });
}

@ -56,7 +56,7 @@ export default function ChannelMessageForm({
setLoadingMessage("Signing...");
const signed = await requestSignature(draft);
const writeRelays = clientRelaysService.getWriteUrls();
const writeRelays = clientRelaysService.outbox.urls;
new NostrPublishAction("Send DM", writeRelays, signed);
reset();

@ -43,7 +43,7 @@ export default function CommunityJoinButton({
const signed = await requestSignature(draft);
new NostrPublishAction(isSubscribed ? "Unsubscribe" : "Subscribe", clientRelaysService.getWriteUrls(), signed);
new NostrPublishAction(isSubscribed ? "Unsubscribe" : "Subscribe", clientRelaysService.outbox.urls, signed);
} catch (e) {
if (e instanceof Error) toast({ description: e.message, status: "error" });
}

@ -17,7 +17,7 @@ import { parseCoordinate } from "../../helpers/nostr/events";
import UserAvatarLink from "../../components/user-avatar-link";
import { AddressPointer } from "nostr-tools/lib/types/nip19";
export function useUsersJoinedCommunitiesLists(pubkeys: string[], additionalRelays: string[] = []) {
export function useUsersJoinedCommunitiesLists(pubkeys: string[], additionalRelays?: Iterable<string>) {
const readRelays = useReadRelayUrls(additionalRelays);
const communityListsSubjects = useMemo(() => {
return pubkeys.map((pubkey) =>

@ -38,7 +38,6 @@ import {
getCommunityName,
} from "../../helpers/nostr/communities";
import NostrPublishAction from "../../classes/nostr-publish-action";
import { unique } from "../../helpers/array";
import clientRelaysService from "../../services/client-relays";
import replaceableEventLoaderService, { createCoordinate } from "../../services/replaceable-event-requester";
import { getImageSize } from "../../helpers/image";
@ -52,6 +51,7 @@ import { getEventCoordinate, sortByDate } from "../../helpers/nostr/events";
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
import ApprovedEvent from "../community/components/community-approved-post";
import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status";
import RelaySet from "../../classes/relay-set";
function CommunitiesHomePage() {
const toast = useToast();
@ -92,7 +92,7 @@ function CommunitiesHomePage() {
const signed = await requestSignature(draft);
new NostrPublishAction(
"Create Community",
unique([...clientRelaysService.getWriteUrls(), ...values.relays]),
RelaySet.from(clientRelaysService.writeRelays.value, values.relays),
signed,
);

@ -71,7 +71,7 @@ export default function CommunityEditModal({
const signed = await requestSignature(draft);
new NostrPublishAction(
"Update Community",
unique([...clientRelaysService.getWriteUrls(), ...values.relays]),
unique([...clientRelaysService.outbox.urls, ...values.relays]),
signed,
);

@ -36,7 +36,7 @@ export default function PostVoteButtons({ event, ...props }: Omit<CardProps, "ch
const signed = await requestSignature(draft);
if (signed) {
const writeRelays = clientRelaysService.getWriteUrls();
const writeRelays = clientRelaysService.outbox.urls;
new NostrPublishAction("Reaction", unique([...writeRelays, ...additionalRelays]), signed);
eventReactionsService.handleEvent(signed);
}

@ -21,7 +21,7 @@ function useCommunityPointer() {
export default function CommunityView() {
const pointer = useCommunityPointer();
const community = useReplaceableEvent(pointer, [], { alwaysRequest: true });
const community = useReplaceableEvent(pointer, undefined, { alwaysRequest: true });
if (!community) return <Spinner />;

@ -21,10 +21,10 @@ import { CheckIcon } from "../../../components/icons";
import { useSigningContext } from "../../../providers/global/signing-provider";
import useCurrentAccount from "../../../hooks/use-current-account";
import NostrPublishAction from "../../../classes/nostr-publish-action";
import { useWriteRelayUrls } from "../../../hooks/use-client-relays";
import CommunityPost from "../components/community-post";
import { RouterContext } from "../community-home";
import useUserMuteFilter from "../../../hooks/use-user-mute-filter";
import { useWriteRelayUrls } from "../../../hooks/use-client-relays";
type PendingProps = {
event: NostrEvent;

@ -10,7 +10,6 @@ import useSubject from "../../hooks/use-subject";
import RequireCurrentAccount from "../../providers/route/require-current-account";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import useCurrentAccount from "../../hooks/use-current-account";
import { useReadRelayUrls } from "../../hooks/use-client-relays";
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status";
@ -24,6 +23,7 @@ import { useRouterMarker } from "../../providers/drawer-sub-view-provider";
import TimelineLoader from "../../classes/timeline-loader";
import DirectMessageBlock from "./components/direct-message-block";
import useParamsProfilePointer from "../../hooks/use-params-pubkey-pointer";
import { useUserInbox } from "../../hooks/use-user-mailboxes";
/** This is broken out from DirectMessageChatPage for performance reasons. Don't use outside of file */
const ChatLog = memo(({ timeline }: { timeline: TimelineLoader }) => {
@ -70,8 +70,8 @@ function DirectMessageChatPage({ pubkey }: { pubkey: string }) {
marker.reset();
}, [marker, navigate]);
const myInbox = useReadRelayUrls();
const timeline = useTimelineLoader(`${pubkey}-${account.pubkey}-messages`, myInbox, [
const readRelays = useUserInbox(account.pubkey);
const timeline = useTimelineLoader(`${pubkey}-${account.pubkey}-messages`, readRelays, [
{
kinds: [kinds.EncryptedDirectMessage],
"#p": [account.pubkey],

@ -8,12 +8,14 @@ import { useSigningContext } from "../../../providers/global/signing-provider";
import MagicTextArea, { RefType } from "../../../components/magic-textarea";
import { useTextAreaUploadFileWithForm } from "../../../hooks/use-textarea-upload-file";
import clientRelaysService from "../../../services/client-relays";
import { unique } from "../../../helpers/array";
import { DraftNostrEvent } from "../../../types/nostr-event";
import NostrPublishAction from "../../../classes/nostr-publish-action";
import { useUserRelays } from "../../../hooks/use-user-relays";
import { RelayMode } from "../../../classes/relay";
import { useDecryptionContext } from "../../../providers/global/dycryption-provider";
import useUserMailboxes from "../../../hooks/use-user-mailboxes";
import useCurrentAccount from "../../../hooks/use-current-account";
import userMailboxesService from "../../../services/user-mailboxes";
import accountService from "../../../services/account";
import RelaySet from "../../../classes/relay-set";
export default function SendMessageForm({
pubkey,
@ -37,9 +39,7 @@ export default function SendMessageForm({
const textAreaRef = useRef<HTMLTextAreaElement | null>(null);
const { onPaste } = useTextAreaUploadFileWithForm(autocompleteRef, getValues, setValue);
const usersInbox = useUserRelays(pubkey)
.filter((r) => r.mode & RelayMode.READ)
.map((r) => r.url);
const userMailboxes = useUserMailboxes(pubkey);
const sendMessage = handleSubmit(async (values) => {
try {
if (!values.content) return;
@ -59,8 +59,8 @@ export default function SendMessageForm({
setLoadingMessage("Signing...");
const signed = await requestSignature(event);
const writeRelays = clientRelaysService.getWriteUrls();
const relays = unique([...writeRelays, ...usersInbox]);
const relays = RelaySet.from(clientRelaysService.outbox);
if (userMailboxes?.inbox) relays.merge(userMailboxes.inbox);
new NostrPublishAction("Send DM", relays, signed);
reset();

@ -8,7 +8,6 @@ import {
Card,
CardBody,
CardHeader,
CloseButton,
Code,
Heading,
Spinner,
@ -27,21 +26,18 @@ import { InlineInvoiceCard } from "../../../components/inline-invoice-card";
import NostrPublishAction from "../../../classes/nostr-publish-action";
import { useSigningContext } from "../../../providers/global/signing-provider";
import { DraftNostrEvent } from "../../../types/nostr-event";
import { unique } from "../../../helpers/array";
import clientRelaysService from "../../../services/client-relays";
import { useUserRelays } from "../../../hooks/use-user-relays";
import { useReadRelayUrls } from "../../../hooks/use-client-relays";
import { RelayMode } from "../../../classes/relay";
import { DVMAvatarLink } from "./dvm-avatar";
import DVMLink from "./dvm-name";
import { AddressPointer } from "nostr-tools/lib/types/nip19";
import useUserMailboxes from "../../../hooks/use-user-mailboxes";
import RelaySet from "../../../classes/relay-set";
function NextPageButton({ chain, pointer }: { pointer: AddressPointer; chain: ChainedDVMJob[] }) {
const toast = useToast();
const { requestSignature } = useSigningContext();
const dvmRelays = useUserRelays(pointer.pubkey)
.filter((r) => r.mode & RelayMode.READ)
.map((r) => r.url);
const dvmRelays = useUserMailboxes(pointer.pubkey)?.relays;
const readRelays = useReadRelayUrls();
const lastJob = chain[chain.length - 1];
@ -60,7 +56,7 @@ function NextPageButton({ chain, pointer }: { pointer: AddressPointer; chain: Ch
};
const signed = await requestSignature(draft);
new NostrPublishAction("Next Page", unique([...clientRelaysService.getWriteUrls(), ...dvmRelays]), signed);
new NostrPublishAction("Next Page", RelaySet.from(clientRelaysService.outbox, dvmRelays), signed);
} catch (e) {
if (e instanceof Error) toast({ status: "error", description: e.message });
}

@ -31,7 +31,6 @@ import VerticalPageLayout from "../../components/vertical-page-layout";
import useSubject from "../../hooks/use-subject";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import { useReadRelayUrls } from "../../hooks/use-client-relays";
import { useUserRelays } from "../../hooks/use-user-relays";
import { useSigningContext } from "../../providers/global/signing-provider";
import useCurrentAccount from "../../hooks/use-current-account";
import RequireCurrentAccount from "../../providers/route/require-current-account";
@ -41,8 +40,9 @@ import DebugChains from "./components/debug-chains";
import Feed from "./components/feed";
import { AddressPointer } from "nostr-tools/lib/types/nip19";
import useParamsAddressPointer from "../../hooks/use-params-address-pointer";
import useDVMMetadata from "../../hooks/use-dvm-metadata";
import DVMParams from "./components/dvm-params";
import useUserMailboxes from "../../hooks/use-user-mailboxes";
import RelaySet from "../../classes/relay-set";
function DVMFeedPage({ pointer }: { pointer: AddressPointer }) {
const [since] = useState(() => dayjs().subtract(1, "hour").unix());
@ -51,7 +51,7 @@ function DVMFeedPage({ pointer }: { pointer: AddressPointer }) {
const account = useCurrentAccount()!;
const debugModal = useDisclosure();
const dvmRelays = useUserRelays(pointer.pubkey).map((r) => r.url);
const dvmRelays = useUserMailboxes(pointer.pubkey)?.relays;
const readRelays = useReadRelayUrls(dvmRelays);
const timeline = useTimelineLoader(`${pointer.kind}:${pointer.pubkey}:${pointer.identifier}-jobs`, readRelays, [
{ authors: [account.pubkey], "#p": [pointer.pubkey], kinds: [DVM_CONTENT_DISCOVERY_JOB_KIND], since },
@ -89,7 +89,7 @@ function DVMFeedPage({ pointer }: { pointer: AddressPointer }) {
};
const signed = await requestSignature(draft);
new NostrPublishAction("Request Feed", unique([...clientRelaysService.getWriteUrls(), ...dvmRelays]), signed);
new NostrPublishAction("Request Feed", RelaySet.from(clientRelaysService.outbox, dvmRelays), signed);
} catch (e) {
if (e instanceof Error) toast({ status: "error", description: e.message });
}

@ -45,7 +45,7 @@ export default function EmojiPackCreateModal({ onClose, ...props }: Omit<ModalPr
try {
const signed = await requestSignature(draft);
const pub = new NostrPublishAction("Create emoji pack", clientRelaysService.getWriteUrls(), signed);
const pub = new NostrPublishAction("Create emoji pack", clientRelaysService.outbox.urls, signed);
replaceableEventLoaderService.handleEvent(signed);
navigate(`/emojis/${getSharableEventAddress(signed)}`);
} catch (e) {

@ -38,7 +38,7 @@ export default function EmojiPackFavoriteButton({
const signed = await requestSignature(draft);
const pub = new NostrPublishAction(
isFavorite ? "Unfavorite Emoji pack" : "Favorite emoji pack",
clientRelaysService.getWriteUrls(),
clientRelaysService.outbox.urls,
signed,
);
replaceableEventLoaderService.handleEvent(signed);

@ -119,7 +119,7 @@ function EmojiPackPage({ pack }: { pack: NostrEvent }) {
};
const signed = await requestSignature(draft);
const pub = new NostrPublishAction("Update emoji pack", clientRelaysService.getWriteUrls(), signed);
const pub = new NostrPublishAction("Update emoji pack", clientRelaysService.outbox.urls, signed);
replaceableEventLoaderService.handleEvent(signed);
setEditing(false);
};

@ -45,7 +45,7 @@ export default function ListFavoriteButton({
setLoading(true);
const draft = isFavorite ? listRemoveCoordinate(prev, coordinate) : listAddCoordinate(prev, coordinate);
const signed = await requestSignature(draft);
const pub = new NostrPublishAction("Favorite list", clientRelaysService.getWriteUrls(), signed);
const pub = new NostrPublishAction("Favorite list", clientRelaysService.outbox.urls, signed);
replaceableEventLoaderService.handleEvent(signed);
} catch (e) {
if (e instanceof Error) toast({ description: e.message, status: "error" });

@ -54,7 +54,7 @@ export default function NewListModal({
kind: values.kind,
};
const signed = await requestSignature(draft);
const pub = new NostrPublishAction("Create list", clientRelaysService.getWriteUrls(), signed);
const pub = new NostrPublishAction("Create list", clientRelaysService.outbox.urls, signed);
if (onCreated) onCreated(signed);
} catch (e) {

@ -25,7 +25,7 @@ export default function UserCard({ pubkey, relay, list, ...props }: UserCardProp
const handleRemoveFromList = useAsyncErrorHandler(async () => {
const draft = listRemovePerson(list, pubkey);
const signed = await requestSignature(draft);
const pub = new NostrPublishAction("Remove from list", clientRelaysService.getWriteUrls(), signed);
const pub = new NostrPublishAction("Remove from list", clientRelaysService.outbox.urls, signed);
}, [list, requestSignature]);
return (

@ -1,15 +1,11 @@
import { PropsWithChildren } from "react";
import {
Box,
Button,
ButtonProps,
Card,
CardBody,
CardFooter,
CardHeader,
CardProps,
Checkbox,
CheckboxProps,
Flex,
Heading,
IconButton,
@ -31,11 +27,7 @@ import { RelayFavicon } from "../../../components/relay-favicon";
import { CodeIcon } from "../../../components/icons";
import UserLink from "../../../components/user-link";
import UserAvatar from "../../../components/user-avatar";
import { useClientRelays } from "../../../hooks/use-client-relays";
import clientRelaysService from "../../../services/client-relays";
import { RelayMode } from "../../../classes/relay";
import { UserDnsIdentityIcon } from "../../../components/user-dns-identity-icon";
import useCurrentAccount from "../../../hooks/use-current-account";
import RawJson from "../../../components/debug-modals/raw-json";
import { RelayShareButton } from "./relay-share-button";
import useRelayStats from "../../../hooks/use-relay-stats";
@ -78,52 +70,52 @@ export function RelayMetadata({ url, extended }: { url: string; extended?: boole
);
}
export function RelayJoinAction({ url, ...props }: { url: string } & Omit<ButtonProps, "children" | "onClick">) {
const account = useCurrentAccount();
const clientRelays = useClientRelays();
const relayConfig = clientRelays.find((r) => r.url === url);
// export function RelayJoinAction({ url, ...props }: { url: string } & Omit<ButtonProps, "children" | "onClick">) {
// const account = useCurrentAccount();
// const clientRelays = useClientRelays();
// const relayConfig = clientRelays.find((r) => r.url === url);
return relayConfig ? (
<Button
colorScheme="red"
variant="outline"
onClick={() => clientRelaysService.removeRelay(url)}
isDisabled={!account}
{...props}
>
Leave
</Button>
) : (
<Button
colorScheme="green"
onClick={() => clientRelaysService.addRelay(url, RelayMode.ALL)}
isDisabled={!account}
{...props}
>
Join
</Button>
);
}
// return relayConfig ? (
// <Button
// colorScheme="red"
// variant="outline"
// onClick={() => clientRelaysService.removeRelay(url)}
// isDisabled={!account}
// {...props}
// >
// Leave
// </Button>
// ) : (
// <Button
// colorScheme="green"
// onClick={() => clientRelaysService.addRelay(url, RelayMode.ALL)}
// isDisabled={!account}
// {...props}
// >
// Join
// </Button>
// );
// }
export function RelayModeAction({
url,
...props
}: { url: string } & Omit<CheckboxProps, "children" | "isChecked" | "onChange">) {
const clientRelays = useClientRelays();
const relayConfig = clientRelays.find((r) => r.url === url);
// export function RelayModeAction({
// url,
// ...props
// }: { url: string } & Omit<CheckboxProps, "children" | "isChecked" | "onChange">) {
// const clientRelays = useClientRelays();
// const relayConfig = clientRelays.find((r) => r.url === url);
return relayConfig ? (
<Checkbox
isChecked={!!(relayConfig.mode & RelayMode.WRITE)}
onChange={(e) => {
clientRelaysService.updateRelay(relayConfig.url, e.target.checked ? RelayMode.WRITE : RelayMode.READ);
}}
{...props}
>
Write
</Checkbox>
) : null;
}
// return relayConfig ? (
// <Checkbox
// isChecked={!!(relayConfig.mode & RelayMode.WRITE)}
// onChange={(e) => {
// clientRelaysService.updateRelay(relayConfig.url, e.target.checked ? RelayMode.WRITE : RelayMode.READ);
// }}
// {...props}
// >
// Write
// </Checkbox>
// ) : null;
// }
export function RelayDebugButton({ url, ...props }: { url: string } & Omit<IconButtonProps, "icon" | "aria-label">) {
const { info } = useRelayInfo(url);
@ -174,8 +166,8 @@ export default function RelayCard({ url, ...props }: { url: string } & Omit<Card
<RelayMetadata url={url} />
</CardBody>
<CardFooter p="2" as={Flex} gap="2">
<RelayJoinAction url={url} size="sm" />
<RelayModeAction url={url} />
{/* <RelayJoinAction url={url} size="sm" /> */}
{/* <RelayModeAction url={url} /> */}
<RelayShareButton relay={url} ml="auto" size="sm" />
<RelayDebugButton url={url} size="sm" title="Show raw NIP-11 metadata" />

@ -17,7 +17,7 @@ export function RelayShareButton({
const recommendRelay = useCallback(async () => {
try {
const writeRelays = clientRelaysService.getWriteUrls();
const writeRelays = clientRelaysService.outbox.urls;
const draft: DraftNostrEvent = {
kind: 2,

@ -1,9 +1,8 @@
import { useDeferredValue, useMemo, useState } from "react";
import { useAsync } from "react-use";
import { Link as RouterLink } from "react-router-dom";
import { Button, Divider, Flex, Heading, Input, SimpleGrid, Spacer, Switch, useDisclosure } from "@chakra-ui/react";
import { Button, Flex, Heading, Input, SimpleGrid, Spacer, useDisclosure } from "@chakra-ui/react";
import { useClientRelays } from "../../hooks/use-client-relays";
import relayPoolService from "../../services/relay-pool";
import AddCustomRelayModal from "./components/add-custom-modal";
import RelayCard from "./components/relay-card";
@ -12,6 +11,7 @@ import { RelayMode } from "../../classes/relay";
import { ErrorBoundary } from "../../components/error-boundary";
import VerticalPageLayout from "../../components/vertical-page-layout";
import { isValidRelayURL } from "../../helpers/relay";
import { useReadRelayUrls, useWriteRelayUrls } from "../../hooks/use-client-relays";
export default function RelaysView() {
const [search, setSearch] = useState("");
@ -19,10 +19,11 @@ export default function RelaysView() {
const isSearching = deboundedSearch.length > 2;
const addRelayModal = useDisclosure();
const clientRelays = useClientRelays().map((r) => r.url);
const readRelays = useReadRelayUrls();
const writeRelays = useWriteRelayUrls();
const discoveredRelays = relayPoolService
.getRelays()
.filter((r) => !clientRelays.includes(r.url))
.filter((r) => !readRelays.has(r.url) && !writeRelays.has(r.url))
.map((r) => r.url)
.filter(isValidRelayURL);
@ -32,11 +33,11 @@ export default function RelaysView() {
const filteredRelays = useMemo(() => {
if (isSearching) {
return onlineRelays.filter((url) => url.includes(deboundedSearch));
return onlineRelays.filter((url) => url.toLowerCase().includes(deboundedSearch.toLowerCase()));
}
return clientRelays;
}, [isSearching, deboundedSearch, onlineRelays, clientRelays]);
return [...readRelays];
}, [isSearching, deboundedSearch, onlineRelays, readRelays]);
return (
<VerticalPageLayout>

@ -16,12 +16,12 @@ import { Link as RouterLink, useNavigate } from "react-router-dom";
import VerticalPageLayout from "../../components/vertical-page-layout";
import { getPubkeysFromList } from "../../helpers/nostr/lists";
import { useClientRelays, useReadRelayUrls } from "../../hooks/use-client-relays";
import { useReadRelayUrls } from "../../hooks/use-client-relays";
import useCurrentAccount from "../../hooks/use-current-account";
import useSubjects from "../../hooks/use-subjects";
import useUserContactList from "../../hooks/use-user-contact-list";
import RequireCurrentAccount from "../../providers/route/require-current-account";
import userRelaysService from "../../services/user-relays";
import userMailboxesService from "../../services/user-mailboxes";
import { NostrEvent } from "../../types/nostr-event";
import { RelayFavicon } from "../../components/relay-favicon";
import { ChevronLeftIcon } from "../../components/icons";
@ -30,12 +30,14 @@ import { RelayMetadata, RelayPaidTag } from "./components/relay-card";
function usePopularContactsRelays(list?: NostrEvent) {
const readRelays = useReadRelayUrls();
const subs = list ? getPubkeysFromList(list).map((p) => userRelaysService.requestRelays(p.pubkey, readRelays)) : [];
const subs = list
? getPubkeysFromList(list).map((p) => userMailboxesService.requestMailboxes(p.pubkey, readRelays))
: [];
const contactsRelays = useSubjects(subs);
const relayScore: Record<string, string[]> = {};
for (const { relays, pubkey } of contactsRelays) {
for (const { url } of relays) {
for (const url of relays) {
relayScore[url] = relayScore[url] || [];
relayScore[url].push(pubkey);
}
@ -76,10 +78,7 @@ function PopularRelaysPage() {
const account = useCurrentAccount();
const contacts = useUserContactList(account?.pubkey);
const clientRelays = useClientRelays().map((r) => r.url);
const popularRelays = usePopularContactsRelays(contacts).filter(
(r) => !clientRelays.includes(r.url) && r.pubkeys.length > 1,
);
const popularRelays = usePopularContactsRelays(contacts).filter((r) => r.pubkeys.length > 1);
return (
<VerticalPageLayout>

@ -15,7 +15,7 @@ import {
} from "@chakra-ui/react";
import { useRelayInfo } from "../../../hooks/use-relay-info";
import { RelayDebugButton, RelayJoinAction, RelayMetadata } from "../components/relay-card";
import { RelayDebugButton, RelayMetadata } from "../components/relay-card";
import SupportedNIPs from "../components/supported-nips";
import { ExternalLinkIcon } from "../../../components/icons";
import RelayReviewForm from "./relay-review-form";
@ -63,7 +63,7 @@ function RelayPage({ relay }: { relay: string }) {
>
More info
</Button>
<RelayJoinAction url={relay} />
{/* <RelayJoinAction url={relay} /> */}
</ButtonGroup>
</Flex>
<RelayMetadata url={relay} extended />

@ -10,9 +10,8 @@ import { nostrBuildUploadImage } from "../../helpers/nostr-build";
import NostrPublishAction from "../../classes/nostr-publish-action";
import accountService from "../../services/account";
import signingService from "../../services/signing";
import clientRelaysService from "../../services/client-relays";
import { RelayMode } from "../../classes/relay";
import { COMMON_CONTACT_RELAY } from "../../const";
import { DraftNostrEvent } from "../../types/nostr-event";
export default function CreateStep({
metadata,
@ -68,7 +67,14 @@ export default function CreateStep({
accountService.switchAccount(pubkey);
// set relays
await clientRelaysService.postUpdatedRelays(relays.map((url) => ({ url, mode: RelayMode.ALL })));
const draft: DraftNostrEvent = {
kind: kinds.RelayList,
content: "",
tags: relays.map((url) => ["r", url]),
created_at: dayjs().unix(),
};
const signed = finalizeEvent(draft, hex);
new NostrPublishAction("Set Mailbox Relays", relays, signed);
onSubmit(bytesToHex(hex));
} catch (e) {

@ -1,16 +1,5 @@
import { useMemo } from "react";
import {
Box,
Card,
CardBody,
CardHeader,
CardProps,
Flex,
Heading,
Image,
LinkBox,
LinkOverlay,
} from "@chakra-ui/react";
import { Card, CardBody, CardHeader, CardProps, Heading, Image, LinkBox, LinkOverlay } from "@chakra-ui/react";
import { useReadRelayUrls } from "../../../hooks/use-client-relays";
import { useRelaySelectionRelays } from "../../../providers/local/relay-selection-provider";
@ -24,10 +13,10 @@ import OpenGraphCard from "../../../components/open-graph-card";
export const STREAMER_CARDS_TYPE = 17777;
export const STREAMER_CARD_TYPE = 37777;
function useStreamerCardsCords(pubkey: string, relays: string[]) {
function useStreamerCardsCords(pubkey: string, relays: Iterable<string>) {
const sub = useMemo(
() => replaceableEventLoaderService.requestEvent(relays, STREAMER_CARDS_TYPE, pubkey),
[pubkey, relays.join("|")],
[pubkey, relays],
);
const streamerCards = useSubject(sub);

@ -4,8 +4,6 @@ import { useForm } from "react-hook-form";
import { ParsedStream, buildChatMessage } from "../../../../helpers/nostr/stream";
import { useRelaySelectionRelays } from "../../../../providers/local/relay-selection-provider";
import { useUserRelays } from "../../../../hooks/use-user-relays";
import { RelayMode } from "../../../../classes/relay";
import { unique } from "../../../../helpers/array";
import { useSigningContext } from "../../../../providers/global/signing-provider";
import NostrPublishAction from "../../../../classes/nostr-publish-action";
@ -14,14 +12,13 @@ import { useContextEmojis } from "../../../../providers/global/emoji-provider";
import { MagicInput, RefType } from "../../../../components/magic-textarea";
import StreamZapButton from "../../components/stream-zap-button";
import { nostrBuildUploadImage } from "../../../../helpers/nostr-build";
import { useUserInbox } from "../../../../hooks/use-user-mailboxes";
export default function ChatMessageForm({ stream, hideZapButton }: { stream: ParsedStream; hideZapButton?: boolean }) {
const toast = useToast();
const emojis = useContextEmojis();
const streamRelays = useRelaySelectionRelays();
const hostReadRelays = useUserRelays(stream.host)
.filter((r) => r.mode & RelayMode.READ)
.map((r) => r.url);
const hostReadRelays = useUserInbox(stream.host);
const relays = useMemo(() => unique([...streamRelays, ...hostReadRelays]), [hostReadRelays, streamRelays]);

@ -1,5 +1,5 @@
import { ReactNode, useMemo } from "react";
import { Card, Flex, Heading, Link, Spinner } from "@chakra-ui/react";
import { Card, Heading, Link, Spinner } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { nip19 } from "nostr-tools";

@ -25,7 +25,7 @@ import { useUserMetadata } from "../../hooks/use-user-metadata";
import { useNavigate } from "react-router-dom";
import { ChevronLeftIcon } from "../../components/icons";
export function useUsersMuteLists(pubkeys: string[], additionalRelays: string[] = []) {
export function useUsersMuteLists(pubkeys: string[], additionalRelays?: Iterable<string>) {
const readRelays = useReadRelayUrls(additionalRelays);
const muteListSubjects = useMemo(() => {
return pubkeys.map((pubkey) => replaceableEventLoaderService.requestEvent(readRelays, MUTE_LIST_KIND, pubkey));

@ -44,7 +44,7 @@ export default function NoteTextToSpeechPage({ note }: { note: NostrEvent }) {
};
const signed = await requestSignature(draft);
new NostrPublishAction("Request Reading", clientRelaysService.getWriteUrls(), signed);
new NostrPublishAction("Request Reading", clientRelaysService.outbox.urls, signed);
} catch (e) {
if (e instanceof Error) toast({ status: "error", description: e.message });
}

@ -57,7 +57,7 @@ export function NoteTranslationsPage({ note }: { note: NostrEvent }) {
};
const signed = await requestSignature(draft);
new NostrPublishAction("Request Translation", clientRelaysService.getWriteUrls(), signed);
new NostrPublishAction("Request Translation", clientRelaysService.outbox.urls, signed);
} catch (e) {
if (e instanceof Error) toast({ status: "error", description: e.message });
}

@ -119,7 +119,7 @@ export default function NewTorrentView() {
};
const signed = await requestSignature(draft);
new NostrPublishAction("Publish Torrent", clientRelaysService.getWriteUrls(), signed);
new NostrPublishAction("Publish Torrent", clientRelaysService.outbox.urls, signed);
navigate(`/torrents/${nip19.noteEncode(signed.id)}`);
} catch (e) {

@ -17,14 +17,13 @@ import {
import accountService from "../../../services/account";
import { useUserMetadata } from "../../../hooks/use-user-metadata";
import { getUserDisplayName } from "../../../helpers/user-metadata";
import { useUserRelays } from "../../../hooks/use-user-relays";
import { RelayMode } from "../../../classes/relay";
import UserDebugModal from "../../../components/debug-modals/user-debug-modal";
import { useSharableProfileId } from "../../../hooks/use-shareable-profile-id";
import { buildAppSelectUrl } from "../../../helpers/nostr/apps";
import { truncatedId } from "../../../helpers/nostr/events";
import useUserMuteActions from "../../../hooks/use-user-mute-actions";
import useCurrentAccount from "../../../hooks/use-current-account";
import userMailboxesService from "../../../services/user-mailboxes";
export const UserProfileMenu = ({
pubkey,
@ -34,18 +33,17 @@ export const UserProfileMenu = ({
const toast = useToast();
const account = useCurrentAccount();
const metadata = useUserMetadata(pubkey);
const userRelays = useUserRelays(pubkey);
const infoModal = useDisclosure();
const sharableId = useSharableProfileId(pubkey);
const { isMuted, mute, unmute } = useUserMuteActions(pubkey);
const loginAsUser = () => {
const readRelays = userRelays.filter((r) => r.mode === RelayMode.READ).map((r) => r.url) ?? [];
const relays = userMailboxesService.getMailboxes(pubkey).value?.outbox.urls;
if (!accountService.hasAccount(pubkey)) {
accountService.addAccount({
type: "pubkey",
pubkey,
relays: readRelays,
relays,
readonly: true,
});
}

@ -33,11 +33,9 @@ import { getUserDisplayName } from "../../helpers/user-metadata";
import { useAppTitle } from "../../hooks/use-app-title";
import { useReadRelayUrls } from "../../hooks/use-client-relays";
import relayScoreboardService from "../../services/relay-scoreboard";
import { RelayMode } from "../../classes/relay";
import { AdditionalRelayProvider } from "../../providers/local/additional-relay-context";
import { unique } from "../../helpers/array";
import { RelayFavicon } from "../../components/relay-favicon";
import { useUserRelays } from "../../hooks/use-user-relays";
import Header from "./components/header";
import { ErrorBoundary } from "../../components/error-boundary";
import useEventExists from "../../hooks/use-event-exists";
@ -46,6 +44,7 @@ import { STREAM_KIND } from "../../helpers/nostr/stream";
import { TORRENT_KIND } from "../../helpers/nostr/torrents";
import { GOAL_KIND } from "../../helpers/nostr/goal";
import useParamsProfilePointer from "../../hooks/use-params-pubkey-pointer";
import useUserMailboxes from "../../hooks/use-user-mailboxes";
const tabs = [
{ label: "About", path: "about" },
@ -67,16 +66,10 @@ const tabs = [
{ label: "Muted by", path: "muted-by" },
];
function useUserTopRelays(pubkey: string, count: number = 4) {
const readRelays = useReadRelayUrls();
// get user relays
const userRelays = useUserRelays(pubkey, readRelays)
.filter((r) => r.mode & RelayMode.WRITE)
.map((r) => r.url);
// merge the users relays with client relays
if (userRelays.length === 0) return readRelays;
const sorted = relayScoreboardService.getRankedRelays(userRelays);
function useUserBestOutbox(pubkey: string, count: number = 4) {
const mailbox = useUserMailboxes(pubkey);
const relays = useReadRelayUrls(mailbox?.outbox);
const sorted = relayScoreboardService.getRankedRelays(relays);
return !count ? sorted : sorted.slice(0, count);
}
@ -84,7 +77,7 @@ const UserView = () => {
const { pubkey, relays: pointerRelays = [] } = useParamsProfilePointer();
const navigate = useNavigate();
const [relayCount, setRelayCount] = useState(4);
const userTopRelays = useUserTopRelays(pubkey, relayCount);
const userTopRelays = useUserBestOutbox(pubkey, relayCount);
const relayModal = useDisclosure();
const readRelays = unique([...userTopRelays, ...pointerRelays]);

@ -1,20 +1,19 @@
import { useOutletContext, Link as RouterLink } from "react-router-dom";
import { Button, Flex, Heading, Spacer, StackDivider, Tag, VStack } from "@chakra-ui/react";
import { useUserRelays } from "../../hooks/use-user-relays";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import { truncatedId } from "../../helpers/nostr/events";
import { useReadRelayUrls } from "../../hooks/use-client-relays";
import useSubject from "../../hooks/use-subject";
import { NostrEvent } from "../../types/nostr-event";
import RelayReviewNote from "../relays/components/relay-review-note";
import { RelayFavicon } from "../../components/relay-favicon";
import { RelayDebugButton, RelayJoinAction, RelayMetadata } from "../relays/components/relay-card";
import { RelayDebugButton, RelayMetadata } from "../relays/components/relay-card";
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import { useRelayInfo } from "../../hooks/use-relay-info";
import { ErrorBoundary } from "../../components/error-boundary";
import { RelayShareButton } from "../relays/components/relay-share-button";
import useUserMailboxes from "../../hooks/use-user-mailboxes";
function Relay({ url, reviews }: { url: string; reviews: NostrEvent[] }) {
const { info } = useRelayInfo(url);
@ -37,7 +36,7 @@ function Relay({ url, reviews }: { url: string; reviews: NostrEvent[] }) {
<Button as={RouterLink} to={`/global?relay=${url}`} size="sm">
Notes
</Button>
<RelayJoinAction url={url} size="sm" />
{/* <RelayJoinAction url={url} size="sm" /> */}
</Flex>
<RelayMetadata url={url} />
{reviews.length > 0 && (
@ -57,9 +56,9 @@ function getRelayReviews(url: string, events: NostrEvent[]) {
const UserRelaysTab = () => {
const { pubkey } = useOutletContext() as { pubkey: string };
const userRelays = useUserRelays(pubkey);
const mailboxes = useUserMailboxes(pubkey);
const readRelays = useReadRelayUrls(userRelays.map((r) => r.url));
const readRelays = useReadRelayUrls(mailboxes?.outbox);
const timeline = useTimelineLoader(`${pubkey}-relay-reviews`, readRelays, {
authors: [pubkey],
kinds: [1985],
@ -72,21 +71,36 @@ const UserRelaysTab = () => {
const otherReviews = reviews.filter((e) => {
const url = e.tags.find((t) => t[0] === "r")?.[1];
return !userRelays.some((r) => r.url === url);
return url && !mailboxes?.relays.has(url);
});
return (
<IntersectionObserverProvider callback={callback}>
<Heading size="lg" ml="2" mt="2">
Inboxes
</Heading>
<VStack divider={<StackDivider />} py="2" align="stretch">
{userRelays.map((relayConfig) => (
{mailboxes?.inbox.urls.map((url) => (
<ErrorBoundary>
<Relay key={relayConfig.url} url={relayConfig.url} reviews={getRelayReviews(relayConfig.url, reviews)} />
<Relay key={url} url={url} reviews={getRelayReviews(url, reviews)} />
</ErrorBoundary>
))}
</VStack>
<Heading size="lg" ml="2" mt="2">
Outboxes
</Heading>
<VStack divider={<StackDivider />} py="2" align="stretch">
{mailboxes?.outbox.urls.map((url) => (
<ErrorBoundary>
<Relay key={url} url={url} reviews={getRelayReviews(url, reviews)} />
</ErrorBoundary>
))}
</VStack>
{otherReviews.length > 0 && (
<>
<Heading>Other Reviews</Heading>
<Heading size="lg" ml="2" mt="2">
Reviews
</Heading>
<Flex direction="column" gap="2" pb="8">
{otherReviews.map((event) => (
<RelayReviewNote key={event.id} event={event} />