UNPKG

stream-chat

Version:

JS SDK for the Stream Chat API

1,336 lines (1,328 loc) 596 kB
"use strict"; 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 __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; 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 )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { AbstractOfflineDB: () => AbstractOfflineDB, Allow: () => Allow, AllowAll: () => AllowAll, AnyResource: () => AnyResource, AnyRole: () => AnyRole, AttachmentManager: () => AttachmentManager, AttachmentPostUploadMiddlewareExecutor: () => AttachmentPostUploadMiddlewareExecutor, AttachmentPreUploadMiddlewareExecutor: () => AttachmentPreUploadMiddlewareExecutor, BasePaginator: () => BasePaginator, BaseSearchSource: () => BaseSearchSource, BaseSearchSourceSync: () => BaseSearchSourceSync, BuiltinPermissions: () => BuiltinPermissions, BuiltinRoles: () => BuiltinRoles, Campaign: () => Campaign, Channel: () => Channel, ChannelManager: () => ChannelManager, ChannelSearchSource: () => ChannelSearchSource, ChannelState: () => ChannelState, CheckSignature: () => CheckSignature, ClientState: () => ClientState, CommandSearchSource: () => CommandSearchSource, CustomDataManager: () => CustomDataManager, DEFAULT_ATTACHMENT_MANAGER_CONFIG: () => DEFAULT_ATTACHMENT_MANAGER_CONFIG, DEFAULT_CHANNEL_MANAGER_OPTIONS: () => DEFAULT_CHANNEL_MANAGER_OPTIONS, DEFAULT_CHANNEL_MANAGER_PAGINATION_OPTIONS: () => DEFAULT_CHANNEL_MANAGER_PAGINATION_OPTIONS, DEFAULT_COMPOSER_CONFIG: () => DEFAULT_COMPOSER_CONFIG, DEFAULT_LINK_PREVIEW_MANAGER_CONFIG: () => DEFAULT_LINK_PREVIEW_MANAGER_CONFIG, DEFAULT_LOCATION_COMPOSER_CONFIG: () => DEFAULT_LOCATION_COMPOSER_CONFIG, DEFAULT_PAGINATION_OPTIONS: () => DEFAULT_PAGINATION_OPTIONS, DEFAULT_REMINDER_MANAGER_CONFIG: () => DEFAULT_REMINDER_MANAGER_CONFIG, DEFAULT_STOP_REFRESH_BOUNDARY_MS: () => DEFAULT_STOP_REFRESH_BOUNDARY_MS, DEFAULT_TEXT_COMPOSER_CONFIG: () => DEFAULT_TEXT_COMPOSER_CONFIG, Deny: () => Deny, DenyAll: () => DenyAll, DevToken: () => DevToken, EVENT_MAP: () => EVENT_MAP, ErrorFromResponse: () => ErrorFromResponse, FilterBuilder: () => FilterBuilder, FixedSizeQueueCache: () => FixedSizeQueueCache, InsightMetrics: () => InsightMetrics, JWTServerToken: () => JWTServerToken, JWTUserToken: () => JWTUserToken, LinkPreviewStatus: () => LinkPreviewStatus, LinkPreviewsManager: () => LinkPreviewsManager, LiveLocationManager: () => LiveLocationManager, LocationComposer: () => LocationComposer, MAX_POLL_OPTIONS: () => MAX_POLL_OPTIONS, MODERATION_ENTITY_TYPES: () => MODERATION_ENTITY_TYPES, MaxPriority: () => MaxPriority, MentionsSearchSource: () => MentionsSearchSource, MergedStateStore: () => MergedStateStore, MessageComposer: () => MessageComposer, MessageComposerMiddlewareExecutor: () => MessageComposerMiddlewareExecutor, MessageDeliveryReporter: () => MessageDeliveryReporter, MessageDraftComposerMiddlewareExecutor: () => MessageDraftComposerMiddlewareExecutor, MessageReceiptsTracker: () => MessageReceiptsTracker, MessageSearchSource: () => MessageSearchSource, MiddlewareExecutor: () => MiddlewareExecutor, MinPriority: () => MinPriority, Moderation: () => Moderation, NotificationManager: () => NotificationManager, OfflineDBSyncManager: () => OfflineDBSyncManager, OfflineError: () => OfflineError, Permission: () => Permission, Poll: () => Poll, PollComposer: () => PollComposer, PollComposerCompositionMiddlewareExecutor: () => PollComposerCompositionMiddlewareExecutor, PollComposerStateMiddlewareExecutor: () => PollComposerStateMiddlewareExecutor, PollManager: () => PollManager, Product: () => Product, Reminder: () => Reminder, ReminderManager: () => ReminderManager, ReminderPaginator: () => ReminderPaginator, ReminderTimer: () => ReminderTimer, SearchController: () => SearchController, Segment: () => Segment, StableWSConnection: () => StableWSConnection, StateStore: () => StateStore, StreamChat: () => StreamChat, THREAD_MANAGER_INITIAL_STATE: () => THREAD_MANAGER_INITIAL_STATE, TextComposer: () => TextComposer, TextComposerMiddlewareExecutor: () => TextComposerMiddlewareExecutor, Thread: () => Thread, ThreadManager: () => ThreadManager, TokenManager: () => TokenManager, UPDATE_LIVE_LOCATION_REQUEST_MIN_THROTTLE_TIMEOUT: () => UPDATE_LIVE_LOCATION_REQUEST_MIN_THROTTLE_TIMEOUT, UserFromToken: () => UserFromToken, UserSearchSource: () => UserSearchSource, VALID_MAX_VOTES_VALUE_REGEX: () => VALID_MAX_VOTES_VALUE_REGEX, VotingVisibility: () => VotingVisibility, accentsMap: () => accentsMap, buildWsFatalInsight: () => buildWsFatalInsight, buildWsSuccessAfterFailureInsight: () => buildWsSuccessAfterFailureInsight, calculateLevenshtein: () => calculateLevenshtein, channelManagerEventToHandlerMapping: () => channelManagerEventToHandlerMapping, chatCodes: () => chatCodes, createActiveCommandGuardMiddleware: () => createActiveCommandGuardMiddleware, createAttachmentsCompositionMiddleware: () => createAttachmentsCompositionMiddleware, createBlockedAttachmentUploadNotificationMiddleware: () => createBlockedAttachmentUploadNotificationMiddleware, createCommandInjectionMiddleware: () => createCommandInjectionMiddleware, createCommandStringExtractionMiddleware: () => createCommandStringExtractionMiddleware, createCommandsMiddleware: () => createCommandsMiddleware, createCompositionDataCleanupMiddleware: () => createCompositionDataCleanupMiddleware, createCompositionValidationMiddleware: () => createCompositionValidationMiddleware, createCustomDataCompositionMiddleware: () => createCustomDataCompositionMiddleware, createDraftAttachmentsCompositionMiddleware: () => createDraftAttachmentsCompositionMiddleware, createDraftCommandInjectionMiddleware: () => createDraftCommandInjectionMiddleware, createDraftCompositionValidationMiddleware: () => createDraftCompositionValidationMiddleware, createDraftCustomDataCompositionMiddleware: () => createDraftCustomDataCompositionMiddleware, createDraftLinkPreviewsCompositionMiddleware: () => createDraftLinkPreviewsCompositionMiddleware, createDraftMessageComposerStateCompositionMiddleware: () => createDraftMessageComposerStateCompositionMiddleware, createDraftTextComposerCompositionMiddleware: () => createDraftTextComposerCompositionMiddleware, createFileFromBlobs: () => createFileFromBlobs, createLinkPreviewsCompositionMiddleware: () => createLinkPreviewsCompositionMiddleware, createMentionsMiddleware: () => createMentionsMiddleware, createMessageComposerStateCompositionMiddleware: () => createMessageComposerStateCompositionMiddleware, createPollComposerStateMiddleware: () => createPollComposerStateMiddleware, createPostUploadAttachmentEnrichmentMiddleware: () => createPostUploadAttachmentEnrichmentMiddleware, createSharedLocationCompositionMiddleware: () => createSharedLocationCompositionMiddleware, createTextComposerCompositionMiddleware: () => createTextComposerCompositionMiddleware, createTextComposerPreValidationMiddleware: () => createTextComposerPreValidationMiddleware, createUploadConfigCheckMiddleware: () => createUploadConfigCheckMiddleware, createUploadErrorHandlerMiddleware: () => createUploadErrorHandlerMiddleware, decodeBase64: () => decodeBase64, defaultPollFieldBlurEventValidators: () => defaultPollFieldBlurEventValidators, defaultPollFieldChangeEventValidators: () => defaultPollFieldChangeEventValidators, encodeBase64: () => encodeBase64, ensureIsLocalAttachment: () => ensureIsLocalAttachment, escapeRegExp: () => escapeRegExp, extractPollData: () => extractPollData, extractPollEnrichedData: () => extractPollEnrichedData, formatMessage: () => formatMessage, generateFileName: () => generateFileName, getAttachmentTypeFromMimeType: () => getAttachmentTypeFromMimeType, getCompleteCommandInString: () => getCompleteCommandInString, getExtensionFromMimeType: () => getExtensionFromMimeType, getTokenizedSuggestionDisplayName: () => getTokenizedSuggestionDisplayName, getTriggerCharWithToken: () => getTriggerCharWithToken, insertItemWithTrigger: () => insertItemWithTrigger, isAudioAttachment: () => isAudioAttachment, isBlobButNotFile: () => isBlobButNotFile, isFile: () => isFile, isFileAttachment: () => isFileAttachment, isFileList: () => isFileList, isFileReference: () => isFileReference, isImageAttachment: () => isImageAttachment, isImageFile: () => isImageFile, isLocalAttachment: () => isLocalAttachment, isLocalAudioAttachment: () => isLocalAudioAttachment, isLocalFileAttachment: () => isLocalFileAttachment, isLocalImageAttachment: () => isLocalImageAttachment, isLocalUploadAttachment: () => isLocalUploadAttachment, isLocalVideoAttachment: () => isLocalVideoAttachment, isLocalVoiceRecordingAttachment: () => isLocalVoiceRecordingAttachment, isOwnUser: () => isOwnUser, isPatch: () => isPatch, isScrapedContent: () => isScrapedContent, isSharedLocationResponse: () => isSharedLocationResponse, isTargetedOptionTextUpdate: () => isTargetedOptionTextUpdate, isUploadedAttachment: () => isUploadedAttachment, isVideoAttachment: () => isVideoAttachment, isVoiceRecordingAttachment: () => isVoiceRecordingAttachment, isVoteAnswer: () => isVoteAnswer, localMessageToNewMessagePayload: () => localMessageToNewMessagePayload, logChatPromiseExecution: () => logChatPromiseExecution, mapPollStateToResponse: () => mapPollStateToResponse, pollCompositionStateProcessors: () => pollCompositionStateProcessors, pollStateChangeValidators: () => pollStateChangeValidators, postInsights: () => postInsights, promoteChannel: () => promoteChannel, readFileAsArrayBuffer: () => readFileAsArrayBuffer, removeDiacritics: () => removeDiacritics, replaceWordWithEntity: () => replaceWordWithEntity, textIsEmpty: () => textIsEmpty, timeLeftMs: () => timeLeftMs }); module.exports = __toCommonJS(index_exports); // src/base64.ts var import_base64_js = require("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) => (0, import_base64_js.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/client.ts var import_axios3 = __toESM(require("axios")); var import_https = __toESM(require("https")); // src/utils.ts var import_form_data = __toESM(require("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 import_form_data.default(); 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 (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, 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 [firstPageMsgIsFirstInSet, lastPageMsgIsLastInSet] = [ firstPageMsg?.id && firstPageMsg.id === parentSet.messages[0]?.id, lastPageMsg?.id && lastPageMsg.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, messagePaginationOptions }) => { const newPagination = { ...parentSet.pagination }; const { id_around } = messagePaginationOptions || {}; if (!id_around) return newPagination; let hasPrev; let hasNext; const [firstPageMsg, lastPageMsg] = [returnedPage[0], returnedPage.slice(-1)[0]]; const [firstPageMsgIsFirstInSet, lastPageMsgIsLastInSet] = [ firstPageMsg?.id === parentSet.messages[0]?.id, lastPageMsg?.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, messagePaginationOptions }) => { const newPagination = { ...parentSet.pagination }; let hasPrev; let hasNext; const [firstPageMsg, lastPageMsg] = [returnedPage[0], returnedPage.slice(-1)[0]]; const [firstPageMsgIsFirstInSet, lastPageMsgIsLastInSet] = [ firstPageMsg?.id && firstPageMsg.id === parentSet.messages[0]?.id, lastPageMsg?.id && lastPageMsg.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) => { const messagesFilteredLocally = params.returnedPage.filter(({ shadowed }) => shadowed); if (params.parentSet.messages.length + messagesFilteredLocally.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 ); for (let i = 0; i < messagesToAdd.length; i += 1) { const isFromShadowBannedUser = messagesTo