This commit is contained in:
hzrd149 2023-02-10 12:16:15 -06:00
parent a873d6fdb7
commit 5553bce0fe
22 changed files with 117 additions and 146 deletions

View File

@ -52,23 +52,20 @@
## TODO
- add `client` tag to published events
- Rebuild relays view to show relay info and settings NIP-11
- add button for creating lightning invoice via WebLN
- make app a valid web share target https://developer.chrome.com/articles/web-share-target/
- make app handle image files
- block notes based on content
- implement NIP-56 and blocking
- allow user to select relay or following list when fetching replies (default to my relays + following?)
- massive thread note1dapvuu8fl09yjtg2gyr2h6nypaffl2sq0aj5raz86463qk5kpyzqlxvtc3
- sort replies by date
- filter list of followers by users the user has blocked/reported (stops bots/spammers from showing up at followers)
- Add client side relay groups
- Add mentions in posts (https://css-tricks.com/so-you-want-to-build-an-mention-autocomplete-feature/)
- Add note embeds
- Add "repost" button that mentions the note
- Add preview tab to note modal
- Add mentions in posts (https://css-tricks.com/so-you-want-to-build-an-mention-autocomplete-feature/)
- add `client` tag to published events
- Save note drafts and let users manage them
- Add support for relay favicons
- make app a valid web share target https://developer.chrome.com/articles/web-share-target/
- implement NIP-56 and blocking
- block notes based on content
## Setup

View File

@ -1,5 +1,5 @@
import { Subject, Subscription } from "rxjs";
import { relayPool } from "../services/relays";
import relayPoolService from "../services/relay-pool";
import { NostrEvent } from "../types/nostr-event";
export type PostResult = { url: string; message?: string; status: boolean };
@ -9,7 +9,7 @@ export function nostrPostAction(relays: string[], event: NostrEvent, timeout: nu
let remaining = new Set<Subscription>();
for (const url of relays) {
const relay = relayPool.requestRelay(url);
const relay = relayPoolService.requestRelay(url);
const sub = relay.onCommandResult.subscribe((result) => {
if (result.eventId === event.id) {

View File

@ -1,8 +1,8 @@
import { Subject, Subscription as RxSubscription } from "rxjs";
import { NostrEvent } from "../types/nostr-event";
import { NostrQuery } from "../types/nostr-query";
import { Relay } from "../services/relays";
import relayPool from "../services/relays/relay-pool";
import relayPoolService from "../services/relay-pool";
import { Relay } from "./relay";
let lastId = 0;
@ -22,7 +22,7 @@ export class NostrRequest {
constructor(relayUrls: string[], timeout?: number) {
this.id = `request-${lastId++}`;
this.relays = new Set(relayUrls.map((url) => relayPool.requestRelay(url)));
this.relays = new Set(relayUrls.map((url) => relayPoolService.requestRelay(url)));
for (const relay of this.relays) {
const cleanup: RxSubscription[] = [];

View File

@ -1,9 +1,8 @@
import { Subject, SubscriptionLike } from "rxjs";
import { NostrEvent } from "../types/nostr-event";
import { NostrOutgoingMessage, NostrQuery } from "../types/nostr-query";
import { Relay } from "../services/relays";
import { IncomingEvent } from "../services/relays/relay";
import relayPool from "../services/relays/relay-pool";
import { IncomingEvent, Relay } from "./relay";
import relayPoolService from "../services/relay-pool";
let lastId = 0;
@ -27,7 +26,7 @@ export class NostrSubscription {
this.name = name;
this.relayUrls = relayUrls;
this.relays = relayUrls.map((url) => relayPool.requestRelay(url));
this.relays = relayUrls.map((url) => relayPoolService.requestRelay(url));
}
private handleEvent(event: IncomingEvent) {
if (this.state === NostrSubscription.OPEN && event.subId === this.id && !this.seenEvents.has(event.body.id)) {
@ -51,7 +50,7 @@ export class NostrSubscription {
}
for (const url of this.relayUrls) {
relayPool.addClaim(url, this);
relayPoolService.addClaim(url, this);
}
}
/** listen for event and open events from relays */
@ -60,7 +59,7 @@ export class NostrSubscription {
this.cleanup.clear();
for (const url of this.relayUrls) {
relayPool.removeClaim(url, this);
relayPoolService.removeClaim(url, this);
}
}
@ -88,7 +87,7 @@ export class NostrSubscription {
}
setRelays(relays: string[]) {
this.unsubscribeFromRelays();
const newRelays = relays.map((url) => relayPool.requestRelay(url));
const newRelays = relays.map((url) => relayPoolService.requestRelay(url));
for (const relay of this.relays) {
if (!newRelays.includes(relay)) {

View File

@ -1,6 +1,6 @@
import { Subject } from "rxjs";
import { RawIncomingNostrEvent, NostrEvent } from "../../types/nostr-event";
import { NostrOutgoingMessage } from "../../types/nostr-query";
import { RawIncomingNostrEvent, NostrEvent } from "../types/nostr-event";
import { NostrOutgoingMessage } from "../types/nostr-query";
export type IncomingEvent = {
type: "EVENT";

View File

@ -1,93 +0,0 @@
import { Subject } from "rxjs";
import { NostrEvent } from "../types/nostr-event";
import { NostrQuery } from "../types/nostr-query";
import { NostrRequest } from "./nostr-request";
function mergeSets<T extends unknown>(to: Set<T>, from: Iterable<T>) {
for (const el of from) {
to.add(el);
}
}
export type getQueryKeyFn<QueryT> = (query: QueryT) => string;
export type mergeQueriesFn<QueryT> = (a: QueryT, b: QueryT) => QueryT | undefined | null;
export type getEventQueryKeyFn = (event: NostrEvent) => string;
type PendingRequest<QueryT = NostrQuery> = {
query: QueryT;
subject: Subject<NostrEvent>;
relays: Set<string>;
};
/** @deprecated incomplete */
export class RequestManager<QueryT extends NostrQuery> {
private getQueryKey: getQueryKeyFn<QueryT>;
private mergeQueries: mergeQueriesFn<QueryT>;
private getEventQueryKey: getEventQueryKeyFn;
private runningRequests = new Map<string, NostrRequest>();
private requestQueue = new Map<string, PendingRequest<QueryT>>();
constructor(
getQueryKey: getQueryKeyFn<QueryT>,
mergeQueries: mergeQueriesFn<QueryT>,
getEventQueryKey: getEventQueryKeyFn
) {
this.getQueryKey = getQueryKey;
this.mergeQueries = mergeQueries;
this.getEventQueryKey = getEventQueryKey;
}
request(query: QueryT, relays: string[]) {
const key = this.getQueryKey(query);
if (this.runningRequests.has(key)) throw new Error("requesting a currently running query");
const pending = this.requestQueue.get(key);
if (pending) {
mergeSets(pending.relays, relays);
return pending.subject;
}
const subject = new Subject<NostrEvent>();
this.requestQueue.set(key, {
query,
relays: new Set(relays),
subject,
});
return subject;
}
batch() {
const requests: PendingRequest<QueryT>[] = [];
for (const [key, pending] of this.requestQueue) {
let wasMerged = false;
if (requests.length > 0) {
for (const request of requests) {
const merged = this.mergeQueries(request.query, pending.query);
if (merged) {
request.query = merged;
request.subject.subscribe(pending.subject);
mergeSets(request.relays, pending.relays);
wasMerged = true;
break;
}
}
}
// if there are no requests. or pending failed to merge create new request
if (!wasMerged) {
const subject = new Subject<NostrEvent>();
subject.subscribe(pending.subject);
requests.push({ query: pending.query, subject, relays: pending.relays });
}
}
for (const request of requests) {
const r = new NostrRequest(Array.from(request.relays));
r.onEvent.subscribe(request.subject);
r.start(request.query);
}
}
}

View File

@ -10,20 +10,20 @@ import {
ModalCloseButton,
Button,
} from "@chakra-ui/react";
import { Relay } from "../services/relays";
import relayPool from "../services/relays/relay-pool";
import relayPoolService from "../services/relay-pool";
import { useInterval } from "react-use";
import { RelayStatus } from "./relay-status";
import { useIsMobile } from "../hooks/use-is-mobile";
import { RelayIcon } from "./icons";
import { Relay } from "../classes/relay";
export const ConnectedRelays = () => {
const isMobile = useIsMobile();
const { isOpen, onOpen, onClose } = useDisclosure();
const [relays, setRelays] = useState<Relay[]>(relayPool.getRelays());
const [relays, setRelays] = useState<Relay[]>(relayPoolService.getRelays());
useInterval(() => {
setRelays(relayPool.getRelays());
setRelays(relayPoolService.getRelays());
}, 1000);
const connected = relays.filter((relay) => relay.okay);

View File

@ -1,5 +1,5 @@
import React, { useState } from "react";
import { Box, Button, ButtonGroup, Heading, IconButton, Text } from "@chakra-ui/react";
import { Box, Button, ButtonGroup, IconButton, Text } from "@chakra-ui/react";
import { requestProvider } from "webln";
import { getReadableAmount, parsePaymentRequest } from "../helpers/bolt11";
import { useAsync } from "react-use";

View File

@ -16,11 +16,11 @@ import { nostrPostAction } from "../../classes/nostr-post-action";
import { NostrRequest } from "../../classes/nostr-request";
import useSubject from "../../hooks/use-subject";
import { getEventRelays, handleEventFromRelay } from "../../services/event-relays";
import { relayPool } from "../../services/relays";
import { NostrEvent } from "../../types/nostr-event";
import { RelayIcon, SearchIcon } from "../icons";
import { RelayFavicon } from "../relay-favicon";
import { useReadRelayUrls, useWriteRelayUrls } from "../../hooks/use-client-relays";
import relayPoolService from "../../services/relay-pool";
export type NoteRelaysProps = Omit<IconButtonProps, "icon" | "aria-label"> & {
event: NostrEvent;
@ -56,7 +56,7 @@ export const NoteRelays = memo(({ event, ...props }: NoteRelaysProps) => {
action.subscribe({
next: (result) => {
if (result.status) {
handleEventFromRelay(relayPool.requestRelay(result.url, false), event);
handleEventFromRelay(relayPoolService.requestRelay(result.url, false), event);
}
},
complete: () => {

View File

@ -1,6 +1,7 @@
import { Badge, useForceUpdate } from "@chakra-ui/react";
import { useInterval } from "react-use";
import { Relay, relayPool } from "../services/relays";
import { Relay } from "../classes/relay";
import relayPoolService from "../services/relay-pool";
const getStatusText = (relay: Relay) => {
if (relay.connecting) return "Connecting...";
@ -20,7 +21,7 @@ const getStatusColor = (relay: Relay) => {
export const RelayStatus = ({ url }: { url: string }) => {
const update = useForceUpdate();
const relay = relayPool.requestRelay(url, false);
const relay = relayPoolService.requestRelay(url, false);
useInterval(() => update(), 500);

View File

@ -1,7 +1,7 @@
import moment from "moment";
import { getEventRelays } from "../services/event-relays";
import { DraftNostrEvent, isETag, isPTag, NostrEvent, RTag } from "../types/nostr-event";
import { RelayConfig, RelayMode } from "../services/relays/relay";
import { RelayConfig, RelayMode } from "../classes/relay";
export function isReply(event: NostrEvent | DraftNostrEvent) {
return !!event.tags.find((tag) => isETag(tag) && tag[3] !== "mention");

View File

@ -0,0 +1,8 @@
import { useAsync } from "react-use";
import relayInfoService from "../services/relay-info";
export function useRelayInfo(relay: string) {
const { value: info, loading, error } = useAsync(() => relayInfoService.getInfo(relay));
return { info, loading, error };
}

View File

@ -1,6 +1,6 @@
import { unique } from "../helpers/array";
import clientRelaysService from "../services/client-relays";
import { RelayMode } from "../services/relays/relay";
import { RelayMode } from "../classes/relay";
import useSubject from "./use-subject";
export function useClientRelays(mode: RelayMode = RelayMode.READ) {

View File

@ -4,7 +4,7 @@ import { nostrPostAction } from "../classes/nostr-post-action";
import { unique } from "../helpers/array";
import { DraftNostrEvent, RTag } from "../types/nostr-event";
import identity from "./identity";
import { RelayConfig, RelayMode } from "./relays/relay";
import { RelayConfig, RelayMode } from "../classes/relay";
import userRelaysService from "./user-relays";
export type RelayDirectory = Record<string, { read: boolean; write: boolean }>;

View File

@ -34,10 +34,10 @@ const MIGRATIONS: MigrationFunction[] = [
dnsIdentifiers.createIndex("domain", "domain", { unique: false });
dnsIdentifiers.createIndex("updated", "updated", { unique: false });
const pubkeyRelayWeights = db.createObjectStore("pubkeyRelayWeights", { keyPath: "pubkey" });
db.createObjectStore("pubkeyRelayWeights", { keyPath: "pubkey" });
// setup data
const settings = db.createObjectStore("settings");
db.createObjectStore("settings");
db.createObjectStore("relayInfo");
},
];
@ -65,6 +65,7 @@ export async function clearCacheData() {
}
export async function deleteDatabase() {
db.close();
await deleteDB(dbName);
window.location.reload();
}

View File

@ -1,6 +1,6 @@
import { DBSchema } from "idb";
import { NostrEvent } from "../../types/nostr-event";
import { RelayConfig } from "../relays/relay";
import { RelayInformationDocument } from "../relay-info";
export interface CustomSchema extends DBSchema {
userMetadata: {
@ -21,7 +21,7 @@ export interface CustomSchema extends DBSchema {
};
userRelays: {
key: string;
value: { pubkey: string; relays: {url: string, mode: number}[]; created_at: number };
value: { pubkey: string; relays: { url: string; mode: number }[]; created_at: number };
indexes: { created_at: number };
};
dnsIdentifiers: {
@ -29,6 +29,7 @@ export interface CustomSchema extends DBSchema {
value: { name: string; domain: string; pubkey: string; relays: string[]; updated: number };
indexes: { name: string; domain: string; pubkey: string; updated: number };
};
relayInfo: { key: string; value: RelayInformationDocument };
pubkeyRelayWeights: {
key: string;
value: { pubkey: string; relays: Record<string, number>; updated: number };

View File

@ -1,6 +1,7 @@
import { BehaviorSubject } from "rxjs";
import { Relay } from "../classes/relay";
import { NostrEvent } from "../types/nostr-event";
import { Relay, relayPool } from "./relays";
import relayPoolService from "./relay-pool";
const eventRelays = new Map<string, BehaviorSubject<string[]>>();
@ -21,7 +22,7 @@ export function handleEventFromRelay(relay: Relay, event: NostrEvent) {
}
}
relayPool.onRelayCreated.subscribe((relay) => {
relayPoolService.onRelayCreated.subscribe((relay) => {
relay.onEvent.subscribe(({ body: event }) => {
handleEventFromRelay(relay, event);
});

View File

@ -0,0 +1,57 @@
import db from "./db";
export type RelayInformationDocument = {
name: string;
description: string;
pubkey: string;
contact: string;
supported_nips: string;
software: string;
version: string;
};
export type DnsIdentity = {
name: string;
domain: string;
pubkey: string;
relays: string[];
};
async function fetchInfo(relay: string) {
const url = new URL(relay);
url.protocol = url.protocol === "ws:" ? "http" : "https";
const infoDoc = await fetch(url, { headers: { Accept: "application/nostr+json" } }).then(
(res) => res.json() as Promise<RelayInformationDocument>
);
await db.put("relayInfo", infoDoc, relay);
return infoDoc;
}
async function getInfo(relay: string) {
const cached = await db.get("relayInfo", relay);
if (cached) return cached;
// TODO: if it fails, maybe cache a failure message
return fetchInfo(relay);
}
const pending: Record<string, ReturnType<typeof getInfo> | undefined> = {};
function dedupedGetIdentity(relay: string) {
const request = pending[relay];
if (request) return request;
return (pending[relay] = getInfo(relay));
}
export const relayInfoService = {
fetchInfo,
getInfo: dedupedGetIdentity,
};
if (import.meta.env.DEV) {
// @ts-ignore
window.relayInfoService = relayInfoService;
}
export default relayInfoService;

View File

@ -1,7 +1,7 @@
import { Subject } from "rxjs";
import { Relay } from "./relay";
import { Relay } from "../classes/relay";
export class RelayPool {
export class RelayPoolService {
relays = new Map<string, Relay>();
relayClaims = new Map<string, Set<any>>();
onRelayCreated = new Subject<Relay>();
@ -64,11 +64,11 @@ export class RelayPool {
}
}
const relayPool = new RelayPool();
const relayPoolService = new RelayPoolService();
if (import.meta.env.DEV) {
// @ts-ignore
window.relayPool = relayPool;
window.relayPoolService = relayPoolService;
}
export default relayPool;
export default relayPoolService;

View File

@ -1,4 +0,0 @@
import relayPool, { RelayPool } from "./relay-pool";
import { Relay } from "./relay";
export { relayPool, Relay, RelayPool };

View File

@ -3,7 +3,7 @@ import { NostrSubscription } from "../classes/nostr-subscription";
import { PubkeySubjectCache } from "../classes/pubkey-subject-cache";
import { isRTag, NostrEvent } from "../types/nostr-event";
import { NostrQuery } from "../types/nostr-query";
import { RelayConfig } from "./relays/relay";
import { RelayConfig } from "../classes/relay";
import { parseRTag } from "../helpers/nostr-event";
import clientRelaysService from "./client-relays";

View File

@ -19,13 +19,16 @@ import { TrashIcon, UndoIcon } from "../../components/icons";
import useSubject from "../../hooks/use-subject";
import { RelayFavicon } from "../../components/relay-favicon";
import clientRelaysService from "../../services/client-relays";
import { RelayConfig, RelayMode } from "../../services/relays/relay";
import { RelayConfig, RelayMode } from "../../classes/relay";
import { useList } from "react-use";
import { RelayUrlInput } from "../../components/relay-url-input";
import { useRelayInfo } from "../../hooks/use-client-relays copy";
export const RelaysView = () => {
const relays = useSubject(clientRelaysService.relays);
const info = useRelayInfo("wss://nostr.wine");
const [pendingAdd, addActions] = useList<RelayConfig>([]);
const [pendingRemove, removeActions] = useList<RelayConfig>([]);