mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-29 11:12:12 +01:00
replace rxjs and update everything
This commit is contained in:
parent
bfbebdb37a
commit
63dec9eb2c
@ -26,7 +26,6 @@
|
||||
"react-router-dom": "^6.5.0",
|
||||
"react-singleton-hook": "^4.0.1",
|
||||
"react-use": "^17.4.0",
|
||||
"rxjs": "^7.8.0",
|
||||
"webln": "^0.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -6,8 +6,7 @@ import { Page } from "./components/page";
|
||||
import { SettingsView } from "./views/settings";
|
||||
import { LoginView } from "./views/login";
|
||||
import { ProfileView } from "./views/profile";
|
||||
import useSubject from "./hooks/use-subject";
|
||||
import identity from "./services/identity";
|
||||
import identityService from "./services/identity";
|
||||
import { FollowingTab } from "./views/home/following-tab";
|
||||
import { DiscoverTab } from "./views/home/discover-tab";
|
||||
import { GlobalTab } from "./views/home/global-tab";
|
||||
@ -23,10 +22,11 @@ import { LoginStartView } from "./views/login/start";
|
||||
import { LoginNpubView } from "./views/login/npub";
|
||||
import NotificationsView from "./views/notifications";
|
||||
import { RelaysView } from "./views/relays";
|
||||
import useSubject from "./hooks/use-subject";
|
||||
|
||||
const RequireSetup = ({ children }: { children: JSX.Element }) => {
|
||||
let location = useLocation();
|
||||
const setup = useSubject(identity.setup);
|
||||
const setup = useSubject(identityService.setup);
|
||||
|
||||
if (!setup) return <Navigate to="/login" state={{ from: location.pathname }} replace />;
|
||||
|
||||
|
@ -2,6 +2,7 @@ import { NostrEvent } from "../types/nostr-event";
|
||||
import { PubkeyEventRequester } from "./pubkey-event-requester";
|
||||
|
||||
export class CachedPubkeyEventRequester extends PubkeyEventRequester {
|
||||
private readCacheDedupe = new Map<string, Promise<NostrEvent | undefined>>();
|
||||
async readCache(pubkey: string): Promise<NostrEvent | undefined> {
|
||||
return undefined;
|
||||
}
|
||||
@ -16,7 +17,13 @@ export class CachedPubkeyEventRequester extends PubkeyEventRequester {
|
||||
const sub = this.getSubject(pubkey);
|
||||
|
||||
if (!sub.value || alwaysRequest) {
|
||||
this.readCache(pubkey).then((cached) => {
|
||||
// only call this.readCache once per pubkey
|
||||
const promise = this.readCacheDedupe.get(pubkey) || this.readCache(pubkey);
|
||||
this.readCacheDedupe.set(pubkey, promise);
|
||||
|
||||
promise.then((cached) => {
|
||||
this.readCacheDedupe.delete(pubkey);
|
||||
|
||||
if (cached && (!sub.value || cached.created_at > sub.value.created_at)) {
|
||||
sub.next(cached);
|
||||
}
|
||||
|
21
src/classes/deferred.ts
Normal file
21
src/classes/deferred.ts
Normal file
@ -0,0 +1,21 @@
|
||||
export type Deferred<T> = Promise<T> & {
|
||||
resolve: (value?: T | PromiseLike<T>) => void;
|
||||
reject: (reason?: any) => void;
|
||||
};
|
||||
|
||||
export default function createDefer<T>() {
|
||||
let _resolve: (value?: T | PromiseLike<T>) => void;
|
||||
let _reject: (reason?: any) => void;
|
||||
const promise = new Promise<T>((resolve, reject) => {
|
||||
// @ts-ignore
|
||||
_resolve = resolve;
|
||||
_reject = reject;
|
||||
}) as Deferred<T>;
|
||||
|
||||
// @ts-ignore
|
||||
promise.resolve = _resolve;
|
||||
// @ts-ignore
|
||||
promise.reject = _reject;
|
||||
|
||||
return promise;
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { Subject, SubscriptionLike } from "rxjs";
|
||||
import { Subject } from "./subject";
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import { NostrOutgoingMessage, NostrQuery } from "../types/nostr-query";
|
||||
import { IncomingEvent, Relay } from "./relay";
|
||||
@ -40,13 +40,10 @@ export class NostrMultiSubscription {
|
||||
}
|
||||
}
|
||||
|
||||
private cleanup = new Map<Relay, SubscriptionLike>();
|
||||
/** listen for event and open events from relays */
|
||||
private subscribeToRelays() {
|
||||
for (const relay of this.relays) {
|
||||
if (!this.cleanup.has(relay)) {
|
||||
this.cleanup.set(relay, relay.onEvent.subscribe(this.handleEvent.bind(this)));
|
||||
}
|
||||
relay.onEvent.subscribe(this.handleEvent, this);
|
||||
}
|
||||
|
||||
for (const url of this.relayUrls) {
|
||||
@ -55,8 +52,9 @@ export class NostrMultiSubscription {
|
||||
}
|
||||
/** listen for event and open events from relays */
|
||||
private unsubscribeFromRelays() {
|
||||
this.cleanup.forEach((sub) => sub.unsubscribe());
|
||||
this.cleanup.clear();
|
||||
for (const relay of this.relays) {
|
||||
relay.onEvent.unsubscribe(this.handleEvent, this);
|
||||
}
|
||||
|
||||
for (const url of this.relayUrls) {
|
||||
relayPoolService.removeClaim(url, this);
|
||||
|
@ -1,17 +1,20 @@
|
||||
import { Subject, Subscription } from "rxjs";
|
||||
import relayPoolService from "../services/relay-pool";
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import Deferred from "./deferred";
|
||||
import { IncomingCommandResult, Relay } from "./relay";
|
||||
import { ListenerFn, Subject } from "./subject";
|
||||
|
||||
export type PostResult = { url: string; message?: string; status: boolean };
|
||||
|
||||
export function nostrPostAction(relays: string[], event: NostrEvent, timeout: number = 5000) {
|
||||
const subject = new Subject<PostResult>();
|
||||
let remaining = new Set<Subscription>();
|
||||
const onComplete = new Deferred<void>();
|
||||
const remaining = new Map<Relay, ListenerFn<IncomingCommandResult>>();
|
||||
|
||||
for (const url of relays) {
|
||||
const relay = relayPoolService.requestRelay(url);
|
||||
|
||||
const sub = relay.onCommandResult.subscribe((result) => {
|
||||
const handler = (result: IncomingCommandResult) => {
|
||||
if (result.eventId === event.id) {
|
||||
subject.next({
|
||||
url,
|
||||
@ -19,12 +22,13 @@ export function nostrPostAction(relays: string[], event: NostrEvent, timeout: nu
|
||||
message: result.message,
|
||||
});
|
||||
|
||||
sub.unsubscribe();
|
||||
remaining.delete(sub);
|
||||
if (remaining.size === 0) subject.complete();
|
||||
relay.onCommandResult.unsubscribe(handler);
|
||||
remaining.delete(relay);
|
||||
if (remaining.size === 0) onComplete.resolve();
|
||||
}
|
||||
});
|
||||
remaining.add(sub);
|
||||
};
|
||||
relay.onCommandResult.subscribe(handler);
|
||||
remaining.set(relay, handler);
|
||||
|
||||
// send event
|
||||
relay.send(["EVENT", event]);
|
||||
@ -32,12 +36,15 @@ export function nostrPostAction(relays: string[], event: NostrEvent, timeout: nu
|
||||
|
||||
setTimeout(() => {
|
||||
if (remaining.size > 0) {
|
||||
for (const sub of remaining) {
|
||||
sub.unsubscribe();
|
||||
for (const [relay, handler] of remaining) {
|
||||
relay.onCommandResult.unsubscribe(handler);
|
||||
}
|
||||
subject.complete();
|
||||
onComplete.resolve();
|
||||
}
|
||||
}, timeout);
|
||||
|
||||
return subject;
|
||||
return {
|
||||
results: subject,
|
||||
onComplete,
|
||||
};
|
||||
}
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { Subject, Subscription as RxSubscription } from "rxjs";
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import { NostrQuery } from "../types/nostr-query";
|
||||
import relayPoolService from "../services/relay-pool";
|
||||
import { Relay } from "./relay";
|
||||
import { IncomingEOSE, IncomingEvent, Relay } from "./relay";
|
||||
import Subject from "./subject";
|
||||
import Deferred from "./deferred";
|
||||
|
||||
let lastId = 0;
|
||||
|
||||
@ -15,9 +16,10 @@ export class NostrRequest {
|
||||
id: string;
|
||||
timeout: number;
|
||||
relays: Set<Relay>;
|
||||
relayCleanup = new Map<Relay, RxSubscription[]>();
|
||||
relayCleanup = new Map<Relay, Function>();
|
||||
state = NostrRequest.IDLE;
|
||||
onEvent = new Subject<NostrEvent>();
|
||||
onComplete = new Deferred<void>();
|
||||
seenEvents = new Set<string>();
|
||||
|
||||
constructor(relayUrls: string[], timeout?: number) {
|
||||
@ -25,26 +27,25 @@ export class NostrRequest {
|
||||
this.relays = new Set(relayUrls.map((url) => relayPoolService.requestRelay(url)));
|
||||
|
||||
for (const relay of this.relays) {
|
||||
const cleanup: RxSubscription[] = [];
|
||||
const handleEOSE = (event: IncomingEOSE) => {
|
||||
if (event.subId === this.id) {
|
||||
this.handleEndOfEvents(relay);
|
||||
}
|
||||
};
|
||||
relay.onEOSE.subscribe(handleEOSE);
|
||||
|
||||
cleanup.push(
|
||||
relay.onEOSE.subscribe((event) => {
|
||||
if (event.subId === this.id) {
|
||||
this.handleEndOfEvents(relay);
|
||||
}
|
||||
})
|
||||
);
|
||||
const handleEvent = (event: IncomingEvent) => {
|
||||
if (this.state === NostrRequest.RUNNING && event.subId === this.id && !this.seenEvents.has(event.body.id)) {
|
||||
this.onEvent.next(event.body);
|
||||
this.seenEvents.add(event.body.id);
|
||||
}
|
||||
};
|
||||
relay.onEvent.subscribe(handleEvent);
|
||||
|
||||
cleanup.push(
|
||||
relay.onEvent.subscribe((event) => {
|
||||
if (this.state === NostrRequest.RUNNING && event.subId === this.id && !this.seenEvents.has(event.body.id)) {
|
||||
this.onEvent.next(event.body);
|
||||
this.seenEvents.add(event.body.id);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this.relayCleanup.set(relay, cleanup);
|
||||
this.relayCleanup.set(relay, () => {
|
||||
relay.onEOSE.unsubscribe(handleEOSE);
|
||||
relay.onEvent.unsubscribe(handleEvent);
|
||||
});
|
||||
}
|
||||
|
||||
this.timeout = timeout ?? REQUEST_DEFAULT_TIMEOUT;
|
||||
@ -54,12 +55,12 @@ export class NostrRequest {
|
||||
this.relays.delete(relay);
|
||||
relay.send(["CLOSE", this.id]);
|
||||
|
||||
const cleanup = this.relayCleanup.get(relay) ?? [];
|
||||
for (const fn of cleanup) fn.unsubscribe();
|
||||
const cleanup = this.relayCleanup.get(relay);
|
||||
if (cleanup) cleanup();
|
||||
|
||||
if (this.relays.size === 0) {
|
||||
this.state = NostrRequest.COMPLETE;
|
||||
this.onEvent.complete();
|
||||
this.onComplete.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,12 +88,12 @@ export class NostrRequest {
|
||||
for (const relay of this.relays) {
|
||||
relay.send(["CLOSE", this.id]);
|
||||
}
|
||||
for (const [relay, fns] of this.relayCleanup) {
|
||||
for (const fn of fns) fn.unsubscribe();
|
||||
for (const [relay, cleanup] of this.relayCleanup) {
|
||||
if (cleanup) cleanup();
|
||||
}
|
||||
this.relayCleanup = new Map();
|
||||
this.relays = new Set();
|
||||
this.onEvent.complete();
|
||||
this.onComplete.resolve();
|
||||
|
||||
console.log(`NostrRequest: ${this.id} complete`);
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { Subject, SubscriptionLike } from "rxjs";
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import { NostrOutgoingMessage, NostrQuery } from "../types/nostr-query";
|
||||
import { IncomingEOSE, IncomingEvent, Relay } from "./relay";
|
||||
import relayPoolService from "../services/relay-pool";
|
||||
import { Subject } from "./subject";
|
||||
|
||||
let lastId = 10000;
|
||||
|
||||
@ -26,20 +26,26 @@ export class NostrSubscription {
|
||||
|
||||
this.relay = relayPoolService.requestRelay(relayUrl);
|
||||
|
||||
this.relay.onEvent.subscribe(this.handleEvent.bind(this));
|
||||
this.relay.onEOSE.subscribe(this.handleEOSE.bind(this));
|
||||
this.onEvent.connectWithHandler(this.relay.onEvent, (event, next) => {
|
||||
if (this.state === NostrSubscription.OPEN) next(event.body);
|
||||
});
|
||||
this.onEOSE.connectWithHandler(this.relay.onEOSE, (eose, next) => {
|
||||
if (this.state === NostrSubscription.OPEN) next(eose);
|
||||
});
|
||||
// this.relay.onEvent.subscribe(this.handleEvent.bind(this));
|
||||
// this.relay.onEOSE.subscribe(this.handleEOSE.bind(this));
|
||||
}
|
||||
|
||||
private handleEvent(event: IncomingEvent) {
|
||||
if (this.state === NostrSubscription.OPEN && event.subId === this.id) {
|
||||
this.onEvent.next(event.body);
|
||||
}
|
||||
}
|
||||
private handleEOSE(eose: IncomingEOSE) {
|
||||
if (this.state === NostrSubscription.OPEN && eose.subId === this.id) {
|
||||
this.onEOSE.next(eose);
|
||||
}
|
||||
}
|
||||
// private handleEvent(event: IncomingEvent) {
|
||||
// if (this.state === NostrSubscription.OPEN && event.subId === this.id) {
|
||||
// this.onEvent.next(event.body);
|
||||
// }
|
||||
// }
|
||||
// private handleEOSE(eose: IncomingEOSE) {
|
||||
// if (this.state === NostrSubscription.OPEN && eose.subId === this.id) {
|
||||
// this.onEOSE.next(eose);
|
||||
// }
|
||||
// }
|
||||
|
||||
send(message: NostrOutgoingMessage) {
|
||||
this.relay.send(message);
|
||||
|
@ -1,8 +1,8 @@
|
||||
import moment from "moment";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { NostrSubscription } from "./nostr-subscription";
|
||||
import { SuperMap } from "./super-map";
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import Subject from "./subject";
|
||||
|
||||
type pubkey = string;
|
||||
type relay = string;
|
||||
@ -11,9 +11,7 @@ class PubkeyEventRequestSubscription {
|
||||
private subscription: NostrSubscription;
|
||||
private kind: number;
|
||||
|
||||
private subjects = new SuperMap<pubkey, BehaviorSubject<NostrEvent | undefined>>(
|
||||
() => new BehaviorSubject<NostrEvent | undefined>(undefined)
|
||||
);
|
||||
private subjects = new SuperMap<pubkey, Subject<NostrEvent>>(() => new Subject<NostrEvent>());
|
||||
|
||||
private requestNext = new Set<pubkey>();
|
||||
|
||||
@ -95,9 +93,7 @@ class PubkeyEventRequestSubscription {
|
||||
export class PubkeyEventRequester {
|
||||
private kind: number;
|
||||
private name?: string;
|
||||
private subjects = new SuperMap<pubkey, BehaviorSubject<NostrEvent | undefined>>(
|
||||
() => new BehaviorSubject<NostrEvent | undefined>(undefined)
|
||||
);
|
||||
private subjects = new SuperMap<pubkey, Subject<NostrEvent>>(() => new Subject<NostrEvent>());
|
||||
|
||||
private subscriptions = new SuperMap<relay, PubkeyEventRequestSubscription>(
|
||||
(relay) => new PubkeyEventRequestSubscription(relay, this.kind, this.name)
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import Subject from "./subject";
|
||||
|
||||
export class PubkeySubjectCache<T> {
|
||||
subjects = new Map<string, BehaviorSubject<T | null>>();
|
||||
subjects = new Map<string, Subject<T | null>>();
|
||||
relays = new Map<string, Set<string>>();
|
||||
dirty = false;
|
||||
|
||||
@ -11,7 +11,7 @@ export class PubkeySubjectCache<T> {
|
||||
getSubject(pubkey: string) {
|
||||
let subject = this.subjects.get(pubkey);
|
||||
if (!subject) {
|
||||
subject = new BehaviorSubject<T | null>(null);
|
||||
subject = new Subject<T | null>(null);
|
||||
this.subjects.set(pubkey, subject);
|
||||
this.dirty = true;
|
||||
}
|
||||
@ -43,7 +43,7 @@ export class PubkeySubjectCache<T> {
|
||||
prune() {
|
||||
const prunedKeys: string[] = [];
|
||||
for (const [key, subject] of this.subjects) {
|
||||
if (!subject.observed) {
|
||||
if (!subject.hasListeners) {
|
||||
this.subjects.delete(key);
|
||||
this.relays.delete(key);
|
||||
prunedKeys.push(key);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Subject } from "rxjs";
|
||||
import { RawIncomingNostrEvent, NostrEvent } from "../types/nostr-event";
|
||||
import { NostrOutgoingMessage } from "../types/nostr-query";
|
||||
import { Subject } from "./subject";
|
||||
|
||||
export type IncomingEvent = {
|
||||
type: "EVENT";
|
||||
|
100
src/classes/subject.ts
Normal file
100
src/classes/subject.ts
Normal file
@ -0,0 +1,100 @@
|
||||
export type ListenerFn<T> = (value: T) => void;
|
||||
interface Connectable<Value> {
|
||||
value?: Value;
|
||||
subscribe(listener: ListenerFn<Value>, ctx?: Object): this;
|
||||
unsubscribe(listener: ListenerFn<Value>, ctx?: Object): this;
|
||||
}
|
||||
interface ConnectableApi<T> {
|
||||
connect(connectable: Connectable<T>): this;
|
||||
disconnect(connectable: Connectable<T>): this;
|
||||
}
|
||||
type Connection<From, To = From, Prev = To> = (value: From, next: (value: To) => any, prevValue: Prev) => void;
|
||||
|
||||
export class Subject<Value> implements Connectable<Value> {
|
||||
listeners: [ListenerFn<Value>, Object | undefined][] = [];
|
||||
|
||||
value?: Value;
|
||||
constructor(value?: Value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
next(value: Value) {
|
||||
this.value = value;
|
||||
for (const [listener, ctx] of this.listeners) {
|
||||
if (ctx) listener.call(ctx, value);
|
||||
else listener(value);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private findListener(callback: ListenerFn<Value>, ctx?: Object) {
|
||||
return this.listeners.find((l) => {
|
||||
return l[0] === callback && l[1] === ctx;
|
||||
});
|
||||
}
|
||||
|
||||
subscribe(listener: ListenerFn<Value>, ctx?: Object) {
|
||||
if (!this.findListener(listener, ctx)) {
|
||||
this.listeners.push([listener, ctx]);
|
||||
|
||||
if (this.value !== undefined) {
|
||||
if (ctx) listener.call(ctx, this.value);
|
||||
else listener(this.value);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
unsubscribe(listener: ListenerFn<Value>, ctx?: Object) {
|
||||
const entry = this.findListener(listener, ctx);
|
||||
if (entry) {
|
||||
this.listeners = this.listeners.filter((l) => l !== entry);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
get hasListeners() {
|
||||
return this.listeners.length > 0;
|
||||
}
|
||||
|
||||
upstream = new Map<Connectable<any>, ListenerFn<any>>();
|
||||
|
||||
connect(connectable: Connectable<Value>) {
|
||||
if (!this.upstream.has(connectable)) {
|
||||
const handler = this.next;
|
||||
this.upstream.set(connectable, handler);
|
||||
connectable.subscribe(handler, this);
|
||||
|
||||
if (connectable.value !== undefined) {
|
||||
handler(connectable.value);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
connectWithHandler<From>(connectable: Connectable<From>, connection: Connection<From, Value, typeof this.value>) {
|
||||
if (!this.upstream.has(connectable)) {
|
||||
const handler = (value: From) => {
|
||||
connection(value, this.next.bind(this), this.value);
|
||||
};
|
||||
this.upstream.set(connectable, handler);
|
||||
connectable.subscribe(handler, this);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
disconnect(connectable: Connectable<any>) {
|
||||
const handler = this.upstream.get(connectable);
|
||||
if (handler) {
|
||||
this.upstream.delete(connectable);
|
||||
connectable.unsubscribe(handler, this);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export class PersistentSubject<Value> extends Subject<Value> implements ConnectableApi<Value> {
|
||||
value: Value;
|
||||
constructor(value: Value) {
|
||||
super();
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
export default Subject;
|
@ -1,14 +1,14 @@
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { getReferences } from "../helpers/nostr-event";
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import { NostrRequest } from "./nostr-request";
|
||||
import { NostrMultiSubscription } from "./nostr-multi-subscription";
|
||||
import Subject, { PersistentSubject } from "./subject";
|
||||
|
||||
export class ThreadLoader {
|
||||
loading = new BehaviorSubject(false);
|
||||
focusId = new BehaviorSubject("");
|
||||
rootId = new BehaviorSubject("");
|
||||
events = new BehaviorSubject<Record<string, NostrEvent>>({});
|
||||
loading = new PersistentSubject(false);
|
||||
focusId = new PersistentSubject<string>("");
|
||||
rootId = new PersistentSubject<string>("");
|
||||
events = new PersistentSubject<Record<string, NostrEvent>>({});
|
||||
|
||||
private relays: string[];
|
||||
private subscription: NostrMultiSubscription;
|
||||
@ -92,7 +92,7 @@ export class ThreadLoader {
|
||||
}
|
||||
|
||||
open() {
|
||||
if (!this.loading.value && this.events.value[this.focusId.value]) {
|
||||
if (!this.loading.value && this.focusId.value && this.events.value[this.focusId.value]) {
|
||||
this.loadEvent();
|
||||
}
|
||||
this.updateSubscription();
|
||||
|
@ -1,9 +1,9 @@
|
||||
import moment from "moment";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import { NostrQuery } from "../types/nostr-query";
|
||||
import { NostrRequest } from "./nostr-request";
|
||||
import { NostrMultiSubscription } from "./nostr-multi-subscription";
|
||||
import Subject, { PersistentSubject } from "./subject";
|
||||
|
||||
export type NostrQueryWithStart = NostrQuery & { since: number };
|
||||
|
||||
@ -16,9 +16,9 @@ export type TimelineLoaderOptions = Partial<Options>;
|
||||
export class TimelineLoader {
|
||||
relays: string[];
|
||||
query: NostrQueryWithStart;
|
||||
events = new BehaviorSubject<NostrEvent[]>([]);
|
||||
loading = new BehaviorSubject(false);
|
||||
page = new BehaviorSubject(0);
|
||||
events = new PersistentSubject<NostrEvent[]>([]);
|
||||
loading = new PersistentSubject(false);
|
||||
page = new PersistentSubject(0);
|
||||
private seenEvents = new Set<string>();
|
||||
private subscription: NostrMultiSubscription;
|
||||
private opts: Options = { pageSize: moment.duration(1, "hour").asSeconds() };
|
||||
@ -69,18 +69,16 @@ export class TimelineLoader {
|
||||
loadMore() {
|
||||
if (this.loading.value) return;
|
||||
|
||||
const query = { ...this.query, ...this.getPageDates(this.page.value) };
|
||||
const query = { ...this.query, ...this.getPageDates(this.page.value ?? 0) };
|
||||
const request = new NostrRequest(this.relays);
|
||||
request.onEvent.subscribe({
|
||||
next: this.handleEvent.bind(this),
|
||||
complete: () => {
|
||||
this.loading.next(false);
|
||||
},
|
||||
request.onEvent.subscribe(this.handleEvent, this);
|
||||
request.onComplete.then(() => {
|
||||
this.loading.next(false);
|
||||
});
|
||||
request.start(query);
|
||||
|
||||
this.loading.next(true);
|
||||
this.page.next(this.page.value + 1);
|
||||
this.page.next(this.page.value ?? 0 + 1);
|
||||
}
|
||||
|
||||
forgetEvents() {
|
||||
|
@ -5,7 +5,6 @@ import { getUserDisplayName } from "../helpers/user-metadata";
|
||||
import useSubject from "../hooks/use-subject";
|
||||
import { useUserMetadata } from "../hooks/use-user-metadata";
|
||||
import clientFollowingService from "../services/client-following";
|
||||
import identity from "../services/identity";
|
||||
import { UserAvatar } from "./user-avatar";
|
||||
|
||||
const FollowingListItem = ({ pubkey }: { pubkey: string }) => {
|
||||
@ -28,7 +27,6 @@ const FollowingListItem = ({ pubkey }: { pubkey: string }) => {
|
||||
};
|
||||
|
||||
export const FollowingList = () => {
|
||||
const pubkey = useSubject(identity.pubkey);
|
||||
const following = useSubject(clientFollowingService.following);
|
||||
|
||||
if (!following) return <SkeletonText />;
|
||||
|
@ -8,8 +8,7 @@ import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip-19";
|
||||
|
||||
import { NoteContents } from "./note-contents";
|
||||
import { NoteMenu } from "./note-menu";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import identity from "../../services/identity";
|
||||
import identityService from "../../services/identity";
|
||||
import { useUserContacts } from "../../hooks/use-user-contacts";
|
||||
import { UserTipButton } from "../user-tip-button";
|
||||
import { NoteRelays } from "./note-relays";
|
||||
@ -21,6 +20,7 @@ import { buildReply } from "../../helpers/nostr-event";
|
||||
import { UserDnsIdentityIcon } from "../user-dns-identity";
|
||||
import { useReadonlyMode } from "../../hooks/use-readonly-mode";
|
||||
import { convertTimestampToDate } from "../../helpers/date";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
|
||||
export type NoteProps = {
|
||||
event: NostrEvent;
|
||||
@ -31,7 +31,7 @@ export const Note = React.memo(({ event, maxHeight }: NoteProps) => {
|
||||
const readonly = useReadonlyMode();
|
||||
const { openModal } = useContext(PostModalContext);
|
||||
|
||||
const pubkey = useSubject(identity.pubkey);
|
||||
const pubkey = useSubject(identityService.pubkey) ?? "";
|
||||
const contacts = useUserContacts(pubkey);
|
||||
const following = contacts?.contacts || [];
|
||||
|
||||
|
@ -14,13 +14,13 @@ import {
|
||||
} from "@chakra-ui/react";
|
||||
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 { 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";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
|
||||
export type NoteRelaysProps = Omit<IconButtonProps, "icon" | "aria-label"> & {
|
||||
event: NostrEvent;
|
||||
@ -36,10 +36,8 @@ export const NoteRelays = memo(({ event, ...props }: NoteRelaysProps) => {
|
||||
setQuerying(true);
|
||||
const request = new NostrRequest(readRelays);
|
||||
request.start({ ids: [event.id] });
|
||||
request.onEvent.subscribe({
|
||||
complete() {
|
||||
setQuerying(false);
|
||||
},
|
||||
request.onComplete.then(() => {
|
||||
setQuerying(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
@ -51,18 +49,15 @@ export const NoteRelays = memo(({ event, ...props }: NoteRelaysProps) => {
|
||||
}
|
||||
|
||||
setBroadcasting(true);
|
||||
const action = nostrPostAction(missingRelays, event, 5000);
|
||||
const { results, onComplete } = nostrPostAction(missingRelays, event, 5000);
|
||||
|
||||
action.subscribe({
|
||||
next: (result) => {
|
||||
if (result.status) {
|
||||
handleEventFromRelay(relayPoolService.requestRelay(result.url, false), event);
|
||||
}
|
||||
},
|
||||
complete: () => {
|
||||
setBroadcasting(false);
|
||||
},
|
||||
results.subscribe((result) => {
|
||||
if (result.status) {
|
||||
handleEventFromRelay(relayPoolService.requestRelay(result.url, false), event);
|
||||
}
|
||||
});
|
||||
|
||||
onComplete.then(() => setBroadcasting(false));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
@ -6,17 +6,17 @@ import { ErrorBoundary } from "./error-boundary";
|
||||
import { ConnectedRelays } from "./connected-relays";
|
||||
|
||||
import { useIsMobile } from "../hooks/use-is-mobile";
|
||||
import identity from "../services/identity";
|
||||
import identityService from "../services/identity";
|
||||
import { FollowingList } from "./following-list";
|
||||
import { ReloadPrompt } from "./reload-prompt";
|
||||
import { PostModalProvider } from "../providers/post-modal-provider";
|
||||
import { useReadonlyMode } from "../hooks/use-readonly-mode";
|
||||
import { ProfileButton } from "./profile-button";
|
||||
import useSubject from "../hooks/use-subject";
|
||||
import { UserAvatarLink } from "./user-avatar-link";
|
||||
import useSubject from "../hooks/use-subject";
|
||||
|
||||
const MobileProfileHeader = () => {
|
||||
const pubkey = useSubject(identity.pubkey);
|
||||
const pubkey = useSubject(identityService.pubkey) ?? "";
|
||||
const readonly = useReadonlyMode();
|
||||
|
||||
return (
|
||||
@ -27,7 +27,7 @@ const MobileProfileHeader = () => {
|
||||
colorScheme="red"
|
||||
textAlign="center"
|
||||
variant="link"
|
||||
onClick={() => confirm("Exit readonly mode?") && identity.logout()}
|
||||
onClick={() => confirm("Exit readonly mode?") && identityService.logout()}
|
||||
>
|
||||
Readonly Mode
|
||||
</Button>
|
||||
@ -96,7 +96,7 @@ const DesktopSideNav = () => {
|
||||
<Button onClick={() => navigate("/settings")} leftIcon={<SettingsIcon />}>
|
||||
Settings
|
||||
</Button>
|
||||
<Button onClick={() => identity.logout()} leftIcon={<LogoutIcon />}>
|
||||
<Button onClick={() => identityService.logout()} leftIcon={<LogoutIcon />}>
|
||||
Logout
|
||||
</Button>
|
||||
{readonly && (
|
||||
|
@ -57,12 +57,10 @@ export const PostModal = ({ isOpen, onClose, initialDraft }: PostModalProps) =>
|
||||
setWaiting(false);
|
||||
setSignedEvent(event);
|
||||
|
||||
const postResults = nostrPostAction(writeRelays, event);
|
||||
const { results } = nostrPostAction(writeRelays, event);
|
||||
|
||||
postResults.subscribe({
|
||||
next(result) {
|
||||
resultsActions.push(result);
|
||||
},
|
||||
results.subscribe((result) => {
|
||||
resultsActions.push(result);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { Box, LinkBox, Text } from "@chakra-ui/react";
|
||||
import { Link } from "react-router-dom";
|
||||
import useSubject from "../hooks/use-subject";
|
||||
import identity from "../services/identity";
|
||||
import identityService from "../services/identity";
|
||||
import { UserAvatar } from "./user-avatar";
|
||||
import { useUserMetadata } from "../hooks/use-user-metadata";
|
||||
import { normalizeToBech32 } from "../helpers/nip-19";
|
||||
import { truncatedId } from "../helpers/nostr-event";
|
||||
import useSubject from "../hooks/use-subject";
|
||||
|
||||
export const ProfileButton = () => {
|
||||
const pubkey = useSubject(identity.pubkey);
|
||||
const pubkey = useSubject(identityService.pubkey) ?? "";
|
||||
const metadata = useUserMetadata(pubkey);
|
||||
|
||||
return (
|
||||
|
@ -4,8 +4,8 @@ import { useUserMetadata } from "../hooks/use-user-metadata";
|
||||
import { useAsync } from "react-use";
|
||||
import { getIdenticon } from "../services/identicon";
|
||||
import { safeUrl } from "../helpers/parse";
|
||||
import useSubject from "../hooks/use-subject";
|
||||
import settings from "../services/settings";
|
||||
import useSubject from "../hooks/use-subject";
|
||||
|
||||
export const UserIdenticon = React.memo(({ pubkey }: { pubkey: string }) => {
|
||||
const { value: identicon } = useAsync(() => getIdenticon(pubkey), [pubkey]);
|
||||
|
@ -8,7 +8,7 @@ export const UserFollowButton = ({
|
||||
...props
|
||||
}: { pubkey: string } & Omit<ButtonProps, "onClick" | "isLoading" | "isDisabled">) => {
|
||||
const readonly = useReadonlyMode();
|
||||
const following = useSubject(clientFollowingService.following);
|
||||
const following = useSubject(clientFollowingService.following) ?? [];
|
||||
const savingDraft = useSubject(clientFollowingService.savingDraft);
|
||||
|
||||
const isFollowing = following.some((t) => t[1] === pubkey);
|
||||
|
@ -57,7 +57,7 @@ export function getReferences(event: NostrEvent | DraftNostrEvent) {
|
||||
|
||||
export function buildReply(event: NostrEvent): DraftNostrEvent {
|
||||
const refs = getReferences(event);
|
||||
const relay = getEventRelays(event.id).getValue()[0];
|
||||
const relay = getEventRelays(event.id).value?.[0] ?? "";
|
||||
|
||||
const tags: NostrEvent["tags"] = [];
|
||||
|
||||
|
@ -15,11 +15,12 @@ export type Kind0ParsedContent = {
|
||||
nip05?: string;
|
||||
};
|
||||
|
||||
export function parseKind0Event(event: NostrEvent): Kind0ParsedContent | undefined {
|
||||
export function parseKind0Event(event: NostrEvent): Kind0ParsedContent {
|
||||
if (event.kind !== 0) throw new Error("expected a kind 0 event");
|
||||
try {
|
||||
return JSON.parse(event.content) as Kind0ParsedContent;
|
||||
} catch (e) {}
|
||||
return {};
|
||||
}
|
||||
|
||||
export function getUserDisplayName(metadata: Kind0ParsedContent | undefined, pubkey: string) {
|
||||
|
@ -4,7 +4,7 @@ import { RelayMode } from "../classes/relay";
|
||||
import useSubject from "./use-subject";
|
||||
|
||||
export function useClientRelays(mode: RelayMode = RelayMode.READ) {
|
||||
const relays = useSubject(clientRelaysService.relays);
|
||||
const relays = useSubject(clientRelaysService.relays) ?? [];
|
||||
|
||||
return relays.filter((r) => r.mode & mode);
|
||||
}
|
||||
|
@ -1,26 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { NostrMultiSubscription } from "../classes/nostr-multi-subscription";
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
|
||||
export function useEventDir(subscription: NostrMultiSubscription, filter?: (event: NostrEvent) => boolean) {
|
||||
const [events, setEvents] = useState<Record<string, NostrEvent>>({});
|
||||
|
||||
useEffect(() => {
|
||||
const s = subscription.onEvent.subscribe((event) => {
|
||||
if (filter && !filter(event)) return;
|
||||
|
||||
setEvents((dir) => {
|
||||
if (!dir[event.id]) {
|
||||
return { [event.id]: event, ...dir };
|
||||
}
|
||||
return dir;
|
||||
});
|
||||
});
|
||||
|
||||
return () => s.unsubscribe();
|
||||
}, [subscription]);
|
||||
|
||||
const reset = useCallback(() => setEvents({}), [setEvents]);
|
||||
|
||||
return { events, reset };
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import identity from "../services/identity";
|
||||
import identityService from "../services/identity";
|
||||
import useSubject from "./use-subject";
|
||||
|
||||
export function useReadonlyMode() {
|
||||
return useSubject(identity.readonly);
|
||||
return useSubject(identityService.readonly);
|
||||
}
|
||||
|
@ -1,12 +1,20 @@
|
||||
import { useObservable } from "react-use";
|
||||
import { BehaviorSubject, Subject } from "rxjs";
|
||||
import { useEffect, useState } from "react";
|
||||
import { PersistentSubject, Subject } from "../classes/subject";
|
||||
|
||||
function useSubject<T>(subject: BehaviorSubject<T>): T;
|
||||
function useSubject<T>(subject: Subject<T>): T | undefined;
|
||||
function useSubject<T>(subject: Subject<T>): T | undefined {
|
||||
if (subject instanceof BehaviorSubject) {
|
||||
return useObservable(subject, subject.getValue());
|
||||
} else return useObservable(subject);
|
||||
function useSubject<Value extends unknown>(subject: PersistentSubject<Value>): Value;
|
||||
function useSubject<Value extends unknown>(subject: Subject<Value>): Value | undefined;
|
||||
function useSubject<Value extends unknown>(subject: Subject<Value>) {
|
||||
const [value, setValue] = useState(subject.value);
|
||||
useEffect(() => {
|
||||
const handler = (value: Value) => setValue(value);
|
||||
subject.subscribe(handler);
|
||||
|
||||
return () => {
|
||||
subject.unsubscribe(handler);
|
||||
};
|
||||
}, [subject, setValue]);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export default useSubject;
|
||||
|
@ -1,33 +0,0 @@
|
||||
import { useRef } from "react";
|
||||
import { useDeepCompareEffect, useUnmount } from "react-use";
|
||||
import { NostrMultiSubscription } from "../classes/nostr-multi-subscription";
|
||||
import { NostrQuery } from "../types/nostr-query";
|
||||
import { useReadRelayUrls } from "./use-client-relays";
|
||||
|
||||
type Options = {
|
||||
name?: string;
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
/** @deprecated */
|
||||
export function useSubscription(query: NostrQuery, opts?: Options) {
|
||||
const relays = useReadRelayUrls();
|
||||
const sub = useRef<NostrMultiSubscription | null>(null);
|
||||
sub.current = sub.current || new NostrMultiSubscription(relays, undefined, opts?.name);
|
||||
|
||||
useDeepCompareEffect(() => {
|
||||
if (sub.current) {
|
||||
sub.current.setQuery(query);
|
||||
if (opts?.enabled ?? true) sub.current.open();
|
||||
else sub.current.close();
|
||||
}
|
||||
}, [query]);
|
||||
useUnmount(() => {
|
||||
if (sub.current) {
|
||||
sub.current.close();
|
||||
sub.current = null;
|
||||
}
|
||||
});
|
||||
|
||||
return sub.current as NostrMultiSubscription;
|
||||
}
|
@ -29,10 +29,10 @@ export function useThreadLoader(eventId: string, opts?: Options) {
|
||||
loader.close();
|
||||
});
|
||||
|
||||
const events = useSubject(loader.events);
|
||||
const events = useSubject(loader.events) ?? {};
|
||||
const loading = useSubject(loader.loading);
|
||||
const rootId = useSubject(loader.rootId);
|
||||
const focusId = useSubject(loader.focusId);
|
||||
const rootId = useSubject(loader.rootId) ?? "";
|
||||
const focusId = useSubject(loader.focusId) ?? "";
|
||||
const thread = useMemo(() => linkEvents(Object.values(events)), [events]);
|
||||
|
||||
return {
|
||||
|
@ -12,7 +12,7 @@ export function useUserContacts(pubkey: string, additionalRelays: string[] = [],
|
||||
() => userContactsService.requestContacts(pubkey, relays, alwaysRequest),
|
||||
[pubkey, relays, alwaysRequest]
|
||||
);
|
||||
const contacts = useSubject(observable) ?? undefined;
|
||||
const contacts = useSubject(observable);
|
||||
|
||||
return contacts;
|
||||
}
|
||||
|
@ -3,11 +3,11 @@ import userFollowersService from "../services/user-followers";
|
||||
import useSubject from "./use-subject";
|
||||
|
||||
export function useUserFollowers(pubkey: string, relays: string[] = [], alwaysRequest = false) {
|
||||
const observable = useMemo(
|
||||
const subject = useMemo(
|
||||
() => userFollowersService.requestFollowers(pubkey, relays, alwaysRequest),
|
||||
[pubkey, alwaysRequest]
|
||||
);
|
||||
const followers = useSubject(observable) ?? undefined;
|
||||
const followers = useSubject(subject) ?? undefined;
|
||||
|
||||
return followers;
|
||||
}
|
||||
|
@ -1,12 +1,10 @@
|
||||
import { useMemo } from "react";
|
||||
import { unique } from "../helpers/array";
|
||||
import userMetadataService from "../services/user-metadata";
|
||||
import { useReadRelayUrls } from "./use-client-relays";
|
||||
import useSubject from "./use-subject";
|
||||
|
||||
export function useUserMetadata(pubkey: string, additionalRelays: string[] = [], alwaysRequest = false) {
|
||||
const clientRelays = useReadRelayUrls();
|
||||
const relays = useMemo(() => unique(clientRelays.concat(additionalRelays)), [additionalRelays.join(",")]);
|
||||
const relays = useReadRelayUrls(additionalRelays);
|
||||
|
||||
const subject = useMemo(
|
||||
() => userMetadataService.requestMetadata(pubkey, relays, alwaysRequest),
|
||||
|
@ -12,7 +12,7 @@ export function useUserRelays(pubkey: string, additionalRelays: string[] = [], a
|
||||
() => userRelaysService.requestRelays(pubkey, relays, alwaysRequest),
|
||||
[pubkey, relays, alwaysRequest]
|
||||
);
|
||||
const contacts = useSubject(observable) ?? undefined;
|
||||
const contacts = useSubject(observable);
|
||||
|
||||
return contacts;
|
||||
}
|
||||
|
@ -1,45 +1,47 @@
|
||||
import moment from "moment";
|
||||
import { BehaviorSubject, lastValueFrom, Subscription } from "rxjs";
|
||||
import { nostrPostAction } from "../classes/nostr-post-action";
|
||||
import { PersistentSubject, Subject } from "../classes/subject";
|
||||
import { DraftNostrEvent, PTag } from "../types/nostr-event";
|
||||
import clientRelaysService from "./client-relays";
|
||||
import identity from "./identity";
|
||||
import userContactsService from "./user-contacts";
|
||||
import identityService from "./identity";
|
||||
import userContactsService, { UserContacts } from "./user-contacts";
|
||||
|
||||
export type RelayDirectory = Record<string, { read: boolean; write: boolean }>;
|
||||
|
||||
const following = new BehaviorSubject<PTag[]>([]);
|
||||
const pendingDraft = new BehaviorSubject<DraftNostrEvent | null>(null);
|
||||
const savingDraft = new BehaviorSubject(false);
|
||||
const following = new PersistentSubject<PTag[]>([]);
|
||||
const pendingDraft = new PersistentSubject<DraftNostrEvent | null>(null);
|
||||
const savingDraft = new PersistentSubject(false);
|
||||
|
||||
let sub: Subscription | undefined;
|
||||
function handleNewContacts(contacts: UserContacts | undefined) {
|
||||
if (!contacts) return;
|
||||
|
||||
following.next(
|
||||
contacts.contacts.map((key) => {
|
||||
const relay = contacts.contactRelay[key];
|
||||
if (relay) return ["p", key, relay];
|
||||
else return ["p", key];
|
||||
})
|
||||
);
|
||||
|
||||
// reset the pending list since we just got a new contacts list
|
||||
pendingDraft.next(null);
|
||||
}
|
||||
|
||||
let sub: Subject<UserContacts> | undefined;
|
||||
function updateSub() {
|
||||
if (sub) {
|
||||
sub.unsubscribe();
|
||||
sub.unsubscribe(handleNewContacts);
|
||||
sub = undefined;
|
||||
}
|
||||
|
||||
if (identity.pubkey.value) {
|
||||
sub = userContactsService
|
||||
.requestContacts(identity.pubkey.value, clientRelaysService.getReadUrls(), true)
|
||||
.subscribe((userContacts) => {
|
||||
if (!userContacts) return;
|
||||
if (identityService.pubkey.value) {
|
||||
sub = userContactsService.requestContacts(identityService.pubkey.value, clientRelaysService.getReadUrls(), true);
|
||||
|
||||
following.next(
|
||||
userContacts.contacts.map((key) => {
|
||||
const relay = userContacts.contactRelay[key];
|
||||
if (relay) return ["p", key, relay];
|
||||
else return ["p", key];
|
||||
})
|
||||
);
|
||||
|
||||
// reset the pending list since we just got a new contacts list
|
||||
pendingDraft.next(null);
|
||||
});
|
||||
sub.subscribe(handleNewContacts);
|
||||
}
|
||||
}
|
||||
|
||||
identity.pubkey.subscribe(() => {
|
||||
identityService.pubkey.subscribe(() => {
|
||||
// clear the following list until a new one can be fetched
|
||||
following.next([]);
|
||||
|
||||
@ -51,7 +53,7 @@ clientRelaysService.readRelays.subscribe(() => {
|
||||
});
|
||||
|
||||
function isFollowing(pubkey: string) {
|
||||
return following.value.some((t) => t[1] === pubkey);
|
||||
return !!following.value?.some((t) => t[1] === pubkey);
|
||||
}
|
||||
|
||||
function getDraftEvent(): DraftNostrEvent {
|
||||
@ -75,7 +77,7 @@ async function savePending() {
|
||||
const event = await window.nostr.signEvent(draft);
|
||||
|
||||
const results = nostrPostAction(clientRelaysService.getWriteUrls(), event);
|
||||
await lastValueFrom(results);
|
||||
await results.onComplete;
|
||||
|
||||
savingDraft.next(false);
|
||||
|
||||
@ -86,9 +88,10 @@ async function savePending() {
|
||||
|
||||
function addContact(pubkey: string, relay?: string) {
|
||||
const newTag: PTag = relay ? ["p", pubkey, relay] : ["p", pubkey];
|
||||
const pTags = following.value;
|
||||
if (isFollowing(pubkey)) {
|
||||
following.next(
|
||||
following.value.map((t) => {
|
||||
pTags.map((t) => {
|
||||
if (t[1] === pubkey) {
|
||||
return newTag;
|
||||
}
|
||||
@ -96,20 +99,21 @@ function addContact(pubkey: string, relay?: string) {
|
||||
})
|
||||
);
|
||||
} else {
|
||||
following.next([...following.value, newTag]);
|
||||
following.next([...pTags, newTag]);
|
||||
}
|
||||
|
||||
pendingDraft.next(getDraftEvent());
|
||||
}
|
||||
function removeContact(pubkey: string) {
|
||||
if (isFollowing(pubkey)) {
|
||||
following.next(following.value.filter((t) => t[1] !== pubkey));
|
||||
const pTags = following.value;
|
||||
following.next(pTags.filter((t) => t[1] !== pubkey));
|
||||
pendingDraft.next(getDraftEvent());
|
||||
}
|
||||
}
|
||||
|
||||
const clientFollowingService = {
|
||||
following: following,
|
||||
following,
|
||||
isFollowing,
|
||||
savingDraft,
|
||||
savePending,
|
||||
|
@ -1,37 +1,44 @@
|
||||
import moment from "moment";
|
||||
import { BehaviorSubject, lastValueFrom, Subscription } from "rxjs";
|
||||
import { nostrPostAction } from "../classes/nostr-post-action";
|
||||
import { unique } from "../helpers/array";
|
||||
import { DraftNostrEvent, RTag } from "../types/nostr-event";
|
||||
import identity from "./identity";
|
||||
import identityService from "./identity";
|
||||
import { RelayConfig, RelayMode } from "../classes/relay";
|
||||
import userRelaysService from "./user-relays";
|
||||
import userRelaysService, { UserRelays } from "./user-relays";
|
||||
import { PersistentSubject, Subject } from "../classes/subject";
|
||||
|
||||
export type RelayDirectory = Record<string, { read: boolean; write: boolean }>;
|
||||
|
||||
class ClientRelayService {
|
||||
bootstrapRelays = new Set<string>();
|
||||
relays = new BehaviorSubject<RelayConfig[]>([]);
|
||||
writeRelays = new BehaviorSubject<RelayConfig[]>([]);
|
||||
readRelays = new BehaviorSubject<RelayConfig[]>([]);
|
||||
relays = new PersistentSubject<RelayConfig[]>([
|
||||
//default relay list
|
||||
{ url: "wss://relay.damus.io", mode: RelayMode.READ },
|
||||
{ url: "wss://relay.snort.social", mode: RelayMode.READ },
|
||||
{ url: "wss://nos.lol", mode: RelayMode.READ },
|
||||
{ url: "wss://brb.io", mode: RelayMode.READ },
|
||||
]);
|
||||
writeRelays = new PersistentSubject<RelayConfig[]>([]);
|
||||
readRelays = new PersistentSubject<RelayConfig[]>([]);
|
||||
|
||||
constructor() {
|
||||
let sub: Subscription;
|
||||
identity.pubkey.subscribe((pubkey) => {
|
||||
let lastSubject: Subject<UserRelays> | undefined;
|
||||
identityService.pubkey.subscribe((pubkey) => {
|
||||
// clear the relay list until a new one can be fetched
|
||||
this.relays.next([]);
|
||||
// this.relays.next([]);
|
||||
|
||||
if (sub) sub.unsubscribe();
|
||||
if (lastSubject) {
|
||||
lastSubject.unsubscribe(this.handleRelayChanged, this);
|
||||
lastSubject = undefined;
|
||||
}
|
||||
|
||||
sub = userRelaysService.requestRelays(pubkey, Array.from(this.bootstrapRelays), true).subscribe((userRelays) => {
|
||||
if (!userRelays) return;
|
||||
lastSubject = userRelaysService.requestRelays(pubkey, Array.from(this.bootstrapRelays), true);
|
||||
|
||||
this.relays.next(userRelays.relays);
|
||||
});
|
||||
lastSubject.subscribe(this.handleRelayChanged, this);
|
||||
});
|
||||
|
||||
// add preset relays fromm nip07 extension to bootstrap list
|
||||
identity.relays.subscribe((presetRelays) => {
|
||||
identityService.relays.subscribe((presetRelays) => {
|
||||
for (const [url, opts] of Object.entries(presetRelays)) {
|
||||
if (opts.read) {
|
||||
clientRelaysService.bootstrapRelays.add(url);
|
||||
@ -43,6 +50,10 @@ class ClientRelayService {
|
||||
this.relays.subscribe((relays) => this.readRelays.next(relays.filter((r) => r.mode & RelayMode.READ)));
|
||||
}
|
||||
|
||||
private handleRelayChanged(relays: UserRelays) {
|
||||
this.relays.next(relays.relays);
|
||||
}
|
||||
|
||||
async postUpdatedRelays(newRelays: RelayConfig[]) {
|
||||
const rTags: RTag[] = newRelays.map((r) => {
|
||||
switch (r.mode) {
|
||||
@ -71,7 +82,7 @@ class ClientRelayService {
|
||||
const event = await window.nostr.signEvent(draft);
|
||||
|
||||
const results = nostrPostAction(writeUrls, event);
|
||||
await lastValueFrom(results);
|
||||
await results.onComplete;
|
||||
|
||||
// pass new event to the user relay service
|
||||
userRelaysService.handleEvent(event);
|
||||
@ -79,10 +90,10 @@ class ClientRelayService {
|
||||
}
|
||||
|
||||
getWriteUrls() {
|
||||
return this.relays.value.filter((r) => r.mode & RelayMode.WRITE).map((r) => r.url);
|
||||
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);
|
||||
return this.relays.value?.filter((r) => r.mode & RelayMode.READ).map((r) => r.url);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -27,6 +27,11 @@ const MIGRATIONS: MigrationFunction[] = [
|
||||
});
|
||||
contacts.createIndex("created_at", "created_at");
|
||||
|
||||
const userFollows = db.createObjectStore("userFollows", {
|
||||
keyPath: "pubkey",
|
||||
});
|
||||
userFollows.createIndex("follows", "follows", { multiEntry: true, unique: false });
|
||||
|
||||
const dnsIdentifiers = db.createObjectStore("dnsIdentifiers");
|
||||
dnsIdentifiers.createIndex("pubkey", "pubkey", { unique: false });
|
||||
dnsIdentifiers.createIndex("name", "name", { unique: false });
|
||||
|
@ -18,6 +18,11 @@ export interface CustomSchema extends DBSchema {
|
||||
value: NostrEvent;
|
||||
indexes: { created_at: number };
|
||||
};
|
||||
userFollows: {
|
||||
key: string;
|
||||
value: { pubkey: string; follows: string[] };
|
||||
indexes: { follows: string };
|
||||
};
|
||||
dnsIdentifiers: {
|
||||
key: string;
|
||||
value: { name: string; domain: string; pubkey: string; relays: string[]; updated: number };
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { Relay } from "../classes/relay";
|
||||
import { PersistentSubject } from "../classes/subject";
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import relayPoolService from "./relay-pool";
|
||||
|
||||
const eventRelays = new Map<string, BehaviorSubject<string[]>>();
|
||||
const eventRelays = new Map<string, PersistentSubject<string[]>>();
|
||||
|
||||
export function getEventRelays(id: string) {
|
||||
let relays = eventRelays.get(id);
|
||||
if (!relays) {
|
||||
relays = new BehaviorSubject<string[]>([]);
|
||||
relays = new PersistentSubject<string[]>([]);
|
||||
eventRelays.set(id, relays);
|
||||
}
|
||||
return relays;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { PersistentSubject, Subject } from "../classes/subject";
|
||||
import settings from "./settings";
|
||||
|
||||
export type PresetRelays = Record<string, { read: boolean; write: boolean }>;
|
||||
@ -10,12 +10,12 @@ export type SavedIdentity = {
|
||||
};
|
||||
|
||||
class IdentityService {
|
||||
loading = new BehaviorSubject(true);
|
||||
setup = new BehaviorSubject(false);
|
||||
pubkey = new BehaviorSubject("");
|
||||
readonly = new BehaviorSubject(false);
|
||||
loading = new PersistentSubject(false);
|
||||
setup = new PersistentSubject(false);
|
||||
pubkey = new Subject<string>();
|
||||
readonly = new PersistentSubject(false);
|
||||
// directory of relays provided by nip07 extension
|
||||
relays = new BehaviorSubject<PresetRelays>({});
|
||||
relays = new Subject<PresetRelays>({});
|
||||
private useExtension: boolean = false;
|
||||
private secKey: string | undefined = undefined;
|
||||
|
||||
@ -40,20 +40,24 @@ class IdentityService {
|
||||
|
||||
async loginWithExtension() {
|
||||
if (window.nostr) {
|
||||
this.loading.next(true);
|
||||
const pubkey = await window.nostr.getPublicKey();
|
||||
const relays = await window.nostr.getRelays();
|
||||
try {
|
||||
this.loading.next(true);
|
||||
const pubkey = await window.nostr.getPublicKey();
|
||||
const relays = await window.nostr.getRelays();
|
||||
|
||||
if (Array.isArray(relays)) {
|
||||
this.relays.next(relays.reduce<PresetRelays>((d, r) => ({ ...d, [r]: { read: true, write: true } }), {}));
|
||||
} else {
|
||||
this.relays.next(relays);
|
||||
if (Array.isArray(relays)) {
|
||||
this.relays.next(relays.reduce<PresetRelays>((d, r) => ({ ...d, [r]: { read: true, write: true } }), {}));
|
||||
} else {
|
||||
this.relays.next(relays);
|
||||
}
|
||||
|
||||
settings.identity.next({
|
||||
pubkey,
|
||||
useExtension: true,
|
||||
});
|
||||
} catch (e) {
|
||||
this.loading.next(false);
|
||||
}
|
||||
|
||||
settings.identity.next({
|
||||
pubkey,
|
||||
useExtension: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -77,11 +81,11 @@ class IdentityService {
|
||||
}
|
||||
}
|
||||
|
||||
const identity = new IdentityService();
|
||||
const identityService = new IdentityService();
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
// @ts-ignore
|
||||
window.identity = identity;
|
||||
window.identity = identityService;
|
||||
}
|
||||
|
||||
export default identity;
|
||||
export default identityService;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Subject } from "rxjs";
|
||||
import { Relay } from "../classes/relay";
|
||||
import Subject from "../classes/subject";
|
||||
|
||||
export class RelayPoolService {
|
||||
relays = new Map<string, Relay>();
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { PersistentSubject } from "../classes/subject";
|
||||
import db from "./db";
|
||||
import { SavedIdentity } from "./identity";
|
||||
|
||||
const settings = {
|
||||
identity: new BehaviorSubject<SavedIdentity | null>(null),
|
||||
blurImages: new BehaviorSubject(true),
|
||||
autoShowMedia: new BehaviorSubject(true),
|
||||
proxyUserMedia: new BehaviorSubject(false),
|
||||
identity: new PersistentSubject<SavedIdentity | null>(null),
|
||||
blurImages: new PersistentSubject(true),
|
||||
autoShowMedia: new PersistentSubject(true),
|
||||
proxyUserMedia: new PersistentSubject(false),
|
||||
};
|
||||
|
||||
async function loadSettings() {
|
||||
@ -18,7 +18,6 @@ async function loadSettings() {
|
||||
if (value !== undefined) subject.next(value);
|
||||
|
||||
// save
|
||||
// @ts-ignore
|
||||
subject.subscribe((newValue) => {
|
||||
if (loading) return;
|
||||
db.put("settings", newValue, key);
|
||||
|
@ -1,16 +1,9 @@
|
||||
import { isPTag, NostrEvent } from "../types/nostr-event";
|
||||
import { NostrQuery } from "../types/nostr-query";
|
||||
import { PubkeySubjectCache } from "../classes/pubkey-subject-cache";
|
||||
import { NostrMultiSubscription } from "../classes/nostr-multi-subscription";
|
||||
import { safeJson } from "../helpers/parse";
|
||||
import db from "./db";
|
||||
import settings from "./settings";
|
||||
import userFollowersService from "./user-followers";
|
||||
import pubkeyRelayWeightsService from "./pubkey-relay-weights";
|
||||
import clientRelaysService from "./client-relays";
|
||||
import { CachedPubkeyEventRequester } from "../classes/cached-pubkey-event-requester";
|
||||
import { SuperMap } from "../classes/super-map";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import Subject from "../classes/subject";
|
||||
|
||||
export type UserContacts = {
|
||||
pubkey: string;
|
||||
@ -51,19 +44,13 @@ class UserContactsService extends CachedPubkeyEventRequester {
|
||||
return db.put("userContacts", event);
|
||||
}
|
||||
|
||||
// TODO: rxjs behavior subject dose not feel like the right thing to use here
|
||||
private parsedSubjects = new SuperMap<string, BehaviorSubject<UserContacts | undefined>>(
|
||||
() => new BehaviorSubject<UserContacts | undefined>(undefined)
|
||||
);
|
||||
private parsedConnected = new WeakSet<any>();
|
||||
private parsedSubjects = new SuperMap<string, Subject<UserContacts>>(() => new Subject<UserContacts>());
|
||||
requestContacts(pubkey: string, relays: string[], alwaysRequest = false) {
|
||||
const sub = this.parsedSubjects.get(pubkey);
|
||||
|
||||
const requestSub = this.requestEvent(pubkey, relays, alwaysRequest);
|
||||
if (!this.parsedConnected.has(requestSub)) {
|
||||
requestSub.subscribe((event) => event && sub.next(parseContacts(event)));
|
||||
this.parsedConnected.add(requestSub);
|
||||
}
|
||||
|
||||
sub.connectWithHandler(requestSub, (event, next) => next(parseContacts(event)));
|
||||
|
||||
return sub;
|
||||
}
|
||||
@ -75,95 +62,6 @@ setInterval(() => {
|
||||
userContactsService.update();
|
||||
}, 1000 * 2);
|
||||
|
||||
// const subscription = new NostrMultiSubscription([], undefined, "user-contacts");
|
||||
// const subjects = new PubkeySubjectCache<UserContacts>();
|
||||
// const forceRequestedKeys = new Set<string>();
|
||||
|
||||
// function requestContacts(pubkey: string, additionalRelays: string[] = [], alwaysRequest = false) {
|
||||
// let subject = subjects.getSubject(pubkey);
|
||||
|
||||
// if (additionalRelays.length) subjects.addRelays(pubkey, additionalRelays);
|
||||
|
||||
// if (alwaysRequest) forceRequestedKeys.add(pubkey);
|
||||
|
||||
// if (!subject.value) {
|
||||
// db.get("userContacts", pubkey).then((cached) => {
|
||||
// if (cached) subject.next(cached);
|
||||
// });
|
||||
// }
|
||||
|
||||
// return subject;
|
||||
// }
|
||||
|
||||
// function flushRequests() {
|
||||
// if (!subjects.dirty) return;
|
||||
|
||||
// const pubkeys = new Set<string>();
|
||||
// const relayUrls = new Set<string>();
|
||||
|
||||
// const pending = subjects.getAllPubkeysMissingData(Array.from(forceRequestedKeys));
|
||||
// for (const key of pending.pubkeys) pubkeys.add(key);
|
||||
// for (const url of pending.relays) relayUrls.add(url);
|
||||
|
||||
// if (pubkeys.size === 0) return;
|
||||
|
||||
// const clientRelays = clientRelaysService.getReadUrls();
|
||||
// for (const url of clientRelays) relayUrls.add(url);
|
||||
|
||||
// const query: NostrQuery = { authors: Array.from(pubkeys), kinds: [3] };
|
||||
|
||||
// subscription.setRelays(Array.from(relayUrls));
|
||||
// subscription.setQuery(query);
|
||||
// if (subscription.state !== NostrMultiSubscription.OPEN) {
|
||||
// subscription.open();
|
||||
// }
|
||||
// subjects.dirty = false;
|
||||
// }
|
||||
|
||||
// function receiveEvent(event: NostrEvent) {
|
||||
// if (event.kind !== 3) return;
|
||||
|
||||
// const parsed = parseContacts(event);
|
||||
|
||||
// if (subjects.hasSubject(event.pubkey)) {
|
||||
// const subject = subjects.getSubject(event.pubkey);
|
||||
// const latest = subject.getValue();
|
||||
// // make sure the event is newer than whats in the subject
|
||||
// if (!latest || event.created_at > latest.created_at) {
|
||||
// subject.next(parsed);
|
||||
// // send it to the db
|
||||
// db.put("userContacts", parsed);
|
||||
// // add it to the pubkey relay weights
|
||||
// pubkeyRelayWeightsService.handleContactList(parsed);
|
||||
// }
|
||||
// } else {
|
||||
// db.get("userContacts", event.pubkey).then((cached) => {
|
||||
// // make sure the event is newer than whats in the db
|
||||
// if (!cached || event.created_at > cached.created_at) {
|
||||
// db.put("userContacts", parsed);
|
||||
// // add it to the pubkey relay weights
|
||||
// pubkeyRelayWeightsService.handleContactList(parsed);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
// forceRequestedKeys.delete(event.pubkey);
|
||||
// }
|
||||
|
||||
// subscription.onEvent.subscribe((event) => {
|
||||
// // add the event to the followers service so it can update
|
||||
// userFollowersService.receiveEvent(event);
|
||||
// receiveEvent(event);
|
||||
// });
|
||||
|
||||
// // flush requests every second
|
||||
// setInterval(() => {
|
||||
// subjects.prune();
|
||||
// flushRequests();
|
||||
// }, 1000 * 2);
|
||||
|
||||
// const userContactsService = { requestContacts, flushRequests, subjects, receiveEvent };
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
// @ts-ignore
|
||||
window.userContactsService = userContactsService;
|
||||
|
@ -3,10 +3,10 @@ import { NostrQuery } from "../types/nostr-query";
|
||||
import { PubkeySubjectCache } from "../classes/pubkey-subject-cache";
|
||||
import { NostrMultiSubscription } from "../classes/nostr-multi-subscription";
|
||||
import db from "./db";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { getReferences } from "../helpers/nostr-event";
|
||||
import userContactsService from "./user-contacts";
|
||||
import clientRelaysService from "./client-relays";
|
||||
import { Subject } from "../classes/subject";
|
||||
|
||||
const subscription = new NostrMultiSubscription([], undefined, "user-followers");
|
||||
const subjects = new PubkeySubjectCache<string[]>();
|
||||
@ -14,7 +14,7 @@ const forceRequestedKeys = new Set<string>();
|
||||
|
||||
export type UserFollowers = Set<string>;
|
||||
|
||||
function mergeNext(subject: BehaviorSubject<string[] | null>, next: string[]) {
|
||||
function mergeNext(subject: Subject<string[] | null>, next: string[]) {
|
||||
let arr = subject.value ? Array.from(subject.value) : [];
|
||||
for (const key of next) {
|
||||
if (!arr.includes(key)) arr.push(key);
|
||||
@ -28,9 +28,9 @@ function requestFollowers(pubkey: string, additionalRelays: string[] = [], alway
|
||||
|
||||
if (additionalRelays.length) subjects.addRelays(pubkey, additionalRelays);
|
||||
|
||||
// db.getAllKeysFromIndex("userContacts", "contacts", pubkey).then((cached) => {
|
||||
// mergeNext(subject, cached);
|
||||
// });
|
||||
db.getAllKeysFromIndex("userFollows", "follows", pubkey).then((cached) => {
|
||||
mergeNext(subject, cached);
|
||||
});
|
||||
|
||||
if (alwaysRequest) forceRequestedKeys.add(pubkey);
|
||||
|
||||
@ -77,6 +77,8 @@ function receiveEvent(event: NostrEvent) {
|
||||
forceRequestedKeys.delete(pubkey);
|
||||
}
|
||||
}
|
||||
|
||||
db.put("userFollows", { pubkey: event.pubkey, follows: refs.pubkeys });
|
||||
}
|
||||
|
||||
subscription.onEvent.subscribe((event) => {
|
||||
|
@ -1,9 +1,9 @@
|
||||
import db from "./db";
|
||||
import { CachedPubkeyEventRequester } from "../classes/cached-pubkey-event-requester";
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { Kind0ParsedContent, parseKind0Event } from "../helpers/user-metadata";
|
||||
import { SuperMap } from "../classes/super-map";
|
||||
import Subject from "../classes/subject";
|
||||
|
||||
class UserMetadataService extends CachedPubkeyEventRequester {
|
||||
constructor() {
|
||||
@ -17,19 +17,13 @@ class UserMetadataService extends CachedPubkeyEventRequester {
|
||||
return db.put("userMetadata", event);
|
||||
}
|
||||
|
||||
// TODO: rxjs behavior subject dose not feel like the right thing to use here
|
||||
private parsedSubjects = new SuperMap<string, BehaviorSubject<Kind0ParsedContent | undefined>>(
|
||||
() => new BehaviorSubject<Kind0ParsedContent | undefined>(undefined)
|
||||
);
|
||||
private parsedConnected = new WeakSet<any>();
|
||||
private parsedSubjects = new SuperMap<string, Subject<Kind0ParsedContent>>(() => new Subject<Kind0ParsedContent>());
|
||||
requestMetadata(pubkey: string, relays: string[], alwaysRequest = false) {
|
||||
const sub = this.parsedSubjects.get(pubkey);
|
||||
|
||||
const requestSub = this.requestEvent(pubkey, relays, alwaysRequest);
|
||||
if (!this.parsedConnected.has(requestSub)) {
|
||||
requestSub.subscribe((event) => event && sub.next(parseKind0Event(event)));
|
||||
this.parsedConnected.add(requestSub);
|
||||
}
|
||||
|
||||
sub.connectWithHandler(requestSub, (event, next) => next(parseKind0Event(event)));
|
||||
|
||||
return sub;
|
||||
}
|
||||
|
@ -3,8 +3,8 @@ import { isRTag, NostrEvent } from "../types/nostr-event";
|
||||
import { RelayConfig } from "../classes/relay";
|
||||
import { parseRTag } from "../helpers/nostr-event";
|
||||
import { CachedPubkeyEventRequester } from "../classes/cached-pubkey-event-requester";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { SuperMap } from "../classes/super-map";
|
||||
import Subject from "../classes/subject";
|
||||
|
||||
export type UserRelays = {
|
||||
pubkey: string;
|
||||
@ -33,96 +33,18 @@ class UserRelaysService extends CachedPubkeyEventRequester {
|
||||
}
|
||||
|
||||
// TODO: rxjs behavior subject dose not feel like the right thing to use here
|
||||
private relaysSubjects = new SuperMap<string, BehaviorSubject<UserRelays | undefined>>(
|
||||
() => new BehaviorSubject<UserRelays | undefined>(undefined)
|
||||
);
|
||||
private parentSubConnected = new WeakSet<any>();
|
||||
private parsedSubjects = new SuperMap<string, Subject<UserRelays>>(() => new Subject<UserRelays>());
|
||||
requestRelays(pubkey: string, relays: string[], alwaysRequest = false) {
|
||||
const sub = this.relaysSubjects.get(pubkey);
|
||||
const sub = this.parsedSubjects.get(pubkey);
|
||||
|
||||
const requestSub = this.requestEvent(pubkey, relays, alwaysRequest);
|
||||
if (!this.parentSubConnected.has(requestSub)) {
|
||||
requestSub.subscribe((event) => event && sub.next(parseRelaysEvent(event)));
|
||||
this.parentSubConnected.add(requestSub);
|
||||
}
|
||||
|
||||
sub.connectWithHandler(requestSub, (event, next) => next(parseRelaysEvent(event)));
|
||||
|
||||
return sub;
|
||||
}
|
||||
}
|
||||
|
||||
// const subscription = new NostrMultiSubscription([], undefined, "user-relays");
|
||||
// const subjects = new PubkeySubjectCache<UserRelays>();
|
||||
// const forceRequestedKeys = new Set<string>();
|
||||
|
||||
// function requestRelays(pubkey: string, relays: string[], alwaysRequest = false) {
|
||||
// let subject = subjects.getSubject(pubkey);
|
||||
|
||||
// if (relays.length) subjects.addRelays(pubkey, relays);
|
||||
|
||||
// if (alwaysRequest) forceRequestedKeys.add(pubkey);
|
||||
|
||||
// if (!subject.value) {
|
||||
// db.get("userRelays", pubkey).then((cached) => {
|
||||
// if (cached) {
|
||||
// subject.next(cached);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
// return subject;
|
||||
// }
|
||||
|
||||
// function flushRequests() {
|
||||
// if (!subjects.dirty) return;
|
||||
|
||||
// const pubkeys = new Set<string>();
|
||||
// const relayUrls = new Set<string>();
|
||||
|
||||
// const pending = subjects.getAllPubkeysMissingData(Array.from(forceRequestedKeys));
|
||||
// for (const key of pending.pubkeys) pubkeys.add(key);
|
||||
// for (const url of pending.relays) relayUrls.add(url);
|
||||
|
||||
// if (pubkeys.size === 0) return;
|
||||
|
||||
// const clientRelays = clientRelaysService.readRelays.value;
|
||||
// for (const relay of clientRelays) relayUrls.add(relay.url);
|
||||
|
||||
// const query: NostrQuery = { authors: Array.from(pubkeys), kinds: [10002] };
|
||||
|
||||
// subscription.setRelays(Array.from(relayUrls));
|
||||
// subscription.setQuery(query);
|
||||
// if (subscription.state !== NostrMultiSubscription.OPEN) {
|
||||
// subscription.open();
|
||||
// }
|
||||
// subjects.dirty = false;
|
||||
// }
|
||||
|
||||
// function receiveEvent(event: NostrEvent) {
|
||||
// const subject = subjects.getSubject(event.pubkey);
|
||||
// const latest = subject.getValue();
|
||||
// if (!latest || event.created_at > latest.created_at) {
|
||||
// const userRelays = {
|
||||
// pubkey: event.pubkey,
|
||||
// relays: event.tags.filter(isRTag).map(parseRTag),
|
||||
// created_at: event.created_at,
|
||||
// };
|
||||
|
||||
// subject.next(userRelays);
|
||||
// db.put("userRelays", userRelays);
|
||||
// forceRequestedKeys.delete(event.pubkey);
|
||||
// }
|
||||
// }
|
||||
|
||||
// subscription.onEvent.subscribe(receiveEvent);
|
||||
|
||||
// // flush requests every second
|
||||
// setInterval(() => {
|
||||
// subjects.prune();
|
||||
// flushRequests();
|
||||
// }, 1000 * 2);
|
||||
|
||||
// const userRelaysService = { requestRelays, flushRequests, subjects, receiveEvent };
|
||||
|
||||
const userRelaysService = new UserRelaysService();
|
||||
|
||||
setInterval(() => {
|
||||
|
@ -1,48 +1,47 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button, Flex, Spinner } from "@chakra-ui/react";
|
||||
import moment from "moment";
|
||||
import { mergeAll, from } from "rxjs";
|
||||
import { Note } from "../../components/note";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import { useUserContacts } from "../../hooks/use-user-contacts";
|
||||
import identity from "../../services/identity";
|
||||
import identityService from "../../services/identity";
|
||||
import userContactsService from "../../services/user-contacts";
|
||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||
import { isNote } from "../../helpers/nostr-event";
|
||||
import { useAppTitle } from "../../hooks/use-app-title";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
|
||||
function useExtendedContacts(pubkey: string) {
|
||||
const readRelays = useReadRelayUrls();
|
||||
useAppTitle("discover");
|
||||
const [extendedContacts, setExtendedContacts] = useState<string[]>([]);
|
||||
const contacts = useUserContacts(pubkey);
|
||||
|
||||
useEffect(() => {
|
||||
if (contacts) {
|
||||
const following = contacts.contacts;
|
||||
const subject = contacts.contacts.map((contact) => userContactsService.requestContacts(contact, readRelays));
|
||||
// useEffect(() => {
|
||||
// if (contacts) {
|
||||
// const following = contacts.contacts;
|
||||
// const subject = contacts.contacts.map((contact) => userContactsService.requestContacts(contact, readRelays));
|
||||
|
||||
const rxSub = from(subject)
|
||||
.pipe(mergeAll())
|
||||
.subscribe((contacts) => {
|
||||
if (contacts) {
|
||||
setExtendedContacts((value) => {
|
||||
const more = contacts.contacts.filter((key) => !following.includes(key));
|
||||
return Array.from(new Set([...value, ...more]));
|
||||
});
|
||||
}
|
||||
});
|
||||
// const rxSub = from(subject)
|
||||
// .pipe(mergeAll())
|
||||
// .subscribe((contacts) => {
|
||||
// if (contacts) {
|
||||
// setExtendedContacts((value) => {
|
||||
// const more = contacts.contacts.filter((key) => !following.includes(key));
|
||||
// return Array.from(new Set([...value, ...more]));
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
|
||||
return () => rxSub.unsubscribe();
|
||||
}
|
||||
}, [contacts, setExtendedContacts]);
|
||||
// return () => rxSub.unsubscribe();
|
||||
// }
|
||||
// }, [contacts, setExtendedContacts]);
|
||||
|
||||
return extendedContacts;
|
||||
}
|
||||
|
||||
export const DiscoverTab = () => {
|
||||
const pubkey = useSubject(identity.pubkey);
|
||||
useAppTitle("discover");
|
||||
const pubkey = useSubject(identityService.pubkey) ?? "";
|
||||
const relays = useReadRelayUrls();
|
||||
|
||||
const contactsOfContacts = useExtendedContacts(pubkey);
|
||||
|
@ -3,19 +3,19 @@ import { useSearchParams } from "react-router-dom";
|
||||
import moment from "moment";
|
||||
import { Note } from "../../components/note";
|
||||
import { isNote } from "../../helpers/nostr-event";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||
import { useUserContacts } from "../../hooks/use-user-contacts";
|
||||
import identity from "../../services/identity";
|
||||
import identityService from "../../services/identity";
|
||||
import { AddIcon } from "@chakra-ui/icons";
|
||||
import { useContext } from "react";
|
||||
import { PostModalContext } from "../../providers/post-modal-provider";
|
||||
import { useReadonlyMode } from "../../hooks/use-readonly-mode";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
|
||||
export const FollowingTab = () => {
|
||||
const readonly = useReadonlyMode();
|
||||
const pubkey = useSubject(identity.pubkey);
|
||||
const pubkey = useSubject(identityService.pubkey) ?? "";
|
||||
const relays = useReadRelayUrls();
|
||||
const { openModal } = useContext(PostModalContext);
|
||||
const contacts = useUserContacts(pubkey);
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { Avatar, Box, Flex, Heading } from "@chakra-ui/react";
|
||||
import { Navigate, Outlet, useLocation } from "react-router-dom";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import identity from "../../services/identity";
|
||||
import identityService from "../../services/identity";
|
||||
|
||||
export const LoginView = () => {
|
||||
const setup = useSubject(identity.setup);
|
||||
const setup = useSubject(identityService.setup);
|
||||
const location = useLocation();
|
||||
|
||||
if (setup) return <Navigate to={location.state?.from ?? "/"} replace />;
|
||||
|
@ -3,7 +3,7 @@ import { Button, Flex, FormControl, FormHelperText, FormLabel, Input, Link, useT
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { RelayUrlInput } from "../../components/relay-url-input";
|
||||
import { normalizeToHex } from "../../helpers/nip-19";
|
||||
import identity from "../../services/identity";
|
||||
import identityService from "../../services/identity";
|
||||
import clientRelaysService from "../../services/client-relays";
|
||||
|
||||
export const LoginNpubView = () => {
|
||||
@ -20,7 +20,7 @@ export const LoginNpubView = () => {
|
||||
return toast({ status: "error", title: "Invalid npub" });
|
||||
}
|
||||
|
||||
identity.loginWithPubkey(pubkey);
|
||||
identityService.loginWithPubkey(pubkey);
|
||||
|
||||
clientRelaysService.bootstrapRelays.add(relayUrl);
|
||||
};
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { Alert, AlertDescription, AlertIcon, AlertTitle, Box, Button, Spinner } from "@chakra-ui/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import identity from "../../services/identity";
|
||||
import identityService from "../../services/identity";
|
||||
|
||||
export const LoginStartView = () => {
|
||||
const navigate = useNavigate();
|
||||
const loading = useSubject(identity.loading);
|
||||
const loading = useSubject(identityService.loading);
|
||||
if (loading) return <Spinner />;
|
||||
|
||||
return (
|
||||
@ -17,7 +17,7 @@ export const LoginStartView = () => {
|
||||
<AlertDescription>There are bugs and things will break.</AlertDescription>
|
||||
</Box>
|
||||
</Alert>
|
||||
<Button onClick={() => identity.loginWithExtension()} colorScheme="brand">
|
||||
<Button onClick={() => identityService.loginWithExtension()} colorScheme="brand">
|
||||
Use browser extension
|
||||
</Button>
|
||||
<Button variant="link" onClick={() => navigate("./npub")}>
|
||||
|
@ -8,7 +8,7 @@ import { convertTimestampToDate } from "../../helpers/date";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||
import identity from "../../services/identity";
|
||||
import identityService from "../../services/identity";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
|
||||
const Kind1Notification = ({ event }: { event: NostrEvent }) => {
|
||||
@ -39,7 +39,7 @@ const NotificationItem = memo(({ event }: { event: NostrEvent }) => {
|
||||
|
||||
const NotificationsView = () => {
|
||||
const readRelays = useReadRelayUrls();
|
||||
const pubkey = useSubject(identity.pubkey);
|
||||
const pubkey = useSubject(identityService.pubkey) ?? "";
|
||||
const { events, loading, loadMore } = useTimelineLoader(
|
||||
"notifications",
|
||||
readRelays,
|
||||
|
@ -3,7 +3,7 @@ import { useMemo } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import { useUserMetadata } from "../../hooks/use-user-metadata";
|
||||
import identity from "../../services/identity";
|
||||
import identityService from "../../services/identity";
|
||||
|
||||
type FormData = {
|
||||
displayName?: string;
|
||||
@ -60,7 +60,7 @@ const MetadataForm = ({ defaultValues, onSubmit }: MetadataFormProps) => {
|
||||
};
|
||||
|
||||
export const ProfileEditView = () => {
|
||||
const pubkey = useSubject(identity.pubkey);
|
||||
const pubkey = useSubject(identityService.pubkey) ?? "";
|
||||
const metadata = useUserMetadata(pubkey);
|
||||
|
||||
const defaultValues = useMemo<FormData>(
|
||||
|
@ -1,10 +1,9 @@
|
||||
import { Flex } from "@chakra-ui/react";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import identity from "../../services/identity";
|
||||
import identityService from "../../services/identity";
|
||||
import { ProfileEditView } from "./edit";
|
||||
|
||||
export const ProfileView = () => {
|
||||
const pubkey = useSubject(identity.pubkey);
|
||||
const pubkey = useSubject(identityService.pubkey) ?? "";
|
||||
|
||||
return <ProfileEditView />;
|
||||
};
|
||||
|
@ -16,19 +16,16 @@ import {
|
||||
} from "@chakra-ui/react";
|
||||
import { SyntheticEvent, useEffect, useState } from "react";
|
||||
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 "../../classes/relay";
|
||||
import { useList } from "react-use";
|
||||
import { RelayUrlInput } from "../../components/relay-url-input";
|
||||
import { useRelayInfo } from "../../hooks/use-client-relays copy";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
|
||||
export const RelaysView = () => {
|
||||
const relays = useSubject(clientRelaysService.relays);
|
||||
|
||||
const info = useRelayInfo("wss://nostr.wine");
|
||||
|
||||
const [pendingAdd, addActions] = useList<RelayConfig>([]);
|
||||
const [pendingRemove, removeActions] = useList<RelayConfig>([]);
|
||||
|
||||
|
@ -15,10 +15,10 @@ import {
|
||||
FormHelperText,
|
||||
} from "@chakra-ui/react";
|
||||
import { useState } from "react";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import settings from "../../services/settings";
|
||||
import { clearCacheData, deleteDatabase } from "../../services/db";
|
||||
import identity from "../../services/identity";
|
||||
import identityService from "../../services/identity";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
|
||||
export const SettingsView = () => {
|
||||
const blurImages = useSubject(settings.blurImages);
|
||||
@ -171,7 +171,7 @@ export const SettingsView = () => {
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
<Flex gap="2" padding="4">
|
||||
<Button onClick={() => identity.logout()}>Logout</Button>
|
||||
<Button onClick={() => identityService.logout()}>Logout</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
|
@ -3,7 +3,7 @@ import { MenuIconButton, MenuIconButtonProps } from "../../../components/menu-ic
|
||||
|
||||
import { IMAGE_ICONS, SpyIcon } from "../../../components/icons";
|
||||
import { Bech32Prefix, normalizeToBech32 } from "../../../helpers/nip-19";
|
||||
import identity from "../../../services/identity";
|
||||
import identityService from "../../../services/identity";
|
||||
import { useUserMetadata } from "../../../hooks/use-user-metadata";
|
||||
import { getUserDisplayName } from "../../../helpers/user-metadata";
|
||||
|
||||
@ -13,8 +13,8 @@ export const UserProfileMenu = ({ pubkey, ...props }: { pubkey: string } & Omit<
|
||||
|
||||
const loginAsUser = () => {
|
||||
if (confirm(`Do you want to logout and login as ${getUserDisplayName(metadata, pubkey)}?`)) {
|
||||
identity.logout();
|
||||
identity.loginWithPubkey(pubkey);
|
||||
identityService.logout();
|
||||
identityService.loginWithPubkey(pubkey);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -24,7 +24,7 @@ import { truncatedId } from "../../helpers/nostr-event";
|
||||
import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip-19";
|
||||
import { KeyIcon, SettingsIcon } from "../../components/icons";
|
||||
import { CopyIconButton } from "../../components/copy-icon-button";
|
||||
import identity from "../../services/identity";
|
||||
import identityService from "../../services/identity";
|
||||
import { UserFollowButton } from "../../components/user-follow-button";
|
||||
import { useAppTitle } from "../../hooks/use-app-title";
|
||||
|
||||
@ -48,7 +48,7 @@ const UserView = () => {
|
||||
|
||||
const metadata = useUserMetadata(pubkey, [], true);
|
||||
const npub = normalizeToBech32(pubkey, Bech32Prefix.Pubkey);
|
||||
const isSelf = pubkey === identity.pubkey.value;
|
||||
const isSelf = pubkey === identityService.pubkey.value;
|
||||
|
||||
useAppTitle(getUserDisplayName(metadata, npub ?? pubkey));
|
||||
|
||||
|
@ -4678,13 +4678,6 @@ run-parallel@^1.1.9:
|
||||
dependencies:
|
||||
queue-microtask "^1.2.2"
|
||||
|
||||
rxjs@^7.8.0:
|
||||
version "7.8.0"
|
||||
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.0.tgz#90a938862a82888ff4c7359811a595e14e1e09a4"
|
||||
integrity sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==
|
||||
dependencies:
|
||||
tslib "^2.1.0"
|
||||
|
||||
safe-buffer@^5.1.0:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
|
||||
|
Loading…
x
Reference in New Issue
Block a user