UNPKG

stream-chat

Version:

JS SDK for the Stream Chat API

1,464 lines (1,451 loc) 636 kB
var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __commonJS = (cb, mod) => function __require() { return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); // (disabled):https var require_https = __commonJS({ "(disabled):https"() { } }); // (disabled):node_modules/jsonwebtoken/index.js var require_jsonwebtoken = __commonJS({ "(disabled):node_modules/jsonwebtoken/index.js"() { } }); // (disabled):crypto var require_crypto = __commonJS({ "(disabled):crypto"() { } }); // (disabled):zlib var require_zlib = __commonJS({ "(disabled):zlib"() { } }); // src/base64.ts import { fromByteArray } from "base64-js"; function isString(arrayOrString) { return typeof arrayOrString === "string"; } function isMapStringCallback(arrayOrString, callback) { return !!callback && isString(arrayOrString); } function map(arrayOrString, callback) { const res = []; if (isString(arrayOrString) && isMapStringCallback(arrayOrString, callback)) { for (let k = 0, len = arrayOrString.length; k < len; k++) { if (arrayOrString.charAt(k)) { const kValue = arrayOrString.charAt(k); const mappedValue = callback(kValue, k, arrayOrString); res[k] = mappedValue; } } } else if (!isString(arrayOrString) && !isMapStringCallback(arrayOrString, callback)) { for (let k = 0, len = arrayOrString.length; k < len; k++) { if (k in arrayOrString) { const kValue = arrayOrString[k]; const mappedValue = callback(kValue, k, arrayOrString); res[k] = mappedValue; } } } return res; } var encodeBase64 = (data) => fromByteArray(new Uint8Array(map(data, (char) => char.charCodeAt(0)))); var decodeBase64 = (s) => { const e = {}, w = String.fromCharCode, L = s.length; let i, b = 0, c, x, l = 0, a, r = ""; const A = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; for (i = 0; i < 64; i++) { e[A.charAt(i)] = i; } for (x = 0; x < L; x++) { c = e[s.charAt(x)]; b = (b << 6) + c; l += 6; while (l >= 8) { ((a = b >>> (l -= 8) & 255) || x < L - 2) && (r += w(a)); } } return r; }; // src/campaign.ts var Campaign = class { constructor(client, id, data) { this.client = client; this.id = id; this.data = data; } async create() { const body = { id: this.id, message_template: this.data?.message_template, segment_ids: this.data?.segment_ids, sender_id: this.data?.sender_id, sender_mode: this.data?.sender_mode, sender_visibility: this.data?.sender_visibility, channel_template: this.data?.channel_template, create_channels: this.data?.create_channels, show_channels: this.data?.show_channels, description: this.data?.description, name: this.data?.name, skip_push: this.data?.skip_push, skip_webhook: this.data?.skip_webhook, user_ids: this.data?.user_ids }; const result = await this.client.createCampaign(body); this.id = result.campaign.id; this.data = result.campaign; return result; } verifyCampaignId() { if (!this.id) { throw new Error( "Campaign id is missing. Either create the campaign using campaign.create() or set the id during instantiation - const campaign = client.campaign(id)" ); } } async start(options) { this.verifyCampaignId(); return await this.client.startCampaign(this.id, options); } update(data) { this.verifyCampaignId(); return this.client.updateCampaign(this.id, data); } async delete() { this.verifyCampaignId(); return await this.client.deleteCampaign(this.id); } stop() { this.verifyCampaignId(); return this.client.stopCampaign(this.id); } get(options) { this.verifyCampaignId(); return this.client.getCampaign(this.id, options); } }; // src/channel_batch_updater.ts var ChannelBatchUpdater = class { constructor(client) { this.client = client; } // Member operations /** * addMembers - Add members to channels matching the filter * * @param {UpdateChannelsBatchFilters} filter Filter to select channels * @param {string[] | NewMemberPayload[]} members Members to add * @return {Promise<APIResponse & UpdateChannelsBatchResponse>} The server response */ async addMembers(filter, members) { return await this.client.updateChannelsBatch({ operation: "addMembers", filter, members }); } /** * removeMembers - Remove members from channels matching the filter * * @param {UpdateChannelsBatchFilters} filter Filter to select channels * @param {string[]} members Member IDs to remove * @return {Promise<APIResponse & UpdateChannelsBatchResponse>} The server response */ async removeMembers(filter, members) { return await this.client.updateChannelsBatch({ operation: "removeMembers", filter, members }); } /** * inviteMembers - Invite members to channels matching the filter * * @param {UpdateChannelsBatchFilters} filter Filter to select channels * @param {string[] | NewMemberPayload[]} members Members to invite * @return {Promise<APIResponse & UpdateChannelsBatchResponse>} The server response */ async inviteMembers(filter, members) { return await this.client.updateChannelsBatch({ operation: "inviteMembers", filter, members }); } /** * addModerators - Add moderators to channels matching the filter * * @param {UpdateChannelsBatchFilters} filter Filter to select channels * @param {string[]} members Member IDs to promote to moderator * @return {Promise<APIResponse & UpdateChannelsBatchResponse>} The server response */ async addModerators(filter, members) { return await this.client.updateChannelsBatch({ operation: "addModerators", filter, members }); } /** * demoteModerators - Remove moderator role from members in channels matching the filter * * @param {UpdateChannelsBatchFilters} filter Filter to select channels * @param {string[]} members Member IDs to demote * @return {Promise<APIResponse & UpdateChannelsBatchResponse>} The server response */ async demoteModerators(filter, members) { return await this.client.updateChannelsBatch({ operation: "demoteModerators", filter, members }); } /** * assignRoles - Assign roles to members in channels matching the filter * * @param {UpdateChannelsBatchFilters} filter Filter to select channels * @param {NewMemberPayload[]} members Members with role assignments * @return {Promise<APIResponse & UpdateChannelsBatchResponse>} The server response */ async assignRoles(filter, members) { return await this.client.updateChannelsBatch({ operation: "assignRoles", filter, members }); } // Visibility operations /** * hide - Hide channels matching the filter * * @param {UpdateChannelsBatchFilters} filter Filter to select channels * @return {Promise<APIResponse & UpdateChannelsBatchResponse>} The server response */ async hide(filter) { return await this.client.updateChannelsBatch({ operation: "hide", filter }); } /** * show - Show channels matching the filter * * @param {UpdateChannelsBatchFilters} filter Filter to select channels * @return {Promise<APIResponse & UpdateChannelsBatchResponse>} The server response */ async show(filter) { return await this.client.updateChannelsBatch({ operation: "show", filter }); } /** * archive - Archive channels matching the filter * * @param {UpdateChannelsBatchFilters} filter Filter to select channels * @return {Promise<APIResponse & UpdateChannelsBatchResponse>} The server response */ async archive(filter) { return await this.client.updateChannelsBatch({ operation: "archive", filter }); } /** * unarchive - Unarchive channels matching the filter * * @param {UpdateChannelsBatchFilters} filter Filter to select channels * @return {Promise<APIResponse & UpdateChannelsBatchResponse>} The server response */ async unarchive(filter) { return await this.client.updateChannelsBatch({ operation: "unarchive", filter }); } // Data operations /** * updateData - Update data on channels matching the filter * * @param {UpdateChannelsBatchFilters} filter Filter to select channels * @param {BatchChannelDataUpdate} data Data to update * @return {Promise<APIResponse & UpdateChannelsBatchResponse>} The server response */ async updateData(filter, data) { return await this.client.updateChannelsBatch({ operation: "updateData", filter, data }); } }; // src/client.ts var import_https = __toESM(require_https()); import axios3 from "axios"; // src/utils.ts import FormData from "form-data"; // src/constants.ts var DEFAULT_QUERY_CHANNELS_MESSAGE_LIST_PAGE_SIZE = 25; var DEFAULT_QUERY_CHANNEL_MESSAGE_LIST_PAGE_SIZE = 100; var DEFAULT_MESSAGE_SET_PAGINATION = Object.freeze({ hasNext: false, hasPrev: false }); var DEFAULT_UPLOAD_SIZE_LIMIT_BYTES = 100 * 1024 * 1024; var API_MAX_FILES_ALLOWED_PER_MESSAGE = 10; var MAX_CHANNEL_MEMBER_COUNT_IN_CHANNEL_QUERY = 100; var RESERVED_UPDATED_MESSAGE_FIELDS = Object.freeze({ // Dates should not be converted back to ISO strings as JS looses precision on them (milliseconds) created_at: true, deleted_at: true, pinned_at: true, updated_at: true, command: true, // Back-end enriches these fields mentioned_users: true, quoted_message: true, // Client-specific fields latest_reactions: true, own_reactions: true, reaction_counts: true, reply_count: true, // Message text related fields that shouldn't be in update i18n: true, type: true, html: true, __html: true, user: true }); var LOCAL_MESSAGE_FIELDS = Object.freeze({ error: true }); var DEFAULT_QUERY_CHANNELS_RETRY_COUNT = 3; var DEFAULT_QUERY_CHANNELS_MS_BETWEEN_RETRIES = 1e3; // src/utils.ts function logChatPromiseExecution(promise, name) { promise.then().catch((error) => { console.warn(`failed to do ${name}, ran into error: `, error); }); } var sleep = (m) => new Promise((r) => setTimeout(r, m)); function isFunction(value) { return typeof value === "function" || value instanceof Function || Object.prototype.toString.call(value) === "[object Function]"; } var chatCodes = { TOKEN_EXPIRED: 40, WS_CLOSED_SUCCESS: 1e3 }; function isReadableStream(obj) { return obj !== null && typeof obj === "object" && (obj.readable || typeof obj._read === "function"); } function isBuffer(obj) { return obj != null && obj.constructor != null && // @ts-expect-error expected typeof obj.constructor.isBuffer === "function" && // @ts-expect-error expected obj.constructor.isBuffer(obj); } function isFileWebAPI(uri) { return typeof window !== "undefined" && "File" in window && uri instanceof File; } function isOwnUser(user) { return user?.total_unread_count !== void 0; } function isBlobWebAPI(uri) { return typeof window !== "undefined" && "Blob" in window && uri instanceof Blob; } function isOwnUserBaseProperty(property) { const ownUserBaseProperties = { channel_mutes: true, devices: true, mutes: true, total_unread_count: true, unread_channels: true, unread_count: true, unread_threads: true, invisible: true, privacy_settings: true, roles: true, push_preferences: true, total_unread_count_by_team: true }; return ownUserBaseProperties[property]; } function addFileToFormData(uri, name, contentType) { const data = new FormData(); if (isReadableStream(uri) || isBuffer(uri) || isFileWebAPI(uri) || isBlobWebAPI(uri)) { if (name) data.append("file", uri, name); else data.append("file", uri); } else { data.append("file", { uri, name: name || uri.split("/").reverse()[0], contentType: contentType || void 0, type: contentType || void 0 }); } return data; } function normalizeQuerySort(sort) { const sortFields = []; const sortArr = Array.isArray(sort) ? sort : [sort]; for (const item of sortArr) { const entries = Object.entries(item); if (entries.length > 1) { console.warn( "client._buildSort() - multiple fields in a single sort object detected. Object's field order is not guaranteed" ); } for (const [field, direction] of entries) { sortFields.push({ field, direction }); } } return sortFields; } function retryInterval(numberOfFailures) { const max = Math.min(500 + numberOfFailures * 2e3, 25e3); const min = Math.min(Math.max(250, (numberOfFailures - 1) * 2e3), 25e3); return Math.floor(Math.random() * (max - min) + min); } function randomId() { return generateUUIDv4(); } function hex(bytes) { let s = ""; for (let i = 0; i < bytes.length; i++) { s += bytes[i].toString(16).padStart(2, "0"); } return s; } function generateUUIDv4() { const bytes = getRandomBytes(16); bytes[6] = bytes[6] & 15 | 64; bytes[8] = bytes[8] & 191 | 128; return hex(bytes.subarray(0, 4)) + "-" + hex(bytes.subarray(4, 6)) + "-" + hex(bytes.subarray(6, 8)) + "-" + hex(bytes.subarray(8, 10)) + "-" + hex(bytes.subarray(10, 16)); } function getRandomValuesWithMathRandom(bytes) { const max = Math.pow(2, 8 * bytes.byteLength / bytes.length); for (let i = 0; i < bytes.length; i++) { bytes[i] = Math.random() * max; } } var getRandomValues = (() => { if (typeof crypto !== "undefined" && typeof crypto?.getRandomValues !== "undefined") { return crypto.getRandomValues.bind(crypto); } else if (typeof msCrypto !== "undefined") { return msCrypto.getRandomValues.bind(msCrypto); } else { return getRandomValuesWithMathRandom; } })(); function getRandomBytes(length) { const bytes = new Uint8Array(length); getRandomValues(bytes); return bytes; } function convertErrorToJson(err) { const jsonObj = {}; if (!err) return jsonObj; try { Object.getOwnPropertyNames(err).forEach((key) => { jsonObj[key] = Object.getOwnPropertyDescriptor(err, key); }); } catch (_) { return { error: "failed to serialize the error" }; } return jsonObj; } function isOnline() { const nav = typeof navigator !== "undefined" ? navigator : typeof window !== "undefined" && window.navigator ? window.navigator : void 0; if (!nav) { console.warn( "isOnline failed to access window.navigator and assume browser is online" ); return true; } if (typeof nav.onLine !== "boolean") { return true; } return nav.onLine; } function addConnectionEventListeners(cb) { if (typeof window !== "undefined" && window.addEventListener) { window.addEventListener("offline", cb); window.addEventListener("online", cb); } } function removeConnectionEventListeners(cb) { if (typeof window !== "undefined" && window.removeEventListener) { window.removeEventListener("offline", cb); window.removeEventListener("online", cb); } } var axiosParamsSerializer = (params) => { const newParams = []; for (const k in params) { if (params[k] === void 0) continue; if (Array.isArray(params[k]) || typeof params[k] === "object") { newParams.push(`${k}=${encodeURIComponent(JSON.stringify(params[k]))}`); } else { newParams.push(`${k}=${encodeURIComponent(params[k])}`); } } return newParams.join("&"); }; function formatMessage(message) { const toLocalMessageBase = (msg) => { if (!msg) return null; return { ...msg, created_at: msg.created_at ? new Date(msg.created_at) : /* @__PURE__ */ new Date(), deleted_at: msg.deleted_at ? new Date(msg.deleted_at) : null, pinned_at: msg.pinned_at ? new Date(msg.pinned_at) : null, reaction_groups: maybeGetReactionGroupsFallback( msg.reaction_groups, msg.reaction_counts, msg.reaction_scores ), status: msg.status || "received", updated_at: msg.updated_at ? new Date(msg.updated_at) : /* @__PURE__ */ new Date() }; }; return { ...toLocalMessageBase(message), error: message.error ?? null, quoted_message: toLocalMessageBase(message.quoted_message) }; } function unformatMessage(message) { const toMessageResponseBase = (msg) => { if (!msg) return null; const newDateString = (/* @__PURE__ */ new Date()).toISOString(); return { ...msg, created_at: message.created_at ? message.created_at.toISOString() : newDateString, deleted_at: message.deleted_at ? message.deleted_at.toISOString() : void 0, pinned_at: message.pinned_at ? message.pinned_at.toISOString() : void 0, updated_at: message.updated_at ? message.updated_at.toISOString() : newDateString }; }; return { ...toMessageResponseBase(message), quoted_message: toMessageResponseBase(message.quoted_message) }; } var localMessageToNewMessagePayload = (localMessage) => { const { // Remove all timestamp fields and client-specific fields. // Field pinned_at can therefore be earlier than created_at as new message payload can hold it. created_at, updated_at, deleted_at, // Client-specific fields error, status, // Reaction related fields latest_reactions, own_reactions, reaction_counts, reaction_scores, reply_count, // Message text related fields that shouldn't be in update command, html, i18n, quoted_message, mentioned_users, // Message content related fields ...messageFields } = localMessage; return { ...messageFields, pinned_at: messageFields.pinned_at?.toISOString(), mentioned_users: mentioned_users?.map((user) => user.id) }; }; var toUpdatedMessagePayload = (message) => { const reservedKeys = { ...RESERVED_UPDATED_MESSAGE_FIELDS, ...LOCAL_MESSAGE_FIELDS }; const messageFields = Object.fromEntries( Object.entries(message).filter( ([key]) => !reservedKeys[key] ) ); return { ...messageFields, pinned: !!message.pinned_at, mentioned_users: message.mentioned_users?.map( (user) => typeof user === "string" ? user : user.id ) }; }; var toDeletedMessage = ({ message, deletedAt, hardDelete = false }) => { if (hardDelete) { return { attachments: [], cid: message.cid, created_at: message.created_at, deleted_at: deletedAt, id: message.id, latest_reactions: [], mentioned_users: [], own_reactions: [], parent_id: message.parent_id, reply_count: message.reply_count, status: message.status, thread_participants: message.thread_participants, type: "deleted", updated_at: message.updated_at, user: message.user }; } else { return { ...message, attachments: [], type: "deleted", deleted_at: deletedAt }; } }; var deleteUserMessages = ({ messages, user, hardDelete = false, deletedAt }) => { for (let i = 0; i < messages.length; i++) { const message = messages[i]; if (message.user?.id === user.id) { messages[i] = message.type === "deleted" ? message : toDeletedMessage({ message, hardDelete, deletedAt }); } if (messages[i].quoted_message && message.quoted_message?.user?.id === user.id) { messages[i].quoted_message = message.quoted_message.type === "deleted" ? message.quoted_message : toDeletedMessage({ message: messages[i].quoted_message, hardDelete, deletedAt }); } } }; var findIndexInSortedArray = ({ needle, sortedArray, selectKey, selectValueToCompare = (e) => e, sortDirection = "ascending" }) => { if (!sortedArray.length) return 0; let left = 0; let right = sortedArray.length - 1; let middle = 0; const recalculateMiddle = () => { middle = Math.round((left + right) / 2); }; const comparableNeedle = selectValueToCompare(needle); while (left <= right) { recalculateMiddle(); const comparableMiddle = selectValueToCompare(sortedArray[middle]); if (sortDirection === "ascending" && comparableNeedle < comparableMiddle || sortDirection === "descending" && comparableNeedle >= comparableMiddle) { right = middle - 1; } else { left = middle + 1; } } if (selectKey) { const needleKey = selectKey(needle); const step = sortDirection === "ascending" ? -1 : 1; for (let i = left + step; 0 <= i && i < sortedArray.length && selectValueToCompare(sortedArray[i]) === comparableNeedle; i += step) { if (selectKey(sortedArray[i]) === needleKey) { return i; } } } return left; }; function addToMessageList(messages, newMessage, timestampChanged = false, sortBy = "created_at", addIfDoesNotExist = true) { const addMessageToList = addIfDoesNotExist || timestampChanged; let newMessages = [...messages]; if (timestampChanged) { newMessages = newMessages.filter( (message) => !(message.id && newMessage.id === message.id) ); } if (newMessages.length === 0 && addMessageToList) { return newMessages.concat(newMessage); } else if (newMessages.length === 0) { return newMessages; } const messageTime = newMessage[sortBy].getTime(); const messageIsNewest = newMessages.at(-1)[sortBy].getTime() < messageTime; if (messageIsNewest && addMessageToList) { return newMessages.concat(newMessage); } else if (messageIsNewest) { return newMessages; } const insertionIndex = findIndexInSortedArray({ needle: newMessage, sortedArray: newMessages, sortDirection: "ascending", // eslint-disable-next-line @typescript-eslint/no-non-null-assertion selectValueToCompare: (m) => m[sortBy].getTime(), selectKey: (m) => m.id }); if (!timestampChanged && newMessage.id && newMessages[insertionIndex] && newMessage.id === newMessages[insertionIndex].id) { newMessages[insertionIndex] = newMessage; return newMessages; } if (addMessageToList) { newMessages.splice(insertionIndex, 0, newMessage); } return newMessages; } function maybeGetReactionGroupsFallback(groups, counts, scores) { if (groups) { return groups; } if (counts && scores) { const fallback = {}; for (const type of Object.keys(counts)) { fallback[type] = { count: counts[type], sum_scores: scores[type] }; } return fallback; } return null; } var debounce = (fn, timeout = 0, { leading = false, trailing = true } = {}) => { let runningTimeout = null; let argsForTrailingExecution = null; let lastResult; const debouncedFn = (...args) => { if (runningTimeout) { clearTimeout(runningTimeout); } else if (leading) { lastResult = fn(...args); } if (trailing) argsForTrailingExecution = args; const timeoutHandler = () => { if (argsForTrailingExecution) { lastResult = fn(...argsForTrailingExecution); argsForTrailingExecution = null; } runningTimeout = null; }; runningTimeout = setTimeout(timeoutHandler, timeout); return lastResult; }; debouncedFn.cancel = () => { if (runningTimeout) clearTimeout(runningTimeout); }; debouncedFn.flush = () => { if (runningTimeout) { clearTimeout(runningTimeout); runningTimeout = null; if (argsForTrailingExecution) { lastResult = fn(...argsForTrailingExecution); } } return lastResult; }; return debouncedFn; }; var throttle = (fn, timeout = 200, { leading = true, trailing = false } = {}) => { let runningTimeout = null; let storedArgs = null; return (...args) => { if (runningTimeout) { if (trailing) storedArgs = args; return; } if (leading) fn(...args); const timeoutHandler = () => { if (storedArgs) { fn(...storedArgs); storedArgs = null; runningTimeout = setTimeout(timeoutHandler, timeout); return; } runningTimeout = null; }; runningTimeout = setTimeout(timeoutHandler, timeout); }; }; var get = (obj, path) => path.split(".").reduce((acc, key) => { if (acc && typeof acc === "object" && key in acc) { return acc[key]; } return void 0; }, obj); var uniqBy = (array, iteratee) => { if (!Array.isArray(array)) return []; const seen = /* @__PURE__ */ new Set(); return array.filter((item) => { const key = typeof iteratee === "function" ? iteratee(item) : get(item, iteratee); if (seen.has(key)) return false; seen.add(key); return true; }); }; function binarySearchByDateEqualOrNearestGreater(array, targetDate) { let left = 0; let right = array.length - 1; while (left <= right) { const mid = Math.floor((left + right) / 2); const midCreatedAt = array[mid].created_at; if (!midCreatedAt) { left += 1; continue; } const midDate = new Date(midCreatedAt); if (midDate.getTime() === targetDate.getTime()) { return mid; } else if (midDate.getTime() < targetDate.getTime()) { left = mid + 1; } else { right = mid - 1; } } return left; } var messagePaginationCreatedAtAround = ({ parentSet, requestedPageSize, returnedPage, filteredReturnedPage, messagePaginationOptions }) => { const newPagination = { ...parentSet.pagination }; if (!messagePaginationOptions?.created_at_around) return newPagination; let hasPrev; let hasNext; let updateHasPrev; let updateHasNext; const createdAtAroundDate = new Date(messagePaginationOptions.created_at_around); const [firstPageMsg, lastPageMsg] = [returnedPage[0], returnedPage.slice(-1)[0]]; const wholePageHasNewerMessages = !!firstPageMsg?.created_at && new Date(firstPageMsg.created_at) > createdAtAroundDate; const wholePageHasOlderMessages = !!lastPageMsg?.created_at && new Date(lastPageMsg.created_at) < createdAtAroundDate; const requestedPageSizeNotMet = requestedPageSize > parentSet.messages.length && requestedPageSize > returnedPage.length; const noMoreMessages = (requestedPageSize > parentSet.messages.length || parentSet.messages.length >= returnedPage.length) && requestedPageSize > returnedPage.length; if (wholePageHasNewerMessages) { hasPrev = false; updateHasPrev = true; if (requestedPageSizeNotMet) { hasNext = false; updateHasNext = true; } } else if (wholePageHasOlderMessages) { hasNext = false; updateHasNext = true; if (requestedPageSizeNotMet) { hasPrev = false; updateHasPrev = true; } } else if (noMoreMessages) { hasNext = hasPrev = false; updateHasPrev = updateHasNext = true; } else { const [firstFilteredPageMsg, lastFilteredPageMsg] = [ filteredReturnedPage[0], filteredReturnedPage.slice(-1)[0] ]; const [firstPageMsgIsFirstInSet, lastPageMsgIsLastInSet] = [ firstFilteredPageMsg?.id && firstFilteredPageMsg.id === parentSet.messages[0]?.id, lastFilteredPageMsg?.id && lastFilteredPageMsg.id === parentSet.messages.slice(-1)[0]?.id ]; updateHasPrev = firstPageMsgIsFirstInSet; updateHasNext = lastPageMsgIsLastInSet; const midPointByCount = Math.floor(returnedPage.length / 2); const midPointByCreationDate = binarySearchByDateEqualOrNearestGreater( returnedPage, createdAtAroundDate ); if (midPointByCreationDate !== -1) { hasPrev = midPointByCount <= midPointByCreationDate; hasNext = midPointByCount >= midPointByCreationDate; } } if (updateHasPrev && typeof hasPrev !== "undefined") newPagination.hasPrev = hasPrev; if (updateHasNext && typeof hasNext !== "undefined") newPagination.hasNext = hasNext; return newPagination; }; var messagePaginationIdAround = ({ parentSet, requestedPageSize, returnedPage, filteredReturnedPage, messagePaginationOptions }) => { const newPagination = { ...parentSet.pagination }; const { id_around } = messagePaginationOptions || {}; if (!id_around) return newPagination; let hasPrev; let hasNext; const [firstFilteredPageMsg, lastFilteredPageMsg] = [ filteredReturnedPage[0], filteredReturnedPage.slice(-1)[0] ]; const [firstPageMsgIsFirstInSet, lastPageMsgIsLastInSet] = [ firstFilteredPageMsg?.id === parentSet.messages[0]?.id, lastFilteredPageMsg?.id === parentSet.messages.slice(-1)[0]?.id ]; let updateHasPrev = firstPageMsgIsFirstInSet; let updateHasNext = lastPageMsgIsLastInSet; const midPoint = Math.floor(returnedPage.length / 2); const noMoreMessages = (requestedPageSize > parentSet.messages.length || parentSet.messages.length >= returnedPage.length) && requestedPageSize > returnedPage.length; if (noMoreMessages) { hasNext = hasPrev = false; updateHasPrev = updateHasNext = true; } else if (!returnedPage[midPoint]) { return newPagination; } else if (returnedPage[midPoint].id === id_around) { hasPrev = hasNext = true; } else { let targetMsg; const halves = [returnedPage.slice(0, midPoint), returnedPage.slice(midPoint)]; hasPrev = hasNext = true; for (let i = 0; i < halves.length; i++) { targetMsg = halves[i].find((message) => message.id === id_around); if (targetMsg && i === 0) { hasPrev = false; } if (targetMsg && i === 1) { hasNext = false; } } } if (updateHasPrev && typeof hasPrev !== "undefined") newPagination.hasPrev = hasPrev; if (updateHasNext && typeof hasNext !== "undefined") newPagination.hasNext = hasNext; return newPagination; }; var messagePaginationLinear = ({ parentSet, requestedPageSize, returnedPage, filteredReturnedPage, messagePaginationOptions }) => { const newPagination = { ...parentSet.pagination }; let hasPrev; let hasNext; const [firstFilteredPageMsg, lastFilteredPageMsg] = [ filteredReturnedPage[0], filteredReturnedPage.slice(-1)[0] ]; const [firstPageMsgIsFirstInSet, lastPageMsgIsLastInSet] = [ firstFilteredPageMsg?.id && firstFilteredPageMsg.id === parentSet.messages[0]?.id, lastFilteredPageMsg?.id && lastFilteredPageMsg.id === parentSet.messages.slice(-1)[0]?.id ]; const queriedNextMessages = messagePaginationOptions && (messagePaginationOptions.created_at_after_or_equal || messagePaginationOptions.created_at_after || messagePaginationOptions.id_gt || messagePaginationOptions.id_gte); const queriedPrevMessages = typeof messagePaginationOptions === "undefined" ? true : messagePaginationOptions.created_at_before_or_equal || messagePaginationOptions.created_at_before || messagePaginationOptions.id_lt || messagePaginationOptions.id_lte || messagePaginationOptions.offset; const containsUnrecognizedOptionsOnly = !queriedNextMessages && !queriedPrevMessages && !messagePaginationOptions?.id_around && !messagePaginationOptions?.created_at_around; const hasMore = returnedPage.length >= requestedPageSize; if (typeof queriedPrevMessages !== "undefined" || containsUnrecognizedOptionsOnly) { hasPrev = hasMore; } if (typeof queriedNextMessages !== "undefined") { hasNext = hasMore; } const returnedPageIsEmpty = returnedPage.length === 0; if ((firstPageMsgIsFirstInSet || returnedPageIsEmpty) && typeof hasPrev !== "undefined") newPagination.hasPrev = hasPrev; if ((lastPageMsgIsLastInSet || returnedPageIsEmpty) && typeof hasNext !== "undefined") newPagination.hasNext = hasNext; return newPagination; }; var messageSetPagination = (params) => { if (params.parentSet.messages.length + (params.returnedPage.length - params.filteredReturnedPage.length) < params.returnedPage.length) { params.logger?.( "error", "Corrupted message set state: parent set size < returned page size" ); return params.parentSet.pagination; } if (params.messagePaginationOptions?.created_at_around) { return messagePaginationCreatedAtAround(params); } else if (params.messagePaginationOptions?.id_around) { return messagePaginationIdAround(params); } else { return messagePaginationLinear(params); } }; var WATCH_QUERY_IN_PROGRESS_FOR_CHANNEL = {}; var getAndWatchChannel = async ({ channel, client, id, members, options, type }) => { if (!channel && !type) { throw new Error("Channel or channel type have to be provided to query a channel."); } const channelToWatch = channel || client.channel(type, id, { members }); const originalCid = channelToWatch.id ? channelToWatch.cid : members && members.length ? generateChannelTempCid(channelToWatch.type, members) : void 0; if (!originalCid) { throw new Error( "Channel ID or channel members array have to be provided to query a channel." ); } const queryPromise = WATCH_QUERY_IN_PROGRESS_FOR_CHANNEL[originalCid]; if (queryPromise) { await queryPromise; } else { try { WATCH_QUERY_IN_PROGRESS_FOR_CHANNEL[originalCid] = channelToWatch.watch(options); await WATCH_QUERY_IN_PROGRESS_FOR_CHANNEL[originalCid]; } finally { delete WATCH_QUERY_IN_PROGRESS_FOR_CHANNEL[originalCid]; } } return channelToWatch; }; var generateChannelTempCid = (channelType, members) => { if (!members) return; const membersStr = [...members].sort().join(","); if (!membersStr) return; return `${channelType}:!members-${membersStr}`; }; var isChannelPinned = (channel) => { if (!channel) return false; const member = channel.state.membership; return !!member?.pinned_at; }; var isChannelArchived = (channel) => { if (!channel) return false; const member = channel.state.membership; return !!member?.archived_at; }; var shouldConsiderArchivedChannels = (filters) => { if (!filters) return false; return typeof filters.archived === "boolean"; }; var extractSortValue = ({ atIndex, sort, targetKey }) => { if (!sort) return null; let option = null; if (Array.isArray(sort)) { option = sort[atIndex] ?? null; } else { let index = 0; for (const key in sort) { if (index !== atIndex) { index++; continue; } if (key !== targetKey) { return null; } option = sort; break; } } return option?.[targetKey] ?? null; }; var shouldConsiderPinnedChannels = (sort) => { const value = findPinnedAtSortOrder({ sort }); if (typeof value !== "number") return false; return Math.abs(value) === 1; }; var findPinnedAtSortOrder = ({ sort }) => extractSortValue({ atIndex: 0, sort, targetKey: "pinned_at" }); var findLastPinnedChannelIndex = ({ channels }) => { let lastPinnedChannelIndex = null; for (const channel of channels) { if (!isChannelPinned(channel)) break; if (typeof lastPinnedChannelIndex === "number") { lastPinnedChannelIndex++; } else { lastPinnedChannelIndex = 0; } } return lastPinnedChannelIndex; }; var promoteChannel = ({ channels, channelToMove, channelToMoveIndexWithinChannels, sort }) => { const targetChannelIndex = channelToMoveIndexWithinChannels ?? channels.findIndex((channel) => channel.cid === channelToMove.cid); const targetChannelExistsWithinList = targetChannelIndex >= 0; const targetChannelAlreadyAtTheTop = targetChannelIndex === 0; const considerPinnedChannels = shouldConsiderPinnedChannels(sort); const isTargetChannelPinned = isChannelPinned(channelToMove); if (targetChannelAlreadyAtTheTop || considerPinnedChannels && isTargetChannelPinned) { return channels; } const newChannels = [...channels]; if (targetChannelExistsWithinList) { newChannels.splice(targetChannelIndex, 1); } let lastPinnedChannelIndex = null; if (considerPinnedChannels) { lastPinnedChannelIndex = findLastPinnedChannelIndex({ channels: newChannels }); } newChannels.splice( typeof lastPinnedChannelIndex === "number" ? lastPinnedChannelIndex + 1 : 0, 0, channelToMove ); return newChannels; }; var isDate = (value) => !!value.getTime; var isLocalMessage = (message) => isDate(message.created_at); var runDetached = (callback, options) => { const { context, onSuccessCallback = () => void 0, onErrorCallback } = options ?? {}; const defaultOnError = (error) => { console.log(`An error has occurred in context ${context}: ${error}`); }; const onError = onErrorCallback ?? defaultOnError; let promise = callback; if (onSuccessCallback) { promise = promise.then(onSuccessCallback); } promise.catch(onError); }; var isBlockedMessage = (message) => message.type === "error" && (message.moderation_details?.action === "MESSAGE_RESPONSE_ACTION_REMOVE" || message.moderation?.action === "remove"); // src/channel_state.ts var messageSetBounds = (a, b) => ({ newestMessageA: new Date(a[0]?.created_at ?? 0), oldestMessageA: new Date(a.slice(-1)[0]?.created_at ?? 0), newestMessageB: new Date(b[0]?.created_at ?? 0), oldestMessageB: new Date(b.slice(-1)[0]?.created_at ?? 0) }); var aContainsOrEqualsB = (a, b) => { const { newestMessageA, newestMessageB, oldestMessageA, oldestMessageB } = messageSetBounds(a, b); return newestMessageA >= newestMessageB && oldestMessageB >= oldestMessageA; }; var aOverlapsB = (a, b) => { const { newestMessageA, newestMessageB, oldestMessageA, oldestMessageB } = messageSetBounds(a, b); return oldestMessageA < oldestMessageB && oldestMessageB < newestMessageA && newestMessageA < newestMessageB; }; var messageSetsOverlapByTimestamp = (a, b) => aContainsOrEqualsB(a, b) || aContainsOrEqualsB(b, a) || aOverlapsB(a, b) || aOverlapsB(b, a); var ChannelState = class { constructor(channel) { /** * Disjoint lists of messages * Users can jump in the message list (with searching) and this can result in disjoint lists of messages * The state manages these lists and merges them when lists overlap * The messages array contains the currently active set */ this.messageSets = []; /** * Takes the message object, parses the dates, sets `__html` * and sets the status to `received` if missing; returns a new message object. * * @param {MessageResponse} message `MessageResponse` object */ this.formatMessage = (message) => formatMessage(message); /** * Setter for isUpToDate. * * @param isUpToDate Flag which indicates if channel state contain latest/recent messages or no. * This flag should be managed by UI sdks using a setter - setIsUpToDate. * When false, any new message (received by websocket event - message.new) will not * be pushed on to message list. */ this.setIsUpToDate = (isUpToDate) => { this.isUpToDate = isUpToDate; }; this.removeMessageFromArray = (msgArray, msg) => { const result = msgArray.filter( (message) => !(!!message.id && !!msg.id && message.id === msg.id) ); return { removed: result.length < msgArray.length, result }; }; /** * Updates the message.user property with updated user object, for messages. * * @param {UserResponse} user */ this.updateUserMessages = (user) => { const _updateUserMessages = (messages, user2) => { for (let i = 0; i < messages.length; i++) { const m = messages[i]; if (m.user?.id === user2.id) { messages[i] = { ...m, user: user2 }; } } }; this.messageSets.forEach((set) => _updateUserMessages(set.messages, user)); for (const parentId in this.threads) { _updateUserMessages(this.threads[parentId], user); } _updateUserMessages(this.pinnedMessages, user); }; /** * Marks the messages as deleted, from deleted user. * * @param {UserResponse} user * @param {boolean} hardDelete */ this.deleteUserMessages = (user, hardDelete = false, deletedAt) => { this.messageSets.forEach( ({ messages }) => deleteUserMessages({ messages, user, hardDelete, deletedAt: deletedAt ?? null }) ); for (const parentId in this.threads) { deleteUserMessages({ messages: this.threads[parentId], user, hardDelete, deletedAt: deletedAt ?? null }); } deleteUserMessages({ messages: this.pinnedMessages, user, hardDelete, deletedAt: deletedAt ?? null }); }; /** * Identifies the set index into which a message set would pertain if its first item's creation date corresponded to oldestTimestampMs. * @param oldestTimestampMs */ this.findMessageSetByOldestTimestamp = (oldestTimestampMs) => { let lo = 0, hi = this.messageSets.length; while (lo < hi) { const mid = lo + hi >>> 1; const msgSet = this.messageSets[mid]; if (msgSet.messages.length === 0) return -1; const oldestMessageTimestampInSet = msgSet.messages[0].created_at.getTime(); if (oldestMessageTimestampInSet <= oldestTimestampMs) hi = mid; else lo = mid + 1; } return lo; }; this._channel = channel; this.watcher_count = 0; this.typing = {}; this.read = {}; this.initMessages(); this.pinnedMessages = []; this.pending_messages = []; this.threads = {}; this.mutedUsers = []; this.watchers = {}; this.members = {}; this.membership = {}; this.unreadCount = 0; this.isUpToDate = true; this.last_message_at = channel?.state?.last_message_at != null ? new Date(channel.state.last_message_at) : null; } get messages() { return this.messageSets.find((s) => s.isCurrent)?.messages || []; } set messages(messages) { const index = this.messageSets.findIndex((s) => s.isCurrent); this.messageSets[index].messages = messages; } /** * The list of latest messages * The messages array not always contains the latest messages (for example if a user searched for an earlier message, that is in a different message set) */ get latestMessages() { return this.messageSets.find((s) => s.isLatest)?.messages || []; } set latestMessages(messages) { const index = this.messageSets.findIndex((s) => s.isLatest); this.messageSets[index].messages = messages; } get messagePagination() { return this.messageSets.find((s) => s.isCurrent)?.pagination || DEFAULT_MESSAGE_SET_PAGINATION; } pruneOldest(maxMessages) { const currentIndex = this.messageSets.findIndex((s) => s.isCurrent); if (this.messageSets[currentIndex].isLatest) { const newMessages = this.messageSets[currentIndex].messages; this.messageSets[currentIndex].messages = newMessages.slice(-maxMessages); this.messageSets[currentIndex].pagination.hasPrev = true; } } /** * addMessageSorted - Add a message to the state * * @param {MessageResponse} newMessage A new message * @param {boolean} timestampChanged Whether updating a message with changed created_at value. * @param {boolean} addIfDoesNotExist Add message if it is not in the list, used to prevent out of order updated messages from being added. * @param {MessageSetType} messageSetToAddToIfDoesNotExist Which message set to add to if message is not in the list (only used if addIfDoesNotExist is true) */ addMessageSorted(newMessage, timestampChanged = false, addIfDoesNotExist = true, messageSetToAddToIfDoesNotExist = "latest") { return this.addMessagesSorted( [newMessage], timestampChanged, false, addIfDoesNotExist, messageSetToAddToIfDoesNotExist ); } /** * addMessagesSorted - Add the list of messages to state and resorts the messages * * @param {Array<MessageResponse>} newMessages A list of messages * @param {boolean} timestampChanged Whether updating messages with changed created_at value. * @param {boolean} initializing Whether channel is being initialized. * @param {boolean} addIfDoesNotExist Add message if it is not in the list, used to prevent out of order updated messages from being added. * @param {MessageSetType} messageSetToAddToIfDoesNotExist Which message set to add to if messages are not in the list (only used if addIfDoesNotExist is true) * */ addMessagesSorted(newMessages, timestampChanged = false, initializing = false, addIfDoesNotExist = true, messageSetToAddToIfDoesNotExist = "current") { const { messagesToAdd, targetMessageSetIndex } = this.findTargetMessageSet( newMessages, addIfDoesNotExist, messageSetToAddToIfDoesNotExist ); const filteredMessageIds = []; for (let i = 0; i < messagesToAdd.length; i += 1) { const isFromShadowBannedUser = messagesToAdd[i].shadowed; if (isFromShadowBannedUser && addIfDoesNotExist) { filteredMessageIds.push(messagesToAdd[i].id); continue; } const isMessageFormatted = messagesToAdd[i].created_at instanceof Date; let message; if (isMessageFormatted) { message = messagesToAdd[i]; } else { message = this.formatMessage(messagesToAdd[i]); if (message.user && this._channel?.cid) { this._channel.getClient().state.updateUserReference(message.user, this._channel.cid); } if (initializing && message.id && this.threads[message.id] && !this._channel.getClient().preventThreadCleanup) { delete this.threads[message.id]; } const shouldSkipLastMessageAtUpdate = this._channel.getConfig()?.skip_last_msg_update_for_system_msgs && message.type === "system"; if (!shouldSkipLastMessageAtUpdate && (!this.last_message_at || message.created_at.getTime() > this.last_message_at.getTime())) { this.last_message_at = new Date(message.created_at.getTime()); } } const parentID = message.parent_id; if ((!parentID || message.show_in_channel) && targetMessageSetIndex !== -1) { this.messageSets[targetMessageSetIndex].messages = this._addToMessageList( this.messageSets[targetMessageSetIndex].messages, message, timestampChanged, "created_at", addIfDoesNotExist ); } if (parentID && !initializing) { const thread = this.threads[parentID] || []; this.threads[parentID] = this._addToMessageList( thread, message, timestampChanged, "created_at", addIfDoesNotExist ); } } return { messageSet: this.messageSets[targetMessageSetIndex], filteredMessageIds }; } /** * addPinnedMessages - adds messages in pinnedMessages property * * @param {Array<MessageResponse>} pinnedMessages A list of pinned messages * */ addPinnedMessages(pinnedMessages) { for (let i = 0; i < pinnedMessages.length; i += 1) { this.addPinnedMessage(pinnedMessages[i]); } } /** * addPinnedMessage - adds message in pinnedMessages * * @param {MessageResponse} pinnedMessage message to update * */ addPinnedMessage(pinnedMessage) { this.pinnedMessages = this._addToMessageList( this.pinnedMessages, this.formatMessage(pinnedMessage), false, "pinned_at" ); } /** * removePinnedMessage - removes pinned message from pinnedMessages * * @param {MessageResponse} message message to remove * */ removePinnedMessage(message) { const { result } = this.removeMessageFromArray(this.pinnedMessages, message); this.pinnedMessages = result; } addReaction(reaction, message, enforce_unique) { const messageWithReaction = message; let messageFromState; if (!messageWithReaction) { messageFromState = this.findMessage(reaction.message_id); } if (!messageWithReaction && !messageFromState) { return; } const messageToUpdate = messageWithReaction ?? messageFromState; const updateData = { id: messageToUpdate?.id, parent_id: messageToUpdate?.parent_id, pinned: messageToUpdate?.pinned, show_in_channel: messageToUpdate?.show_in_channel }; this._updateMessage(updateData, (msg) => { if (messageWithReaction) { const updatedMessage = { ...messageWithReaction }; messageWithReaction.own_reactions = this._addOwnReactionToMessage( msg.own_reactions, reaction, enforce_unique ); updatedMessage.own_reactions = this._channel.getClient().userID === reaction.user_id ? messageWithReaction.own_reactions : msg.own_reactions; return this.formatMessage(updatedMessage); } if (messageFromState) { return this._addReactionToState(messageFromState, reaction, enforce_unique); } return msg; }); return messageWithReaction ?? messageFromState; } _addReactionToState(messageFromState, reaction, enforce_unique) { if (!messageFromState.reaction_groups) {