Add Chat module (#270)
* add packages/status-js/src/client/chat.ts * cache received but skipped waku messages * comment missing Map type in generate proto type * handle removed and added chats * remove lodash * use this.client.waku instead of this.waku in community.ts * rename community props * fix waku reference in Community * fix community reference in chat * handle chat.description changes * fix references in ui * revert use of contentTopic * fix callback on fetch * use clock to limit fetch * fix use of clock for community description * remove notes * remove await * use Object.keys() for diff in chats * fix typo * add decryption key in start() * set content topics when adding decryption keys * add getter with tmp name * rename community handler to handleDescription * return this.description !== undefined * add options to constructor
This commit is contained in:
parent
29b0e23319
commit
062c29d6fa
|
@ -46,7 +46,6 @@
|
||||||
"ethereum-cryptography": "^1.0.3",
|
"ethereum-cryptography": "^1.0.3",
|
||||||
"js-sha3": "^0.8.0",
|
"js-sha3": "^0.8.0",
|
||||||
"js-waku": "^0.23.0",
|
"js-waku": "^0.23.0",
|
||||||
"lodash": "^4.17.21",
|
|
||||||
"long": "^5.2.0",
|
"long": "^5.2.0",
|
||||||
"pbkdf2": "^3.1.2",
|
"pbkdf2": "^3.1.2",
|
||||||
"protobufjs": "^6.11.3",
|
"protobufjs": "^6.11.3",
|
||||||
|
@ -57,7 +56,6 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bn.js": "^5.1.0",
|
"@types/bn.js": "^5.1.0",
|
||||||
"@types/elliptic": "^6.4.14",
|
"@types/elliptic": "^6.4.14",
|
||||||
"@types/lodash": "^4.14.182",
|
|
||||||
"@types/pbkdf2": "^3.1.0",
|
"@types/pbkdf2": "^3.1.0",
|
||||||
"@types/secp256k1": "^4.0.3",
|
"@types/secp256k1": "^4.0.3",
|
||||||
"@types/uuid": "^8.3.3",
|
"@types/uuid": "^8.3.3",
|
||||||
|
|
|
@ -117,6 +117,7 @@ export interface CommunityDescription {
|
||||||
members: CommunityMember
|
members: CommunityMember
|
||||||
permissions: CommunityPermissions
|
permissions: CommunityPermissions
|
||||||
identity: ChatIdentity
|
identity: ChatIdentity
|
||||||
|
// fixme!: Map
|
||||||
chats: CommunityChat
|
chats: CommunityChat
|
||||||
banList: string[]
|
banList: string[]
|
||||||
categories: CommunityCategory
|
categories: CommunityCategory
|
||||||
|
|
|
@ -17,7 +17,7 @@ export interface ClientOptions {
|
||||||
}
|
}
|
||||||
|
|
||||||
class Client {
|
class Client {
|
||||||
private waku: Waku
|
public waku: Waku
|
||||||
public readonly wakuMessages: Set<string>
|
public readonly wakuMessages: Set<string>
|
||||||
|
|
||||||
public account?: Account
|
public account?: Account
|
||||||
|
@ -29,7 +29,7 @@ class Client {
|
||||||
this.wakuMessages = new Set()
|
this.wakuMessages = new Set()
|
||||||
|
|
||||||
// Community
|
// Community
|
||||||
this.community = new Community(this, waku, options.publicKey)
|
this.community = new Community(this, options.publicKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
static async start(options: ClientOptions) {
|
static async start(options: ClientOptions) {
|
||||||
|
@ -72,7 +72,7 @@ class Client {
|
||||||
// this.account = undefined
|
// this.account = undefined
|
||||||
// }
|
// }
|
||||||
|
|
||||||
public sendMessage = async (
|
public sendWakuMessage = async (
|
||||||
type: keyof typeof ApplicationMetadataMessage.Type,
|
type: keyof typeof ApplicationMetadataMessage.Type,
|
||||||
payload: Uint8Array,
|
payload: Uint8Array,
|
||||||
contentTopic: string,
|
contentTopic: string,
|
||||||
|
|
|
@ -0,0 +1,409 @@
|
||||||
|
import { hexToBytes } from 'ethereum-cryptography/utils'
|
||||||
|
import { PageDirection } from 'js-waku'
|
||||||
|
|
||||||
|
import { ChatMessage as ChatMessageProto } from '~/protos/chat-message'
|
||||||
|
import { CommunityRequestToJoin } from '~/protos/communities'
|
||||||
|
import { EmojiReaction } from '~/protos/emoji-reaction'
|
||||||
|
|
||||||
|
import { idToContentTopic } from '../contentTopic'
|
||||||
|
import { createSymKeyFromPassword } from '../encryption'
|
||||||
|
import { getReactions } from './community/get-reactions'
|
||||||
|
|
||||||
|
import type { MessageType } from '../../protos/enums'
|
||||||
|
import type { Client } from '../client'
|
||||||
|
import type { Community } from './community/community'
|
||||||
|
import type { Reactions } from './community/get-reactions'
|
||||||
|
import type { ImageMessage } from '~/src/proto/communities/v1/chat_message'
|
||||||
|
import type { CommunityChat } from '~/src/proto/communities/v1/communities'
|
||||||
|
import type { WakuMessage } from 'js-waku'
|
||||||
|
|
||||||
|
export type ChatMessage = ChatMessageProto & {
|
||||||
|
messageId: string
|
||||||
|
pinned: boolean
|
||||||
|
reactions: Reactions
|
||||||
|
chatUuid: string
|
||||||
|
responseToMessage?: Omit<ChatMessage, 'responseToMessage'>
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Chat {
|
||||||
|
private readonly community: Community
|
||||||
|
private readonly client: Client
|
||||||
|
|
||||||
|
public readonly uuid: string
|
||||||
|
public readonly id: string
|
||||||
|
public readonly contentTopic: string
|
||||||
|
public readonly type: MessageType.COMMUNITY_CHAT
|
||||||
|
public readonly symmetricKey: Uint8Array
|
||||||
|
public description: CommunityChat
|
||||||
|
public readonly chatCallbacks: Set<(description: CommunityChat) => void>
|
||||||
|
public messages: ChatMessage[]
|
||||||
|
public readonly messageCallbacks: Set<(messages: ChatMessage[]) => void>
|
||||||
|
|
||||||
|
constructor(options: {
|
||||||
|
community: Community
|
||||||
|
client: Client
|
||||||
|
uuid: string
|
||||||
|
id: string
|
||||||
|
contentTopic: string
|
||||||
|
type: MessageType.COMMUNITY_CHAT
|
||||||
|
symmetricKey: Uint8Array
|
||||||
|
description: CommunityChat
|
||||||
|
}) {
|
||||||
|
this.client = options.client
|
||||||
|
this.community = options.community
|
||||||
|
|
||||||
|
this.uuid = options.uuid
|
||||||
|
this.id = options.id
|
||||||
|
this.contentTopic = options.contentTopic
|
||||||
|
this.type = options.type
|
||||||
|
this.symmetricKey = options.symmetricKey
|
||||||
|
this.description = options.description
|
||||||
|
|
||||||
|
this.chatCallbacks = new Set()
|
||||||
|
this.messages = []
|
||||||
|
this.messageCallbacks = new Set()
|
||||||
|
}
|
||||||
|
|
||||||
|
public static create = async (
|
||||||
|
community: Community,
|
||||||
|
client: Client,
|
||||||
|
uuid: string,
|
||||||
|
type: MessageType.COMMUNITY_CHAT,
|
||||||
|
description: CommunityChat
|
||||||
|
) => {
|
||||||
|
const id = `${community.publicKey}${uuid}`
|
||||||
|
const contentTopic = idToContentTopic(id)
|
||||||
|
const symmetricKey = await createSymKeyFromPassword(id)
|
||||||
|
|
||||||
|
return new Chat({
|
||||||
|
community,
|
||||||
|
client,
|
||||||
|
uuid,
|
||||||
|
id,
|
||||||
|
contentTopic,
|
||||||
|
type,
|
||||||
|
symmetricKey,
|
||||||
|
description,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public getMessages = () => {
|
||||||
|
return this.messages
|
||||||
|
}
|
||||||
|
|
||||||
|
public onChange = (callback: (description: CommunityChat) => void) => {
|
||||||
|
this.chatCallbacks.add(callback)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
this.chatCallbacks.delete(callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public emitChange = (description: CommunityChat) => {
|
||||||
|
this.chatCallbacks.forEach(callback => callback(description))
|
||||||
|
}
|
||||||
|
|
||||||
|
public onMessage = (
|
||||||
|
callback: (messages: ChatMessage[]) => void
|
||||||
|
): (() => void) => {
|
||||||
|
this.messageCallbacks.add(callback)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
this.messageCallbacks.delete(callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public fetchMessages = async (
|
||||||
|
options: { start: Date },
|
||||||
|
callback: (messages: ChatMessage[]) => void
|
||||||
|
) => {
|
||||||
|
const startTime = options.start
|
||||||
|
const endTime = new Date()
|
||||||
|
|
||||||
|
let _oldestClock: BigInt | undefined
|
||||||
|
let _oldestMessageTime: Date | undefined
|
||||||
|
|
||||||
|
if (this.messages.length) {
|
||||||
|
_oldestClock = this.messages[0].clock
|
||||||
|
_oldestMessageTime = new Date(Number(this.messages[0].timestamp))
|
||||||
|
|
||||||
|
// already handled
|
||||||
|
if (_oldestMessageTime <= options.start) {
|
||||||
|
callback(this.messages)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.client.waku.store.queryHistory([this.contentTopic], {
|
||||||
|
timeFilter: {
|
||||||
|
startTime: startTime,
|
||||||
|
endTime: endTime,
|
||||||
|
},
|
||||||
|
pageSize: 50,
|
||||||
|
// most recent page first
|
||||||
|
pageDirection: PageDirection.BACKWARD,
|
||||||
|
decryptionKeys: [this.symmetricKey],
|
||||||
|
callback: (wakuMessages: WakuMessage[]) => {
|
||||||
|
// oldest message first
|
||||||
|
for (const wakuMessage of wakuMessages) {
|
||||||
|
this.client.handleWakuMessage(wakuMessage)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// callback
|
||||||
|
// more not found
|
||||||
|
if (
|
||||||
|
_oldestClock &&
|
||||||
|
this.messages.length &&
|
||||||
|
_oldestClock >= this.messages[0].clock
|
||||||
|
) {
|
||||||
|
callback([])
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(this.messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
public emitMessages = (messages: ChatMessage[]) => {
|
||||||
|
// fixme!: don't emit on backfill
|
||||||
|
this.messageCallbacks.forEach(callback => callback(messages))
|
||||||
|
}
|
||||||
|
|
||||||
|
public handleChange = (description: CommunityChat) => {
|
||||||
|
// state
|
||||||
|
this.description = description
|
||||||
|
|
||||||
|
// callback
|
||||||
|
this.emitChange(description)
|
||||||
|
}
|
||||||
|
|
||||||
|
public handleNewMessage = (message: ChatMessage) => {
|
||||||
|
let messageIndex = this.messages.length
|
||||||
|
while (messageIndex > 0) {
|
||||||
|
const _message = this.messages[messageIndex - 1]
|
||||||
|
|
||||||
|
if (_message.clock <= message.clock) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
messageIndex--
|
||||||
|
}
|
||||||
|
|
||||||
|
let responseToMessageIndex = this.messages.length
|
||||||
|
while (--responseToMessageIndex >= 0) {
|
||||||
|
const _message = this.messages[responseToMessageIndex]
|
||||||
|
|
||||||
|
if (_message.messageId === message.responseTo) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responseToMessageIndex >= 0) {
|
||||||
|
message.responseToMessage = this.messages[responseToMessageIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
// state
|
||||||
|
this.messages.splice(messageIndex, 0, message)
|
||||||
|
|
||||||
|
// callback
|
||||||
|
this.emitMessages(this.messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
public handleEditedMessage = (messageId: string, text: string) => {
|
||||||
|
let messageIndex = this.messages.length
|
||||||
|
while (--messageIndex >= 0) {
|
||||||
|
const _message = this.messages[messageIndex]
|
||||||
|
|
||||||
|
if (_message.messageId === messageId) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// original not found
|
||||||
|
if (messageIndex < 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.messages[messageIndex] = {
|
||||||
|
...this.messages[messageIndex],
|
||||||
|
text,
|
||||||
|
}
|
||||||
|
|
||||||
|
// callback
|
||||||
|
this.emitMessages(this.messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
public handleDeletedMessage = (messageId: string) => {
|
||||||
|
let messageIndex = this.messages.length
|
||||||
|
while (--messageIndex >= 0) {
|
||||||
|
const _message = this.messages[messageIndex]
|
||||||
|
|
||||||
|
if (_message.messageId === messageId) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messageIndex < 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.messages.splice(messageIndex, 1)
|
||||||
|
|
||||||
|
this.emitMessages(this.messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
public handlePinnedMessage = (messageId: string, pinned?: boolean) => {
|
||||||
|
let messageIndex = this.messages.length
|
||||||
|
while (--messageIndex >= 0) {
|
||||||
|
const _message = this.messages[messageIndex]
|
||||||
|
|
||||||
|
if (_message.messageId === messageId) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messageIndex < 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.messages[messageIndex].pinned = Boolean(pinned)
|
||||||
|
|
||||||
|
this.emitMessages(this.messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
public handleEmojiReaction = (
|
||||||
|
messageId: string,
|
||||||
|
reaction: EmojiReaction,
|
||||||
|
isMe: boolean
|
||||||
|
) => {
|
||||||
|
let messageIndex = this.messages.length
|
||||||
|
while (--messageIndex >= 0) {
|
||||||
|
const _message = this.messages[messageIndex]
|
||||||
|
|
||||||
|
if (_message.messageId === messageId) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messageIndex < 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.messages[messageIndex].reactions = getReactions(
|
||||||
|
reaction,
|
||||||
|
this.messages[messageIndex].reactions,
|
||||||
|
isMe
|
||||||
|
)
|
||||||
|
|
||||||
|
this.emitMessages(this.messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
public sendTextMessage = async (text: string, responseTo?: string) => {
|
||||||
|
// TODO: protos does not support optional fields :-(
|
||||||
|
const payload = ChatMessageProto.encode({
|
||||||
|
clock: BigInt(Date.now()),
|
||||||
|
timestamp: BigInt(Date.now()),
|
||||||
|
text,
|
||||||
|
responseTo: responseTo ?? '',
|
||||||
|
ensName: '',
|
||||||
|
chatId: this.id,
|
||||||
|
messageType: 'COMMUNITY_CHAT',
|
||||||
|
contentType: ChatMessageProto.ContentType.TEXT_PLAIN,
|
||||||
|
sticker: { hash: '', pack: 0 },
|
||||||
|
image: {
|
||||||
|
type: 'JPEG',
|
||||||
|
payload: new Uint8Array([]),
|
||||||
|
},
|
||||||
|
audio: {
|
||||||
|
type: 'AAC',
|
||||||
|
payload: new Uint8Array([]),
|
||||||
|
durationMs: BigInt(0),
|
||||||
|
},
|
||||||
|
community: new Uint8Array([]),
|
||||||
|
grant: new Uint8Array([]),
|
||||||
|
displayName: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
await this.client.sendWakuMessage(
|
||||||
|
'TYPE_CHAT_MESSAGE',
|
||||||
|
payload,
|
||||||
|
this.contentTopic,
|
||||||
|
this.symmetricKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public sendImageMessage = async (image: ImageMessage) => {
|
||||||
|
const payload = ChatMessageProto.encode({
|
||||||
|
clock: BigInt(Date.now()),
|
||||||
|
timestamp: BigInt(Date.now()),
|
||||||
|
text: '',
|
||||||
|
responseTo: '',
|
||||||
|
ensName: '',
|
||||||
|
chatId: this.id,
|
||||||
|
messageType: 'COMMUNITY_CHAT',
|
||||||
|
contentType: ChatMessageProto.ContentType.TEXT_PLAIN,
|
||||||
|
sticker: { hash: '', pack: 0 },
|
||||||
|
image: {
|
||||||
|
type: image.type,
|
||||||
|
payload: image.payload,
|
||||||
|
},
|
||||||
|
audio: {
|
||||||
|
type: 'AAC',
|
||||||
|
payload: new Uint8Array([]),
|
||||||
|
durationMs: BigInt(0),
|
||||||
|
},
|
||||||
|
community: new Uint8Array([]),
|
||||||
|
grant: new Uint8Array([]),
|
||||||
|
displayName: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
await this.client.sendWakuMessage(
|
||||||
|
'TYPE_CHAT_MESSAGE',
|
||||||
|
payload,
|
||||||
|
this.contentTopic,
|
||||||
|
this.symmetricKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public sendReaction = async (
|
||||||
|
chatId: string,
|
||||||
|
messageId: string,
|
||||||
|
reaction: EmojiReaction.Type
|
||||||
|
) => {
|
||||||
|
const payload = EmojiReaction.encode({
|
||||||
|
clock: BigInt(Date.now()),
|
||||||
|
chatId: chatId,
|
||||||
|
messageType: 'COMMUNITY_CHAT',
|
||||||
|
grant: new Uint8Array([]),
|
||||||
|
messageId,
|
||||||
|
retracted: false,
|
||||||
|
type: reaction,
|
||||||
|
})
|
||||||
|
|
||||||
|
await this.client.sendWakuMessage(
|
||||||
|
'TYPE_EMOJI_REACTION',
|
||||||
|
payload,
|
||||||
|
this.contentTopic,
|
||||||
|
this.symmetricKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public requestToJoin = async () => {
|
||||||
|
const payload = CommunityRequestToJoin.encode({
|
||||||
|
clock: BigInt(Date.now()),
|
||||||
|
chatId: this.id,
|
||||||
|
communityId: hexToBytes(this.community.publicKey.replace(/^0[xX]/, '')),
|
||||||
|
ensName: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
await this.client.sendWakuMessage(
|
||||||
|
'TYPE_COMMUNITY_REQUEST_TO_JOIN',
|
||||||
|
payload,
|
||||||
|
this.contentTopic,
|
||||||
|
this.symmetricKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,82 +1,69 @@
|
||||||
import { hexToBytes } from 'ethereum-cryptography/utils'
|
import { waku_message } from 'js-waku'
|
||||||
import { PageDirection, waku_message } from 'js-waku'
|
|
||||||
import difference from 'lodash/difference'
|
|
||||||
|
|
||||||
import { ChatMessage } from '~/protos/chat-message'
|
import { MessageType } from '~/protos/enums'
|
||||||
import { CommunityRequestToJoin } from '~/protos/communities'
|
import { getDifferenceByKeys } from '~/src/helpers/get-difference-by-keys'
|
||||||
import { EmojiReaction } from '~/protos/emoji-reaction'
|
|
||||||
|
|
||||||
import { idToContentTopic } from '../../contentTopic'
|
import { idToContentTopic } from '../../contentTopic'
|
||||||
import { createSymKeyFromPassword } from '../../encryption'
|
import { createSymKeyFromPassword } from '../../encryption'
|
||||||
import { createChannelContentTopics } from './create-channel-content-topics'
|
import { Chat } from '../chat'
|
||||||
|
|
||||||
import type { Client } from '../../client'
|
import type { Client } from '../../client'
|
||||||
import type { CommunityDescription } from '../../wire/community_description'
|
import type {
|
||||||
import type { Reactions } from './get-reactions'
|
CommunityChat,
|
||||||
import type { ImageMessage } from '~/src/proto/communities/v1/chat_message'
|
CommunityDescription,
|
||||||
import type { Waku } from 'js-waku'
|
} from '~/src/proto/communities/v1/communities'
|
||||||
|
|
||||||
export type CommunityMetadataType = CommunityDescription['proto']
|
|
||||||
|
|
||||||
export type MessageType = ChatMessage & {
|
|
||||||
messageId: string
|
|
||||||
pinned: boolean
|
|
||||||
reactions: Reactions
|
|
||||||
channelId: string
|
|
||||||
responseToMessage?: Omit<MessageType, 'responseToMessage'>
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Community {
|
export class Community {
|
||||||
private client: Client
|
private client: Client
|
||||||
private waku: Waku
|
|
||||||
public communityPublicKey: string
|
|
||||||
private communityContentTopic!: string
|
|
||||||
private communityDecryptionKey!: Uint8Array
|
|
||||||
public communityMetadata!: CommunityMetadataType // state
|
|
||||||
public channelMessages: Partial<{ [key: string]: MessageType[] }> = {} // state
|
|
||||||
public channelMessagesCallbacks: {
|
|
||||||
[key: string]: (messages: MessageType[]) => void
|
|
||||||
} = {}
|
|
||||||
public communityCallback:
|
|
||||||
| ((community: CommunityMetadataType) => void)
|
|
||||||
| undefined
|
|
||||||
|
|
||||||
constructor(client: Client, waku: Waku, publicKey: string) {
|
public publicKey: string
|
||||||
|
private contentTopic!: string
|
||||||
|
private symmetricKey!: Uint8Array
|
||||||
|
public description!: CommunityDescription
|
||||||
|
public chats: Map<string, Chat>
|
||||||
|
public callback: ((description: CommunityDescription) => void) | undefined
|
||||||
|
|
||||||
|
constructor(client: Client, publicKey: string) {
|
||||||
this.client = client
|
this.client = client
|
||||||
this.waku = waku
|
this.publicKey = publicKey
|
||||||
this.communityPublicKey = publicKey
|
|
||||||
|
this.chats = new Map()
|
||||||
}
|
}
|
||||||
|
|
||||||
public async start() {
|
public async start() {
|
||||||
this.communityContentTopic = idToContentTopic(this.communityPublicKey)
|
this.contentTopic = idToContentTopic(this.publicKey)
|
||||||
this.communityDecryptionKey = await createSymKeyFromPassword(
|
this.symmetricKey = await createSymKeyFromPassword(this.publicKey)
|
||||||
this.communityPublicKey
|
|
||||||
)
|
|
||||||
|
|
||||||
// Waku
|
// Waku
|
||||||
this.waku.store.addDecryptionKey(this.communityDecryptionKey)
|
this.client.waku.store.addDecryptionKey(this.symmetricKey, {
|
||||||
|
contentTopics: [this.contentTopic],
|
||||||
|
})
|
||||||
|
this.client.waku.relay.addDecryptionKey(this.symmetricKey, {
|
||||||
|
contentTopics: [this.contentTopic],
|
||||||
|
})
|
||||||
|
|
||||||
// Community
|
// Community
|
||||||
const communityMetadata = await this.fetchCommunity()
|
const description = await this.fetch()
|
||||||
|
|
||||||
if (!communityMetadata) {
|
if (!description) {
|
||||||
throw new Error('Failed to intiliaze Community')
|
throw new Error('Failed to intiliaze Community')
|
||||||
}
|
}
|
||||||
|
|
||||||
this.communityMetadata = communityMetadata
|
this.description = description
|
||||||
|
|
||||||
await this.observeCommunity()
|
this.observe()
|
||||||
|
|
||||||
// Channels
|
// Chats
|
||||||
await this.observeChannelMessages(Object.keys(this.communityMetadata.chats))
|
await this.observeChatMessages(this.description.chats)
|
||||||
}
|
}
|
||||||
|
|
||||||
public fetchCommunity = async () => {
|
// todo: rename this to chats when changing references in ui
|
||||||
let communityMetadata: CommunityMetadataType | undefined
|
public get _chats() {
|
||||||
let shouldStop = false
|
return [...this.chats.values()]
|
||||||
|
}
|
||||||
|
|
||||||
await this.waku.store.queryHistory([this.communityContentTopic], {
|
public fetch = async () => {
|
||||||
decryptionKeys: [this.communityDecryptionKey],
|
await this.client.waku.store.queryHistory([this.contentTopic], {
|
||||||
// oldest message first
|
// oldest message first
|
||||||
callback: wakuMessages => {
|
callback: wakuMessages => {
|
||||||
let index = wakuMessages.length
|
let index = wakuMessages.length
|
||||||
|
@ -85,380 +72,117 @@ export class Community {
|
||||||
while (--index >= 0) {
|
while (--index >= 0) {
|
||||||
this.client.handleWakuMessage(wakuMessages[index])
|
this.client.handleWakuMessage(wakuMessages[index])
|
||||||
|
|
||||||
if (!this.communityMetadata) {
|
return this.description !== undefined
|
||||||
return shouldStop
|
|
||||||
}
|
|
||||||
|
|
||||||
communityMetadata = this.communityMetadata
|
|
||||||
shouldStop = true
|
|
||||||
|
|
||||||
return shouldStop
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return communityMetadata
|
return this.description
|
||||||
}
|
}
|
||||||
|
|
||||||
public createFetchChannelMessages = async (
|
private observe = () => {
|
||||||
channelId: string,
|
this.client.waku.relay.addObserver(this.client.handleWakuMessage, [
|
||||||
callback: (messages: MessageType[]) => void
|
this.contentTopic,
|
||||||
) => {
|
|
||||||
const id = `${this.communityPublicKey}${channelId}`
|
|
||||||
const channelContentTopic = idToContentTopic(id)
|
|
||||||
const symKey = await createSymKeyFromPassword(id)
|
|
||||||
|
|
||||||
return async (options: { start: Date }) => {
|
|
||||||
const startTime = options.start
|
|
||||||
const endTime = new Date()
|
|
||||||
|
|
||||||
const _messages = this.channelMessages[channelId] || []
|
|
||||||
let _oldestMessageTime: Date | undefined = undefined
|
|
||||||
|
|
||||||
if (_messages.length) {
|
|
||||||
_oldestMessageTime = new Date(Number(_messages[0].timestamp))
|
|
||||||
|
|
||||||
if (_oldestMessageTime <= options.start) {
|
|
||||||
callback(_messages)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.waku.store.queryHistory([channelContentTopic], {
|
|
||||||
timeFilter: {
|
|
||||||
startTime: startTime,
|
|
||||||
endTime: endTime,
|
|
||||||
},
|
|
||||||
pageSize: 50,
|
|
||||||
// most recent page first
|
|
||||||
pageDirection: PageDirection.BACKWARD,
|
|
||||||
decryptionKeys: [symKey],
|
|
||||||
callback: wakuMessages => {
|
|
||||||
// oldest message first
|
|
||||||
for (const wakuMessage of wakuMessages) {
|
|
||||||
this.client.handleWakuMessage(wakuMessage)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// callback
|
|
||||||
if (
|
|
||||||
_oldestMessageTime &&
|
|
||||||
this.channelMessages[channelId]?.length &&
|
|
||||||
_oldestMessageTime >=
|
|
||||||
new Date(Number(this.channelMessages[channelId]![0].timestamp))
|
|
||||||
) {
|
|
||||||
callback([])
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
callback(this.channelMessages[channelId] ?? [])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private observeCommunity = () => {
|
|
||||||
this.waku.relay.addDecryptionKey(this.communityDecryptionKey)
|
|
||||||
this.waku.relay.addObserver(this.client.handleWakuMessage, [
|
|
||||||
this.communityContentTopic,
|
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
private observeChannelMessages = async (chatsIds: string[]) => {
|
private observeChatMessages = async (
|
||||||
const symKeyPromises = chatsIds.map(async (chatId: string) => {
|
chatDescriptions: CommunityDescription['chats']
|
||||||
const id = `${this.communityPublicKey}${chatId}`
|
) => {
|
||||||
const channelContentTopic = idToContentTopic(id)
|
const chatPromises = Object.entries(chatDescriptions).map(
|
||||||
|
async ([chatUuid, chatDescription]: [string, CommunityChat]) => {
|
||||||
|
const chat = await Chat.create(
|
||||||
|
this,
|
||||||
|
this.client,
|
||||||
|
chatUuid,
|
||||||
|
MessageType.COMMUNITY_CHAT,
|
||||||
|
chatDescription
|
||||||
|
)
|
||||||
|
const contentTopic = chat.contentTopic
|
||||||
|
|
||||||
const symKey = await createSymKeyFromPassword(id)
|
this.chats.set(chatUuid, chat)
|
||||||
|
|
||||||
this.waku.relay.addDecryptionKey(symKey, {
|
this.client.waku.relay.addDecryptionKey(chat.symmetricKey, {
|
||||||
method: waku_message.DecryptionMethod.Symmetric,
|
method: waku_message.DecryptionMethod.Symmetric,
|
||||||
contentTopics: [channelContentTopic],
|
contentTopics: [contentTopic],
|
||||||
})
|
})
|
||||||
|
|
||||||
return channelContentTopic
|
return contentTopic
|
||||||
})
|
|
||||||
const contentTopics = await Promise.all(symKeyPromises)
|
|
||||||
|
|
||||||
this.waku.relay.addObserver(this.client.handleWakuMessage, contentTopics)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private unobserveChannelMessages = (chatIds: string[]) => {
|
|
||||||
const contentTopics = createChannelContentTopics(
|
|
||||||
chatIds,
|
|
||||||
this.communityPublicKey
|
|
||||||
)
|
)
|
||||||
|
|
||||||
this.waku.relay.deleteObserver(this.client.handleWakuMessage, contentTopics)
|
const contentTopics = await Promise.all(chatPromises)
|
||||||
|
|
||||||
|
this.client.waku.relay.addObserver(
|
||||||
|
this.client.handleWakuMessage,
|
||||||
|
contentTopics
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public handleCommunityMetadataEvent = (
|
private unobserveChatMessages = (
|
||||||
communityMetadata: CommunityMetadataType
|
chatDescription: CommunityDescription['chats']
|
||||||
) => {
|
) => {
|
||||||
if (this.communityMetadata) {
|
const contentTopics = Object.keys(chatDescription).map(chatUuid => {
|
||||||
if (this.communityMetadata.clock > communityMetadata.clock) {
|
const chat = this.chats.get(chatUuid)
|
||||||
|
const contentTopic = chat!.contentTopic
|
||||||
|
|
||||||
|
this.chats.delete(chatUuid)
|
||||||
|
|
||||||
|
return contentTopic
|
||||||
|
})
|
||||||
|
|
||||||
|
this.client.waku.relay.deleteObserver(
|
||||||
|
this.client.handleWakuMessage,
|
||||||
|
contentTopics
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public handleDescription = (description: CommunityDescription) => {
|
||||||
|
if (this.description) {
|
||||||
|
// already handled
|
||||||
|
if (this.description.clock >= description.clock) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Channels
|
// Chats
|
||||||
const removedChats = difference(
|
// observe
|
||||||
Object.keys(this.communityMetadata.chats),
|
const removedChats = getDifferenceByKeys(
|
||||||
Object.keys(communityMetadata.chats)
|
this.description.chats,
|
||||||
|
description.chats
|
||||||
)
|
)
|
||||||
const addedChats = difference(
|
if (Object.keys(removedChats).length) {
|
||||||
Object.keys(communityMetadata.chats),
|
this.unobserveChatMessages(removedChats)
|
||||||
Object.keys(this.communityMetadata.chats)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (removedChats.length) {
|
|
||||||
this.unobserveChannelMessages(removedChats)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (addedChats.length) {
|
const addedChats = getDifferenceByKeys(
|
||||||
this.observeChannelMessages(addedChats)
|
description.chats,
|
||||||
|
this.description.chats
|
||||||
|
)
|
||||||
|
if (Object.keys(addedChats).length) {
|
||||||
|
this.observeChatMessages(addedChats)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Community
|
// Community
|
||||||
this.communityMetadata = communityMetadata
|
|
||||||
this.communityCallback?.(communityMetadata)
|
|
||||||
}
|
|
||||||
|
|
||||||
public handleChannelChatMessageNewEvent = (chatMessage: MessageType) => {
|
|
||||||
const _messages = this.channelMessages[chatMessage.channelId] || []
|
|
||||||
|
|
||||||
// findIndexLeft
|
|
||||||
// const index = _messages.findIndex(({ timestamp }) => {
|
|
||||||
// new Date(Number(timestamp)) > new Date(Number(message.timestamp))
|
|
||||||
// })
|
|
||||||
// findIndexRight
|
|
||||||
let messageIndex = _messages.length
|
|
||||||
while (messageIndex > 0) {
|
|
||||||
const _message = _messages[messageIndex - 1]
|
|
||||||
|
|
||||||
// if (_message.messageId === chatMessage.messageId) {
|
|
||||||
// messageIndex = -1
|
|
||||||
|
|
||||||
// break
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (_message.clock <= chatMessage.clock) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
messageIndex--
|
|
||||||
}
|
|
||||||
|
|
||||||
// // already received
|
|
||||||
// if (messageIndex < 0) {
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
|
|
||||||
// replied
|
|
||||||
let responsedToMessageIndex = _messages.length
|
|
||||||
while (--responsedToMessageIndex >= 0) {
|
|
||||||
const _message = _messages[responsedToMessageIndex]
|
|
||||||
|
|
||||||
if (_message.messageId === chatMessage.responseTo) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (responsedToMessageIndex >= 0) {
|
|
||||||
chatMessage.responseToMessage = _messages[responsedToMessageIndex]
|
|
||||||
}
|
|
||||||
|
|
||||||
_messages.splice(messageIndex, 0, chatMessage)
|
|
||||||
|
|
||||||
// state
|
// state
|
||||||
const channelId = _messages[0].channelId
|
this.description = description
|
||||||
|
|
||||||
this.channelMessages[channelId] = _messages
|
|
||||||
|
|
||||||
// callback
|
// callback
|
||||||
this.channelMessagesCallbacks[channelId]?.(this.channelMessages[channelId]!)
|
this.callback?.(this.description)
|
||||||
|
|
||||||
|
// Chats
|
||||||
|
// handle
|
||||||
|
Object.entries(this.description.chats).forEach(
|
||||||
|
([chatUuid, chatDescription]) =>
|
||||||
|
this.chats.get(chatUuid)?.handleChange(chatDescription)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public getMessages(channelId: string): MessageType[] {
|
public onChange = (callback: (description: CommunityDescription) => void) => {
|
||||||
return this.channelMessages[channelId] ?? []
|
this.callback = callback
|
||||||
}
|
|
||||||
|
|
||||||
public onCommunityUpdate = (
|
|
||||||
callback: (community: CommunityMetadataType) => void
|
|
||||||
) => {
|
|
||||||
this.communityCallback = callback
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
this.communityCallback = undefined
|
this.callback = undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public onChannelMessageUpdate = (
|
|
||||||
channelId: string,
|
|
||||||
callback: (messages: MessageType[]) => void
|
|
||||||
) => {
|
|
||||||
this.channelMessagesCallbacks[channelId] = callback
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
delete this.channelMessagesCallbacks[channelId]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public sendTextMessage = async (
|
|
||||||
chatUuid: string,
|
|
||||||
text: string,
|
|
||||||
responseTo?: string
|
|
||||||
) => {
|
|
||||||
const chat = this.communityMetadata.chats[chatUuid]
|
|
||||||
|
|
||||||
if (!chat) {
|
|
||||||
throw new Error('Chat not found')
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: move to chat instance
|
|
||||||
const chatId = `${this.communityPublicKey}${chatUuid}`
|
|
||||||
const channelContentTopic = idToContentTopic(chatId)
|
|
||||||
const symKey = await createSymKeyFromPassword(chatId)
|
|
||||||
|
|
||||||
// TODO: protos does not support optional fields :-(
|
|
||||||
const payload = ChatMessage.encode({
|
|
||||||
clock: BigInt(Date.now()),
|
|
||||||
timestamp: BigInt(Date.now()),
|
|
||||||
text,
|
|
||||||
responseTo: responseTo ?? '',
|
|
||||||
ensName: '',
|
|
||||||
chatId,
|
|
||||||
messageType: 'COMMUNITY_CHAT',
|
|
||||||
contentType: ChatMessage.ContentType.TEXT_PLAIN,
|
|
||||||
sticker: { hash: '', pack: 0 },
|
|
||||||
image: {
|
|
||||||
type: 'JPEG',
|
|
||||||
payload: new Uint8Array([]),
|
|
||||||
},
|
|
||||||
audio: {
|
|
||||||
type: 'AAC',
|
|
||||||
payload: new Uint8Array([]),
|
|
||||||
durationMs: BigInt(0),
|
|
||||||
},
|
|
||||||
community: new Uint8Array([]),
|
|
||||||
grant: new Uint8Array([]),
|
|
||||||
displayName: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
await this.client.sendMessage(
|
|
||||||
'TYPE_CHAT_MESSAGE',
|
|
||||||
payload,
|
|
||||||
channelContentTopic,
|
|
||||||
symKey
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
public sendImageMessage = async (chatUuid: string, image: ImageMessage) => {
|
|
||||||
const chat = this.communityMetadata.chats[chatUuid]
|
|
||||||
|
|
||||||
if (!chat) {
|
|
||||||
throw new Error('Chat not found')
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: move to chat instance
|
|
||||||
const chatId = `${this.communityPublicKey}${chatUuid}`
|
|
||||||
const channelContentTopic = idToContentTopic(chatId)
|
|
||||||
const symKey = await createSymKeyFromPassword(chatId)
|
|
||||||
|
|
||||||
const payload = ChatMessage.encode({
|
|
||||||
clock: BigInt(Date.now()),
|
|
||||||
timestamp: BigInt(Date.now()),
|
|
||||||
text: '',
|
|
||||||
responseTo: responseTo ?? '',
|
|
||||||
ensName: '',
|
|
||||||
chatId,
|
|
||||||
messageType: 'COMMUNITY_CHAT',
|
|
||||||
contentType: ChatMessage.ContentType.TEXT_PLAIN,
|
|
||||||
sticker: { hash: '', pack: 0 },
|
|
||||||
image: {
|
|
||||||
type: image.type,
|
|
||||||
payload: image.payload,
|
|
||||||
},
|
|
||||||
audio: {
|
|
||||||
type: 'AAC',
|
|
||||||
payload: new Uint8Array([]),
|
|
||||||
durationMs: BigInt(0),
|
|
||||||
},
|
|
||||||
community: new Uint8Array([]),
|
|
||||||
grant: new Uint8Array([]),
|
|
||||||
displayName: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
await this.client.sendMessage(
|
|
||||||
'TYPE_CHAT_MESSAGE',
|
|
||||||
payload,
|
|
||||||
channelContentTopic,
|
|
||||||
symKey
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
public sendReaction = async (
|
|
||||||
chatId: string,
|
|
||||||
messageId: string,
|
|
||||||
reaction: EmojiReaction.Type
|
|
||||||
) => {
|
|
||||||
// const chat = this.communityMetadata.chats[chatId]
|
|
||||||
|
|
||||||
// if (!chat) {
|
|
||||||
// throw new Error('Chat not found')
|
|
||||||
// }
|
|
||||||
|
|
||||||
// TODO: move to chat instance
|
|
||||||
// const chatId = `${this.communityPublicKey}${chatUuid}`
|
|
||||||
const channelContentTopic = idToContentTopic(chatId)
|
|
||||||
const symKey = await createSymKeyFromPassword(chatId)
|
|
||||||
|
|
||||||
// TODO: protos does not support optional fields :-(
|
|
||||||
const payload = EmojiReaction.encode({
|
|
||||||
clock: BigInt(Date.now()),
|
|
||||||
chatId: chatId,
|
|
||||||
messageType: 'COMMUNITY_CHAT',
|
|
||||||
grant: new Uint8Array([]),
|
|
||||||
messageId,
|
|
||||||
retracted: false,
|
|
||||||
type: reaction,
|
|
||||||
})
|
|
||||||
|
|
||||||
await this.client.sendMessage(
|
|
||||||
'TYPE_EMOJI_REACTION',
|
|
||||||
payload,
|
|
||||||
channelContentTopic,
|
|
||||||
symKey
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
public requestToJoin = async (chatUuid: string) => {
|
|
||||||
if (!this.client.account) {
|
|
||||||
throw new Error('Account not found')
|
|
||||||
}
|
|
||||||
|
|
||||||
const chat = this.communityMetadata.chats[chatUuid]
|
|
||||||
|
|
||||||
if (!chat) {
|
|
||||||
throw new Error('Chat not found')
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: move to chat instance
|
|
||||||
const chatId = `${this.communityPublicKey}${chatUuid}`
|
|
||||||
|
|
||||||
const payload = CommunityRequestToJoin.encode({
|
|
||||||
clock: BigInt(Date.now()),
|
|
||||||
chatId,
|
|
||||||
communityId: hexToBytes(this.communityPublicKey.replace(/^0[xX]/, '')),
|
|
||||||
ensName: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
await this.client.sendMessage(
|
|
||||||
'TYPE_COMMUNITY_REQUEST_TO_JOIN',
|
|
||||||
payload,
|
|
||||||
this.communityContentTopic,
|
|
||||||
this.communityDecryptionKey
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
import { idToContentTopic } from '../../contentTopic'
|
|
||||||
|
|
||||||
export function createChannelContentTopics(
|
|
||||||
channelIds: string[],
|
|
||||||
communityPublicKey: string
|
|
||||||
) {
|
|
||||||
const channelTopics = channelIds.map(channelId => {
|
|
||||||
const id = `${communityPublicKey}${channelId}`
|
|
||||||
const channelContentTopic = idToContentTopic(id)
|
|
||||||
|
|
||||||
return channelContentTopic
|
|
||||||
})
|
|
||||||
|
|
||||||
return channelTopics
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
export function getChannelId(chatId: string) {
|
|
||||||
return chatId.slice(68)
|
|
||||||
}
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
export function getChatUuid(chatId: string) {
|
||||||
|
return chatId.slice(68)
|
||||||
|
}
|
|
@ -12,13 +12,12 @@ import { ProtocolMessage } from '../../../protos/protocol-message'
|
||||||
import { CommunityDescription } from '../../proto/communities/v1/communities'
|
import { CommunityDescription } from '../../proto/communities/v1/communities'
|
||||||
import { payloadToId } from '../../utils/payload-to-id'
|
import { payloadToId } from '../../utils/payload-to-id'
|
||||||
import { recoverPublicKey } from '../../utils/recover-public-key'
|
import { recoverPublicKey } from '../../utils/recover-public-key'
|
||||||
import { getChannelId } from './get-channel-id'
|
import { getChatUuid } from './get-chat-uuid'
|
||||||
import { getReactions } from './get-reactions'
|
|
||||||
import { mapChatMessage } from './map-chat-message'
|
import { mapChatMessage } from './map-chat-message'
|
||||||
|
|
||||||
import type { Account } from '../../account'
|
import type { Account } from '../../account'
|
||||||
import type { Client } from '../../client'
|
import type { Client } from '../../client'
|
||||||
import type { Community /*, MessageType*/ } from './community'
|
import type { Community } from './community'
|
||||||
import type { WakuMessage } from 'js-waku'
|
import type { WakuMessage } from 'js-waku'
|
||||||
|
|
||||||
export function handleWakuMessage(
|
export function handleWakuMessage(
|
||||||
|
@ -57,13 +56,13 @@ export function handleWakuMessage(
|
||||||
decodedMetadata.payload
|
decodedMetadata.payload
|
||||||
)
|
)
|
||||||
|
|
||||||
const wakuMessageId = payloadToId(
|
const messageId = payloadToId(
|
||||||
decodedProtocol?.publicMessage || decodedMetadata.payload,
|
decodedProtocol?.publicMessage || decodedMetadata.payload,
|
||||||
publicKey
|
publicKey
|
||||||
)
|
)
|
||||||
|
|
||||||
// already handled
|
// already handled
|
||||||
if (client.wakuMessages.has(wakuMessageId)) {
|
if (client.wakuMessages.has(messageId)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,7 +75,7 @@ export function handleWakuMessage(
|
||||||
const decodedPayload = CommunityDescription.decode(messageToDecode)
|
const decodedPayload = CommunityDescription.decode(messageToDecode)
|
||||||
|
|
||||||
// handle (state and callback)
|
// handle (state and callback)
|
||||||
community.handleCommunityMetadataEvent(decodedPayload)
|
community.handleDescription(decodedPayload)
|
||||||
|
|
||||||
success = true
|
success = true
|
||||||
|
|
||||||
|
@ -88,17 +87,16 @@ export function handleWakuMessage(
|
||||||
const decodedPayload = ChatMessage.decode(messageToDecode)
|
const decodedPayload = ChatMessage.decode(messageToDecode)
|
||||||
|
|
||||||
// TODO?: ignore community.channelMessages which are messageType !== COMMUNITY_CHAT
|
// TODO?: ignore community.channelMessages which are messageType !== COMMUNITY_CHAT
|
||||||
|
const chatUuid = getChatUuid(decodedPayload.chatId)
|
||||||
|
|
||||||
// map
|
// map
|
||||||
const channelId = getChannelId(decodedPayload.chatId)
|
|
||||||
|
|
||||||
const chatMessage = mapChatMessage(decodedPayload, {
|
const chatMessage = mapChatMessage(decodedPayload, {
|
||||||
messageId: wakuMessageId,
|
messageId,
|
||||||
channelId,
|
chatUuid,
|
||||||
})
|
})
|
||||||
|
|
||||||
// handle
|
// handle
|
||||||
community.handleChannelChatMessageNewEvent(chatMessage)
|
community.chats.get(chatUuid)?.handleNewMessage(chatMessage)
|
||||||
|
|
||||||
success = true
|
success = true
|
||||||
|
|
||||||
|
@ -109,40 +107,11 @@ export function handleWakuMessage(
|
||||||
const decodedPayload = EditMessage.decode(messageToDecode)
|
const decodedPayload = EditMessage.decode(messageToDecode)
|
||||||
|
|
||||||
const messageId = decodedPayload.messageId
|
const messageId = decodedPayload.messageId
|
||||||
const channelId = getChannelId(decodedPayload.chatId)
|
const chatUuid = getChatUuid(decodedPayload.chatId)
|
||||||
|
|
||||||
const _messages = community.channelMessages[channelId] || []
|
community.chats
|
||||||
|
.get(chatUuid)
|
||||||
let index = _messages.length
|
?.handleEditedMessage(messageId, decodedPayload.text)
|
||||||
while (--index >= 0) {
|
|
||||||
const _message = _messages[index]
|
|
||||||
|
|
||||||
if (_message.messageId === messageId) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// original not found
|
|
||||||
if (index < 0) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
const _message = _messages[index]
|
|
||||||
|
|
||||||
const message = {
|
|
||||||
..._message,
|
|
||||||
text: decodedPayload.text,
|
|
||||||
}
|
|
||||||
|
|
||||||
_messages[index] = message
|
|
||||||
|
|
||||||
// state
|
|
||||||
community.channelMessages[channelId] = _messages
|
|
||||||
|
|
||||||
// callback
|
|
||||||
community.channelMessagesCallbacks[channelId]?.(
|
|
||||||
community.channelMessages[channelId]!
|
|
||||||
)
|
|
||||||
|
|
||||||
success = true
|
success = true
|
||||||
|
|
||||||
|
@ -153,33 +122,9 @@ export function handleWakuMessage(
|
||||||
const decodedPayload = DeleteMessage.decode(messageToDecode)
|
const decodedPayload = DeleteMessage.decode(messageToDecode)
|
||||||
|
|
||||||
const messageId = decodedPayload.messageId
|
const messageId = decodedPayload.messageId
|
||||||
const channelId = getChannelId(decodedPayload.chatId)
|
const chatUuid = getChatUuid(decodedPayload.chatId)
|
||||||
|
|
||||||
const _messages = community.channelMessages[channelId] || []
|
community.chats.get(chatUuid)?.handleDeletedMessage(messageId)
|
||||||
|
|
||||||
let index = _messages.length
|
|
||||||
while (--index >= 0) {
|
|
||||||
const _message = _messages[index]
|
|
||||||
|
|
||||||
if (_message.messageId === messageId) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// original not found
|
|
||||||
if (index < 0) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
_messages.splice(index, 1)
|
|
||||||
|
|
||||||
// state
|
|
||||||
community.channelMessages[channelId] = _messages
|
|
||||||
|
|
||||||
// callback
|
|
||||||
community.channelMessagesCallbacks[channelId]?.(
|
|
||||||
community.channelMessages[channelId]!
|
|
||||||
)
|
|
||||||
|
|
||||||
success = true
|
success = true
|
||||||
|
|
||||||
|
@ -190,33 +135,11 @@ export function handleWakuMessage(
|
||||||
const decodedPayload = PinMessage.decode(messageToDecode)
|
const decodedPayload = PinMessage.decode(messageToDecode)
|
||||||
|
|
||||||
const messageId = decodedPayload.messageId
|
const messageId = decodedPayload.messageId
|
||||||
const channelId = getChannelId(decodedPayload.chatId)
|
const chatUuid = getChatUuid(decodedPayload.chatId)
|
||||||
|
|
||||||
const _messages = community.channelMessages[channelId] || []
|
community.chats
|
||||||
|
.get(chatUuid)
|
||||||
let index = _messages.length
|
?.handlePinnedMessage(messageId, decodedPayload.pinned)
|
||||||
while (--index >= 0) {
|
|
||||||
const _message = _messages[index]
|
|
||||||
|
|
||||||
if (_message.messageId === messageId) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// original not found
|
|
||||||
if (index < 0) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
_messages[index].pinned = Boolean(decodedPayload.pinned)
|
|
||||||
|
|
||||||
// state
|
|
||||||
community.channelMessages[channelId] = _messages
|
|
||||||
|
|
||||||
// callback
|
|
||||||
community.channelMessagesCallbacks[channelId]?.(
|
|
||||||
community.channelMessages[channelId]!
|
|
||||||
)
|
|
||||||
|
|
||||||
success = true
|
success = true
|
||||||
|
|
||||||
|
@ -227,41 +150,12 @@ export function handleWakuMessage(
|
||||||
const decodedPayload = EmojiReaction.decode(messageToDecode)
|
const decodedPayload = EmojiReaction.decode(messageToDecode)
|
||||||
|
|
||||||
const messageId = decodedPayload.messageId
|
const messageId = decodedPayload.messageId
|
||||||
const channelId = getChannelId(decodedPayload.chatId)
|
const chatUuid = getChatUuid(decodedPayload.chatId)
|
||||||
|
const isMe = account?.publicKey === `0x${bytesToHex(publicKey)}`
|
||||||
|
|
||||||
const _messages = community.channelMessages[channelId] || []
|
community.chats
|
||||||
|
.get(chatUuid)
|
||||||
let index = _messages.length
|
?.handleEmojiReaction(messageId, decodedPayload, isMe)
|
||||||
while (--index >= 0) {
|
|
||||||
const _message = _messages[index]
|
|
||||||
|
|
||||||
if (_message.messageId === messageId) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// original not found
|
|
||||||
if (index < 0) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
const _message = _messages[index]
|
|
||||||
const isMe =
|
|
||||||
account?.publicKey === `0x${bytesToHex(wakuMessage.signaturePublicKey)}`
|
|
||||||
|
|
||||||
_messages[index].reactions = getReactions(
|
|
||||||
decodedPayload,
|
|
||||||
_message.reactions,
|
|
||||||
isMe
|
|
||||||
)
|
|
||||||
|
|
||||||
// state
|
|
||||||
community.channelMessages[channelId] = _messages
|
|
||||||
|
|
||||||
// callback
|
|
||||||
community.channelMessagesCallbacks[channelId]?.(
|
|
||||||
community.channelMessages[channelId]!
|
|
||||||
)
|
|
||||||
|
|
||||||
success = true
|
success = true
|
||||||
|
|
||||||
|
@ -269,11 +163,13 @@ export function handleWakuMessage(
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
success = true
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
client.wakuMessages.add(wakuMessageId)
|
client.wakuMessages.add(messageId)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
|
@ -1,22 +1,19 @@
|
||||||
import type { ChatMessage } from '../../../protos/chat-message'
|
import type { ChatMessage } from '../chat'
|
||||||
import type { MessageType } from './community'
|
import type { ChatMessage as ChatMessageProto } from '~/protos/chat-message'
|
||||||
// import type { Reactions } from './set-reactions'
|
|
||||||
|
|
||||||
export function mapChatMessage(
|
export function mapChatMessage(
|
||||||
decodedMessage: ChatMessage,
|
decodedMessage: ChatMessageProto,
|
||||||
props: {
|
props: {
|
||||||
messageId: string
|
messageId: string
|
||||||
channelId: string
|
chatUuid: string
|
||||||
// pinned: boolean
|
|
||||||
// reactions: Reactions
|
|
||||||
}
|
}
|
||||||
): MessageType {
|
): ChatMessage {
|
||||||
const { messageId, channelId } = props
|
const { messageId, chatUuid } = props
|
||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
...decodedMessage,
|
...decodedMessage,
|
||||||
messageId,
|
messageId,
|
||||||
channelId,
|
chatUuid,
|
||||||
pinned: false,
|
pinned: false,
|
||||||
reactions: {
|
reactions: {
|
||||||
THUMBS_UP: {
|
THUMBS_UP: {
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
export function getDifferenceByKeys<T extends Record<string, unknown>>(
|
||||||
|
a: T,
|
||||||
|
b: T
|
||||||
|
): T {
|
||||||
|
const initialValue: Record<string, unknown> = {}
|
||||||
|
|
||||||
|
const result = Object.entries(a).reduce((result, [key, value]) => {
|
||||||
|
if (!b[key]) {
|
||||||
|
result[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}, initialValue)
|
||||||
|
|
||||||
|
return result as T
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
export type { Account } from './account'
|
export type { Account } from './account'
|
||||||
export type { Client, ClientOptions } from './client'
|
export type { Client, ClientOptions } from './client'
|
||||||
export { createClient } from './client'
|
export { createClient } from './client'
|
||||||
export type { Community, MessageType } from './client/community/community'
|
export type { ChatMessage as Message } from './client/chat'
|
||||||
|
export type { Community } from './client/community/community'
|
||||||
|
|
|
@ -11,14 +11,14 @@ const Context = createContext<State | undefined>(undefined)
|
||||||
type State = {
|
type State = {
|
||||||
loading: boolean
|
loading: boolean
|
||||||
client: Client | undefined
|
client: Client | undefined
|
||||||
community: Community['communityMetadata'] | undefined
|
community: Community['description'] | undefined
|
||||||
account: Account | undefined
|
account: Account | undefined
|
||||||
dispatch?: React.Dispatch<Action>
|
dispatch?: React.Dispatch<Action>
|
||||||
}
|
}
|
||||||
|
|
||||||
type Action =
|
type Action =
|
||||||
| { type: 'INIT'; client: Client }
|
| { type: 'INIT'; client: Client }
|
||||||
| { type: 'UPDATE_COMMUNITY'; community: Community['communityMetadata'] }
|
| { type: 'UPDATE_COMMUNITY'; community: Community['description'] }
|
||||||
| { type: 'SET_ACCOUNT'; account: Account }
|
| { type: 'SET_ACCOUNT'; account: Account }
|
||||||
| { type: 'REMOVE_ACCOUNT' }
|
| { type: 'REMOVE_ACCOUNT' }
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ const reducer = (state: State, action: Action): State => {
|
||||||
...state,
|
...state,
|
||||||
loading: false,
|
loading: false,
|
||||||
client,
|
client,
|
||||||
community: client.community.communityMetadata,
|
community: client.community.description,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'UPDATE_COMMUNITY': {
|
case 'UPDATE_COMMUNITY': {
|
||||||
|
@ -76,7 +76,7 @@ export const ProtocolProvider = (props: Props) => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (client) {
|
if (client) {
|
||||||
return client.community.onCommunityUpdate(community => {
|
return client.community.onChange(community => {
|
||||||
dispatch({ type: 'UPDATE_COMMUNITY', community })
|
dispatch({ type: 'UPDATE_COMMUNITY', community })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -103,7 +103,7 @@ export function useProtocol() {
|
||||||
// we enforce initialization of client before rendering children
|
// we enforce initialization of client before rendering children
|
||||||
return context as State & {
|
return context as State & {
|
||||||
client: Client
|
client: Client
|
||||||
community: Community['communityMetadata']
|
community: Community['description']
|
||||||
dispatch: React.Dispatch<Action>
|
dispatch: React.Dispatch<Action>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { useProtocol } from './provider'
|
||||||
|
|
||||||
import type { Community } from '@status-im/js'
|
import type { Community } from '@status-im/js'
|
||||||
|
|
||||||
export type Chat = Community['communityMetadata']['chats'][0]
|
export type Chat = Community['description']['chats'][0]
|
||||||
|
|
||||||
export const useChat = (id: string): Chat => {
|
export const useChat = (id: string): Chat => {
|
||||||
const { community } = useProtocol()
|
const { community } = useProtocol()
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { useProtocol } from './provider'
|
||||||
|
|
||||||
import type { Community } from '@status-im/js'
|
import type { Community } from '@status-im/js'
|
||||||
|
|
||||||
export type Chat = Community['communityMetadata']['chats'][0] & {
|
export type Chat = Community['description']['chats'][0] & {
|
||||||
id: string
|
id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { useProtocol } from '~/src/protocol'
|
||||||
|
|
||||||
import type { Community } from '@status-im/js'
|
import type { Community } from '@status-im/js'
|
||||||
|
|
||||||
export type Member = Community['communityMetadata']['members'][0]
|
export type Member = Community['description']['members'][0]
|
||||||
|
|
||||||
export const useMembers = (): string[] => {
|
export const useMembers = (): string[] => {
|
||||||
const { community } = useProtocol()
|
const { community } = useProtocol()
|
||||||
|
|
|
@ -91,20 +91,20 @@ export const useMessages = (channelId: string): Result => {
|
||||||
// const [state, dispatch] = useReducer<Result>((state,action) => {}, {})
|
// const [state, dispatch] = useReducer<Result>((state,action) => {}, {})
|
||||||
|
|
||||||
const [data, setData] = useState<any[]>(() =>
|
const [data, setData] = useState<any[]>(() =>
|
||||||
client.community.getMessages(channelId)
|
client.community.chats.get(channelId).getMessages()
|
||||||
)
|
)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState<Error>()
|
const [error, setError] = useState<Error>()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setData(client.community.getMessages(channelId))
|
setData(client.community.chats.get(channelId).getMessages())
|
||||||
|
|
||||||
const handleUpdate = (messages: Message[]) => {
|
const handleUpdate = (messages: Message[]) => {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
setData(messages)
|
setData(messages)
|
||||||
}
|
}
|
||||||
|
|
||||||
return client.community.onChannelMessageUpdate(channelId, handleUpdate)
|
return client.community.chats.get(channelId).onMessage(handleUpdate)
|
||||||
}, [channelId])
|
}, [channelId])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -77,15 +77,14 @@ export const ChatMessage = (props: Props) => {
|
||||||
const userProfileDialog = useDialog(UserProfileDialog)
|
const userProfileDialog = useDialog(UserProfileDialog)
|
||||||
|
|
||||||
const handleMessageSubmit = (message: string) => {
|
const handleMessageSubmit = (message: string) => {
|
||||||
client.community.sendTextMessage(
|
client.community.get(chatId).sendTextMessage(
|
||||||
chatId,
|
|
||||||
message,
|
message,
|
||||||
'0x0fa999097568d1fdcc39108a08d75340bd2cee5ec59c36799007150d0a9fc896'
|
'0x0fa999097568d1fdcc39108a08d75340bd2cee5ec59c36799007150d0a9fc896'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleReaction = (reaction: Reaction) => {
|
const handleReaction = (reaction: Reaction) => {
|
||||||
client.community.sendReaction(chatId, messageId, reaction)
|
client.community.get(chatId).sendReaction(chatId, messageId, reaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleReplyClick = () => {
|
const handleReplyClick = () => {
|
||||||
|
|
|
@ -82,8 +82,7 @@ export const Chat = () => {
|
||||||
const showMembers = enableMembers && state.showMembers
|
const showMembers = enableMembers && state.showMembers
|
||||||
|
|
||||||
const handleMessageSubmit = (message: string) => {
|
const handleMessageSubmit = (message: string) => {
|
||||||
client.community.sendTextMessage(
|
client.community.get(chatId).sendTextMessage(
|
||||||
chatId,
|
|
||||||
message
|
message
|
||||||
// '0x0fa999097568d1fdcc39108a08d75340bd2cee5ec59c36799007150d0a9fc896'
|
// '0x0fa999097568d1fdcc39108a08d75340bd2cee5ec59c36799007150d0a9fc896'
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue