make things go faster

This commit is contained in:
hzrd149 2025-02-14 22:16:45 -06:00
parent 723668b0fc
commit ad3fbd8673
25 changed files with 151 additions and 323 deletions

View File

@ -85,7 +85,7 @@
"light-bolt11-decoder": "^3.2.0",
"lodash.throttle": "^4.1.1",
"match-sorter": "^8.0.0",
"nanoid": "^5.0.9",
"nanoid": "^5.1.0",
"ngeohash": "^0.6.3",
"nostr-idb": "^2.2.0",
"nostr-signer-capacitor-plugin": "^0.0.3",

96
pnpm-lock.yaml generated
View File

@ -104,25 +104,25 @@ importers:
version: 0.7.2
applesauce-accounts:
specifier: next
version: 0.0.0-next-20250214174945(typescript@5.7.3)
version: 0.0.0-next-20250215041231(typescript@5.7.3)
applesauce-content:
specifier: next
version: 0.0.0-next-20250214174945(typescript@5.7.3)
version: 0.0.0-next-20250215041231(typescript@5.7.3)
applesauce-core:
specifier: next
version: 0.0.0-next-20250214174945(typescript@5.7.3)
version: 0.0.0-next-20250215041231(typescript@5.7.3)
applesauce-factory:
specifier: next
version: 0.0.0-next-20250214174945(typescript@5.7.3)
version: 0.0.0-next-20250215041231(typescript@5.7.3)
applesauce-loaders:
specifier: next
version: 0.0.0-next-20250214174945(typescript@5.7.3)
version: 0.0.0-next-20250215041231(typescript@5.7.3)
applesauce-react:
specifier: next
version: 0.0.0-next-20250214174945(react-dom@19.0.0(react@19.0.0))(typescript@5.7.3)
version: 0.0.0-next-20250215041231(react-dom@19.0.0(react@19.0.0))(typescript@5.7.3)
applesauce-signers:
specifier: next
version: 0.0.0-next-20250214174945(typescript@5.7.3)
version: 0.0.0-next-20250215041231(typescript@5.7.3)
bech32:
specifier: ^2.0.0
version: 2.0.0
@ -211,8 +211,8 @@ importers:
specifier: ^8.0.0
version: 8.0.0
nanoid:
specifier: ^5.0.9
version: 5.0.9
specifier: ^5.1.0
version: 5.1.0
ngeohash:
specifier: ^0.6.3
version: 0.6.3
@ -2189,26 +2189,26 @@ packages:
engines: {node: '>=8.0.0'}
hasBin: true
applesauce-accounts@0.0.0-next-20250214174945:
resolution: {integrity: sha512-kYjSmWeRo/sy8VW6FZiOYo7A8mIzVVLrJRTUoJfC9oASWmDG+Z5k4K6ECRwniFM0XQaootFyhUQH4AzBsI60rw==}
applesauce-accounts@0.0.0-next-20250215041231:
resolution: {integrity: sha512-v5isJLkBdqOqmcDA7aGh8mx5Umrmo07+2WVMJMnwEBCy6gGy7HHWYZtxzoL/k2Cq6m+wnYE9X6aroq6fdFCQIA==}
applesauce-content@0.0.0-next-20250214174945:
resolution: {integrity: sha512-Zm34S6ls8YLNLLn+sLTxMANzuTHucZz7yt50QsJyo8ns75DYoX4SrwWbAltzKWUAPyB68och5gy6WsEZANjf3Q==}
applesauce-content@0.0.0-next-20250215041231:
resolution: {integrity: sha512-1V2vhqBPFDPOJRpz9XC2/Xno4D0tHxHic9VcspxUO/rtuYvwTSAIdSNje39admpd3r++3z6/W4WLy8KFcxYmaA==}
applesauce-core@0.0.0-next-20250214174945:
resolution: {integrity: sha512-kyNj6If4oNREHaH+moIf4+l7WHw6dqMqeFoY7AwPA2rSnIFMEFojrXZkW7pPVmk3zI4S50ptEHPSaQTPQiBjdw==}
applesauce-core@0.0.0-next-20250215041231:
resolution: {integrity: sha512-VjHgDbF3UQhLQhfTVZwWgSys2WQ0WpeGouRIYC38X8C7NSU7utoQ3iYwnQbqHnHee6QeO4SOI23miF0CxdUigA==}
applesauce-factory@0.0.0-next-20250214174945:
resolution: {integrity: sha512-LpMgHWuTVLfSK7izsx5ZhUCnLSqF31BbflfRWgv5LpHxhOvgc5XFMsY76yrn1lFomNIATkxg8KkjdOCAvxNaCw==}
applesauce-factory@0.0.0-next-20250215041231:
resolution: {integrity: sha512-s0AuA1JmO7RZAfDxMcwmmhsazpwPfdGkHlsj2YYRGQo7maQBINCqJeo3pattuWSiuuW8FX1FH7DpfIFgaaHVzA==}
applesauce-loaders@0.0.0-next-20250214174945:
resolution: {integrity: sha512-mFh6kk9zWHlz+fN++cwco2ftBBWXuxZn/uGBM3EfD9lWFrPvs8CVfVYfDmwiRVNqF4/qL2ce9tJsB0w+2aVEOQ==}
applesauce-loaders@0.0.0-next-20250215041231:
resolution: {integrity: sha512-3+yXYXmdwJn1BQY5mpkp2ikdUW8BhqAxA6kdyrd7qymfIA2lTasSKk0lbBhhX5vidJWXRX1dpeP7BmlTAF1IGg==}
applesauce-react@0.0.0-next-20250214174945:
resolution: {integrity: sha512-O6xg+i04YNi2xH4K/dCapsdhMsXzbf0gMkGrq/K4TkAVfkYhBBYtj+vCySkPfJvcnp6LyGU5CC/3sFOlSiF+YQ==}
applesauce-react@0.0.0-next-20250215041231:
resolution: {integrity: sha512-HxDmEub+OkK33nVRvpuHKt4vIId9mB4AmXPabzCzWsLL0ZqaeP6xE4k7RB4YmJdLLbcLIres7u7Tu6bS2UNqHQ==}
applesauce-signers@0.0.0-next-20250214174945:
resolution: {integrity: sha512-qqJ1tCLEq9GPJ644ONeK7v/cD91RS887wPSwnItJlf5VJjOTGY/BEfSXNtpXaoXrSPiJ7SM1wBN0IPCuPB8s1g==}
applesauce-signers@0.0.0-next-20250215041231:
resolution: {integrity: sha512-uzljOtXb17DxI409WulLE2bX8yktwghxBYZOlzmr9Kfe3iGJFCA0SQxtAoK/APrBCEd9fx5XAUTD6VWmisIy7A==}
arg@4.1.3:
resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
@ -4329,8 +4329,8 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
nanoid@5.0.9:
resolution: {integrity: sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q==}
nanoid@5.1.0:
resolution: {integrity: sha512-zDAl/llz8Ue/EblwSYwdxGBYfj46IM1dhjVi8dyp9LQffoIGxJEAHj2oeZ4uNcgycSRcQ83CnfcZqEJzVDLcDw==}
engines: {node: ^18 || >=20}
hasBin: true
@ -8421,24 +8421,24 @@ snapshots:
dependencies:
entities: 2.2.0
applesauce-accounts@0.0.0-next-20250214174945(typescript@5.7.3):
applesauce-accounts@0.0.0-next-20250215041231(typescript@5.7.3):
dependencies:
'@noble/hashes': 1.7.1
applesauce-signers: 0.0.0-next-20250214174945(typescript@5.7.3)
nanoid: 5.0.9
applesauce-signers: 0.0.0-next-20250215041231(typescript@5.7.3)
nanoid: 5.1.0
nostr-tools: 2.10.4(typescript@5.7.3)
rxjs: 7.8.1
transitivePeerDependencies:
- supports-color
- typescript
applesauce-content@0.0.0-next-20250214174945(typescript@5.7.3):
applesauce-content@0.0.0-next-20250215041231(typescript@5.7.3):
dependencies:
'@cashu/cashu-ts': 2.0.0-rc1
'@types/hast': 3.0.4
'@types/mdast': 4.0.4
'@types/unist': 3.0.3
applesauce-core: 0.0.0-next-20250214174945(typescript@5.7.3)
applesauce-core: 0.0.0-next-20250215041231(typescript@5.7.3)
mdast-util-find-and-replace: 3.0.2
nostr-tools: 2.10.4(typescript@5.7.3)
remark: 15.0.1
@ -8449,34 +8449,34 @@ snapshots:
- supports-color
- typescript
applesauce-core@0.0.0-next-20250214174945(typescript@5.7.3):
applesauce-core@0.0.0-next-20250215041231(typescript@5.7.3):
dependencies:
'@scure/base': 1.2.4
debug: 4.4.0
fast-deep-equal: 3.1.3
hash-sum: 2.0.0
light-bolt11-decoder: 3.2.0
nanoid: 5.0.9
nanoid: 5.1.0
nostr-tools: 2.10.4(typescript@5.7.3)
rxjs: 7.8.1
transitivePeerDependencies:
- supports-color
- typescript
applesauce-factory@0.0.0-next-20250214174945(typescript@5.7.3):
applesauce-factory@0.0.0-next-20250215041231(typescript@5.7.3):
dependencies:
applesauce-content: 0.0.0-next-20250214174945(typescript@5.7.3)
applesauce-core: 0.0.0-next-20250214174945(typescript@5.7.3)
nanoid: 5.0.9
applesauce-content: 0.0.0-next-20250215041231(typescript@5.7.3)
applesauce-core: 0.0.0-next-20250215041231(typescript@5.7.3)
nanoid: 5.1.0
nostr-tools: 2.10.4(typescript@5.7.3)
transitivePeerDependencies:
- supports-color
- typescript
applesauce-loaders@0.0.0-next-20250214174945(typescript@5.7.3):
applesauce-loaders@0.0.0-next-20250215041231(typescript@5.7.3):
dependencies:
applesauce-core: 0.0.0-next-20250214174945(typescript@5.7.3)
nanoid: 5.0.9
applesauce-core: 0.0.0-next-20250215041231(typescript@5.7.3)
nanoid: 5.1.0
nostr-tools: 2.10.4(typescript@5.7.3)
rx-nostr: 3.5.0
rxjs: 7.8.1
@ -8484,12 +8484,12 @@ snapshots:
- supports-color
- typescript
applesauce-react@0.0.0-next-20250214174945(react-dom@19.0.0(react@19.0.0))(typescript@5.7.3):
applesauce-react@0.0.0-next-20250215041231(react-dom@19.0.0(react@19.0.0))(typescript@5.7.3):
dependencies:
applesauce-accounts: 0.0.0-next-20250214174945(typescript@5.7.3)
applesauce-content: 0.0.0-next-20250214174945(typescript@5.7.3)
applesauce-core: 0.0.0-next-20250214174945(typescript@5.7.3)
applesauce-factory: 0.0.0-next-20250214174945(typescript@5.7.3)
applesauce-accounts: 0.0.0-next-20250215041231(typescript@5.7.3)
applesauce-content: 0.0.0-next-20250215041231(typescript@5.7.3)
applesauce-core: 0.0.0-next-20250215041231(typescript@5.7.3)
applesauce-factory: 0.0.0-next-20250215041231(typescript@5.7.3)
nostr-tools: 2.10.4(typescript@5.7.3)
observable-hooks: 4.2.4(react-dom@19.0.0(react@19.0.0))(react@18.3.1)(rxjs@7.8.1)
react: 18.3.1
@ -8499,14 +8499,14 @@ snapshots:
- supports-color
- typescript
applesauce-signers@0.0.0-next-20250214174945(typescript@5.7.3):
applesauce-signers@0.0.0-next-20250215041231(typescript@5.7.3):
dependencies:
'@noble/hashes': 1.7.1
'@noble/secp256k1': 1.7.1
'@scure/base': 1.2.4
applesauce-core: 0.0.0-next-20250214174945(typescript@5.7.3)
applesauce-core: 0.0.0-next-20250215041231(typescript@5.7.3)
debug: 4.4.0
nanoid: 5.0.9
nanoid: 5.1.0
nostr-tools: 2.10.4(typescript@5.7.3)
transitivePeerDependencies:
- supports-color
@ -10998,7 +10998,7 @@ snapshots:
nanoid@3.3.8: {}
nanoid@5.0.9: {}
nanoid@5.1.0: {}
napi-build-utils@2.0.0: {}

