add activityCenter.ts

This commit is contained in:
Felicio Mununga 2022-08-22 19:46:43 +02:00
parent 81f2950c19
commit 45cd81eaab
No known key found for this signature in database
GPG Key ID: 0EB8D75C775AB6F1
4 changed files with 150 additions and 0 deletions

View File

@ -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<string, { count: number }> // 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<Notification>
#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<string, { count: number }> = 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)
}
}
}

View File

@ -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<string, Pick<ChatMessage, 'clock' | 'pinned'>>
#reactEvents: Map<string, Pick<ChatMessage, 'clock' | 'reactions'>>
#deleteEvents: Map<string, Pick<ChatMessage, 'clock' | 'signer'>>
#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 = (

View File

@ -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<typeof setInterval>
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)
}

View File

@ -267,6 +267,9 @@ export class Community {
this.contentTopic,
this.symmetricKey
)
// todo?:
// this.client.activityCenter.addJoiningRequestNotification(...)
}
public isOwner = (