nostr-hooks
Version:
React hooks for developing Nostr clients
270 lines • 9.98 kB
JavaScript
import NDK, { NDKNip07Signer, NDKNip46Signer, NDKPrivateKeySigner, NDKRelaySet, } from '@nostr-dev-kit/ndk';
import { produce } from 'immer';
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
export const useStore = create()(persist((set, get) => ({
// ndk state
constructorParams: undefined,
ndk: undefined,
// subscription state
subscriptions: {},
// login state
loginData: {
privateKey: undefined,
loginMethod: undefined,
nip46Address: undefined,
},
// subscription actions
createSubscription: ({ subId, filters, opts, relayUrls, onEvent, autoStart = true, replaceOlderReplaceableEvents = true, }) => {
if (!subId)
return null;
const sub = get().subscriptions[subId];
if (sub) {
set(produce((state) => {
if (!state.subscriptions[subId])
return;
state.subscriptions[subId].listenersCount += 1;
}));
return sub.subscription;
}
const { ndk } = get();
if (!ndk)
return null;
const relaySet = relayUrls ? NDKRelaySet.fromRelayUrls(relayUrls, ndk) : undefined;
const subscription = ndk.subscribe(filters, opts, relaySet, autoStart);
let count = 0;
subscription.on('event', (event) => {
count++;
get().addEvent(subId, event, replaceOlderReplaceableEvents);
onEvent?.(event);
});
subscription.on('eose', () => {
get().setEose(subId, true);
const _lim = filters.some((f) => f.limit && f.limit > 0)
? filters.reduce((acc, f) => acc + (f.limit || 0), 0)
: undefined;
get().setHasMore(subId, Boolean(count > 0 && _lim && _lim > 0 && count >= _lim));
});
set(produce((state) => {
state.subscriptions[subId] = {
subscription,
events: [],
eose: false,
hasMore: false,
listenersCount: 1,
};
}));
return subscription;
},
removeSubscription: (subId) => {
if (!subId)
return;
set(produce((state) => {
if (!state.subscriptions[subId])
return;
state.subscriptions[subId].listenersCount -= 1;
if (state.subscriptions[subId].listenersCount <= 0) {
state.subscriptions[subId]?.subscription?.stop();
delete state.subscriptions[subId];
}
}));
},
addEvent: (subId, event, replaceOlderReplaceableEvents) => set(produce((state) => {
if (!subId)
return;
if (!state.subscriptions[subId])
return;
state.subscriptions[subId].events = [event, ...state.subscriptions[subId].events]
.filter((e, i, a) => a.findIndex((ee) => {
if (ee.isParamReplaceable() && replaceOlderReplaceableEvents) {
return ee.dTag === e.dTag;
}
else {
return ee.id === e.id;
}
}) === i)
.sort((a, b) => a.created_at - b.created_at);
})),
setEose: (subId, eose) => set(produce((state) => {
if (!subId)
return;
if (!state.subscriptions[subId])
return;
state.subscriptions[subId].eose = eose;
})),
setHasMore: (subId, hasMore) => set(produce((state) => {
if (!subId)
return;
if (!state.subscriptions[subId])
return;
state.subscriptions[subId].hasMore = hasMore;
})),
loadMore: (subId, limit, since) => {
if (!subId)
return;
const sub = get().subscriptions[subId];
if (!sub)
return;
if (!sub.hasMore || !sub.eose || !sub.events || !sub.events.length)
return;
const oldestEvent = sub.events[0];
if (!oldestEvent)
return;
const ndk = get().ndk;
if (!ndk)
return;
const untilTimestamp = oldestEvent.created_at - 1;
const loadMoreSub = ndk.subscribe(sub.subscription.filters.map((filter) => ({
...filter,
limit: limit && limit > 0 ? limit : filter.limit,
since: since === undefined ? filter.since : since === null ? undefined : since,
until: untilTimestamp,
})), { ...sub.subscription.opts, closeOnEose: true }, sub.subscription.relaySet);
let count = 0;
loadMoreSub.on('event', (event) => {
count++;
sub.subscription.emit('event', event, event.relay, sub.subscription, false, false);
});
loadMoreSub.on('eose', () => {
const _lim = limit && limit > 0
? limit
: sub.subscription.filters.some((f) => f.limit && f.limit > 0)
? sub.subscription.filters.reduce((acc, f) => acc + (f.limit || 0), 0)
: undefined;
get().setHasMore(subId, Boolean(count > 0 && _lim && _lim > 0 && count >= _lim));
});
},
// ndk actions
initNdk: (constructorParams, update = false) => {
const newConstructorParams = update
? { ...get().constructorParams, ...constructorParams }
: { ...constructorParams };
set(produce((state) => {
state.constructorParams = newConstructorParams;
state.ndk = new NDK(newConstructorParams);
}));
},
setSigner: (signer) => {
const newConstructorParams = { ...get().constructorParams };
set(produce((state) => {
if (signer) {
newConstructorParams.signer = signer;
}
else {
delete newConstructorParams.signer;
}
const ndk = new NDK(newConstructorParams);
state.constructorParams = newConstructorParams;
state.ndk = ndk;
}));
},
// login actions
loginWithExtension: ({ onError, onSuccess, } = {}) => {
const signer = new NDKNip07Signer();
signer
.blockUntilReady()
.then(() => {
set(produce((state) => {
state.loginData.privateKey = undefined;
state.loginData.loginMethod = 'Extension';
state.loginData.nip46Address = undefined;
}));
get().setSigner(signer);
onSuccess?.(signer);
})
.catch((err) => {
set(produce((state) => {
state.loginData.privateKey = undefined;
state.loginData.loginMethod = undefined;
state.loginData.nip46Address = undefined;
}));
onError?.(err);
});
},
loginWithRemoteSigner: ({ nip46Address, onError, onSuccess, } = {}) => {
const { ndk } = get();
if (!ndk) {
onError?.('NDK instance is not initialized');
return;
}
const _addr = !nip46Address ? get().loginData.nip46Address : nip46Address;
if (!_addr) {
set(produce((state) => {
state.loginData.privateKey = undefined;
state.loginData.loginMethod = undefined;
state.loginData.nip46Address = undefined;
}));
onError?.('NIP46 address is empty');
return;
}
const signer = new NDKNip46Signer(ndk, _addr);
signer.on('authUrl', (url) => {
window.open(url, 'auth', 'width=600,height=600');
});
signer
.blockUntilReady()
.then(() => {
set(produce((state) => {
state.loginData.privateKey = undefined;
state.loginData.loginMethod = 'Remote';
state.loginData.nip46Address = _addr;
}));
get().setSigner(signer);
onSuccess?.(signer);
})
.catch((err) => {
set(produce((state) => {
state.loginData.privateKey = undefined;
state.loginData.loginMethod = undefined;
state.loginData.nip46Address = undefined;
}));
onError?.(err);
});
},
loginWithPrivateKey: ({ privateKey, onError, onSuccess, } = {}) => {
const signer = new NDKPrivateKeySigner(privateKey);
signer
.blockUntilReady()
.then(() => {
set(produce((state) => {
state.loginData.privateKey = privateKey;
state.loginData.loginMethod = 'PrivateKey';
state.loginData.nip46Address = undefined;
}));
get().setSigner(signer);
onSuccess?.(signer);
})
.catch((err) => {
set(produce((state) => {
state.loginData.privateKey = undefined;
state.loginData.loginMethod = undefined;
state.loginData.nip46Address = undefined;
}));
onError?.(err);
});
},
loginFromLocalStorage: () => {
const { privateKey, loginMethod, nip46Address } = get().loginData;
if (loginMethod === 'PrivateKey' && privateKey) {
get().loginWithPrivateKey({ privateKey });
}
else if (loginMethod === 'Remote' && nip46Address) {
get().loginWithRemoteSigner({ nip46Address });
}
else if (loginMethod === 'Extension') {
get().loginWithExtension();
}
},
logout: () => {
set(produce((state) => {
state.loginData.privateKey = undefined;
state.loginData.loginMethod = undefined;
state.loginData.nip46Address = undefined;
}));
get().setSigner(undefined);
},
}), {
name: 'ndk-store',
partialize: (state) => ({ loginData: state.loginData }),
}));
//# sourceMappingURL=index.js.map