From f3648c590466b92171a231a37e7a8d5fde8b340c Mon Sep 17 00:00:00 2001 From: Michal Date: Tue, 10 Sep 2024 10:17:05 +0100 Subject: [PATCH] WIP --- packages/notifications/package.json | 3 + packages/notifications/src/index.ts | 76 +++++++++++- .../src/notification-manager.test.ts | 16 ++- .../notifications/src/notification-manager.ts | 109 ++++++++++++++++++ 4 files changed, 201 insertions(+), 3 deletions(-) create mode 100644 packages/notifications/src/notification-manager.ts diff --git a/packages/notifications/package.json b/packages/notifications/package.json index d5b87d1a7d..e58c0a9cf9 100644 --- a/packages/notifications/package.json +++ b/packages/notifications/package.json @@ -216,5 +216,8 @@ "preset": "angular" } } + }, + "dependencies": { + "rxjs": "^7.8.1" } } diff --git a/packages/notifications/src/index.ts b/packages/notifications/src/index.ts index d9c011d044..27ac4700b1 100644 --- a/packages/notifications/src/index.ts +++ b/packages/notifications/src/index.ts @@ -1 +1,75 @@ -export const NotificationManager = {} +import {BehaviorSubject} from 'rxjs' + +export enum NotificationTrigger { + 'TransactionReceived' = 'TransactionReceived', + 'RewardsUpdated' = 'RewardsUpdated', + 'PrimaryTokenPriceChanged' = 'PrimaryTokenPriceChanged', +} + +export interface NotificationTransactionReceivedEvent + extends NotificationEventBase { + trigger: NotificationTrigger.TransactionReceived + metadata: { + previousTxsCounter: number + nextTxsCounter: number + txId: string + isSentByUser: boolean // check local pending + } +} + +export type Group = 'transaction-history' | 'portfolio' + +type NotificationTriggerPerGroup = { + 'transaction-history': + | NotificationTrigger.TransactionReceived + | NotificationTrigger.RewardsUpdated + 'portfolio': NotificationTrigger.PrimaryTokenPriceChanged +} + +export type NotificationEvent = NotificationTransactionReceivedEvent + +type NotificationEventId = string + +interface NotificationEventBase { + id: string // uuid + date: number + isRead: boolean +} + +export type NotificationConfig = { + [NotificationTrigger.PrimaryTokenPriceChanged]: { + notify: boolean + threshold: number + interval: '24h' | '1h' + } + [NotificationTrigger.TransactionReceived]: { + notify: boolean + } + [NotificationTrigger.RewardsUpdated]: { + notify: boolean + } +} + +export type NotificationManager = { + hydrate: () => void // build up subscriptions + unreadCounterByGroup$: BehaviorSubject>> + + // NOTE: events represent a notification event that was trigger by a config rule + events: { + markAllAsRead: () => void + markAsRead(id: NotificationEventId): void + read: () => Promise> + save: (event: Readonly) => void + clear: () => void + } + // NOTE: config sets the ground to what, when, and if should notify user + config: { + read: () => Promise> // return initial if empty + save: (config: Readonly) => Promise + reset: () => Promise + } + destroy: () => void // tear down subscriptions + clear: () => void +} + +// TODO: Add trackers - these are only pointers to last notification event diff --git a/packages/notifications/src/notification-manager.test.ts b/packages/notifications/src/notification-manager.test.ts index 472ad560df..23a18e0852 100644 --- a/packages/notifications/src/notification-manager.test.ts +++ b/packages/notifications/src/notification-manager.test.ts @@ -1,7 +1,19 @@ -import {NotificationManager} from './index' +import {notificationManagerMaker} from './notification-manager' describe('NotificationManager', () => { it('should be defined', () => { - expect(NotificationManager).toBeDefined() + expect(notificationManagerMaker()).toBeDefined() + }) + + it('should return default config if not set', async () => { + const manager = notificationManagerMaker() + const config = await manager.config.read() + expect(config).toEqual({ + PrimaryTokenPriceChanged: { + interval: '24h', + notify: true, + threshold: 10, + }, + }) }) }) diff --git a/packages/notifications/src/notification-manager.ts b/packages/notifications/src/notification-manager.ts new file mode 100644 index 0000000000..63c94955f4 --- /dev/null +++ b/packages/notifications/src/notification-manager.ts @@ -0,0 +1,109 @@ +import { + Group, + NotificationConfig, + NotificationEvent, + NotificationManager, + NotificationTrigger, +} from './index' +import {BehaviorSubject} from 'rxjs' +import {App} from '@yoroi/types' + +type NotificationManagerMakerProps = { + eventsStorage: App.Storage + configStorage: App.Storage +} + +type EventsStorageData = ReadonlyArray +type ConfigStorageData = NotificationConfig + +export const notificationManagerMaker = ({ + eventsStorage, + configStorage, +}: NotificationManagerMakerProps): NotificationManager => { + const hydrate = () => {} + + const events = eventsManagerMaker({storage: eventsStorage}) + const config = configManagerMaker({storage: configStorage}) + + const destroy = () => {} + + const clear = async () => { + await config.reset() + await events.clear() + } + + const unreadCounterByGroup$ = new BehaviorSubject>( + new Map(), + ) + + return {hydrate, clear, destroy, events, config, unreadCounterByGroup$} +} + +const eventsManagerMaker = ({ + storage, +}: { + storage: App.Storage +}): NotificationManager['events'] => { + const events = { + markAllAsRead: async () => { + const allEvents = await events.read() + const modifiedEvents = allEvents.map((event) => ({ + ...event, + isRead: true, + })) + await storage.setItem('events', modifiedEvents) + }, + markAsRead: async (id: string) => { + const allEvents = await events.read() + const modifiedEvents = allEvents.map((event) => + event.id === id ? {...event, isRead: true} : event, + ) + await storage.setItem('events', modifiedEvents) + }, + read: async (): Promise => { + return (await storage.getItem('events')) ?? [] + }, + save: async (event: Readonly) => { + const allEvents = await events.read() + await storage.setItem('events', [...allEvents, event]) + }, + clear: (): Promise => { + return storage.removeItem('events') + }, + } + return events +} + +const configManagerMaker = ({ + storage, +}: { + storage: App.Storage +}): NotificationManager['config'] => { + return { + read: async (): Promise => { + return ( + (await storage.getItem('config')) ?? defaultConfig + ) + }, + save: async (config: NotificationConfig): Promise => { + await storage.setItem('config', config) + }, + reset: async (): Promise => { + return storage.removeItem('config') + }, + } +} + +const defaultConfig: NotificationConfig = { + [NotificationTrigger.PrimaryTokenPriceChanged]: { + notify: true, + threshold: 10, + interval: '24h', + }, + [NotificationTrigger.TransactionReceived]: { + notify: true, + }, + [NotificationTrigger.RewardsUpdated]: { + notify: true, + }, +}