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 flatTags = clone.tags
|
||||
.filter((tag) => ['e', 'p'].includes(tag[0]))
|
||||
.filter((tag) => ['e', 'p', 'd'].includes(tag[0]))
|
||||
.map((tag) => tag.join('_'));
|
||||
|
||||
try {
|
||||
@ -132,6 +132,8 @@ export class EventDB {
|
||||
query.flatTags = { $contains: 'e_' + filter['#e'].map(ID) };
|
||||
} else if (filter['#p']) {
|
||||
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) {
|
||||
query.created_at = { $between: [filter.since, filter.until] };
|
||||
|
@ -283,7 +283,6 @@ const Events = {
|
||||
async saveDMToLocalState(event: DecryptedEvent, chatNode: Node) {
|
||||
const latest = chatNode.get('latest');
|
||||
const e = await latest.once(undefined, true);
|
||||
console.log('latest', e, event);
|
||||
if (!e || !e.created_at || e.created_at < event.created_at) {
|
||||
latest.put({ id: event.id, created_at: event.created_at, text: event.text });
|
||||
}
|
||||
@ -336,7 +335,6 @@ const Events = {
|
||||
|
||||
EventDB.insert(event);
|
||||
if (!maybeSecretChat) {
|
||||
console.log('saving dm to local state', chatId);
|
||||
this.saveDMToLocalState(event, localState.get('chats').get(chatId));
|
||||
}
|
||||
},
|
||||
|
@ -76,6 +76,9 @@ const IndexedDB = {
|
||||
const eventTags =
|
||||
event.tags
|
||||
?.filter((tag) => {
|
||||
if (tag[0] === 'd') {
|
||||
return true;
|
||||
}
|
||||
if (tag[0] === 'e') {
|
||||
return true;
|
||||
}
|
||||
@ -125,7 +128,7 @@ const IndexedDB = {
|
||||
}, 1000),
|
||||
|
||||
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();
|
||||
await db.tags
|
||||
.where('[type+value]')
|
||||
@ -164,6 +167,15 @@ const IndexedDB = {
|
||||
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) {
|
||||
filter.ids.forEach((id) => this.subscribedEventIds.add(id));
|
||||
await this.subscribeToEventIds();
|
||||
|
@ -1,7 +1,7 @@
|
||||
import localForage from 'localforage';
|
||||
import { Event, Filter } from 'nostr-tools';
|
||||
import { route } from 'preact-router';
|
||||
|
||||
import publicState from '@/state/PublicState.ts';
|
||||
import Helpers from '@/utils/Helpers.tsx';
|
||||
|
||||
import localState from '../state/LocalState.ts';
|
||||
@ -11,7 +11,6 @@ import { ID } from '../utils/UniqueIds';
|
||||
import Events from './Events';
|
||||
import IndexedDB from './IndexedDB';
|
||||
import Key from './Key';
|
||||
import { Path } from './path';
|
||||
import PubSub from './PubSub';
|
||||
import Relays from './Relays';
|
||||
import SocialNetwork from './SocialNetwork';
|
||||
@ -25,9 +24,6 @@ try {
|
||||
let loggedIn = false;
|
||||
|
||||
const Session = {
|
||||
public: undefined as Path | undefined,
|
||||
private: undefined as Path | undefined,
|
||||
|
||||
async logOut() {
|
||||
route('/');
|
||||
/*
|
||||
@ -81,57 +77,36 @@ const Session = {
|
||||
SocialNetwork.followersByUser.set(myId, new Set());
|
||||
SocialNetwork.usersByFollowDistance.set(0, new Set([myId]));
|
||||
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) => {
|
||||
if (!globalFilter) {
|
||||
localState.get('globalFilter').put(Events.DEFAULT_GLOBAL_FILTER);
|
||||
}
|
||||
});
|
||||
// TODO move private and public to State.ts
|
||||
this.private = new Path(
|
||||
(...args) => Events.publish(...args),
|
||||
subscribe,
|
||||
() => 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) {
|
||||
Events.notificationsSeenTime = time;
|
||||
Events.updateUnseenNotificationCount();
|
||||
}
|
||||
});
|
||||
this.public.get('settings/colorScheme', (colorScheme) => {
|
||||
if (colorScheme === 'light') {
|
||||
document.documentElement.setAttribute('data-theme', 'light');
|
||||
return;
|
||||
} else if (colorScheme === 'default') {
|
||||
if (window.matchMedia('(prefers-color-scheme: light)').matches) {
|
||||
//OS theme setting detected as dark
|
||||
publicState
|
||||
.get('notifications')
|
||||
.get('lastOpened')
|
||||
.on((time) => {
|
||||
if (time !== Events.notificationsSeenTime) {
|
||||
Events.notificationsSeenTime = time;
|
||||
Events.updateUnseenNotificationCount();
|
||||
}
|
||||
});
|
||||
publicState
|
||||
.get('settings')
|
||||
.get('colorScheme')
|
||||
.on((colorScheme) => {
|
||||
if (colorScheme === 'light') {
|
||||
document.documentElement.setAttribute('data-theme', 'light');
|
||||
return;
|
||||
} else if (colorScheme === 'default') {
|
||||
if (window.matchMedia('(prefers-color-scheme: light)').matches) {
|
||||
//OS theme setting detected as dark
|
||||
document.documentElement.setAttribute('data-theme', 'light');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
});
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
});
|
||||
if (window.location.pathname === '/') {
|
||||
Relays.init();
|
||||
}
|
||||
|
@ -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) => {
|
||||
keys.forEach((key) => {
|
||||
const remainingPath = key.replace(`${path}/`, '');
|
||||
if (key.startsWith(`${path}/`) && !remainingPath.includes('/')) {
|
||||
if (key.startsWith(`${path}/`) && remainingPath.length && !remainingPath.includes('/')) {
|
||||
localForage
|
||||
.getItem<NodeValue | null>(key)
|
||||
.then((result) => {
|
||||
|
@ -23,7 +23,11 @@ export default class MemoryAdapter extends Adapter {
|
||||
list(path: string, callback: Callback): Unsubscribe {
|
||||
for (const [storedPath, storedValue] of this.storage) {
|
||||
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, () => {});
|
||||
}
|
||||
}
|
||||
|
@ -98,7 +98,6 @@ export default class Node {
|
||||
this.parent.children.set(childName, this);
|
||||
}
|
||||
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, () => {
|
||||
this.parent?.map_subscriptions.delete(id);
|
||||
});
|
||||
@ -130,7 +129,7 @@ export default class Node {
|
||||
* @param callback
|
||||
*/
|
||||
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) => {
|
||||
if (value === undefined) {
|
||||
if (returnIfUndefined) {
|
||||
@ -168,10 +167,15 @@ export default class Node {
|
||||
map(callback: Callback): Unsubscribe {
|
||||
const id = this.counter++;
|
||||
this.map_subscriptions.set(id, callback);
|
||||
const latestMap = new Map<string, NodeValue>(); // replace with this.value?
|
||||
|
||||
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()!;
|
||||
console.log('map callback', this.id, childName, 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 publicState from '@/state/PublicState.ts';
|
||||
import ExplorerNode from '@/views/explorer/ExplorerNode.tsx';
|
||||
import View from '@/views/View.tsx';
|
||||
|
||||
@ -14,6 +15,9 @@ const Explorer = ({ p }: Props) => {
|
||||
<div className="m-2 md:mx-4">
|
||||
<ExplorerNode expanded={true} name="Local state" node={localState} />
|
||||
</div>
|
||||
<div className="m-2 md:mx-4">
|
||||
<ExplorerNode expanded={true} name="Public state" node={publicState} />
|
||||
</div>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
@ -5,11 +5,11 @@ import { Event } from 'nostr-tools';
|
||||
import EventDB from '@/nostr/EventDB';
|
||||
import Events from '@/nostr/Events';
|
||||
import Key from '@/nostr/Key';
|
||||
import publicState from '@/state/PublicState.ts';
|
||||
import { RouteProps } from '@/views/types.ts';
|
||||
import View from '@/views/View.tsx';
|
||||
|
||||
import Feed from '../../components/feed/Feed';
|
||||
import Session from '../../nostr/Session';
|
||||
import localState from '../../state/LocalState.ts';
|
||||
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');
|
||||
node.once((saveLastOpened) => {
|
||||
if (saveLastOpened !== false) {
|
||||
// TODO this gets triggered only once per Iris session?
|
||||
const time = Math.floor(Date.now() / 1000);
|
||||
const success = Session.public?.set('notifications/lastOpened', time);
|
||||
if (!success) {
|
||||
console.log('user rejected');
|
||||
// stop pestering if user rejects signature request
|
||||
node.put(false);
|
||||
}
|
||||
console.log('set state');
|
||||
publicState.get('notifications').get('lastOpened').put(time);
|
||||
// TODO if user rejected, stop pestering them with sign prompt
|
||||
localState.get('unseenNotificationCount').put(0);
|
||||
}
|
||||
});
|
||||
}, true);
|
||||
}, 1000),
|
||||
[],
|
||||
);
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { useEffect, useState } from 'preact/hooks';
|
||||
|
||||
import Session from '../../nostr/Session';
|
||||
import localState from '../../state/LocalState.ts';
|
||||
import { translate as t } from '../../translations/Translation.mjs';
|
||||
|
||||
@ -11,10 +10,12 @@ const Appearance = () => {
|
||||
const [showConnectedRelays, setShowConnectedRelays] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// TODO use Nostr.private
|
||||
Session.public?.get('settings/colorScheme', (/*entry*/) => {
|
||||
//setColorScheme(entry.value);
|
||||
// TODO use privateState. or localState?
|
||||
/*
|
||||
publicState.get('settings/colorScheme', (value) => {
|
||||
//setColorScheme(value);
|
||||
});
|
||||
*/
|
||||
localState.get('showConnectedRelays').on(setShowConnectedRelays);
|
||||
}, []);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user