mirror of
https://github.com/irislib/iris-messenger.git
synced 2024-10-18 06:03:22 +00:00
wip PublicState
This commit is contained in:
parent
51ff0e6d7e
commit
739870a454
@ -66,7 +66,7 @@ export class EventDB {
|
|||||||
|
|
||||||
const clone = this.pack(event);
|
const clone = this.pack(event);
|
||||||
const flatTags = clone.tags
|
const flatTags = clone.tags
|
||||||
.filter((tag) => ['e', 'p'].includes(tag[0]))
|
.filter((tag) => ['e', 'p', 'd'].includes(tag[0]))
|
||||||
.map((tag) => tag.join('_'));
|
.map((tag) => tag.join('_'));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -132,6 +132,8 @@ export class EventDB {
|
|||||||
query.flatTags = { $contains: 'e_' + filter['#e'].map(ID) };
|
query.flatTags = { $contains: 'e_' + filter['#e'].map(ID) };
|
||||||
} else if (filter['#p']) {
|
} else if (filter['#p']) {
|
||||||
query.flatTags = { $contains: 'p_' + filter['#p'].map(ID) };
|
query.flatTags = { $contains: 'p_' + filter['#p'].map(ID) };
|
||||||
|
} else if (filter['#d']) {
|
||||||
|
query.flatTags = { $contains: 'd_' + filter['#d'].map(ID) };
|
||||||
}
|
}
|
||||||
if (filter.since && filter.until) {
|
if (filter.since && filter.until) {
|
||||||
query.created_at = { $between: [filter.since, filter.until] };
|
query.created_at = { $between: [filter.since, filter.until] };
|
||||||
|
@ -283,7 +283,6 @@ const Events = {
|
|||||||
async saveDMToLocalState(event: DecryptedEvent, chatNode: Node) {
|
async saveDMToLocalState(event: DecryptedEvent, chatNode: Node) {
|
||||||
const latest = chatNode.get('latest');
|
const latest = chatNode.get('latest');
|
||||||
const e = await latest.once(undefined, true);
|
const e = await latest.once(undefined, true);
|
||||||
console.log('latest', e, event);
|
|
||||||
if (!e || !e.created_at || e.created_at < event.created_at) {
|
if (!e || !e.created_at || e.created_at < event.created_at) {
|
||||||
latest.put({ id: event.id, created_at: event.created_at, text: event.text });
|
latest.put({ id: event.id, created_at: event.created_at, text: event.text });
|
||||||
}
|
}
|
||||||
@ -336,7 +335,6 @@ const Events = {
|
|||||||
|
|
||||||
EventDB.insert(event);
|
EventDB.insert(event);
|
||||||
if (!maybeSecretChat) {
|
if (!maybeSecretChat) {
|
||||||
console.log('saving dm to local state', chatId);
|
|
||||||
this.saveDMToLocalState(event, localState.get('chats').get(chatId));
|
this.saveDMToLocalState(event, localState.get('chats').get(chatId));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -76,6 +76,9 @@ const IndexedDB = {
|
|||||||
const eventTags =
|
const eventTags =
|
||||||
event.tags
|
event.tags
|
||||||
?.filter((tag) => {
|
?.filter((tag) => {
|
||||||
|
if (tag[0] === 'd') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (tag[0] === 'e') {
|
if (tag[0] === 'e') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -125,7 +128,7 @@ const IndexedDB = {
|
|||||||
}, 1000),
|
}, 1000),
|
||||||
|
|
||||||
subscribeToTags: throttle(async function (this: typeof IndexedDB) {
|
subscribeToTags: throttle(async function (this: typeof IndexedDB) {
|
||||||
const tagPairs = [...this.subscribedTags].map((tag) => tag.split('|')); // assuming you used '|' as delimiter
|
const tagPairs = [...this.subscribedTags].map((tag) => tag.split('|'));
|
||||||
this.subscribedTags.clear();
|
this.subscribedTags.clear();
|
||||||
await db.tags
|
await db.tags
|
||||||
.where('[type+value]')
|
.where('[type+value]')
|
||||||
@ -164,6 +167,15 @@ const IndexedDB = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (filter['#d'] && Array.isArray(filter['#d'])) {
|
||||||
|
for (const eventId of filter['#d']) {
|
||||||
|
this.subscribedTags.add('d|' + eventId);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.subscribeToTags();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (filter.ids?.length) {
|
if (filter.ids?.length) {
|
||||||
filter.ids.forEach((id) => this.subscribedEventIds.add(id));
|
filter.ids.forEach((id) => this.subscribedEventIds.add(id));
|
||||||
await this.subscribeToEventIds();
|
await this.subscribeToEventIds();
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import localForage from 'localforage';
|
import localForage from 'localforage';
|
||||||
import { Event, Filter } from 'nostr-tools';
|
|
||||||
import { route } from 'preact-router';
|
import { route } from 'preact-router';
|
||||||
|
|
||||||
|
import publicState from '@/state/PublicState.ts';
|
||||||
import Helpers from '@/utils/Helpers.tsx';
|
import Helpers from '@/utils/Helpers.tsx';
|
||||||
|
|
||||||
import localState from '../state/LocalState.ts';
|
import localState from '../state/LocalState.ts';
|
||||||
@ -11,7 +11,6 @@ import { ID } from '../utils/UniqueIds';
|
|||||||
import Events from './Events';
|
import Events from './Events';
|
||||||
import IndexedDB from './IndexedDB';
|
import IndexedDB from './IndexedDB';
|
||||||
import Key from './Key';
|
import Key from './Key';
|
||||||
import { Path } from './path';
|
|
||||||
import PubSub from './PubSub';
|
import PubSub from './PubSub';
|
||||||
import Relays from './Relays';
|
import Relays from './Relays';
|
||||||
import SocialNetwork from './SocialNetwork';
|
import SocialNetwork from './SocialNetwork';
|
||||||
@ -25,9 +24,6 @@ try {
|
|||||||
let loggedIn = false;
|
let loggedIn = false;
|
||||||
|
|
||||||
const Session = {
|
const Session = {
|
||||||
public: undefined as Path | undefined,
|
|
||||||
private: undefined as Path | undefined,
|
|
||||||
|
|
||||||
async logOut() {
|
async logOut() {
|
||||||
route('/');
|
route('/');
|
||||||
/*
|
/*
|
||||||
@ -81,45 +77,24 @@ const Session = {
|
|||||||
SocialNetwork.followersByUser.set(myId, new Set());
|
SocialNetwork.followersByUser.set(myId, new Set());
|
||||||
SocialNetwork.usersByFollowDistance.set(0, new Set([myId]));
|
SocialNetwork.usersByFollowDistance.set(0, new Set([myId]));
|
||||||
this.loadMyFollowList();
|
this.loadMyFollowList();
|
||||||
const subscribe = (filters: Filter[], callback: (event: Event) => void): string => {
|
|
||||||
const filter = filters[0];
|
|
||||||
const key = filter['#d']?.[0];
|
|
||||||
if (key) {
|
|
||||||
const event = Events.keyValueEvents.get(key);
|
|
||||||
if (event) {
|
|
||||||
callback(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
PubSub.subscribe(filters[0], callback, true);
|
|
||||||
return '0';
|
|
||||||
};
|
|
||||||
localState.get('globalFilter').once((globalFilter) => {
|
localState.get('globalFilter').once((globalFilter) => {
|
||||||
if (!globalFilter) {
|
if (!globalFilter) {
|
||||||
localState.get('globalFilter').put(Events.DEFAULT_GLOBAL_FILTER);
|
localState.get('globalFilter').put(Events.DEFAULT_GLOBAL_FILTER);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// TODO move private and public to State.ts
|
publicState
|
||||||
this.private = new Path(
|
.get('notifications')
|
||||||
(...args) => Events.publish(...args),
|
.get('lastOpened')
|
||||||
subscribe,
|
.on((time) => {
|
||||||
() => this.unsubscribe(),
|
|
||||||
{ authors: [myPub] },
|
|
||||||
(...args) => Key.encrypt(...args),
|
|
||||||
(...args) => Key.decrypt(...args),
|
|
||||||
);
|
|
||||||
this.public = new Path(
|
|
||||||
(...args) => Events.publish(...args),
|
|
||||||
subscribe,
|
|
||||||
() => this.unsubscribe(),
|
|
||||||
{ authors: [myPub] },
|
|
||||||
);
|
|
||||||
this.public.get('notifications/lastOpened', (time) => {
|
|
||||||
if (time !== Events.notificationsSeenTime) {
|
if (time !== Events.notificationsSeenTime) {
|
||||||
Events.notificationsSeenTime = time;
|
Events.notificationsSeenTime = time;
|
||||||
Events.updateUnseenNotificationCount();
|
Events.updateUnseenNotificationCount();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.public.get('settings/colorScheme', (colorScheme) => {
|
publicState
|
||||||
|
.get('settings')
|
||||||
|
.get('colorScheme')
|
||||||
|
.on((colorScheme) => {
|
||||||
if (colorScheme === 'light') {
|
if (colorScheme === 'light') {
|
||||||
document.documentElement.setAttribute('data-theme', 'light');
|
document.documentElement.setAttribute('data-theme', 'light');
|
||||||
return;
|
return;
|
||||||
|
@ -1,228 +0,0 @@
|
|||||||
/**
|
|
||||||
Path API for Nostr, built on NIP33 replaceable-by-tag events of kind 30000.
|
|
||||||
|
|
||||||
```
|
|
||||||
const path = new Path(myPublishFn, mySubscribeFn, myUnsubscribeFn, { authors: [myPubKey} })
|
|
||||||
path.set('reactions/[noteID]', '😎')
|
|
||||||
path.get('reactions/[noteID]', (value, path, event) => console.log(event.pubkey, 'reacted with', value))
|
|
||||||
```
|
|
||||||
|
|
||||||
TODO:
|
|
||||||
```
|
|
||||||
path.list('reactions', (value, path, event) => {
|
|
||||||
console.log(
|
|
||||||
event.pubkey, 'reacted to', path.slice('/')[1], 'with', value
|
|
||||||
)
|
|
||||||
}, { authors: myFollows }
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
In-memory caches the most recent event for the subscribed path, and only calls back with the most recent value.
|
|
||||||
|
|
||||||
This API allows us to build all kinds of applications on top of Nostr (github replacement for example) without having to
|
|
||||||
specify new event kinds all the time and implement them in all applications and relays.
|
|
||||||
|
|
||||||
NIP33: https://github.com/nostr-protocol/nips/blob/master/33.md
|
|
||||||
*/
|
|
||||||
import { Event, Filter, matchFilter } from 'nostr-tools';
|
|
||||||
|
|
||||||
const EVENT_KIND = 30000;
|
|
||||||
|
|
||||||
type CompleteEvent = Event & { id: string };
|
|
||||||
type PathCallback = (value: any, path: string, event: Event) => void;
|
|
||||||
type Listener = {
|
|
||||||
filter: Filter;
|
|
||||||
callback: PathCallback;
|
|
||||||
subscription?: string;
|
|
||||||
off: () => void;
|
|
||||||
};
|
|
||||||
type Publish = (event: Partial<Event>) => Promise<Event>;
|
|
||||||
type Subscribe = (filters: Filter[], callback: (event: Event) => void) => string;
|
|
||||||
type Unsubscribe = (id: string) => void;
|
|
||||||
type Encrypt = (content: string) => Promise<string>;
|
|
||||||
type Decrypt = (content: string) => Promise<string>;
|
|
||||||
|
|
||||||
export function getEventPath(event: Event): string | undefined {
|
|
||||||
return event.tags?.find(([t]) => t === 'd')?.[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getFilterPath(filter: Filter): string | undefined {
|
|
||||||
return filter['#d']?.[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// We can later add other storages like IndexedDB or localStorage
|
|
||||||
class MemoryStorage {
|
|
||||||
eventsByPathAndAuthor = new Map<string, Map<string, Event>>();
|
|
||||||
|
|
||||||
// returns a boolean indicating whether the event was added (newer than existing)
|
|
||||||
set(event: Event): boolean {
|
|
||||||
const path = getEventPath(event);
|
|
||||||
if (!path) {
|
|
||||||
//throw new Error(`event has no d tag: ${JSON.stringify(event)}`)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!this.eventsByPathAndAuthor.has(path)) {
|
|
||||||
this.eventsByPathAndAuthor.set(path, new Map());
|
|
||||||
}
|
|
||||||
let valuesByAuthor = this.eventsByPathAndAuthor.get(path);
|
|
||||||
if (!valuesByAuthor) {
|
|
||||||
valuesByAuthor = new Map();
|
|
||||||
this.eventsByPathAndAuthor.set(path, valuesByAuthor);
|
|
||||||
}
|
|
||||||
const existing = valuesByAuthor?.get(event.pubkey);
|
|
||||||
if (existing && existing.created_at > event.created_at) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
valuesByAuthor.set(event.pubkey, event);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
get(filter: Filter, callback: (event: Event) => void) {
|
|
||||||
const path = getFilterPath(filter);
|
|
||||||
if (!path) {
|
|
||||||
throw new Error(`filter has no #d tag: ${JSON.stringify(filter)}`);
|
|
||||||
}
|
|
||||||
const valuesByAuthor = this.eventsByPathAndAuthor.get(path);
|
|
||||||
if (!valuesByAuthor) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const [author, event] of valuesByAuthor) {
|
|
||||||
if (!filter.authors || filter.authors.indexOf(author) !== -1) {
|
|
||||||
callback(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Path {
|
|
||||||
store: MemoryStorage;
|
|
||||||
listeners: Map<string, Listener>;
|
|
||||||
publish: Publish;
|
|
||||||
subscribe: Subscribe;
|
|
||||||
unsubscribe: Unsubscribe;
|
|
||||||
filter: Filter;
|
|
||||||
encrypt?: Encrypt;
|
|
||||||
decrypt?: Decrypt;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
publish: Publish,
|
|
||||||
subscribe: Subscribe,
|
|
||||||
unsubscribe: Unsubscribe,
|
|
||||||
filter: Filter,
|
|
||||||
encrypt?: Encrypt,
|
|
||||||
decrypt?: Decrypt,
|
|
||||||
) {
|
|
||||||
this.publish = publish;
|
|
||||||
this.subscribe = subscribe;
|
|
||||||
this.unsubscribe = unsubscribe;
|
|
||||||
this.filter = filter;
|
|
||||||
this.encrypt = encrypt;
|
|
||||||
this.decrypt = decrypt;
|
|
||||||
this.store = new MemoryStorage();
|
|
||||||
this.listeners = new Map<string, Listener>();
|
|
||||||
}
|
|
||||||
|
|
||||||
async publishSetEvent(path: string, value: any): Promise<Event> {
|
|
||||||
let content: string;
|
|
||||||
if (this.encrypt) {
|
|
||||||
// TODO: path should be deterministically encrypted hash(path + secret) but NIP07 provides no way for that
|
|
||||||
const contentStr = JSON.stringify(value);
|
|
||||||
content = await this.encrypt(contentStr);
|
|
||||||
if (contentStr === content) {
|
|
||||||
throw new Error(`Encryption failed: ${contentStr} === ${content}`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
content = JSON.stringify(value);
|
|
||||||
}
|
|
||||||
return this.publish({
|
|
||||||
// kind does not accept 30000...
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
kind: EVENT_KIND,
|
|
||||||
tags: [['d', path]],
|
|
||||||
content,
|
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async set(path: string, value: any): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const event = await this.publishSetEvent(path, value);
|
|
||||||
if (event) {
|
|
||||||
if (this.store.set(event)) {
|
|
||||||
this.notifyListeners(event as CompleteEvent);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getEventValue(event: Event): Promise<any> {
|
|
||||||
let value = this.decrypt ? await this.decrypt(event.content) : event.content;
|
|
||||||
try {
|
|
||||||
value = JSON.parse(value);
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(`Failed to parse event content: ${value}`);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
get(path: string, callback: PathCallback, filter = {}): Listener {
|
|
||||||
filter = Object.assign({}, filter, this.filter, {
|
|
||||||
'#d': [path],
|
|
||||||
kinds: [EVENT_KIND],
|
|
||||||
});
|
|
||||||
const listener = this.addListener(filter, callback);
|
|
||||||
this.store.get(filter, (event) => this.callbackFromEvent(event, callback));
|
|
||||||
this.subscribe([filter], async (event) => {
|
|
||||||
if (this.store.set(event)) {
|
|
||||||
this.notifyListeners(event as CompleteEvent);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
addListener(filter: Filter, callback: PathCallback): Listener {
|
|
||||||
const id = Math.random().toString(36).substr(2, 9);
|
|
||||||
const listener: Listener = {
|
|
||||||
filter,
|
|
||||||
callback,
|
|
||||||
off: () => {
|
|
||||||
this.listeners.delete(id);
|
|
||||||
if (listener.subscription) {
|
|
||||||
this.unsubscribe(listener.subscription);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
this.listeners.set(id, listener);
|
|
||||||
return listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
removeListener(id: string) {
|
|
||||||
const listener = this.listeners.get(id);
|
|
||||||
if (listener) {
|
|
||||||
listener.off();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
callbackFromEvent(event: Event, callback: PathCallback) {
|
|
||||||
const path = getEventPath(event);
|
|
||||||
if (!path) {
|
|
||||||
throw new Error(`event has no d tag: ${JSON.stringify(event)}`);
|
|
||||||
}
|
|
||||||
this.getEventValue(event).then((value) => {
|
|
||||||
callback(value, path, event);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async notifyListeners(event: CompleteEvent) {
|
|
||||||
for (const listener of this.listeners.values()) {
|
|
||||||
if (matchFilter(listener.filter, event)) {
|
|
||||||
this.callbackFromEvent(event, listener.callback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
78
src/js/state/IrisNostrAdapter.ts
Normal file
78
src/js/state/IrisNostrAdapter.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import Events from '@/nostr/Events';
|
||||||
|
import Key from '@/nostr/Key';
|
||||||
|
import PubSub from '@/nostr/PubSub';
|
||||||
|
import { Adapter, Callback, NodeValue, Unsubscribe } from '@/state/types.ts';
|
||||||
|
|
||||||
|
export default class IrisNostrAdapter extends Adapter {
|
||||||
|
seenValues = new Map<string, NodeValue>();
|
||||||
|
|
||||||
|
get(path: string, callback: Callback): Unsubscribe {
|
||||||
|
const unsubObj = { fn: null as any };
|
||||||
|
|
||||||
|
unsubObj.fn = PubSub.subscribe(
|
||||||
|
// @ts-ignore
|
||||||
|
{ authors: [Key.getPubKey()], kinds: [30000], '#d': [path] },
|
||||||
|
(event) => {
|
||||||
|
callback(JSON.parse(event.content), path, event.created_at * 1000, () => unsubObj.fn());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return () => unsubObj.fn();
|
||||||
|
}
|
||||||
|
|
||||||
|
async set(path: string, value: NodeValue) {
|
||||||
|
if (value && value.updatedAt === undefined) {
|
||||||
|
throw new Error(`Invalid value: ${JSON.stringify(value)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const seen = this.seenValues.get(path);
|
||||||
|
if (seen && seen.updatedAt <= value.updatedAt) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.seenValues.set(path, value);
|
||||||
|
|
||||||
|
console.log('set state', path, value);
|
||||||
|
|
||||||
|
const directory = path.split('/').slice(0, -1).join('/');
|
||||||
|
const e = await Events.publish({
|
||||||
|
// @ts-ignore
|
||||||
|
kind: 30000,
|
||||||
|
content: JSON.stringify(value.value),
|
||||||
|
created_at: Math.ceil(value.updatedAt / 1000),
|
||||||
|
tags: [
|
||||||
|
['d', path],
|
||||||
|
['f', directory],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
console.log('published state event', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
list(path: string, callback: Callback): Unsubscribe {
|
||||||
|
const unsubObj = { fn: null as any };
|
||||||
|
|
||||||
|
unsubObj.fn = PubSub.subscribe(
|
||||||
|
// @ts-ignore
|
||||||
|
{ authors: [Key.getPubKey()], kinds: [30000] },
|
||||||
|
(event) => {
|
||||||
|
const childPath = event.tags.find((tag) => {
|
||||||
|
if (tag[0] === 'd') {
|
||||||
|
const remainingPath = tag[1].replace(`${path}/`, '');
|
||||||
|
if (
|
||||||
|
remainingPath.length &&
|
||||||
|
tag[1].startsWith(`${path}/`) &&
|
||||||
|
!remainingPath.includes('/')
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})?.[1];
|
||||||
|
|
||||||
|
if (childPath) {
|
||||||
|
callback(JSON.parse(event.content), childPath, event.created_at * 1000, () =>
|
||||||
|
unsubObj.fn(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return () => unsubObj.fn();
|
||||||
|
}
|
||||||
|
}
|
@ -37,7 +37,7 @@ export default class LocalForageAdapter extends Adapter {
|
|||||||
.then((keys) => {
|
.then((keys) => {
|
||||||
keys.forEach((key) => {
|
keys.forEach((key) => {
|
||||||
const remainingPath = key.replace(`${path}/`, '');
|
const remainingPath = key.replace(`${path}/`, '');
|
||||||
if (key.startsWith(`${path}/`) && !remainingPath.includes('/')) {
|
if (key.startsWith(`${path}/`) && remainingPath.length && !remainingPath.includes('/')) {
|
||||||
localForage
|
localForage
|
||||||
.getItem<NodeValue | null>(key)
|
.getItem<NodeValue | null>(key)
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
|
@ -23,7 +23,11 @@ export default class MemoryAdapter extends Adapter {
|
|||||||
list(path: string, callback: Callback): Unsubscribe {
|
list(path: string, callback: Callback): Unsubscribe {
|
||||||
for (const [storedPath, storedValue] of this.storage) {
|
for (const [storedPath, storedValue] of this.storage) {
|
||||||
const remainingPath = storedPath.replace(`${path}/`, '');
|
const remainingPath = storedPath.replace(`${path}/`, '');
|
||||||
if (storedPath.startsWith(`${path}/`) && !remainingPath.includes('/')) {
|
if (
|
||||||
|
storedPath.startsWith(`${path}/`) &&
|
||||||
|
remainingPath.length &&
|
||||||
|
!remainingPath.includes('/')
|
||||||
|
) {
|
||||||
callback(storedValue.value, storedPath, storedValue.updatedAt, () => {});
|
callback(storedValue.value, storedPath, storedValue.updatedAt, () => {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -98,7 +98,6 @@ export default class Node {
|
|||||||
this.parent.children.set(childName, this);
|
this.parent.children.set(childName, this);
|
||||||
}
|
}
|
||||||
for (const [id, callback] of this.parent.map_subscriptions) {
|
for (const [id, callback] of this.parent.map_subscriptions) {
|
||||||
console.log('calling map callback of ', this.parent.id, ' with ', this.id, value);
|
|
||||||
callback(value, this.id, updatedAt, () => {
|
callback(value, this.id, updatedAt, () => {
|
||||||
this.parent?.map_subscriptions.delete(id);
|
this.parent?.map_subscriptions.delete(id);
|
||||||
});
|
});
|
||||||
@ -130,7 +129,7 @@ export default class Node {
|
|||||||
* @param callback
|
* @param callback
|
||||||
*/
|
*/
|
||||||
on(callback: Callback, returnIfUndefined: boolean = false): Unsubscribe {
|
on(callback: Callback, returnIfUndefined: boolean = false): Unsubscribe {
|
||||||
let latest: NodeValue | null = null;
|
let latest: NodeValue | null = null; // replace with this.value?
|
||||||
const cb = (value, path, updatedAt, unsubscribe) => {
|
const cb = (value, path, updatedAt, unsubscribe) => {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
if (returnIfUndefined) {
|
if (returnIfUndefined) {
|
||||||
@ -168,10 +167,15 @@ export default class Node {
|
|||||||
map(callback: Callback): Unsubscribe {
|
map(callback: Callback): Unsubscribe {
|
||||||
const id = this.counter++;
|
const id = this.counter++;
|
||||||
this.map_subscriptions.set(id, callback);
|
this.map_subscriptions.set(id, callback);
|
||||||
|
const latestMap = new Map<string, NodeValue>(); // replace with this.value?
|
||||||
|
|
||||||
const cb = (value, path, updatedAt) => {
|
const cb = (value, path, updatedAt) => {
|
||||||
|
const latest = latestMap.get(path);
|
||||||
|
if (latest && latest.updatedAt >= updatedAt) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
latestMap.set(path, { value, updatedAt });
|
||||||
const childName = path.split('/').pop()!;
|
const childName = path.split('/').pop()!;
|
||||||
console.log('map callback', this.id, childName, value, updatedAt);
|
|
||||||
this.get(childName).put(value, updatedAt);
|
this.get(childName).put(value, updatedAt);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
10
src/js/state/PublicState.ts
Normal file
10
src/js/state/PublicState.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import IrisNostrAdapter from '@/state/IrisNostrAdapter.ts';
|
||||||
|
import MemoryAdapter from '@/state/MemoryAdapter.ts';
|
||||||
|
|
||||||
|
import Node from './Node';
|
||||||
|
|
||||||
|
const publicState = new Node({
|
||||||
|
adapters: [new MemoryAdapter(), new IrisNostrAdapter()],
|
||||||
|
});
|
||||||
|
|
||||||
|
export default publicState;
|
@ -1,4 +1,5 @@
|
|||||||
import localState from '@/state/LocalState.ts';
|
import localState from '@/state/LocalState.ts';
|
||||||
|
import publicState from '@/state/PublicState.ts';
|
||||||
import ExplorerNode from '@/views/explorer/ExplorerNode.tsx';
|
import ExplorerNode from '@/views/explorer/ExplorerNode.tsx';
|
||||||
import View from '@/views/View.tsx';
|
import View from '@/views/View.tsx';
|
||||||
|
|
||||||
@ -14,6 +15,9 @@ const Explorer = ({ p }: Props) => {
|
|||||||
<div className="m-2 md:mx-4">
|
<div className="m-2 md:mx-4">
|
||||||
<ExplorerNode expanded={true} name="Local state" node={localState} />
|
<ExplorerNode expanded={true} name="Local state" node={localState} />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="m-2 md:mx-4">
|
||||||
|
<ExplorerNode expanded={true} name="Public state" node={publicState} />
|
||||||
|
</div>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -5,11 +5,11 @@ import { Event } from 'nostr-tools';
|
|||||||
import EventDB from '@/nostr/EventDB';
|
import EventDB from '@/nostr/EventDB';
|
||||||
import Events from '@/nostr/Events';
|
import Events from '@/nostr/Events';
|
||||||
import Key from '@/nostr/Key';
|
import Key from '@/nostr/Key';
|
||||||
|
import publicState from '@/state/PublicState.ts';
|
||||||
import { RouteProps } from '@/views/types.ts';
|
import { RouteProps } from '@/views/types.ts';
|
||||||
import View from '@/views/View.tsx';
|
import View from '@/views/View.tsx';
|
||||||
|
|
||||||
import Feed from '../../components/feed/Feed';
|
import Feed from '../../components/feed/Feed';
|
||||||
import Session from '../../nostr/Session';
|
|
||||||
import localState from '../../state/LocalState.ts';
|
import localState from '../../state/LocalState.ts';
|
||||||
import { translate as t } from '../../translations/Translation.mjs';
|
import { translate as t } from '../../translations/Translation.mjs';
|
||||||
|
|
||||||
@ -43,16 +43,14 @@ const Notifications: React.FC<RouteProps> = () => {
|
|||||||
const node = localState.get('settings').get('notifications').get('saveLastOpened');
|
const node = localState.get('settings').get('notifications').get('saveLastOpened');
|
||||||
node.once((saveLastOpened) => {
|
node.once((saveLastOpened) => {
|
||||||
if (saveLastOpened !== false) {
|
if (saveLastOpened !== false) {
|
||||||
|
// TODO this gets triggered only once per Iris session?
|
||||||
const time = Math.floor(Date.now() / 1000);
|
const time = Math.floor(Date.now() / 1000);
|
||||||
const success = Session.public?.set('notifications/lastOpened', time);
|
console.log('set state');
|
||||||
if (!success) {
|
publicState.get('notifications').get('lastOpened').put(time);
|
||||||
console.log('user rejected');
|
// TODO if user rejected, stop pestering them with sign prompt
|
||||||
// stop pestering if user rejects signature request
|
|
||||||
node.put(false);
|
|
||||||
}
|
|
||||||
localState.get('unseenNotificationCount').put(0);
|
localState.get('unseenNotificationCount').put(0);
|
||||||
}
|
}
|
||||||
});
|
}, true);
|
||||||
}, 1000),
|
}, 1000),
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { useEffect, useState } from 'preact/hooks';
|
import { useEffect, useState } from 'preact/hooks';
|
||||||
|
|
||||||
import Session from '../../nostr/Session';
|
|
||||||
import localState from '../../state/LocalState.ts';
|
import localState from '../../state/LocalState.ts';
|
||||||
import { translate as t } from '../../translations/Translation.mjs';
|
import { translate as t } from '../../translations/Translation.mjs';
|
||||||
|
|
||||||
@ -11,10 +10,12 @@ const Appearance = () => {
|
|||||||
const [showConnectedRelays, setShowConnectedRelays] = useState(false);
|
const [showConnectedRelays, setShowConnectedRelays] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// TODO use Nostr.private
|
// TODO use privateState. or localState?
|
||||||
Session.public?.get('settings/colorScheme', (/*entry*/) => {
|
/*
|
||||||
//setColorScheme(entry.value);
|
publicState.get('settings/colorScheme', (value) => {
|
||||||
|
//setColorScheme(value);
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
localState.get('showConnectedRelays').on(setShowConnectedRelays);
|
localState.get('showConnectedRelays').on(setShowConnectedRelays);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user