OverviewFilesCommitsIssuesDocsReleasesPeers

keet-deobfuscated / @holepunchto / keet-store / store / chat / message / message.js

324 lines · javascript ref:
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)
}