import { DISPLAY_TYPES as e } from "@holepunchto/keet-core/legacy-constants"
import { first as t, last as s, isEmpty as r } from "../../lib/array"
import { getFileId as n, MEDIA_TYPE as o, parseCoreFile as i } from "../../media/file"
import {
isAudioMimeType as d,
isAudioVoicePath as a,
isImageMimeType as l,
isVideoMimeType as c,
} from "../../lib/file"
import {
LAST_MESSAGE_SEEN_PREFIX as p,
CHAT_MESSAGE_TYPE as m,
CHAT_MESSAGE_FORMAT as g,
MESSAGE_CATEGORY as I,
KNOWN_EVENT_TYPES as h,
CHAT_MESSAGE_REPORT_COUNT as E,
} from "../chat.constant"
import { parseCoreReactions as f } from "../reaction/chat-reaction.model"
import {
displayPartial as A,
displayToSegment as u,
displayReplaceMention as y,
} from "./message-parser"
export const GROUP_LIMIT = 64
const { HTTP_LINK: M, PEAR_LINK: T } = e
export const getLastSeenMessageKey = (e) => `${p}_${e}`
export const UNKNOWN_ID = "unknown_id"
export const createMessageId = (e) => {
if (e?.id?.deviceId && "number" == typeof e?.id?.seq) {
const { deviceId: t, seq: s } = e.id
return `${t}_${s}`
}
return UNKNOWN_ID
}
export const getChatMessageId = (e) => (e && e.id) || UNKNOWN_ID
export const getCoreId = (e) => {
if ("string" != typeof e) throw new Error(`chat message: broken message Id <${e}>`)
const t = e.split("_")
if (2 !== t.length) throw new Error(`chat message: broken message Id <${e}>`)
const s = t[0]
if (!s) throw new Error(`chat message: broken message Id <${e}>`)
const r = Number.parseInt(t[1], 10)
if (!Number.isInteger(r)) throw new Error(`chat message: broken message Id <${e}>`)
return { deviceId: s, seq: r }
}
export const getFormattedCoreEventId = (e, t) =>
"seq" === e && "string" == typeof t ? Number.parseInt(t, 10) : t
export const isSystemEvent = (e) => !!e?.event?.type
export const isCallSystemEvent = (e) =>
!(!e?.event?.type || ![m.CALL_STARTED, m.CALL_HANGUP].includes(e.event.type))
export const isStartCallMessage = (e) => e && e.event?.type === m.CALL_STARTED
export const updateMessage = (e, t) => {
const { memberById: s, blocked: r, local: n, hidden: o, range: i } = t || {}
let d = { ...e }
if (d.chat) {
let { text: e, display: t } = d.chat
const r = d.chat.mentions?.map((e) => ({ ...e })) || []
if (r.length > 0 && s)
for (const n of r) {
const { memberId: r } = n,
o = s[r]
if (o) {
const s = y({ text: e, display: t }, r, o.displayName)
;((t = s.display), (e = s.text), (n.pending = !1))
} else void 0 === n.pending && (n.pending = !0)
}
d.chat = { ...d.chat, display: t, text: e || "", mentions: r }
}
if ((void 0 !== r && (d.blocked = r), void 0 !== n && (d.local = n), (d.hidden = !!o), i)) {
const { start: e, end: t } = i,
s = A({ text: d.chat?.text || "", display: d.chat?.display || [] }, { start: e, end: t })
;((d.chat.text = s.text), (d.chat.display = s.display))
}
return (
(d = {
...d,
chat: {
...d.chat,
segments: u({ text: d.chat?.text, display: d.chat?.display }),
format: g.DISPLAY,
},
}),
d
)
}
export const MESSAGE_TYPE_TO_CATEGORY = {
[m.JOINED]: I.JOINED,
[m.CALL_STARTED]: I.CALL_STARTED,
[m.ROOM_AVATAR_CHANGED]: I.ROOM_AVATAR_CHANGED,
[m.INVITATION_CREATED]: I.INVITATION_CREATED,
[m.LEFT]: I.LEFT,
[m.ROOM_TITLE_CHANGED]: I.ROOM_TITLE_CHANGED,
[m.MEMBER_REMOVED]: I.MEMBER_REMOVED,
[m.CALL_HANGUP]: I.CALL_HANGUP,
[m.CALL_MISSED]: I.CALL_MISSED,
[m.LOCATION_SHARED]: I.LOCATION_SHARED,
}
export const calcMessageCategory = (e) => {
const t = e.event?.type
return t ? (MESSAGE_TYPE_TO_CATEGORY[t] ?? `UNKNOWN_${t}`) : I.NO_CATEGORY
}
const _ = { [I.JOINED]: 64, [I.LEFT]: 64, [I.MEMBER_REMOVED]: 64 }
export const getIsGroupCountLimitReached = (e, t) => t >= ((e && _[e]) || 1)
export const getFileMediaType = (e, t) =>
l(e) ? o.IMAGE : c(e) ? o.VIDEO : d(e) && a(t) ? o.VOICE : d(e) ? o.AUDIO : o.FILE
export const getFileListMediaType = (e) => {
if (!e.length) return o.FILE
const t = new Set()
for (let s = 0; s < e.length; s += 1) {
const r = e[s]
t.add(getFileMediaType(r.mimetype, r.name))
}
if (1 === t.size) {
if (t.has(o.IMAGE)) return o.IMAGE
if (t.has(o.VIDEO)) return o.VIDEO
if (t.has(o.AUDIO)) return o.AUDIO
if (t.has(o.VOICE)) return o.VOICE
} else if (2 === t.size && t.has(o.IMAGE) && t.has(o.VIDEO)) return o.MEDIA
return o.FILE
}
export const parseFiles = (e) => {
const t = [],
s = []
if (isSystemEvent(e)) return { fileId: "", fileIds: s, files: t, mediaType: null, chat: e.chat }
const { chat: r, files: o } = e
if (o)
for (let e = 0; e < o.length; e += 1) {
const r = i(o[e]),
{ id: n } = r
n && (t.push(r), s.push(n))
}
const d = r?.display,
a = d
? d.map((s, r) => {
if ((s.type === M || s.type === T) && s.preview?.file) {
const o = i(s.preview.file),
d = n(o.pointer) || `${createMessageId(e)}-link-preview-${r}`
if (o) return ((o.id = d), t.push(o), { ...s, preview: { ...s.preview, fileId: d } })
}
return s
})
: []
return {
fileId: s[0] || "",
files: t,
mediaType: t.length ? getFileListMediaType(t) : null,
fileIds: s,
chat: { ...r, display: a },
}
}
export const parseSystemEvents = (e) => {
const { message: t } = e,
{ generic: s } = t || {}
return s?.memberJoined
? { type: m.JOINED, memberId: s.memberJoined.memberId }
: s?.memberLeft
? { type: m.LEFT, memberId: s.memberLeft.memberId }
: s?.memberRemoved
? { type: m.MEMBER_REMOVED, memberId: e.memberId, targetMemberId: s.memberRemoved.memberId }
: s?.callStarted
? { type: m.CALL_STARTED }
: s?.callMissed
? { type: m.CALL_MISSED }
: s?.callHangup
? { type: m.CALL_HANGUP }
: s?.invitationCreated
? { type: m.INVITATION_CREATED }
: s?.roomAvatarChanged
? { type: m.ROOM_AVATAR_CHANGED }
: s?.titleChanged
? {
type: m.ROOM_TITLE_CHANGED,
from: s.titleChanged.from,
to: s.titleChanged.to,
}
: s?.locationShared
? {
type: m.LOCATION_SHARED,
latitude: s.locationShared.latitude,
longitude: s.locationShared.longitude,
}
: null
}
export const parseMessage = (e, t) => {
const s = createMessageId(e)
if (s === UNKNOWN_ID)
throw new Error("invalid core message", { cause: JSON.stringify(e, null, 2) })
const { fileId: r = "", mediaType: n, fileIds: o, files: i, chat: d } = parseFiles(e),
a = parseSystemEvents(e),
l = {
id: s,
coreId: e.id,
replyTo: e.replyTo ? createMessageId({ id: e.replyTo }) : void 0,
event: a,
memberId: e.memberId,
targetMemberId: a?.targetMemberId,
seq: e.seq,
timestamp: e.timestamp,
editedAt: e.editedAt,
deleted: e.deleted,
fileId: r,
mediaType: n,
fileIds: o,
chat: d,
reactionCount: f(e.reactions, s).total,
reported: t?.reported || (e.labels?.flags || 0) >= E,
reportedByMe: t?.reportedByMe || !!e.labels?.flagged,
reportedClock: e.labels?.flagsClock || 0,
reportedCount: e.labels?.flags || 0,
labelsEditedAt: e.labels?.editedAt || 0,
forwarded: e.forwarded,
}
return { message: updateMessage(l, t), files: i }
}
export const isMessageChanged = (e, t) =>
!(
e === t ||
!e ||
!t ||
(e.id === t.id &&
e.seq === t.seq &&
e.groupCount === t.groupCount &&
e.groupSeq === t.groupSeq &&
e.isHead === t.isHead &&
e.datetime === t.datetime &&
e.editedAt === t.editedAt &&
e.deleted === t.deleted &&
e.blocked === t.blocked &&
e.hidden === t.hidden &&
e.memberId === t.memberId &&
e.reported === t.reported &&
e.reportedByMe === t.reportedByMe)
)
export const isMessageChangedExcludeGrouping = (e, t) =>
!(
e === t ||
!e ||
!t ||
(e.id === t.id &&
e.seq === t.seq &&
e.editedAt === t.editedAt &&
e.deleted === t.deleted &&
e.blocked === t.blocked &&
e.hidden === t.hidden &&
e.memberId === t.memberId &&
e.reported === t.reported &&
e.reportedByMe === t.reportedByMe)
)
export const CHAT_MESSAGE_FILTER_MAP = {}
export const hasMoreMessages = (e, r) => {
const n = t(e)?.seq,
o = s(e)?.seq,
i = t(r)?.seq,
d = s(r)?.seq
return i > n || d < o
}
export const getChatDisplayNameForMention = ({ message: e, memberById: t }) =>
e.chat?.segments
?.map((s) => {
let r = e?.chat?.text?.slice(s.start, s.end)
for (const e of s.styles) {
const s = e.content?.split("/")?.[0],
n = t?.[s]
n?.displayName && (r = `@${n.displayName}`)
}
return r
})
.join("")
const O = [e.MENTION, e.EMOJI]
export function cutSegmentHighlight(e, t) {
if (r(e)) return e
const s = []
let n = !1
return (
e.forEach((e) => {
e.end <= t
? s.push(e)
: e.start < t && e.end > t
? ((n = !0), s.push({ ...e, end: t, highlightStr: e.highlightStr.slice(0, t - e.start) }))
: (n = !0)
}),
n ? s : e
)
}
export const cutFormattedText = ({ text: e, segments: t, limit: s, addToEndText: n }) => {
let o = s
if (o >= e.length) return { text: e, segments: t, exceedsLimit: !1 }
const i = t.reduce((e, t) => {
if (t.end <= o) e.push(t)
else if (t.start < o && t.end > o) {
const s = t.styles.find((e) => -1 !== O.indexOf(e.type))
o = s ? t.end : o
const n = { ...t, end: o }
if (!r(t.highlight)) {
const e = o - t.start
n.highlight = cutSegmentHighlight(t.highlight, e)
}
e.push(n)
}
return e
}, []),
d = e.length > o
if (!d) return { text: e, segments: t, exceedsLimit: d }
const a = i[i.length - 1]
let l = e.slice(0, o)
const c = /\s/.test(l.slice(-1))
if ((n && (l += c ? n : ` ${n}`), !t)) return { text: l, segments: t, exceedsLimit: d }
if (n) {
const e = c ? n.length : n.length + 1
a.styles.length
? i.push({ start: o, end: o + e, styles: [] })
: (i[i.length - 1] = { ...a, end: a.end + e })
}
return { text: l, segments: i, exceedsLimit: d }
}
export function getIsMessageHidden(e, t) {
for (const s of t) {
const r = t ? CHAT_MESSAGE_FILTER_MAP[s] : void 0
if (r?.(e)) return !0
}
const s = parseSystemEvents(e)?.type
return s && !h.includes(s)
}