const e = require("b4a"),
{ ChatSchema: t } = require("@holepunchto/keet-core-schemas"),
{
FEATURES: i,
GENERIC_EVENTS: s,
DISPLAY_TYPES: a,
isDisplaySupported: n,
} = require("@holepunchto/keet-core-api"),
{ PRIMARY_THREAD_ID: r } = t.Messages,
{
AddChatMessageOperation: l,
AddFileOperation: o,
AddGenericEventOperation: d,
RemoveChatMessageOperation: p,
RemoveFileOperation: h,
RemoveGenericEventOperation: y,
UpdateChatMessageOperation: c,
} = t.Operations,
u = new Set([s.CALL_STARTED, s.ROOM_AVATAR_CHANGED, s.CALL_HANGUP, s.CALL_MISSED])
function v(e) {
return e.filter((e) => e.type === a.MENTION)
}
function m(e) {
return !!e.replyTo
}
function w(t, i) {
return (
(!t && !i) || (t && i && e.equals(t.key, i.key) && t.path === i.path && t.version === i.version)
)
}
async function g(e, t) {
const i = await e.get(t)
return i ? i.value : null
}
module.exports = class {
static Operations = new Set([l.TYPE, c.TYPE, p.TYPE, d.TYPE, y.TYPE, o.TYPE, h.TYPE])
constructor(e) {
;((this.view = e),
(this.members = null),
(this.config = null),
(this.chat = null),
(this.mentionsByMemberId = null),
(this.eventIdsByMemberId = null),
(this.eventIdsByMessageid = null),
(this.eventIdsByClock = null),
(this.drivesByPath = null))
}
initialize() {
;((this.members = this.view.members),
(this.config = this.view.config),
(this.chat = this.view.collections.chat),
(this.mentionsByMemberId = this.view.indexes.mentionsByMemberId),
(this.eventIdsByMessageId = this.view.indexes.eventIdsByMessageId),
(this.eventIdsByClock = this.view.indexes.eventIdsByClock),
(this.drivesByPath = this.view.indexes.drivesByPath))
}
async _ensureMemberCanChat({ id: e }) {
return !((await this.config.isBroadcastRoom()) && !(await this.members.isModerator(e.key)))
}
_applyAddChatMessageOperation(e, { id: t, timestamp: i }) {
const { replyTo: s, parent: a, display: n, ...r } = e.value
return this.addMessage({
id: t,
timestamp: i,
clock: 0,
chat: r,
parent: a,
display: n,
replyTo: s,
})
}
_applyAddFileOperation(e, { id: t, timestamp: i }) {
const s = {
id: t,
timestamp: i,
clock: 0,
parent: e.value.parent,
file: e.value.file,
chat: e.value.description ? { text: e.value.description } : null,
}
return this.addMessage(s)
}
async _applyAddGenericEventOperation(e, { id: t, timestamp: a }) {
if (
u.has(e.value.type) &&
!(
(e.value.type === s.CALL_HANGUP &&
(await this.view.isUnsupportedFeature(i.CALL_HANGUP_EVENT))) ||
(e.value.type === s.CALL_MISSED &&
(await this.view.isUnsupportedFeature(i.CALL_MISSED_EVENT)))
)
)
return this.addMessage({ id: t, timestamp: a, clock: 0, event: e.value })
}
async _applyUpdateChatMessageOperation(t, { id: s }) {
if (m(t.value) && (await this.view.isUnsupportedFeature(i.REPLY_TO))) return
const a = t.value.id,
r = await g(this.eventIdsByMessageId, [a.key, a.seq])
if (!r) return
const l = await g(this.chat, r)
if (!l) return
const [o, d, p] = await Promise.all([
this.members.isModerator(s.key),
this.members.getMemberForDeviceKey(s.key),
this.members.getMemberForDeviceKey(a.key),
])
if (e.equals(d.key, p.key) || o) {
const e = t.value
if (e.display) {
const t = await this.view.internal.getCodeVersion()
e.display = e.display.filter((e) => n(t, e.type))
}
await this.chat.update(r, {
...l,
replyTo: e.replyTo,
display: e.display,
chat: { text: e.text, edited: !0 },
})
}
}
async _applyRemovalOperation(t, { id: i }) {
const s = t.value.id,
a = await g(this.eventIdsByMessageId, [s.key, s.seq])
if (!a) return
const n = await this.chat.get(a)
if (!n) return
const r = n.value,
[l, o, d, p] = await Promise.all([
this.members.isModerator(i.key),
this.members.getMemberForDeviceKey(i.key),
this.members.getMemberForDeviceKey(s.key),
this.members.getInactiveMemberForDeviceKey(s.key),
]),
h = d || p
;(e.equals(o.key, h.key) || l) &&
(await this.config.deletePinnedMessageIfExists(s),
await this.chat.update(a, { ...r, chat: null, file: null, event: null, deleted: !0 }))
}
_applyRemoveChatMessageOperation(e, t) {
return this._applyRemovalOperation(e, t)
}
_applyRemoveFileOperation(e, t) {
return this._applyRemovalOperation(e, t)
}
_applyRemoveGenericEventOperation(e, t) {
return this._applyRemovalOperation(e, t)
}
async _indexClocks(e, t, i) {
t &&
!e &&
(await this.eventIdsByClock.insert(t.clock, i),
await this.eventIdsByMessageId.insert([t.id.key, t.id.seq], i))
}
async _indexMentions(e, t, i) {
const s = e?.display && v(e.display),
a = t?.display && v(t.display)
if (s) for (const { value: e } of s) await this.mentionsByMemberId.delete([e.memberId, ...i])
if (a) for (const { value: e } of a) await this.mentionsByMemberId.insert([e.memberId, ...i])
}
async _indexDrivesByPath(e, t) {
;(e &&
t &&
w(e.file, t.file) &&
e?.file &&
(await this.drivesByPath.delete([e.file.path, e.file.key, e.file.version])),
t?.file && (await this.drivesByPath.insert([t.file.path, t.file.key, t.file.version])))
}
async getThreadLength(e) {
const t = await this.chat.peek({ gte: [e], lte: [e], reverse: !0 })
return t ? t._id[1] + 1 : 0
}
async messageExists(e) {
const t = await this.getMessageById(e)
return t && !0 !== t?.value?.deleted
}
async getMessageById(e) {
const t = await this.getEventId(e)
return t ? await this.chat.get(t) : null
}
async getEventId(e) {
const t = await this.eventIdsByMessageId.get([e.key, e.seq])
return t ? t.value : null
}
async addMessage(e) {
const [t, s, a, l, o] = await Promise.all([
this.view.internal.getCodeVersion(),
this.view.isUnsupportedFeature(i.REPLY_TO),
e.parent ? g(this.eventIdsByMessageId, e.parent) : null,
this.eventIdsByClock.peek({ reverse: !0 }),
this.view.members.isDeviceMuted(e.id.key),
])
if ((m(e) && s) || o) return
let d = r
a && (d = a.clock + 1)
const p = await this.getThreadLength(d)
;((e.clock = l ? l.key + 1 : 0),
e.display && (e.display = e.display.filter((e) => n(t, e.type))),
await this.chat.update([d, p], e, { upsert: !0 }))
}
async autobaseApply(e, t) {
if (await this._ensureMemberCanChat(t))
switch (e.type) {
case l.TYPE:
return this._applyAddChatMessageOperation(e, t)
case o.TYPE:
return this._applyAddFileOperation(e, t)
case d.TYPE:
return this._applyAddGenericEventOperation(e, t)
case p.TYPE:
return this._applyRemoveChatMessageOperation(e, t)
case h.TYPE:
return this._applyRemoveFileOperation(e, t)
case y.TYPE:
return this._applyRemoveGenericEventOperation(e, t)
case c.TYPE:
return this._applyUpdateChatMessageOperation(e, t)
}
}
async autobaseIndex({ prev: e, next: t, id: i }) {
;(await this._indexClocks(e, t, i),
await this._indexMentions(e, t, i),
await this._indexDrivesByPath(e, t, i))
}
}