diff --git a/packages/status-js/src/client/activityCenter.ts b/packages/status-js/src/client/activityCenter.ts new file mode 100644 index 00000000..36eb8b36 --- /dev/null +++ b/packages/status-js/src/client/activityCenter.ts @@ -0,0 +1,125 @@ +// todo: rename to notifications (center?), inbox, or keep same as other platforms +import type { ChatMessage } from './chat' +// import type { Client } from './client' + +// todo?: union +// todo?: rename to Activity +type Notification = { + // fixme?: specify message type (message_reply) + type: 'message' + value: ChatMessage +} + +type ActivityCenterLatest = { + notifications: Notification[] + // todo?: rename count to mentionsAndRepliesCount + unreadChats: Map // id, count (mentions, replies) +} + +// todo?: rename to NotificationCenter +export class ActivityCenter { + // todo?: use client.account for mentions and replies, or in chat.ts + // #client: Client + + #notifications: Set + #callbacks: Set<(latest: ActivityCenterLatest) => void> + + constructor(/* client: Client */) { + // this.#client = client + + this.#notifications = new Set() + this.#callbacks = new Set() + } + + // todo?: rename to latest, change + public getLatest = (): ActivityCenterLatest => { + const notifications: Notification[] = [] + const unreadChats: Map = new Map() + + for (const notification of this.#notifications.values()) { + // todo?: switch + if (notification.type === 'message') { + const chatUuid = notification.value.chatUuid + + const chat = unreadChats.get(chatUuid) + if (chat) { + // fixme!: isReply || isMention + const shouldIncrement = false + if (shouldIncrement) { + chat.count++ + } + } else { + unreadChats.set(chatUuid, { count: 0 }) + } + } + + notifications.push(notification) + } + + // todo?: reverse order + notifications.sort((a, b) => { + if (a.value.clock < b.value.clock) { + return -1 + } + + if (a.value.clock > b.value.clock) { + return 1 + } + + return 0 + }) + + // fixme!?: do not display regular messages, only mentions and replies + // todo?: group notifications (all, unreads, mentions, replies, _chats.{id,count}) + return { notifications, unreadChats } + } + + public addMessageNotification = (value: ChatMessage) => { + this.#notifications.add({ type: 'message', value }) + + this.emitLatest() + } + + /** + * Removes all notifications. + */ + removeNotifications = () => { + this.#notifications.clear() + + this.emitLatest() + } + + /** + * Removes chat message notifications from the Activity Center. For example, + * on only opening or after scrolling to the end. + */ + public removeChatNotifications = (chatUuid: string) => { + // todo?: add chatUuid to "readChats" Set instead and resolve in getNotifications + // triggered by following emit, and clear the set afterwards + for (const notification of this.#notifications) { + if (notification.type !== 'message') { + continue + } + + if (notification.value.chatUuid === chatUuid) { + this.#notifications.delete(notification) + } + } + + this.emitLatest() + } + + private emitLatest = () => { + const latest = this.getLatest() + + this.#callbacks.forEach(callback => callback(latest)) + } + + public onChange = (callback: (latest: ActivityCenterLatest) => void) => { + this.#callbacks.add(callback) + + return () => { + this.#callbacks.delete(callback) + } + } +} diff --git a/packages/status-js/src/client/chat.ts b/packages/status-js/src/client/chat.ts index 36fc3971..67b2613b 100644 --- a/packages/status-js/src/client/chat.ts +++ b/packages/status-js/src/client/chat.ts @@ -35,7 +35,9 @@ export type ChatMessage = ChatMessageProto & { type FetchedMessage = { messageId: string; timestamp?: Date } +// todo?: add isMuted prop, use as condition to add a message/notification to activity center or not export class Chat { + // todo: use # private readonly client: Client #clock: bigint @@ -51,6 +53,7 @@ export class Chat { #pinEvents: Map> #reactEvents: Map> #deleteEvents: Map> + #isActive: boolean #fetchingMessages?: boolean #previousFetchedStartTime?: Date #oldestFetchedMessage?: FetchedMessage @@ -81,6 +84,7 @@ export class Chat { this.#pinEvents = new Map() this.#reactEvents = new Map() this.#deleteEvents = new Map() + this.#isActive = false this.messageCallbacks = new Set() } @@ -142,6 +146,7 @@ export class Chat { return this.#messages.get(id) } + // todo?: delete public onChange = (callback: (description: CommunityChat) => void) => { this.chatCallbacks.add(callback) @@ -158,9 +163,16 @@ export class Chat { callback: (messages: ChatMessage[]) => void ): (() => void) => { this.messageCallbacks.add(callback) + // todo?: set from ui, think use case without an ui + this.#isActive = true + // todo?!: only if in `unreadChats`, keep "unreads" separate from `notifications` + // todo?: only if at the bottom and all unread messages are in view + // todo?: call from ui + this.client.activityCenter.removeChatNotifications(this.uuid) return () => { this.messageCallbacks.delete(callback) + this.#isActive = false } } @@ -305,6 +317,11 @@ export class Chat { // callback this.emitMessages() + + // todo?: if not muted + if (!this.#isActive) { + this.client.activityCenter.addMessageNotifications(newMessage) + } } public handleEditedMessage = ( diff --git a/packages/status-js/src/client/client.ts b/packages/status-js/src/client/client.ts index 97d53d03..45e6a3c8 100644 --- a/packages/status-js/src/client/client.ts +++ b/packages/status-js/src/client/client.ts @@ -12,6 +12,7 @@ import { import { ApplicationMetadataMessage } from '../protos/application-metadata-message' import { Account } from './account' +import { ActivityCenter } from './activityCenter' import { Community } from './community/community' import { handleWakuMessage } from './community/handle-waku-message' @@ -37,6 +38,7 @@ class Client { */ #wakuDisconnectionTimer: ReturnType + public activityCenter: ActivityCenter public account?: Account public community: Community @@ -50,6 +52,9 @@ class Client { this.wakuMessages = new Set() this.#wakuDisconnectionTimer = wakuDisconnectionTimer + // Activity Center + this.activityCenter = new ActivityCenter(/* this */) + // Community this.community = new Community(this, options.publicKey) } diff --git a/packages/status-js/src/client/community/community.ts b/packages/status-js/src/client/community/community.ts index 40c5426e..c2bee8c2 100644 --- a/packages/status-js/src/client/community/community.ts +++ b/packages/status-js/src/client/community/community.ts @@ -267,6 +267,9 @@ export class Community { this.contentTopic, this.symmetricKey ) + + // todo?: + // this.client.activityCenter.addJoiningRequestNotification(...) } public isOwner = (