replace rxjs and update everything

This commit is contained in:
hzrd149 2023-02-13 18:00:40 -06:00
parent bfbebdb37a
commit 63dec9eb2c
60 changed files with 456 additions and 552 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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) => {

View File

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

View File

@ -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(() => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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