View File

@ -11,7 +11,7 @@ import {
import dayjs from "dayjs";
import { kinds } from "nostr-tools";
import { getObservableValue } from "applesauce-core/observable";
import { getInboxes, getInvoice, getOutboxes, safeRelayUrls } from "applesauce-core/helpers";
import { getInboxes, getInvoice, getOutboxes, mergeRelaySets } from "applesauce-core/helpers";
import { getZapSplits } from "applesauce-core/helpers/zap";
import { DraftNostrEvent, NostrEvent, isDTag } from "../../types/nostr-event";
@ -89,7 +89,7 @@ export async function getPayRequestForPubkey(
content: comment ?? "",
tags: [
["p", pubkey],
["relays", ...unique(safeRelayUrls([...userInbox, ...eventRelays, ...outbox, ...additional]))],
["relays", ...mergeRelaySets(userInbox, eventRelays, outbox, additional)],
["amount", String(amount)],
],
};

View File

@ -1,24 +0,0 @@
import { Flex, Text } from "@chakra-ui/react";
import { NostrEvent } from "../../../types/nostr-event";
import UserAvatar from "../../user/user-avatar";
import UserLink from "../../user/user-link";
import RelayCard from "../../../views/relays/components/relay-card";
import { safeRelayUrl } from "../../../helpers/relay";
import useEventIntersectionRef from "../../../hooks/use-event-intersection-ref";
export default function RelayRecommendation({ event }: { event: NostrEvent }) {
const safeUrl = safeRelayUrl(event.content);
const ref = useEventIntersectionRef(event);
return (
<Flex gap="2" direction="column" ref={ref}>
<Flex gap="2">
<UserAvatar pubkey={event.pubkey} size="xs" alignItems="center" />
<UserLink pubkey={event.pubkey} />
<Text>Recommended relay:</Text>
</Flex>
{safeUrl ? <RelayCard url={safeUrl} /> : event.content}
</Flex>
);
}

View File

@ -5,7 +5,6 @@ import { Box, Spinner } from "@chakra-ui/react";
import { ErrorBoundary } from "../../error-boundary";
import ReplyNote from "./reply-note";
import ShareEvent from "./share-event";
import RelayRecommendation from "./relay-recommendation";
import { isReply } from "../../../helpers/nostr/event";
import { NostrEvent } from "../../../types/nostr-event";
import { FLARE_VIDEO_KIND } from "../../../helpers/nostr/video";
@ -36,9 +35,6 @@ function TimelineItem({ event, visible, minHeight }: { event: NostrEvent; visibl
case kinds.LiveEvent:
content = <StreamNote stream={event} />;
break;
case kinds.RecommendRelay:
content = <RelayRecommendation event={event} />;
break;
case kinds.BadgeAward:
content = <BadgeAwardCard award={event} />;
break;

View File

@ -1,18 +1,23 @@
import { safeRelayUrls } from "applesauce-core/helpers";
import { EventFactoryClient } from "applesauce-factory";
import { isSafeRelayURL } from "applesauce-core/helpers/relays";
import { normalizeURL } from "applesauce-core/helpers";
import { kinds } from "nostr-tools";
export const DEFAULT_SEARCH_RELAYS = safeRelayUrls([
function normalizeRelayURLs(relays: string[]) {
return relays.filter(isSafeRelayURL).map(normalizeURL);
}
export const DEFAULT_SEARCH_RELAYS = normalizeRelayURLs([
"wss://relay.nostr.band",
"wss://search.nos.today",
"wss://relay.noswhere.com",
"wss://filter.nostr.wine",
]);
export const WIKI_RELAYS = safeRelayUrls(["wss://relay.wikifreedia.xyz/"]);
export const COMMON_CONTACT_RELAYS = safeRelayUrls(["wss://purplepag.es/"]);
export const WIKI_RELAYS = normalizeRelayURLs(["wss://relay.wikifreedia.xyz/"]);
export const COMMON_CONTACT_RELAYS = normalizeRelayURLs(["wss://purplepag.es/"]);
export const DEFAULT_SIGNAL_RELAYS = safeRelayUrls(["wss://nostrue.com/", "wss://relay.damus.io"]);
export const DEFAULT_NOSTR_CONNECT_RELAYS = safeRelayUrls(["wss://relay.nsec.app/"]);
export const DEFAULT_SIGNAL_RELAYS = normalizeRelayURLs(["wss://nostrue.com/", "wss://relay.damus.io"]);
export const DEFAULT_NOSTR_CONNECT_RELAYS = normalizeRelayURLs(["wss://relay.nsec.app/"]);
export const DEFAULT_ICE_SERVERS: RTCIceServer[] = [
{
@ -33,7 +38,7 @@ export const DEFAULT_ICE_SERVERS: RTCIceServer[] = [
},
];
export const RECOMMENDED_READ_RELAYS = safeRelayUrls([
export const RECOMMENDED_READ_RELAYS = normalizeRelayURLs([
"wss://relay.damus.io/",
"wss://nostr.wine/",
"wss://relay.snort.social/",
@ -41,12 +46,20 @@ export const RECOMMENDED_READ_RELAYS = safeRelayUrls([
"wss://purplerelay.com/",
"wss://nostr.land/",
]);
export const RECOMMENDED_WRITE_RELAYS = safeRelayUrls([
export const RECOMMENDED_WRITE_RELAYS = normalizeRelayURLs([
"wss://relay.damus.io/",
"wss://nos.lol/",
"wss://purplerelay.com/",
]);
export const JAPANESE_RELAYS = normalizeRelayURLs([
"wss://r.kojira.io",
"wss://nrelay-jp.c-stellar.net",
"wss://nostr.fediverse.jp",
"wss://nostr.holybea.com",
"wss://relay-jp.nostr.wirednet.jp",
]);
export const NOSTR_CONNECT_PERMISSIONS = [
"get_public_key",
"nip04_encrypt",

View File

@ -1,21 +1,24 @@
import { safeRelayUrls } from "applesauce-core/helpers";
import { normalizeURL } from "applesauce-core/helpers";
export async function getRelaysFromExt() {
if (!window.nostr) throw new Error("Missing extension");
const read = new Set<string>();
const write = new Set<string>();
const extRelays = (await window.nostr.getRelays?.()) ?? [];
const extRelays = (await window.nostr.getRelays?.()) ?? ([] as string[]);
if (Array.isArray(extRelays)) {
const safeUrls = safeRelayUrls(extRelays);
for (const url of safeUrls) {
for (const url of extRelays) {
read.add(url);
write.add(url);
}
} else {
const readUrls = safeRelayUrls(Object.keys(extRelays).filter((url) => extRelays[url].read));
const readUrls = Object.keys(extRelays)
.filter((url) => extRelays[url].read)
.map(normalizeURL);
for (const url of readUrls) read.add(url);
const writeUrls = safeRelayUrls(Object.keys(extRelays).filter((url) => extRelays[url].write));
const writeUrls = Object.keys(extRelays)
.filter((url) => extRelays[url].write)
.map(normalizeURL);
for (const url of writeUrls) write.add(url);
}

View File

@ -1,13 +1,12 @@
import { nip19 } from "nostr-tools";
import { getPubkeyFromDecodeResult, isHexKey } from "applesauce-core/helpers";
import { safeRelayUrls } from "./relay";
import { isSafeRelayURL } from "applesauce-core/helpers/relays";
export function safeDecode(str: string) {
try {
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);
result.data.relays = result.data.relays.filter(isSafeRelayURL);
return result;
} catch (e) {}
}

View File

@ -1,10 +1,10 @@
import dayjs from "dayjs";
import { EventTemplate, NostrEvent, kinds } from "nostr-tools";
import { isAddressPointerInList, isEventPointerInList, isProfilePointerInList } from "applesauce-core/helpers/lists";
import { mergeRelaySets } from "applesauce-core/helpers";
import { PTag, isDTag, isPTag, isRTag } from "../../types/nostr-event";
import { getEventCoordinate, replaceOrAddSimpleTag } from "./event";
import { safeRelayUrls } from "../relay";
export const USER_GROUPS_LIST_KIND = 10009;
@ -100,9 +100,9 @@ export function getReferencesFromList(event: NostrEvent | EventTemplate) {
}
/** @deprecated this should be moved to applesauce-core if its still needed */
export function getRelaysFromList(event: NostrEvent | EventTemplate) {
if (event.kind === kinds.RelayList) return safeRelayUrls(event.tags.filter(isRTag).map((t) => t[1]));
else return safeRelayUrls(event.tags.filter((t) => t[0] === "relay" && t[1]).map((t) => t[1]) as string[]);
export function getRelaysFromList(event: NostrEvent | EventTemplate): string[] {
if (event.kind === kinds.RelayList) return mergeRelaySets(event.tags.filter(isRTag).map((t) => t[1]));
else return mergeRelaySets(event.tags.filter((t) => t[0] === "relay" && t[1]).map((t) => t[1]));
}
/** @deprecated */

View File

@ -1,16 +1,20 @@
import { kinds } from "nostr-tools";
import { DraftNostrEvent, NostrEvent, RTag, Tag, isRTag } from "../../types/nostr-event";
import { safeRelayUrl } from "../relay";
import { cloneEvent } from "./event";
import { RelayMode } from "../../services/app-relays";
import { isSafeRelayURL } from "../../../../applesauce/packages/core/dist/helpers/relays";
import { normalizeURL } from "applesauce-core/helpers";
/** fixes or removes any bad r tags */
/**
* fixes or removes any bad r tags
* @deprecated
*/
export function cleanRTags(tags: Tag[]) {
const newTags: Tag[] = [];
for (const tag of tags) {
if (tag[0] === "r") {
if (!tag[1]) continue;
const url = safeRelayUrl(tag[1]);
if (!tag[1] || !isSafeRelayURL(tag[1])) continue;
const url = normalizeURL(tag[1]);
if (url) newTags.push(tag[2] ? ["r", url, tag[2]] : ["r", url]);
} else newTags.push(tag);
}
@ -34,10 +38,6 @@ export function createRelayTag(url: string, mode: RelayMode) {
}
}
export function getRelaysFromMailbox(list: NostrEvent | DraftNostrEvent): { url: string; mode: RelayMode }[] {
return cleanRTags(list.tags).filter(isRTag).map(parseRTag);
}
export function addRelayModeToMailbox(list: NostrEvent | undefined, relay: string, mode: RelayMode): DraftNostrEvent {
const draft = cloneEvent(kinds.RelayList, list);
draft.tags = cleanRTags(draft.tags);

View File

@ -1,4 +1,4 @@
import { getEventPointerFromETag, getTagValue, safeRelayUrl } from "applesauce-core/helpers";
import { getEventPointerFromETag, getTagValue, isSafeRelayURL, normalizeURL } from "applesauce-core/helpers";
import { NostrEvent, isPTag } from "../../types/nostr-event";
import dayjs from "dayjs";
@ -46,7 +46,9 @@ export function getStreamRelays(stream: NostrEvent) {
if (tag[0] === "relays") {
found = true;
for (let i = 1; i < tag.length; i++) {
const relay = safeRelayUrl(tag[i]);
if (!isSafeRelayURL(tag[i])) continue;
const relay = normalizeURL(tag[i]);
if (relay && !relays.includes(relay)) relays.push(relay);
}
}

View File

@ -9,52 +9,6 @@ export function getRelayVariations(relay: string) {
} else return [relay, relay + "/"];
}
export function validateRelayURL(relay: string | URL) {
if (typeof relay === "string" && relay.includes(",ws")) throw new Error("Can not have multiple relays in one string");
const url = typeof relay === "string" ? new URL(relay) : relay;
if (url.protocol !== "wss:" && url.protocol !== "ws:") throw new Error("Incorrect protocol");
return url;
}
export function isValidRelayURL(relay: string | URL) {
try {
validateRelayURL(relay);
return true;
} catch (e) {
return false;
}
}
/** @deprecated */
export function normalizeRelayURL(relayUrl: string) {
const url = validateRelayURL(relayUrl);
url.pathname = url.pathname.replace(/\/+/g, "/");
if ((url.port === "80" && url.protocol === "ws:") || (url.port === "443" && url.protocol === "wss:")) url.port = "";
url.searchParams.sort();
url.hash = "";
return url.toString();
}
/** @deprecated */
export function safeNormalizeRelayURL(relayUrl: string) {
try {
return normalizeRelayURL(relayUrl);
} catch (e) {
return null;
}
}
// TODO: move these to helpers/relay
export function safeRelayUrl(relayUrl: string | URL) {
try {
return validateRelayURL(relayUrl).toString();
} catch (e) {
return null;
}
}
export function safeRelayUrls(urls: Iterable<string>): string[] {
return Array.from(urls).map(safeRelayUrl).filter(Boolean) as string[];
}
export function splitNostrFilterByPubkeys(
filter: Filter | Filter[],
relayPubkeyMap: Record<string, string[]>,
@ -134,16 +88,3 @@ const connectionStateSortOrder: ConnectionState[] = [
export function getConnectionStateSort(state: ConnectionState) {
return connectionStateSortOrder.indexOf(state);
}
/** @deprecated use mergeRelaySets from applesauce-core */
export function mergeRelaySets(...sources: (Iterable<string> | undefined)[]) {
const set = new Set<string>();
for (const src of sources) {
if (!src) continue;
for (const url of src) {
const safe = safeRelayUrl(url);
if (safe) set.add(safe);
}
}
return Array.from(set);
}

View File

@ -1,6 +1,6 @@
import React, { useContext } from "react";
import { unique } from "../../helpers/array";
import { safeRelayUrls } from "../../helpers/relay";
import { mergeRelaySets } from "applesauce-core/helpers";
export const RelayContext = React.createContext<string[]>([]);
@ -18,7 +18,7 @@ export function AdditionalRelayProvider({
extend?: boolean;
}) {
const parentRelays = useAdditionalRelayContext();
const safeUrls = safeRelayUrls(extend ? [...parentRelays, ...relays] : relays);
const safeUrls = extend ? mergeRelaySets(parentRelays, relays) : relays;
return <RelayContext.Provider value={unique(safeUrls)}>{children}</RelayContext.Provider>;
}

View File

@ -1,5 +1,5 @@
import { Query } from "applesauce-core";
import { safeRelayUrl } from "applesauce-core/helpers";
import { isSafeRelayURL } from "applesauce-core/helpers";
import { map } from "rxjs/operators";
type TrustedMints = {
@ -27,9 +27,8 @@ export default function TrustedMintsQuery(pubkey: string): Query<TrustedMints |
break;
case "relay":
if (tag[1]) {
const safe = safeRelayUrl(tag[1]);
if (safe) mints.push(safe);
if (tag[1] && isSafeRelayURL(tag[1])) {
mints.push(tag[1]);
}
break;

View File

@ -1,11 +1,10 @@
import { BehaviorSubject, distinctUntilChanged, Observable, pairwise } from "rxjs";
import { CacheRelay, openDB } from "nostr-idb";
import { AbstractRelay } from "nostr-tools/abstract-relay";
import { fakeVerifyEvent, isFromCache } from "applesauce-core/helpers";
import { fakeVerifyEvent, isFromCache, isSafeRelayURL } from "applesauce-core/helpers";
import dayjs from "dayjs";
import { logger } from "../helpers/debug";
import { safeRelayUrl } from "../helpers/relay";
import WasmRelay from "./wasm-relay";
import MemoryRelay from "../classes/memory-relay";
import localSettings from "./local-settings";
@ -50,8 +49,8 @@ async function createRelay(url: string) {
return new WasmRelay();
} else if (url.startsWith("nostr-idb://")) {
return createInternalRelay();
} else if (safeRelayUrl(url)) {
return new AbstractRelay(safeRelayUrl(url)!, { verifyEvent: fakeVerifyEvent });
} else if (isSafeRelayURL(url)) {
return new AbstractRelay(url, { verifyEvent: fakeVerifyEvent });
}
} else if (window.CACHE_RELAY_ENABLED) {
const protocol = location.protocol === "https:" ? "wss:" : "ws:";

View File

@ -6,8 +6,20 @@ import {
Mutes,
processTags,
} from "applesauce-core/helpers";
import { combineLatest, filter, map, mergeMap, Observable, share, tap } from "rxjs";
import {
combineLatest,
filter,
map,
Observable,
ReplaySubject,
share,
switchMap,
tap,
throttleTime,
timer,
} from "rxjs";
import { TimelineQuery, UserMuteQuery } from "applesauce-core/queries";
import { getContentPointers } from "applesauce-factory/helpers";
import { kinds, nip18, nip25, NostrEvent } from "nostr-tools";
import localSettings from "./local-settings";
@ -17,7 +29,6 @@ import { TORRENT_COMMENT_KIND } from "../helpers/nostr/torrents";
import accounts from "./accounts";
import { getThreadReferences, isReply, isRepost } from "../helpers/nostr/event";
import { getPubkeysMentionedInContent } from "../helpers/nostr/post";
import { getContentPointers } from "applesauce-factory/helpers";
export const NotificationTypeSymbol = Symbol("notificationType");
@ -145,7 +156,7 @@ async function handleShare(event: NostrEvent) {
}
const notifications$: Observable<CategorizedEvent[]> = combineLatest([accounts.active$]).pipe(
mergeMap(([account]) => {
switchMap(([account]) => {
if (!account) return [];
const timeline$ = queryStore
@ -164,7 +175,11 @@ const notifications$: Observable<CategorizedEvent[]> = combineLatest([accounts.a
],
})
.pipe(
filter(t => t!== undefined),
// filter out undefined
filter((t) => t !== undefined),
// update timeline at 30fps
throttleTime(1000 / 30),
// trigger logs of extra events
tap((timeline) => {
// handle loading dependencies of each event
for (const event of timeline) {
@ -179,13 +194,17 @@ const notifications$: Observable<CategorizedEvent[]> = combineLatest([accounts.a
}
}
}),
// categorize events
map((timeline) => timeline.map((e) => categorizeEvent(e, account.pubkey))),
);
const mute$ = queryStore.createQuery(UserMuteQuery, account.pubkey);
return combineLatest([timeline$, mute$]).pipe(
// filter events out by mutes
map(([timeline, mutes]) => filterEvents(timeline, account.pubkey, mutes)),
// keep the observable hot for 5 minutes after its unsubscribed
share({ connector: () => new ReplaySubject(1), resetOnComplete: () => timer(5 * 60_000) }),
);
}),
share(),

View File

@ -12,7 +12,6 @@ import {
FlexProps,
Heading,
IconButton,
Spacer,
Text,
} from "@chakra-ui/react";
import { useContext } from "react";
@ -24,13 +23,11 @@ import { getTagValue } from "../../../../helpers/nostr/event";
import DebugEventButton from "../../../../components/debug-modal/debug-event-button";
import SupportedNIPs from "../../../relays/components/supported-nips";
import RelayNotes from "../../../relays/relay-details/relay-notes";
import { safeRelayUrl } from "../../../../helpers/relay";
import { ExternalLinkIcon } from "../../../../components/icons";
import PeopleListProvider from "../../../../providers/local/people-list-provider";
import { getPubkeysFromList } from "../../../../helpers/nostr/lists";
import UserAvatarLink from "../../../../components/user/user-avatar-link";
import UserName from "../../../../components/user/user-name";
import UserDnsIdentity from "../../../../components/user/user-dns-identity";
import RelayFavicon from "../../../../components/relay-favicon";
export default function RelayStatusDetails({ event, ...props }: Omit<FlexProps, "children"> & { event: NostrEvent }) {
@ -40,7 +37,7 @@ export default function RelayStatusDetails({ event, ...props }: Omit<FlexProps,
const software = getTagValue(event, "s");
const version = event.tags.find((t) => t[0] === "l" && t[2] === "nip11.version")?.[1];
const url = identity ? safeRelayUrl(identity) : undefined;
const url = identity;
// gather labels
const misc: Record<string, string[]> = {};

View File

@ -1,6 +1,7 @@
import { memo, useCallback, useContext, useEffect, useMemo, useState } from "react";
import { Button, ButtonGroup, Flex, IconButton } from "@chakra-ui/react";
import { UNSAFE_DataRouterContext, useLocation, useNavigate } from "react-router-dom";
import { mergeRelaySets } from "applesauce-core/helpers";
import { NostrEvent, kinds } from "nostr-tools";
import { ThreadIcon } from "../../components/icons";
@ -24,8 +25,6 @@ import useRouterMarker from "../../hooks/use-router-marker";
import decryptionCacheService from "../../services/decryption-cache";
import UserDnsIdentityIcon from "../../components/user/user-dns-identity-icon";
import UserAvatarLink from "../../components/user/user-avatar-link";
import ContainedSimpleView from "../../components/layout/presets/contained-simple-view";
import { mergeRelaySets } from "../../helpers/relay";
import SimpleView from "../../components/layout/presets/simple-view";
/** This is broken out from DirectMessageChatPage for performance reasons. Don't use outside of file */

View File

@ -1,106 +0,0 @@
import { useState } from "react";
import {
Box,
Button,
ButtonGroup,
Code,
Flex,
FormControl,
FormLabel,
IconButton,
Input,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
ModalProps,
Text,
useDisclosure,
} from "@chakra-ui/react";
import { useDebounce } from "react-use";
import { useRelayInfo } from "../../../hooks/use-relay-info";
import UserAvatar from "../../../components/user/user-avatar";
import UserLink from "../../../components/user/user-link";
import UserDnsIdentity from "../../../components/user/user-dns-identity";
import { CodeIcon } from "../../../components/icons";
import { Metadata } from "./relay-card";
import { safeRelayUrl } from "../../../helpers/relay";
function RelayDetails({ url, debug }: { url: string; debug?: boolean }) {
const { info } = useRelayInfo(url);
if (!info) return null;
return (
<Box>
<Metadata name="Name">{info.name}</Metadata>
<Metadata name="URL">{url}</Metadata>
{info.pubkey && (
<Flex gap="2" alignItems="center">
<Text fontWeight="bold">Owner: </Text>
<UserAvatar pubkey={info.pubkey} size="xs" />
<UserLink pubkey={info.pubkey} />
<UserDnsIdentity pubkey={info.pubkey} onlyIcon />
</Flex>
)}
<Metadata name="NIPs">{info.supported_nips?.join(", ")}</Metadata>
{debug && (
<Code whiteSpace="pre" overflow="auto">
{JSON.stringify(info, null, 2)}
</Code>
)}
</Box>
);
}
export default function AddCustomRelayModal({
onSubmit,
...props
}: { onSubmit: (relay: string) => void } & Omit<ModalProps, "children">) {
const [url, setUrl] = useState("");
const [safeUrl, setSafeUrl] = useState<string>();
const showDebug = useDisclosure();
useDebounce(() => setSafeUrl(safeRelayUrl(url) ?? undefined), 1000, [url]);
return (
<Modal {...props}>
<ModalOverlay />
<ModalContent>
<ModalHeader p="4">Custom Relay</ModalHeader>
<ModalCloseButton />
<ModalBody px="4" py="0" display="flex" flexDirection="column" gap="2">
<FormControl>
<FormLabel>Relay URL</FormLabel>
<Input
type="url"
placeholder="wss://relay.example.com"
value={url}
onChange={(e) => setUrl(e.target.value)}
/>
</FormControl>
{safeUrl && <RelayDetails url={safeUrl} debug={showDebug.isOpen} />}
</ModalBody>
<ModalFooter p="4">
<ButtonGroup size="sm">
{safeUrl && (
<IconButton icon={<CodeIcon />} aria-label="Show JSON" onClick={showDebug.onToggle} variant="ghost" />
)}
<Button variant="ghost" onClick={props.onClose}>
Cancel
</Button>
<Button colorScheme="primary" onClick={() => safeUrl && onSubmit(safeUrl)} isDisabled={!safeUrl}>
Add
</Button>
</ButtonGroup>
</ModalFooter>
</ModalContent>
</Modal>
);
}

View File

@ -13,6 +13,7 @@ import {
Tag,
useDisclosure,
} from "@chakra-ui/react";
import { isSafeRelayURL, normalizeURL } from "applesauce-core/helpers";
import { useRelayInfo } from "../../../hooks/use-relay-info";
import { RelayDebugButton, RelayMetadata } from "../components/relay-card";
@ -23,7 +24,6 @@ import RelayNotes from "./relay-notes";
import PeopleListProvider from "../../../providers/local/people-list-provider";
import PeopleListSelection from "../../../components/people-list-selection/people-list-selection";
import RelayFavicon from "../../../components/relay-favicon";
import { safeRelayUrl } from "../../../helpers/relay";
import RelayUsersTab from "./relay-users";
const RelayDetailsTab = lazy(() => import("./relay-details"));
@ -108,12 +108,11 @@ export default function RelayDetailsView() {
const { relay } = useParams<string>();
if (!relay) return <>No relay url</>;
const safeUrl = safeRelayUrl(relay);
if (!safeUrl) return <>Bad relay url</>;
if (!isSafeRelayURL(relay)) return <>Bad relay url</>;
return (
<PeopleListProvider initList="global">
<RelayPage relay={safeUrl} />
<RelayPage relay={normalizeURL(relay)} />
</PeopleListProvider>
);
}

View File

@ -1,12 +1,12 @@
import { CloseIcon } from "@chakra-ui/icons";
import { Button, Code, Flex, Heading, IconButton, Input, Link, Select, Switch, Text } from "@chakra-ui/react";
import { Button, Flex, Heading, IconButton, Input, Link, Select, Switch, Text } from "@chakra-ui/react";
import { useForm } from "react-hook-form";
import { safeRelayUrl } from "applesauce-core/helpers";
import { useObservable } from "applesauce-react/hooks";
import useAsyncErrorHandler from "../../../../hooks/use-async-error-handler";
import { controlApi$ } from "../../../../services/bakery";
import RelayFavicon from "../../../../components/relay-favicon";
import { isSafeRelayURL, normalizeURL } from "applesauce-core/helpers";
function BroadcastRelay({ relay }: { relay: string }) {
const controlApi = useObservable(controlApi$);
@ -44,8 +44,10 @@ function AddRelayForm() {
const submit = handleSubmit((values) => {
if (!config) return;
const url = safeRelayUrl(values.url);
if (url) controlApi?.setConfigField("gossipBroadcastRelays", [...config.gossipBroadcastRelays, url]);
if (!isSafeRelayURL(values.url)) return;
const url = normalizeURL(values.url);
controlApi?.setConfigField("gossipBroadcastRelays", [...config.gossipBroadcastRelays, url]);
reset();
});

View File

@ -1,8 +1,9 @@
import { Button, Flex, FlexProps, useToast } from "@chakra-ui/react";
import { useForm } from "react-hook-form";
import { safeRelayUrl } from "../../../helpers/relay";
import { RelayUrlInput } from "../../../components/relay-url-input";
import { isSafeRelayURL } from "../../../../../applesauce/packages/core/dist/helpers/relays";
import { normalizeURL } from "applesauce-core/helpers";
export default function AddRelayForm({
onSubmit,
@ -21,9 +22,8 @@ export default function AddRelayForm({
const submit = handleSubmit(async (values) => {
try {
const url = safeRelayUrl(values.url);
if (!url) return;
await onSubmit(url);
if (!isSafeRelayURL(values.url)) return;
await onSubmit(normalizeURL(values.url));
reset();
} catch (error) {
if (error instanceof Error) toast({ status: "error", description: error.message });

View File

@ -4,7 +4,7 @@ import { WarningIcon } from "@chakra-ui/icons";
import { IdentityStatus } from "applesauce-loaders/helpers/dns-identity";
import { mergeRelaySets } from "applesauce-core/helpers";
import { RECOMMENDED_READ_RELAYS, RECOMMENDED_WRITE_RELAYS } from "../../../const";
import { JAPANESE_RELAYS, RECOMMENDED_READ_RELAYS, RECOMMENDED_WRITE_RELAYS } from "../../../const";
import AddRelayForm from "./add-relay-form";
import { useReadRelays, useWriteRelays } from "../../../hooks/use-client-relays";
import { useActiveAccount } from "applesauce-react/hooks";
@ -12,21 +12,12 @@ import RelayControl from "./relay-control";
import { getRelaysFromExt } from "../../../helpers/nip07";
import { useUserDNSIdentity } from "../../../hooks/use-user-dns-identity";
import useUserContactRelays from "../../../hooks/use-user-contact-relays";
import { safeRelayUrls } from "../../../helpers/relay";
import HoverLinkOverlay from "../../../components/hover-link-overlay";
import SimpleView from "../../../components/layout/presets/simple-view";
import localSettings from "../../../services/local-settings";
import { addAppRelay, RelayMode } from "../../../services/app-relays";
import useUserMailboxes from "../../../hooks/use-user-mailboxes";
const JAPANESE_RELAYS = safeRelayUrls([
"wss://r.kojira.io",
"wss://nrelay-jp.c-stellar.net",
"wss://nostr.fediverse.jp",
"wss://nostr.holybea.com",
"wss://relay-jp.nostr.wirednet.jp",
]);
function RelaySetCard({ label, read, write }: { label: string; read: Iterable<string>; write: Iterable<string> }) {
const handleClick = useCallback<MouseEventHandler>((e) => {
e.preventDefault();

View File

@ -18,9 +18,10 @@ import {
import { useNavigate } from "react-router-dom";
import { NostrEvent } from "nostr-tools";
import { NostrConnectSigner } from "applesauce-signers/signers/nostr-connect-signer";
import { parseNIP05Address, ProfileContent, safeParse } from "applesauce-core/helpers";
import { mergeRelaySets, parseNIP05Address, ProfileContent, safeParse } from "applesauce-core/helpers";
import { useAccountManager } from "applesauce-react/hooks";
import { NostrConnectAccount } from "applesauce-accounts/accounts";
import { IdentityStatus } from "applesauce-loaders/helpers/dns-identity";
import { NOSTR_CONNECT_PERMISSIONS } from "../../../const";
import useNip05Providers from "../../../hooks/use-nip05-providers";
@ -30,9 +31,7 @@ import { MetadataAvatar } from "../../../components/user/user-avatar";
import { ErrorBoundary } from "../../../components/error-boundary";
import dnsIdentityLoader from "../../../services/dns-identity-loader";
import useUserProfile from "../../../hooks/use-user-profile";
import { safeRelayUrls } from "../../../helpers/relay";
import { createNostrConnectConnection } from "../../../classes/nostr-connect-connection";
import { IdentityStatus } from "applesauce-loaders/helpers/dns-identity";
function ProviderCard({ onClick, provider }: { onClick: () => void; provider: NostrEvent }) {
const metadata = JSON.parse(provider.content) as ProfileContent;
@ -96,7 +95,7 @@ export default function LoginNostrAddressCreate() {
if (identity.name !== "_") throw new Error("Provider does not own the domain");
if (!identity.hasNip46) throw new Error("Provider does not support NIP-46");
const relays = safeRelayUrls(identity.nip46Relays || identity.relays || []);
const relays = mergeRelaySets(identity.nip46Relays || identity.relays || []);
if (relays.length === 0) throw new Error("Cant find providers relays");
const signer = new NostrConnectSigner({

View File

@ -5,13 +5,13 @@ import { NostrConnectSigner } from "applesauce-signers/signers/nostr-connect-sig
import { useAccountManager } from "applesauce-react/hooks";
import { NostrConnectAccount, ReadonlyAccount } from "applesauce-accounts/accounts";
import { Identity, IdentityStatus } from "applesauce-loaders/helpers/dns-identity";
import { mergeRelaySets } from "applesauce-core/helpers";
import { ReadonlySigner } from "applesauce-signers";
import { useDebounce } from "react-use";
import dnsIdentityLoader from "../../../services/dns-identity-loader";
import { CheckIcon } from "../../../components/icons";
import { NOSTR_CONNECT_PERMISSIONS } from "../../../const";
import { safeRelayUrls } from "../../../helpers/relay";
import { getMatchSimpleEmail } from "../../../helpers/regexp";
import QRCodeScannerButton from "../../../components/qr-code/qr-code-scanner-button";
import { createNostrConnectConnection } from "../../../classes/nostr-connect-connection";
@ -46,7 +46,7 @@ export default function LoginNostrAddressView() {
try {
if (nip05.hasNip46 && nip05.pubkey) {
setLoading("Connecting...");
const relays = safeRelayUrls(nip05.nip46Relays || nip05.relays || []);
const relays = mergeRelaySets(nip05.nip46Relays || nip05.relays || []);
const signer = new NostrConnectSigner({
pubkey: nip05.pubkey,
relays,