UNPKG

stream-chat

Version:

JS SDK for the Stream Chat API

19,981 lines 698 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,
  ChannelBatchUpdater: () => ChannelBatchUpdater,
  ChannelManager: () => ChannelManager,
  ChannelMemberSearchSource: () => ChannelMemberSearchSource,
  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_COMMANDS_CONFIG: () => DEFAULT_COMMANDS_CONFIG,
  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,
  InvalidWebhookError: () => InvalidWebhookError,
  InvalidWebhookErrorMessages: () => InvalidWebhookErrorMessages,
  JWTServerToken: () => JWTServerToken,
  JWTUserToken: () => JWTUserToken,
  LinkPreviewStatus: () => LinkPreviewStatus,
  LinkPreviewsManager: () => LinkPreviewsManager,
  LiveLocationManager: () => LiveLocationManager,
  LocationComposer: () => LocationComposer,
  MAX_POLL_OPTIONS: () => MAX_POLL_OPTIONS,
  MENTION_ONLY_COMMANDS: () => MENTION_ONLY_COMMANDS,
  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,
  UploadManager: () => UploadManager,
  UserFromToken: () => UserFromToken,
  UserGroupPaginator: () => UserGroupPaginator,
  UserSearchSource: () => UserSearchSource,
  VALID_MAX_VOTES_VALUE_REGEX: () => VALID_MAX_VOTES_VALUE_REGEX,
  VotingVisibility: () => VotingVisibility,
  accentsMap: () => accentsMap,
  applyCommandValidatorOverride: () => applyCommandValidatorOverride,
  buildWsFatalInsight: () => buildWsFatalInsight,
  buildWsSuccessAfterFailureInsight: () => buildWsSuccessAfterFailureInsight,
  calculateLevenshtein: () => calculateLevenshtein,
  channelManagerEventToHandlerMapping: () => channelManagerEventToHandlerMapping,
  chatCodes: () => chatCodes,
  createActiveCommandGuardMiddleware: () => createActiveCommandGuardMiddleware,
  createAttachmentsCompositionMiddleware: () => createAttachmentsCompositionMiddleware,
  createBlockedAttachmentUploadNotificationMiddleware: () => createBlockedAttachmentUploadNotificationMiddleware,
  createCommandEffectsMiddleware: () => createCommandEffectsMiddleware,
  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,
  createSendWithPendingUploadsAttachmentsMiddleware: () => createSendWithPendingUploadsAttachmentsMiddleware,
  createSharedLocationCompositionMiddleware: () => createSharedLocationCompositionMiddleware,
  createTextComposerCompositionMiddleware: () => createTextComposerCompositionMiddleware,
  createTextComposerPreValidationMiddleware: () => createTextComposerPreValidationMiddleware,
  createUploadConfigCheckMiddleware: () => createUploadConfigCheckMiddleware,
  createUploadErrorHandlerMiddleware: () => createUploadErrorHandlerMiddleware,
  decodeBase64: () => decodeBase64,
  decodeSnsPayload: () => decodeSnsPayload,
  decodeSqsPayload: () => decodeSqsPayload,
  defaultCommandSendabilityValidator: () => defaultCommandSendabilityValidator,
  defaultPollFieldBlurEventValidators: () => defaultPollFieldBlurEventValidators,
  defaultPollFieldChangeEventValidators: () => defaultPollFieldChangeEventValidators,
  encodeBase64: () => encodeBase64,
  ensureIsLocalAttachment: () => ensureIsLocalAttachment,
  escapeCommandRegExp: () => escapeCommandRegExp,
  extractPollData: () => extractPollData,
  extractPollEnrichedData: () => extractPollEnrichedData,
  formatMessage: () => formatMessage,
  generateFileName: () => generateFileName,
  getAllowedMentionTypesFromCapabilities: () => getAllowedMentionTypesFromCapabilities,
  getAttachmentTypeFromMimeType: () => getAttachmentTypeFromMimeType,
  getCommandByName: () => getCommandByName,
  getCompleteCommandInString: () => getCompleteCommandInString,
  getExtensionFromMimeType: () => getExtensionFromMimeType,
  getMentionedUsersInText: () => getMentionedUsersInText,
  getRawCommandName: () => getRawCommandName,
  getTokenizedSuggestionDisplayName: () => getTokenizedSuggestionDisplayName,
  getTriggerCharWithToken: () => getTriggerCharWithToken,
  gunzipPayload: () => gunzipPayload,
  hasPending: () => hasPending,
  insertItemWithTrigger: () => insertItemWithTrigger,
  isAudioAttachment: () => isAudioAttachment,
  isBlobButNotFile: () => isBlobButNotFile,
  isFile: () => isFile,
  isFileAttachment: () => isFileAttachment,
  isFileList: () => isFileList,
  isFileReference: () => isFileReference,
  isFinishedUpload: () => isFinishedUpload,
  isGiphyAttachment: () => isGiphyAttachment,
  isImageAttachment: () => isImageAttachment,
  isImageFile: () => isImageFile,
  isLocalAttachment: () => isLocalAttachment,
  isLocalAudioAttachment: () => isLocalAudioAttachment,
  isLocalFileAttachment: () => isLocalFileAttachment,
  isLocalImageAttachment: () => isLocalImageAttachment,
  isLocalUploadAttachment: () => isLocalUploadAttachment,
  isLocalVideoAttachment: () => isLocalVideoAttachment,
  isLocalVoiceRecordingAttachment: () => isLocalVoiceRecordingAttachment,
  isOwnUser: () => isOwnUser,
  isPatch: () => isPatch,
  isPendingUpload: () => isPendingUpload,
  isScrapedContent: () => isScrapedContent,
  isSharedLocationResponse: () => isSharedLocationResponse,
  isTargetedOptionTextUpdate: () => isTargetedOptionTextUpdate,
  isUploadCancellation: () => isUploadCancellation,
  isUploadedAttachment: () => isUploadedAttachment,
  isVideoAttachment: () => isVideoAttachment,
  isVoiceRecordingAttachment: () => isVoiceRecordingAttachment,
  isVoteAnswer: () => isVoteAnswer,
  localMessageToNewMessagePayload: () => localMessageToNewMessagePayload,
  logChatPromiseExecution: () => logChatPromiseExecution,
  mapPollStateToResponse: () => mapPollStateToResponse,
  notifyCommandDisabled: () => notifyCommandDisabled,
  notifyCommandNotReady: () => notifyCommandNotReady,
  parseEvent: () => parseEvent,
  parseSns: () => parseSns,
  parseSqs: () => parseSqs,
  pollCompositionStateProcessors: () => pollCompositionStateProcessors,
  pollStateChangeValidators: () => pollStateChangeValidators,
  postInsights: () => postInsights,
  promoteChannel: () => promoteChannel,
  readFileAsArrayBuffer: () => readFileAsArrayBuffer,
  removeDiacritics: () => removeDiacritics,
  replaceWordWithEntity: () => replaceWordWithEntity,
  settled: () => settled,
  stripCommandFromText: () => stripCommandFromText,
  stripMentionTokens: () => stripMentionTokens,
  textIsEmpty: () => textIsEmpty,
  timeLeftMs: () => timeLeftMs,
  verifyAndParseWebhook: () => verifyAndParseWebhook,
  verifySignature: () => verifySignature,
  withCancellation: () => withCancellation,
  withoutConcurrency: () => withoutConcurrency
});
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/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_axios4 = __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_groups: true,
  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];
}
var channelHasReadEvents = (channel) => {
  const ownCapabilities = channel?.data?.own_capabilities;
  return !(Array.isArray(ownCapabilities) && !ownCapabilities.includes("read-events"));
};
var channelTracksReadLocally = (channel) => !channelHasReadEvents(channel) && !!channel?.getClient().options.isLocalUnreadCountEnabled;
var userHasReadReceipts = (client) => client.user?.privacy_settings?.read_receipts?.enabled ?? true;
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 isRemoteUrl = (url) => !!url && /^https?:\/\//i.test(url.trim());
var sanitizeOutgoingAttachments = ({
  client,
  message
}) => {
  const attachments = message.attachments;
  if (!attachments?.length) return message;
  const unresolved = [];
  let changed = false;
  const sanitized = attachments.reduce((acc, attachment) => {
    const { localMetadata, ...rest } = attachment;
    if (!localMetadata) {
      acc.push(attachment);
      return acc;
    }
    changed = true;
    const hasRemoteSource = isRemoteUrl(rest.asset_url) || isRemoteUrl(rest.image_url) || isRemoteUrl(rest.og_scrape_url) || isRemoteUrl(rest.title_link);
    if (hasRemoteSource) {
      acc.push(rest);
    } else {
      unresolved.push(rest);
    }
    return acc;
  }, []);
  if (unresolved.length) {
    const text = `Dropped ${unresolved.length} attachment(s) whose upload never completed. Composing with pending uploads requires awaiting them before sending.`;
    const extraData = { attachments: unresolved, message, tags: ["attachments"] };
    client.logger("warn", text, extraData);
    console.warn(text, extraData);
  }
  return changed ? { ...message, attachments: sanitized } : 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,
    mentioned_groups,
    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;
    };
    const delay = typeof timeout === "function" ? timeout(...args) : timeout;
    runningTimeout = setTimeout(timeoutHandler, delay);
    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) {
      messageFromState.reaction_groups = {};
    }
    if (enforce_unique) {
      for (const ownReaction of messageFromState.own_reactions ?? []) {
        const oldOwnReactionTypeData = messageFromState.reaction_groups[ownReaction.type];
        messageFromState.reaction_groups[ownReaction.type] = {
          ...oldOwnReactionTypeData,
          count: oldOwnReactionTypeData.count - 1,
          sum_scores: oldOwnReactionTypeData.sum_scores - (ownReaction.score ?? 1)
        };
        if (messageFromState.reaction_groups[ownReaction.type].count < 1) {
          delete messageFromState.reaction_groups[ownReaction.type];
        }
      }
    }
    const newReactionGroups = messageFromState.reaction_groups;
    const oldReactionTypeData = newReactionGroups[reaction.type];
    const score = reaction.score ?? 1;
    messageFromState.reaction_groups[reaction.type] = oldReactionTypeData ? {
      ...oldReactionTypeData,
      count: oldReactionTypeData.count + 1,
      sum_scores: oldReactionTypeData.sum_scores + score,
      last_reaction_at: reaction.created_at
    } : {
      count: 1,
      first_reaction_at: reaction.created_at,
      last_reaction_at: reaction.created_at,
      sum_scores: score
    };
    messageFromState.own_reactions = this._addOwnReactionToMessage(
      messageFromState.own_reactions,
      reaction,
      enforce_unique
    );
    const userId = this._channel.getClient().userID;
    messageFromState.latest_reactions = enforce_unique ? [
      ...(messageFromState.latest_reactions || []).filter(
        (r) => r.user_id !== userId
      ),
      reaction
    ] : [...messageFromState.latest_reactions || [], reaction];
    return messageFromState;
  }
  _addOwnReactionToMessage(ownReactions, reaction, enforce_unique) {
    if (enforce_unique) {
      ownReactions = [];
    } else {
      ownReactions = this._removeOwnReactionFromMessage(ownReactions, reaction);
    }
    ownReactions = ownReactions || [];
    if (this._channel.getClient().userID === reaction.user_id) {
      ownReactions.push(reaction);
    }
    return ownReactions;
  }
  _removeOwnReactionFromMessage(ownReactions, reaction) {
    if (ownReactions) {
      return ownReactions.filter(
        (item) => item.user_id !== reaction.user_id || item.type !== reaction.type
      );
    }
    return ownReactions;
  }
  removeReaction(reaction, message) {
    const messageWithRemovedReaction = message;
    let messageFromState;
    if (!messageWithRemovedReaction) {
      messageFromState = this.findMessage(reaction.message_id);
    }
    if (!messageWithRemovedReaction && !messageFromState) {
      return;
    }
    const messageToUpdate = messageWithRemovedReaction ?? 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 (messageWithRemovedReaction) {
        messageWithRemovedReaction.own_reactions = this._removeOwnReactionFromMessage(
          msg.own_reactions,
          reaction
        );
        return this.formatMessage(messageWithRemovedReaction);
      }
      if (messageFromState) {
        return this._removeReactionFromState(messageFromState, reaction);
      }
      return msg;
    });
    return messageWithRemovedReaction;
  }
  _removeReactionFromState(messageFromState, reaction) {
    const reactionToRemove = messageFromState.own_reactions?.find(
      (r) => r.type === reaction.type
    );
    if (reactionToRemove && messageFromState.reaction_groups?.[reactionToRemove.type]) {
      const newReactionGroup = messageFromState.reaction_groups[reactionToRemove.type];
      messageFromState.reaction_groups[reactionToRemove.type] = {
        ...newReactionGroup,
        count: newReactionGroup.count - 1,
        sum_scores: newReactionGroup.sum_scores - (reactionToRemove.score ?? 1)
      };
      if (messageFromState.reaction_groups[reactionToRemove.type].count < 1) {
        delete messageFromState.reaction_groups[reactionToRemove.type];
      }
    }
    messageFromState.own_reactions = messageFromState.own_reactions?.filter(
      (r) => r.type !== reaction.type
    );
    const userId = this._channel.getClient().userID;
    messageFromState.latest_reactions = messageFromState.latest_reactions?.filter(
      (r) => !(r.user_id === userId && r.type === reaction.type)
    );
    return messageFromState;
  }
  _updateQuotedMessageReferences({
    message,
    remove
  }) {
    const parseMessage = (m) => ({
      ...m,
      created_at: m.created_at.toISOString(),
      pinned_at: m.pinned_at?.toISOString(),
      updated_at: m.updated_at?.toISOString()
    });
    const update = (messages) => {
      const updatedMessages = messages.reduce((acc, msg) => {
        if (msg.quoted_message_id === message.id) {
          acc.push({
            ...parseMessage(msg),
            quoted_message: remove ? { ...message, attachments: [] } : message
          });
        }
        return acc;
      }, []);
      this.addMessagesSorted(updatedMessages, true);
    };
    if (!message.parent_id) {
      this.messageSets.forEach((set) => update(set.messages));
    } else if (message.parent_id && this.threads[message.parent_id]) {
      update(this.threads[message.parent_id]);
    }
  }
  removeQuotedMessageReferences(message) {
    this._updateQuotedMessageReferences({ message, remove: true });
  }
  /**
   * Updates all instances of given message in channel state
   * @param message
   * @param updateFunc
   */
  _updateMessage(message, updateFunc) {
    const { parent_id, show_in_channel, pinned } = message;
    if (parent_id && this.threads[parent_id]) {
      const thread = this.threads[parent_id];
      const msgIndex = thread.findIndex((msg) => msg.id === message.id);
      if (msgIndex !== -1) {
        thread[msgIndex] = updateFunc(thread[msgIndex]);
        this.threads[parent_id] = thread;
      }
    }
    if (!show_in_channel && !parent_id || show_in_channel) {
      const messageSetIndex = this.findMessageSetIndex(message);
      if (messageSetIndex !== -1) {
        const msgIndex = this.messageSets[messageSetIndex].messages.findIndex(
          (msg) => msg.id === message.id
        );
        if (msgIndex !== -1) {
          const upMsg = updateFunc(this.messageSets[messageSetIndex].messages[msgIndex]);
          this.messageSets[messageSetIndex].messages[msgIndex] = upMsg;
        }
      }
    }
    if (pinned) {
      const msgIndex = this.pinnedMessages.findIndex((msg) => msg.id === message.id);
      if (msgIndex !== -1) {
        this.pinnedMessages[msgIndex] = updateFunc(this.pinnedMessages[msgIndex]);
      }
    }
  }
  /**
   * _addToMessageList - Adds a message to a list of messages, tries to update first, appends if message isn't found
   *
   * @param {Array<ReturnType<ChannelState['formatMessage']>>} messages A list of messages
   * @param message
   * @param {boolean} timestampChanged Whether updating a message with changed created_at value.
   * @param {string} sortBy field name to use to sort the messages by
   * @param {boolean} addIfDoesNotExist Add message if it is not in the list, used to prevent out of order updated messages from being added.
   */
  _addToMessageList(messages, message, timestampChanged = false, sortBy = "created_at", addIfDoesNotExist = true) {
    return addToMessageList(
      messages,
      message,
      timestampChanged,
      sortBy,
      addIfDoesNotExist
    );
  }
  /**
   * removeMessage - Description
   *
   * @param {{ id: string; parent_id?: string }} messageToRemove Object of the message to remove. Needs to have at id specified.
   *
   * @return {boolean} Returns if the message was removed
   */
  removeMessage(messageToRemove) {
    let isRemoved = false;
    if (messageToRemove.parent_id && this.threads[messageToRemove.parent_id]) {
      const { removed, result: threadMessages } = this.removeMessageFromArray(
        this.threads[messageToRemove.parent_id],
        messageToRemove
      );
      this.threads[messageToRemove.parent_id] = threadMessages;
      isRemoved = removed;
    } else {
      const messageSetIndex = messageToRemove.messageSetIndex ?? this.findMessageSetIndex(messageToRemove);
      if (messageSetIndex !== -1) {
        const { removed, result: messages } = this.removeMessageFromArray(
          this.messageSets[messageSetIndex].messages,
          messageToRemove
        );
        this.messageSets[messageSetIndex].messages = messages;
        isRemoved = removed;
      }
    }
    return isRemoved;
  }
  /**
   * filterErrorMessages - Removes error messages from the channel state.
   *
   */
  filterErrorMessages() {
    const filteredMessages = this.latestMessages.filter(
      (message) => message.type !== "error"
    );
    const blockedMessages = this.latestMessages.filter(isBlockedMessage);
    for (const message of blockedMessages) {
      this._channel.getClient().offlineDb?.hardDeleteMessage({ id: message.id });
    }
    this.latestMessages = filteredMessages;
  }
  /**
   * clean - Remove stale data such as users that stayed in typing state for more than 5 seconds
   */
  clean() {
    const now = /* @__PURE__ */ new Date();
    for (const [userID, lastEvent] of Object.entries(this.typing)) {
      const receivedAt = typeof lastEvent.received_at === "string" ? new Date(lastEvent.received_at) : lastEvent.received_at || /* @__PURE__ */ new Date();
      if (now.getTime() - receivedAt.getTime() > 7e3) {
        delete this.typing[userID];
        this._channel.getClient().dispatchEvent({
          cid: this._channel.cid,
          type: "typing.stop",
          user: { id: userID }
        });
      }
    }
  }
  clearMessages() {
    this.initMessages();
    this.pinnedMessages = [];
  }
  initMessages() {
    this.messageSets = [
      {
        messages: [],
        isLatest: true,
        isCurrent: true,
        pagination: { ...DEFAULT_MESSAGE_SET_PAGINATION }
      }
    ];
  }
  /**
   * loadMessageIntoState - Loads a given message (and messages around it) into the state
   *
   * @param {string} messageId The id of the message, or 'latest' to indicate switching to the latest messages
   * @param {string} parentMessageId The id of the parent message, if we want load a thread reply
   * @param {number} limit The page size if the message has to be queried from the server
   */
  async loadMessageIntoState(messageId, parentMessageId, limit = 25) {
    let messageSetIndex;
    let switchedToMessageSet = false;
    let loadedMessageThread = false;
    const messageIdToFind = parentMessageId || messageId;
    if (messageId === "latest") {
      if (this.messages === this.latestMessages) {
        return;
      }
      messageSetIndex = this.messageSets.findIndex((s) => s.isLatest);
    } else {
      messageSetIndex = this.findMessageSetIndex({ id: messageIdToFind });
    }
    if (messageSetIndex !== -1) {
      this.switchToMessageSet(messageSetIndex);
      switchedToMessageSet = true;
    }
    loadedMessageThread = !parentMessageId || !!this.threads[parentMessageId]?.find((m) => m.id === messageId);
    if (switchedToMessageSet && loadedMessageThread) {
      return;
    }
    if (!switchedToMessageSet) {
      await this._channel.query(
        { messages: { id_around: messageIdToFind, limit } },
        "new"
      );
    }
    if (!loadedMessageThread && parentMessageId) {
      await this._channel.getReplies(parentMessageId, { id_around: messageId, limit });
    }
    messageSetIndex = this.findMessageSetIndex({ id: messageIdToFind });
    if (messageSetIndex !== -1) {
      this.switchToMessageSet(messageSetIndex);
    }
  }
  /**
   * findMessage - Finds a message inside the state
   *
   * @param {string} messageId The id of the message
   * @param {string} parentMessageId The id of the parent message, if we want load a thread reply
   *
   * @return {ReturnType<ChannelState['formatMessage']>} Returns the message, or undefined if the message wasn't found
   */
  findMessage(messageId, parentMessageId) {
    if (parentMessageId) {
      const messages = this.threads[parentMessageId];
      if (!messages) {
        return void 0;
      }
      return messages.find((m) => m.id === messageId);
    }
    const messageSetIndex = this.findMessageSetIndex({ id: messageId });
    if (messageSetIndex === -1) {
      return void 0;
    }
    return this.messageSets[messageSetIndex].messages.find((m) => m.id === messageId);
  }
  findMessageByTimestamp(timestampMs, parentMessageId, exactTsMatch = false) {
    if (parentMessageId && !this.threads[parentMessageId] || this.messageSets.length === 0)
      return null;
    const setIndex = this.findMessageSetByOldestTimestamp(timestampMs);
    const targetMsgSet = this.messageSets[setIndex]?.messages;
    if (!targetMsgSet?.length) return null;
    const firstMsgTimestamp = targetMsgSet[0].created_at.getTime();
    const lastMsgTimestamp = targetMsgSet.slice(-1)[0].created_at.getTime();
    const isOutOfBound = timestampMs < firstMsgTimestamp || lastMsgTimestamp < timestampMs;
    if (isOutOfBound && exactTsMatch) return null;
    let msgIndex = 0, hi = targetMsgSet.length - 1;
    while (msgIndex < hi) {
      const mid = msgIndex + hi >>> 1;
      if (timestampMs <= targetMsgSet[mid].created_at.getTime()) hi = mid;
      else msgIndex = mid + 1;
    }
    const foundMessage = targetMsgSet[msgIndex];
    return !exactTsMatch ? foundMessage : foundMessage.created_at.getTime() === timestampMs ? foundMessage : null;
  }
  switchToMessageSet(index) {
    const currentMessages = this.messageSets.find((s) => s.isCurrent);
    if (!currentMessages) {
      return;
    }
    currentMessages.isCurrent = false;
    this.messageSets[index].isCurrent = true;
  }
  areMessageSetsOverlap(messages1, messages2) {
    return messages1.some((m1) => messages2.find((m2) => m1.id === m2.id));
  }
  findMessageSetIndex(message) {
    return this.messageSets.findIndex(
      (set) => !!set.messages.find((m) => m.id === message.id)
    );
  }
  findTargetMessageSet(newMessages, addIfDoesNotExist = true, messageSetToAddToIfDoesNotExist = "current") {
    let messagesToAdd = newMessages;
    let targetMessageSetIndex;
    if (newMessages.length === 0)
      return { targetMessageSetIndex: 0, messagesToAdd: newMessages };
    if (addIfDoesNotExist) {
      const overlappingMessageSetIndicesByMsgIds = this.messageSets.map((_, i) => i).filter(
        (i) => this.areMessageSetsOverlap(this.messageSets[i].messages, newMessages)
      );
      const overlappingMessageSetIndicesByTimestamp = this.messageSets.map((_, i) => i).filter(
        (i) => messageSetsOverlapByTimestamp(
          this.messageSets[i].messages,
          newMessages.map(formatMessage)
        )
      );
      switch (messageSetToAddToIfDoesNotExist) {
        case "new":
          if (overlappingMessageSetIndicesByMsgIds.length > 0) {
            targetMessageSetIndex = overlappingMessageSetIndicesByMsgIds[0];
          } else if (overlappingMessageSetIndicesByTimestamp.length > 0) {
            targetMessageSetIndex = overlappingMessageSetIndicesByTimestamp[0];
          } else if (newMessages.some((m) => !m.parent_id)) {
            const setIngestIndex = this.findMessageSetByOldestTimestamp(
              // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
              new Date(newMessages[0].created_at).getTime()
            );
            if (setIngestIndex === -1) {
              this.messageSets.push({
                messages: [],
                isCurrent: false,
                isLatest: false,
                pagination: { ...DEFAULT_MESSAGE_SET_PAGINATION }
              });
              targetMessageSetIndex = this.messageSets.length - 1;
            } else {
              const isLatest = setIngestIndex === 0;
              this.messageSets.splice(setIngestIndex, 0, {
                messages: [],
                isCurrent: false,
                isLatest,
                pagination: { ...DEFAULT_MESSAGE_SET_PAGINATION }
                // fixme: it is problematic decide about pagination without having data
              });
              if (isLatest) {
                this.messageSets.slice(1).forEach((set) => {
                  set.isLatest = false;
                });
              }
              targetMessageSetIndex = setIngestIndex;
            }
          }
          break;
        case "current":
          if (overlappingMessageSetIndicesByTimestamp.length > 0) {
            targetMessageSetIndex = overlappingMessageSetIndicesByTimestamp[0];
          } else {
            targetMessageSetIndex = this.messageSets.findIndex((s) => s.isCurrent);
          }
          break;
        case "latest":
          if (overlappingMessageSetIndicesByTimestamp.length > 0) {
            targetMessageSetIndex = overlappingMessageSetIndicesByTimestamp[0];
          } else {
            targetMessageSetIndex = this.messageSets.findIndex((s) => s.isLatest);
          }
          break;
        default:
          targetMessageSetIndex = -1;
      }
      const mergeTargetMessageSetIndex = overlappingMessageSetIndicesByMsgIds.splice(
        0,
        1
      )[0];
      const mergeSourceMessageSetIndices = [...overlappingMessageSetIndicesByMsgIds];
      if (mergeTargetMessageSetIndex !== void 0 && mergeTargetMessageSetIndex !== targetMessageSetIndex) {
        mergeSourceMessageSetIndices.push(targetMessageSetIndex);
      }
      if (mergeSourceMessageSetIndices.length > 0) {
        const target = this.messageSets[mergeTargetMessageSetIndex];
        const sources = this.messageSets.filter(
          (_, i) => mergeSourceMessageSetIndices.indexOf(i) !== -1
        );
        sources.forEach((messageSet) => {
          target.isLatest = target.isLatest || messageSet.isLatest;
          target.isCurrent = target.isCurrent || messageSet.isCurrent;
          target.pagination.hasPrev = messageSet.messages[0].created_at < target.messages[0].created_at ? messageSet.pagination.hasPrev : target.pagination.hasPrev;
          target.pagination.hasNext = target.messages.slice(-1)[0].created_at < messageSet.messages.slice(-1)[0].created_at ? messageSet.pagination.hasNext : target.pagination.hasNext;
          messagesToAdd = [...messagesToAdd, ...messageSet.messages];
        });
        sources.forEach((s) => this.messageSets.splice(this.messageSets.indexOf(s), 1));
        const overlappingMessageSetIndex = this.messageSets.findIndex(
          (s) => this.areMessageSetsOverlap(s.messages, newMessages)
        );
        targetMessageSetIndex = overlappingMessageSetIndex;
      }
    } else {
      targetMessageSetIndex = this.findMessageSetIndex(newMessages[0]);
    }
    return { targetMessageSetIndex, messagesToAdd };
  }
};

// src/store.ts
var isPatch = (value) => typeof value === "function";
var noop = () => {
};
var StateStore = class {
  constructor(value) {
    this.value = value;
    this.handlers = /* @__PURE__ */ new Set();
    this.preprocessors = /* @__PURE__ */ new Set();
    this.partialNext = (partial) => this.next((current) => ({ ...current, ...partial }));
    this.subscribeWithSelector = (selector, handler) => {
      let previouslySelectedValues;
      const wrappedHandler = (nextValue) => {
        const newlySelectedValues = selector(nextValue);
        let hasUpdatedValues = typeof previouslySelectedValues === "undefined";
        for (const key in previouslySelectedValues) {
          if (previouslySelectedValues[key] === newlySelectedValues[key]) continue;
          hasUpdatedValues = true;
          break;
        }
        if (!hasUpdatedValues) return;
        const previouslySelectedValuesCopy = previouslySelectedValues;
        previouslySelectedValues = newlySelectedValues;
        handler(newlySelectedValues, previouslySelectedValuesCopy);
      };
      return this.subscribe(wrappedHandler);
    };
  }
  /**
   * Allows merging two stores only if their keys differ otherwise there's no way to ensure the data type stability.
   * @experimental
   * This method is experimental and may change in future versions.
   */
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  merge(stateStore) {
    return new MergedStateStore({
      original: this,
      merged: stateStore
    });
  }
  next(newValueOrPatch) {
    const newValue = isPatch(newValueOrPatch) ? newValueOrPatch(this.value) : newValueOrPatch;
    if (newValue === this.value) return;
    this.preprocessors.forEach((preprocessor) => preprocessor(newValue, this.value));
    const oldValue = this.value;
    this.value = newValue;
    this.handlers.forEach((handler) => handler(this.value, oldValue));
  }
  getLatestValue() {
    return this.value;
  }
  subscribe(handler) {
    handler(this.value, void 0);
    this.handlers.add(handler);
    return () => {
      this.handlers.delete(handler);
    };
  }
  /**
   * Registers a preprocessor function that will be called before the state is updated.
   *
   * Preprocessors are invoked with the new and previous values whenever `next` or `partialNext` methods
   * are called, allowing you to mutate or react to the new value before it is set. Preprocessors run in the
   * order they were registered.
   *
   * @example
   * ```ts
   * const store = new StateStore<{ count: number; isMaxValue: bool; }>({ count: 0, isMaxValue: false });
   *
   * store.addPreprocessor((nextValue, prevValue) => {
   *   if (nextValue.count > 10) {
   *     nextValue.count = 10; // Clamp the value to a maximum of 10
   *   }
   *
   *   if (nextValue.count === 10) {
   *     nextValue.isMaxValue = true; // Set isMaxValue to true if count is 10
   *   } else {
   *     nextValue.isMaxValue = false; // Reset isMaxValue otherwise
   *   }
   * });
   *
   * store.partialNext({ count: 15 });
   *
   * store.getLatestValue(); // { count: 10, isMaxValue: true }
   *
   * store.partialNext({ count: 5 });
   *
   * store.getLatestValue(); // { count: 5, isMaxValue: false }
   * ```
   *
   * @param preprocessor - The function to be called with the next and previous values before the state is updated.
   * @returns A `RemovePreprocessor` function that removes the preprocessor when called.
   */
  addPreprocessor(preprocessor) {
    this.preprocessors.add(preprocessor);
    return () => {
      this.preprocessors.delete(preprocessor);
    };
  }
};
var MergedStateStore = class _MergedStateStore extends StateStore {
  constructor({ original, merged }) {
    const originalValue = original.getLatestValue();
    const mergedValue = merged.getLatestValue();
    super({
      ...originalValue,
      ...mergedValue
    });
    // override original methods and "disable" them
    this.next = () => {
      console.warn(
        `${_MergedStateStore.name}.next is disabled, call original.next or merged.next instead`
      );
    };
    this.partialNext = () => {
      console.warn(
        `${_MergedStateStore.name}.partialNext is disabled, call original.partialNext or merged.partialNext instead`
      );
    };
    this.cachedOriginalValue = originalValue;
    this.cachedMergedValue = mergedValue;
    this.original = original;
    this.merged = merged;
  }
  /**
   * Subscribes to changes in the merged state store.
   *
   * This method extends the base subscribe functionality to handle the merged nature of this store:
   * 1. The first subscriber triggers registration of helper subscribers that listen to both source stores
   * 2. Changes from either source store are propagated to this merged store
   * 3. Source store values are cached to prevent unnecessary updates
   *
   * When the first subscriber is added, the method sets up listeners on both original and merged stores.
   * These listeners update the combined store value whenever either source store changes.
   * All subscriptions (helpers and the actual handler) are tracked so they can be properly cleaned up.
   *
   * @param handler - The callback function that will be executed when the state changes
   * @returns An unsubscribe function that, when called, removes the subscription and any helper subscriptions
   */
  subscribe(handler) {
    const unsubscribeFunctions = [];
    if (!this.handlers.size) {
      const base = (nextValue) => {
        super.next((currentValue) => ({
          ...currentValue,
          ...nextValue
        }));
      };
      unsubscribeFunctions.push(
        this.original.subscribe((nextValue) => {
          if (nextValue === this.cachedOriginalValue) return;
          this.cachedOriginalValue = nextValue;
          base(nextValue);
        }),
        this.merged.subscribe((nextValue) => {
          if (nextValue === this.cachedMergedValue) return;
          this.cachedMergedValue = nextValue;
          base(nextValue);
        })
      );
    }
    unsubscribeFunctions.push(super.subscribe(handler));
    return () => {
      unsubscribeFunctions.forEach((unsubscribe) => unsubscribe());
    };
  }
  /**
   * Retrieves the latest combined state from both original and merged stores.
   *
   * This method extends the base getLatestValue functionality to ensure the merged store
   * remains in sync with its source stores even when there are no active subscribers.
   *
   * When there are no handlers registered, the method:
   * 1. Fetches the latest values from both source stores
   * 2. Compares them with the cached values to detect changes
   * 3. If changes are detected, updates the internal value and caches
   *    the new source values to maintain consistency
   *
   * This approach ensures that calling getLatestValue() always returns the most
   * up-to-date combined state, even if the merged store hasn't been actively
   * receiving updates through subscriptions.
   *
   * @returns The latest combined state from both original and merged stores
   */
  getLatestValue() {
    if (!this.handlers.size) {
      const originalValue = this.original.getLatestValue();
      const mergedValue = this.merged.getLatestValue();
      if (originalValue !== this.cachedOriginalValue || mergedValue !== this.cachedMergedValue) {
        this.value = {
          ...originalValue,
          ...mergedValue
        };
        this.cachedMergedValue = mergedValue;
        this.cachedOriginalValue = originalValue;
      }
    }
    return super.getLatestValue();
  }
  addPreprocessor() {
    console.warn(
      `${_MergedStateStore.name}.addPreprocessor is disabled, call original.addPreprocessor or merged.addPreprocessor instead`
    );
    return noop;
  }
};

// src/utils/WithSubscriptions.ts
var _WithSubscriptions = class _WithSubscriptions {
  constructor() {
    this.unsubscribeFunctions = /* @__PURE__ */ new Set();
    this.refCount = 0;
  }
  /**
   * Returns a boolean, provides information of whether `registerSubscriptions`
   * method has already been called for this instance.
   */
  get hasSubscriptions() {
    return this.unsubscribeFunctions.size > 0;
  }
  addUnsubscribeFunction(unsubscribeFunction) {
    this.unsubscribeFunctions.add(unsubscribeFunction);
  }
  /**
   * Increments `refCount` by one and returns new value.
   */
  incrementRefCount() {
    return ++this.refCount;
  }
  /**
   * If you re-declare `unregisterSubscriptions` method within your class
   * make sure to run the original too.
   *
   * @example
   * ```ts
   * class T extends WithSubscriptions {
   *  ...
   *  public unregisterSubscriptions = () => {
   *    this.customThing();
   *    return super.unregisterSubscriptions();
   *  }
   * }
   * ```
   */
  unregisterSubscriptions() {
    if (this.refCount > 1) {
      this.refCount--;
      return _WithSubscriptions.symbol;
    }
    this.unsubscribeFunctions.forEach((unsubscribe) => unsubscribe());
    this.unsubscribeFunctions.clear();
    this.refCount = 0;
    return _WithSubscriptions.symbol;
  }
};
/**
 * Workaround for the missing TS keyword - ensures that inheritants
 * overriding `unregisterSubscriptions` call the base method and return
 * its unique symbol value.
 */
_WithSubscriptions.symbol = Symbol(_WithSubscriptions.name);
var WithSubscriptions = _WithSubscriptions;

// src/CooldownTimer.ts
var toDateOrUndefined = (value) => {
  if (value instanceof Date) return value;
  if (typeof value === "string" || typeof value === "number") {
    const parsed = new Date(value);
    if (!Number.isNaN(parsed.getTime())) return parsed;
  }
  return void 0;
};
var CooldownTimer = class extends WithSubscriptions {
  constructor({ channel }) {
    super();
    this.timeout = null;
    this.registerSubscriptions = () => {
      this.incrementRefCount();
      if (this.hasSubscriptions) return;
      this.addUnsubscribeFunction(
        this.channel.on("message.new", (event) => {
          const isOwnMessage = event.message?.user?.id && event.message.user.id === this.getOwnUserId();
          if (!isOwnMessage) return;
          this.setOwnLatestMessageDate(toDateOrUndefined(event.message?.created_at));
        }).unsubscribe
      );
      this.addUnsubscribeFunction(
        this.channel.on("channel.updated", (event) => {
          const cooldownChanged = event.channel?.cooldown !== this.cooldownConfigSeconds;
          if (!cooldownChanged) return;
          this.refresh();
        }).unsubscribe
      );
    };
    this.setCooldownRemaining = (cooldownRemaining) => {
      this.state.partialNext({ cooldownRemaining });
    };
    this.clearTimeout = () => {
      if (!this.timeout) return;
      clearTimeout(this.timeout);
      this.timeout = null;
    };
    this.refresh = () => {
      const { cooldown: cooldownConfigSeconds = 0, own_capabilities } = this.channel.data ?? {};
      const canSkipCooldown = (own_capabilities ?? []).includes("skip-slow-mode");
      const ownLatestMessageDate = this.findOwnLatestMessageDate({
        messages: this.channel.state.latestMessages
      });
      if (cooldownConfigSeconds !== this.cooldownConfigSeconds || ownLatestMessageDate?.getTime() !== this.ownLatestMessageDate?.getTime() || canSkipCooldown !== this.canSkipCooldown) {
        this.state.partialNext({
          cooldownConfigSeconds,
          ownLatestMessageDate,
          canSkipCooldown
        });
      }
      if (this.canSkipCooldown || this.cooldownConfigSeconds === 0) {
        this.clearTimeout();
        if (this.cooldownRemaining !== 0) {
          this.setCooldownRemaining(0);
        }
        return;
      }
      this.recalculate();
    };
    /**
     * Updates the known latest own message date and recomputes remaining time.
     * Prefer calling this when you already know the message date (e.g. from an event).
     */
    this.setOwnLatestMessageDate = (date) => {
      this.state.partialNext({ ownLatestMessageDate: date });
      this.recalculate();
    };
    this.recalculate = () => {
      this.clearTimeout();
      const { cooldownConfigSeconds, ownLatestMessageDate, canSkipCooldown } = this.state.getLatestValue();
      const timeSinceOwnLastMessage = ownLatestMessageDate != null ? (
        // prevent negative values
        Math.max(0, (Date.now() - ownLatestMessageDate.getTime()) / 1e3)
      ) : void 0;
      const remaining = !canSkipCooldown && typeof timeSinceOwnLastMessage !== "undefined" && cooldownConfigSeconds > timeSinceOwnLastMessage ? Math.round(cooldownConfigSeconds - timeSinceOwnLastMessage) : 0;
      if (remaining !== this.cooldownRemaining) {
        this.setCooldownRemaining(remaining);
      }
      if (remaining <= 0) return;
      this.timeout = setTimeout(() => {
        this.recalculate();
      }, 1e3);
    };
    this.channel = channel;
    this.state = new StateStore({
      cooldownConfigSeconds: 0,
      cooldownRemaining: 0,
      ownLatestMessageDate: void 0,
      canSkipCooldown: false
    });
    this.refresh();
  }
  get cooldownConfigSeconds() {
    return this.state.getLatestValue().cooldownConfigSeconds;
  }
  get cooldownRemaining() {
    return this.state.getLatestValue().cooldownRemaining;
  }
  get canSkipCooldown() {
    return this.state.getLatestValue().canSkipCooldown;
  }
  get ownLatestMessageDate() {
    return this.state.getLatestValue().ownLatestMessageDate;
  }
  getOwnUserId() {
    const client = this.channel.getClient();
    return client.userID ?? client.user?.id;
  }
  findOwnLatestMessageDate({
    messages
  }) {
    const ownUserId = this.getOwnUserId();
    if (!ownUserId) return void 0;
    let latest;
    for (let i = messages.length - 1; i >= 0; i -= 1) {
      const message = messages[i];
      if (message.user?.id !== ownUserId) continue;
      const createdAt = toDateOrUndefined(message.created_at);
      if (!createdAt) continue;
      if (!latest || createdAt.getTime() > latest.getTime()) {
        latest = createdAt;
      }
      if (latest.getTime() > createdAt.getTime()) break;
    }
    return latest;
  }
};

// src/messageComposer/attachmentIdentity.ts
var isScrapedContent = (attachment) => !!attachment?.og_scrape_url || !!attachment?.title_link;
var isLocalAttachment = (attachment) => !!attachment?.localMetadata?.id;
var isLocalUploadAttachment = (attachment) => !!attachment?.localMetadata?.uploadState;
var PENDING_UPLOAD_STATES = [
  "pending",
  "uploading"
];
var isPendingUpload = (attachment) => isLocalUploadAttachment(attachment) && PENDING_UPLOAD_STATES.includes(attachment.localMetadata.uploadState);
var isFinishedUpload = (attachment) => isLocalUploadAttachment(attachment) && attachment.localMetadata.uploadState === "finished";
var isFileAttachment = (attachment, supportedVideoFormat = []) => attachment.type === "file" || !!(attachment.mime_type && supportedVideoFormat.indexOf(attachment.mime_type) === -1 && attachment.type !== "video");
var isLocalFileAttachment = (attachment) => isFileAttachment(attachment) && isLocalAttachment(attachment);
var isImageAttachment = (attachment) => attachment.type === "image" && !isScrapedContent(attachment);
var isLocalImageAttachment = (attachment) => isImageAttachment(attachment) && isLocalAttachment(attachment);
var isAudioAttachment = (attachment) => attachment.type === "audio";
var isLocalAudioAttachment = (attachment) => isAudioAttachment(attachment) && isLocalAttachment(attachment);
var isVoiceRecordingAttachment = (attachment) => attachment.type === "voiceRecording";
var isLocalVoiceRecordingAttachment = (attachment) => isVoiceRecordingAttachment(attachment) && isLocalAttachment(attachment);
var isVideoAttachment = (attachment, supportedVideoFormat = []) => attachment.type === "video" || !!(attachment.mime_type && supportedVideoFormat.indexOf(attachment.mime_type) !== -1);
var isLocalVideoAttachment = (attachment) => isVideoAttachment(attachment) && isLocalAttachment(attachment);
var isUploadedAttachment = (attachment) => isAudioAttachment(attachment) || isFileAttachment(attachment) || isImageAttachment(attachment) || isVideoAttachment(attachment) || isVoiceRecordingAttachment(attachment);
var isSharedLocationResponse = (location) => !!location.latitude && !!location.longitude && !!location.channel_cid;
var isGiphyAttachment = (attachment) => attachment.type === "giphy";

// src/messageComposer/fileUtils.ts
var isFile = (fileLike) => !!fileLike.lastModified && !("uri" in fileLike);
var isFileList = (obj) => {
  if (obj === null || obj === void 0) return false;
  if (typeof obj !== "object") return false;
  return typeof FileList !== "undefined" && obj instanceof FileList || "item" in obj && "length" in obj && !Array.isArray(obj);
};
var isBlobButNotFile = (obj) => obj instanceof Blob && !(obj instanceof File);
var isFileReference = (obj) => obj !== null && typeof obj === "object" && !isFile(obj) && !isBlobButNotFile(obj) && typeof obj.name === "string" && typeof obj.uri === "string" && typeof obj.size === "number" && typeof obj.type === "string";
var createFileFromBlobs = ({
  blobsArray,
  fileName,
  mimeType
}) => {
  const concatenatedBlob = new Blob(blobsArray, { type: mimeType });
  return new File([concatenatedBlob], fileName, { type: concatenatedBlob.type });
};
var getExtensionFromMimeType = (mimeType) => {
  const match = mimeType.match(/\/([^/;]+)/);
  return match?.[1];
};
var readFileAsArrayBuffer = (file) => new Promise((resolve, reject) => {
  const fileReader = new FileReader();
  fileReader.onload = () => {
    resolve(fileReader.result);
  };
  fileReader.onerror = () => {
    reject(fileReader.error);
  };
  fileReader.readAsArrayBuffer(file);
});
var generateFileName = (mimeType) => {
  const extension = getExtensionFromMimeType(mimeType);
  return `file_${(/* @__PURE__ */ new Date()).toISOString()}${extension ? "." + extension : ""}`;
};
var isImageFile = (fileLike) => {
  const mimeType = fileLike.type;
  return mimeType.startsWith("image/") && !mimeType.endsWith(".photoshop");
};
var getAttachmentTypeFromMimeType = (mimeType) => {
  if (mimeType.startsWith("image/") && !mimeType.endsWith(".photoshop")) return "image";
  if (mimeType.includes("video/")) return "video";
  if (mimeType.includes("audio/")) return "audio";
  return "file";
};
var ensureIsLocalAttachment = (attachment) => {
  if (!attachment) return null;
  if (isLocalAttachment(attachment)) {
    return attachment;
  }
  const { localMetadata, ...rest } = attachment;
  return {
    localMetadata: {
      ...localMetadata ?? {},
      id: localMetadata?.id || generateUUIDv4()
    },
    ...rest
  };
};

// src/messageComposer/middleware/attachmentManager/postUpload/attachmentEnrichment.ts
var createPostUploadAttachmentEnrichmentMiddleware = (composer) => ({
  id: "stream-io/attachment-manager-middleware/post-upload-enrichment",
  handlers: {
    postProcess: ({
      state,
      discard,
      forward,
      next
    }) => {
      const { attachment, error, response } = state;
      if (error) return forward();
      if (!attachment || !response) return discard();
      const enrichedAttachment = { ...attachment };
      const { id, previewUri } = attachment.localMetadata;
      const attachmentIsStillInComposer = !composer || !!composer.attachmentManager.attachmentsById[id];
      if (previewUri && attachmentIsStillInComposer) {
        if (previewUri.startsWith("blob:")) URL.revokeObjectURL?.(previewUri);
        delete enrichedAttachment.localMetadata.previewUri;
      }
      if (isLocalImageAttachment(attachment)) {
        enrichedAttachment.image_url = response.file;
      } else {
        enrichedAttachment.asset_url = response.file;
      }
      if (response.thumb_url) {
        enrichedAttachment.thumb_url = response.thumb_url;
      }
      return next({
        ...state,
        attachment: enrichedAttachment
      });
    }
  }
});

// src/utils/concurrency.ts
var withoutConcurrency = createRunner(wrapWithContinuationTracking);
var withCancellation = createRunner(wrapWithCancellation);
var pendingPromises = /* @__PURE__ */ new Map();
function hasPending(tag) {
  return pendingPromises.has(tag);
}
async function settled(tag) {
  await pendingPromises.get(tag)?.promise;
}
function createRunner(wrapper) {
  return function run(tag, cb) {
    const { cb: wrapped, onContinued } = wrapper(tag, cb);
    const pending = pendingPromises.get(tag);
    pending?.onContinued();
    const promise = pending ? pending.promise.then(wrapped, wrapped) : wrapped();
    pendingPromises.set(tag, { promise, onContinued });
    return promise;
  };
}
function wrapWithContinuationTracking(tag, cb) {
  let hasContinuation = false;
  const wrapped = () => cb().finally(() => {
    if (!hasContinuation) {
      pendingPromises.delete(tag);
    }
  });
  const onContinued = () => hasContinuation = true;
  return { cb: wrapped, onContinued };
}
function wrapWithCancellation(tag, cb) {
  const ac = new AbortController();
  const wrapped = () => {
    if (ac.signal.aborted) {
      return Promise.resolve("canceled");
    }
    return cb(ac.signal).finally(() => {
      if (!ac.signal.aborted) {
        pendingPromises.delete(tag);
      }
    });
  };
  const onContinued = () => ac.abort();
  return { cb: wrapped, onContinued };
}

// src/middleware.ts
var MiddlewareExecutor = class {
  constructor() {
    this.middleware = [];
    this.id = generateUUIDv4();
  }
  /**
   * The middleware currently installed, in execution order.
   *
   * Read-only: use `use` / `insert` / `replace` / `setOrder` / `remove` to change the chain. It
   * exists so components can ask what the chain is capable of - a middleware may declare
   * behaviour its host has to account for, and the host has no other way to find out.
   */
  get installedMiddleware() {
    return this.middleware;
  }
  use(middleware) {
    this.middleware = this.middleware.concat(middleware);
    return this;
  }
  // todo: document how to re-arrange the order of middleware using replace
  replace(middleware) {
    const newMiddleware = [...this.middleware];
    middleware.forEach((upserted) => {
      const existingIndex = this.middleware.findIndex(
        (existing) => existing.id === upserted.id
      );
      if (existingIndex >= 0) {
        newMiddleware.splice(existingIndex, 1, upserted);
      } else {
        newMiddleware.push(upserted);
      }
    });
    this.middleware = newMiddleware;
    return this;
  }
  insert({
    middleware,
    position,
    unique
  }) {
    if (unique) {
      middleware.forEach((md) => {
        const existingMiddlewareIndex = this.middleware.findIndex((m) => m.id === md.id);
        if (existingMiddlewareIndex >= 0) {
          this.middleware.splice(existingMiddlewareIndex, 1);
        }
      });
    }
    const targetId = position.after || position.before;
    const targetIndex = this.middleware.findIndex((m) => m.id === targetId);
    const insertionIndex = position.after ? targetIndex + 1 : targetIndex;
    this.middleware.splice(insertionIndex, 0, ...middleware);
    return this;
  }
  setOrder(order) {
    this.middleware = order.map((id) => this.middleware.find((middleware) => middleware.id === id)).filter(Boolean);
  }
  remove(middlewareIds) {
    if (!middlewareIds && !middlewareIds.length) return;
    this.middleware = this.middleware.filter(
      (md) => typeof middlewareIds === "string" ? middlewareIds !== md.id : !middlewareIds.includes(md.id)
    );
  }
  async executeMiddlewareChain({
    eventName,
    initialValue,
    mode = "cancelable"
  }) {
    let index = -1;
    const execute = async (i, state, status) => {
      if (i <= index) {
        throw new Error("next() called multiple times");
      }
      index = i;
      const returnFromChain = i === this.middleware.length || status && ["complete", "discard"].includes(status);
      if (returnFromChain) return { state, status };
      const middleware = this.middleware[i];
      const handler = middleware.handlers[eventName];
      if (!handler) {
        return execute(i + 1, state, status);
      }
      const next = (adjustedState) => execute(i + 1, adjustedState);
      const complete = (adjustedState) => execute(i + 1, adjustedState, "complete");
      const discard = () => execute(i + 1, state, "discard");
      const forward = () => execute(i + 1, state);
      return await handler({
        state,
        next,
        complete,
        discard,
        forward
      });
    };
    const result = mode === "cancelable" ? await withCancellation(
      `middleware-execution-${this.id}-${eventName}`,
      async (abortSignal) => {
        const result2 = await execute(0, initialValue);
        if (abortSignal.aborted) {
          return "canceled";
        }
        return result2;
      }
    ) : await execute(0, initialValue);
    return result === "canceled" ? { state: initialValue, status: "discard" } : result;
  }
  async execute({
    eventName,
    initialValue: initialState,
    mode
  }) {
    return await this.executeMiddlewareChain({
      eventName,
      initialValue: initialState,
      mode
    });
  }
};

// src/uploadManager.ts
var import_axios = __toESM(require("axios"));
var isUploadCancellation = (error) => import_axios.default.isCancel(error) || error instanceof Error && error.name === "AbortError";
var initState = () => ({ uploads: {} });
var upsertById = (uploads, record) => ({
  ...uploads,
  [record.id]: { ...uploads[record.id], ...record }
});
var updateById = (uploads, record) => {
  if (!(record.id in uploads)) return null;
  const current = uploads[record.id];
  return { ...uploads, [record.id]: { ...current, ...record } };
};
var UploadManager = class {
  constructor(client) {
    this.client = client;
    this.inFlightUploads = /* @__PURE__ */ new Map();
    this.getUpload = (id) => this.uploads[id];
    /**
     * Clears all upload records.
     * Invoked when the user disconnects so a later session does not inherit stale upload state.
     * Aborts every in-flight upload request via its `UploadRequestOptions.abortSignal`.
     */
    this.reset = () => {
      for (const { abortController } of this.inFlightUploads.values()) {
        abortController.abort();
      }
      this.inFlightUploads.clear();
      this.state.next(initState());
    };
    /**
     * Removes the upload record for `id` if present.
     * If an upload is still in progress, aborts its `UploadRequestOptions.abortSignal`.
     */
    this.deleteUploadRecord = (id) => {
      const flight = this.inFlightUploads.get(id);
      if (flight) {
        this.inFlightUploads.delete(id);
        flight.abortController.abort();
      }
      this.state.next((current) => {
        if (!(id in current.uploads)) return current;
        const uploads = { ...current.uploads };
        delete uploads[id];
        return { ...current, uploads };
      });
    };
    /**
     * Starts an upload for `id`, or returns the existing in-flight promise if one is already running.
     * Uses {@link StreamChat.channel}(`channelCid`) → `messageComposer.attachmentManager.doUploadRequest`.
     * Resolves with that result; rejects if the upload rejects (the record is removed from state either way).
     */
    this.upload = ({
      id,
      channelCid,
      file
    }) => {
      const existing = this.inFlightUploads.get(id);
      if (existing) return existing.promise;
      let resolvePromise;
      let rejectPromise;
      const promise = new Promise((resolve, reject) => {
        resolvePromise = resolve;
        rejectPromise = reject;
      });
      const abortController = new AbortController();
      this.inFlightUploads.set(id, { promise, abortController });
      void (async () => {
        const attachmentManager = this.resolveAttachmentManager(channelCid);
        const trackProgress = attachmentManager.config.trackUploadProgress;
        try {
          this.upsertUpload({
            uploadConfirmationPending: false,
            id,
            uploadProgress: trackProgress ? 0 : void 0
          });
          const onProgress = trackProgress ? (progress) => {
            this.updateUpload({
              id,
              uploadProgress: progress,
              // Only a number says anything about the flush, so a report without one leaves
              // the flag alone: `undefined` means the transport cannot measure this upload,
              // not that bytes were un-sent. Lowering it there would drop a UI that had
              // already gone indeterminate back to a determinate one with nothing to show.
              //
              // The record is removed in the `finally` below, so a record that still exists
              // while reporting 100% can only mean "flushed, awaiting the response".
              ...typeof progress === "number" ? { uploadConfirmationPending: progress >= 100 } : {}
            });
          } : void 0;
          const uploadRequestOptions = {
            abortSignal: abortController.signal,
            ...onProgress ? { onProgress } : {}
          };
          const response = await attachmentManager.doUploadRequest(
            file,
            uploadRequestOptions
          );
          resolvePromise(response);
        } catch (error) {
          rejectPromise(error);
        } finally {
          this.inFlightUploads.delete(id);
          this.deleteUploadRecord(id);
        }
      })();
      return promise;
    };
    this.upsertUpload = (record) => {
      this.state.partialNext({
        uploads: upsertById(this.uploads, record)
      });
    };
    this.updateUpload = (record) => {
      this.state.next((current) => {
        const nextUploads = updateById(current.uploads, record);
        if (!nextUploads) return current;
        return { ...current, uploads: nextUploads };
      });
    };
    this.state = new StateStore(initState());
  }
  resolveAttachmentManager(channelCid) {
    const colon = channelCid.indexOf(":");
    if (colon <= 0 || colon === channelCid.length - 1) {
      throw new Error(`Invalid channelCid: ${channelCid}`);
    }
    const channelType = channelCid.slice(0, colon);
    const channelId = channelCid.slice(colon + 1);
    return this.client.channel(channelType, channelId).messageComposer.attachmentManager;
  }
  get uploads() {
    return this.state.getLatestValue().uploads;
  }
};

// src/messageComposer/middleware/attachmentManager/postUpload/uploadErrorHandler.ts
var createUploadErrorHandlerMiddleware = (composer) => ({
  id: "stream-io/attachment-manager-middleware/upload-error",
  handlers: {
    postProcess: ({
      state,
      discard,
      forward
    }) => {
      const { attachment, error } = state;
      if (!error) return forward();
      if (!attachment) return discard();
      if (isUploadCancellation(error)) return forward();
      const reason = error instanceof Error ? error.message : "unknown error";
      composer.client.notifications.addError({
        message: "Error uploading attachment",
        origin: {
          emitter: "AttachmentManager",
          context: { attachment }
        },
        options: {
          type: "api:attachment:upload:failed",
          metadata: { reason },
          originalError: error
        }
      });
      return forward();
    }
  }
});

// src/messageComposer/middleware/attachmentManager/postUpload/AttachmentPostUploadMiddlewareExecutor.ts
var AttachmentPostUploadMiddlewareExecutor = class extends MiddlewareExecutor {
  constructor({ composer }) {
    super();
    this.use([
      createUploadErrorHandlerMiddleware(composer),
      createPostUploadAttachmentEnrichmentMiddleware(composer)
    ]);
  }
};

// src/messageComposer/middleware/attachmentManager/preUpload/serverUploadConfigCheck.ts
var createUploadConfigCheckMiddleware = (composer) => ({
  id: "stream-io/attachment-manager-middleware/file-upload-config-check",
  handlers: {
    prepare: async ({
      state,
      next,
      discard
    }) => {
      const { attachmentManager } = composer;
      if (!attachmentManager || !state.attachment) return discard();
      const uploadPermissionCheck = await attachmentManager.getUploadConfigCheck(
        state.attachment.localMetadata.file
      );
      const attachment = {
        ...state.attachment,
        localMetadata: {
          ...state.attachment.localMetadata,
          uploadPermissionCheck,
          uploadState: uploadPermissionCheck.uploadBlocked ? "blocked" : "pending"
        }
      };
      return next({
        ...state,
        attachment
      });
    }
  }
});

// src/messageComposer/middleware/attachmentManager/preUpload/blockedUploadNotification.ts
var createBlockedAttachmentUploadNotificationMiddleware = (composer) => ({
  id: "stream-io/attachment-manager-middleware/blocked-upload-notification",
  handlers: {
    prepare: ({
      state: { attachment },
      forward
    }) => {
      if (!attachment) return forward();
      if (attachment.localMetadata.uploadPermissionCheck?.uploadBlocked) {
        composer.client.notifications.addError({
          message: `The attachment upload was blocked`,
          origin: {
            emitter: "AttachmentManager",
            context: { blockedAttachment: attachment }
          },
          options: {
            type: "validation:attachment:upload:blocked",
            metadata: {
              reason: attachment.localMetadata.uploadPermissionCheck?.reason
            }
          }
        });
      }
      return forward();
    }
  }
});

// src/messageComposer/middleware/attachmentManager/preUpload/AttachmentPreUploadMiddlewareExecutor.ts
var AttachmentPreUploadMiddlewareExecutor = class extends MiddlewareExecutor {
  constructor({ composer }) {
    super();
    this.use([
      createUploadConfigCheckMiddleware(composer),
      createBlockedAttachmentUploadNotificationMiddleware(composer)
    ]);
  }
};

// src/utils/mergeWith/mergeWithCore.ts
var isClassInstance = (value) => {
  if (!value || typeof value !== "object") return false;
  if (Array.isArray(value)) return false;
  const proto = Object.getPrototypeOf(value);
  if (proto === null || proto === Object.prototype) return false;
  return value.constructor && value.constructor !== Object;
};
var isEqual = (value1, value2, compareStack = /* @__PURE__ */ new Set(), objectStack1 = /* @__PURE__ */ new WeakSet(), objectStack2 = /* @__PURE__ */ new WeakSet()) => {
  if (value1 === value2) return true;
  if (value1 == null || value2 == null) return false;
  const type1 = typeof value1;
  const type2 = typeof value2;
  if (type1 !== type2) return false;
  if (type1 !== "object") {
    if (value1 !== value1 && value2 !== value2) return true;
    return value1 === value2;
  }
  const obj1 = value1;
  const obj2 = value2;
  if (objectStack1.has(obj1) || objectStack2.has(obj2)) {
    return objectStack1.has(obj1) && objectStack2.has(obj2);
  }
  objectStack1.add(obj1);
  objectStack2.add(obj2);
  if (value1 instanceof Date && value2 instanceof Date) {
    objectStack1.delete(obj1);
    objectStack2.delete(obj2);
    return value1.getTime() === value2.getTime();
  }
  if (value1 instanceof RegExp && value2 instanceof RegExp) {
    objectStack1.delete(obj1);
    objectStack2.delete(obj2);
    return value1.toString() === value2.toString();
  }
  if (isClassInstance(value1) || isClassInstance(value2)) {
    objectStack1.delete(obj1);
    objectStack2.delete(obj2);
    return false;
  }
  const isArray1 = Array.isArray(value1);
  const isArray2 = Array.isArray(value2);
  if (isArray1 !== isArray2) {
    objectStack1.delete(obj1);
    objectStack2.delete(obj2);
    return false;
  }
  if (isArray1 && isArray2) {
    const arr1 = value1;
    const arr2 = value2;
    if (arr1.length !== arr2.length) {
      objectStack1.delete(obj1);
      objectStack2.delete(obj2);
      return false;
    }
    const pairKey2 = [value1, value2];
    if (compareStack.has(pairKey2)) {
      objectStack1.delete(obj1);
      objectStack2.delete(obj2);
      return true;
    }
    compareStack.add(pairKey2);
    for (let i = 0; i < arr1.length; i++) {
      if (!isEqual(arr1[i], arr2[i], compareStack, objectStack1, objectStack2)) {
        compareStack.delete(pairKey2);
        objectStack1.delete(obj1);
        objectStack2.delete(obj2);
        return false;
      }
    }
    compareStack.delete(pairKey2);
    objectStack1.delete(obj1);
    objectStack2.delete(obj2);
    return true;
  }
  const plainObj1 = value1;
  const plainObj2 = value2;
  const keys1 = Object.keys(plainObj1);
  const keys2 = Object.keys(plainObj2);
  if (keys1.length !== keys2.length) {
    objectStack1.delete(obj1);
    objectStack2.delete(obj2);
    return false;
  }
  for (const key of keys2) {
    if (!Object.prototype.hasOwnProperty.call(plainObj1, key)) {
      objectStack1.delete(obj1);
      objectStack2.delete(obj2);
      return false;
    }
  }
  const pairKey = [value1, value2];
  if (compareStack.has(pairKey)) {
    objectStack1.delete(obj1);
    objectStack2.delete(obj2);
    return true;
  }
  compareStack.add(pairKey);
  for (const key of keys1) {
    if (!isEqual(plainObj1[key], plainObj2[key], compareStack, objectStack1, objectStack2)) {
      compareStack.delete(pairKey);
      objectStack1.delete(obj1);
      objectStack2.delete(obj2);
      return false;
    }
  }
  compareStack.delete(pairKey);
  objectStack1.delete(obj1);
  objectStack2.delete(obj2);
  return true;
};
function generateDiff(original, modified) {
  const diffRoot = { children: {} };
  compareAndBuildDiff(original, modified, diffRoot);
  return cleanupDiffTree(diffRoot);
}
function compareAndBuildDiff(original, modified, parentDiffNode, key, compareStack = /* @__PURE__ */ new Set(), objectStack = /* @__PURE__ */ new Set()) {
  if (isEqual(original, modified, new Set(compareStack))) {
    return;
  }
  if (original === void 0 || original === null) {
    if (key !== void 0) {
      parentDiffNode.children[String(key)] = {
        type: "added",
        value: modified,
        children: {}
      };
    }
    return;
  }
  if (typeof original === "object" && original !== null) {
    if (objectStack.has(original)) {
      if (key !== void 0) {
        parentDiffNode.children[String(key)] = {
          type: "circular",
          value: modified,
          oldValue: original,
          children: {}
        };
      }
      return;
    }
    objectStack.add(original);
  }
  const shouldTreatAtomically = typeof original !== "object" || typeof modified !== "object" || original === null || modified === null || Array.isArray(original) !== Array.isArray(modified) || isClassInstance(original) || isClassInstance(modified);
  if (shouldTreatAtomically) {
    if (key !== void 0) {
      parentDiffNode.children[String(key)] = {
        type: "updated",
        value: modified,
        oldValue: original,
        children: {}
      };
    }
    if (typeof original === "object" && original !== null) {
      objectStack.delete(original);
    }
    return;
  }
  const originalObj = original;
  const modifiedObj = modified;
  const currentDiffNode = key !== void 0 ? {
    type: "updated",
    children: {},
    oldValue: original,
    value: modified
  } : parentDiffNode;
  if (key !== void 0) {
    parentDiffNode.children[String(key)] = currentDiffNode;
  }
  const pairKey = [original, modified];
  if (compareStack.has(pairKey)) {
    if (typeof original === "object" && original !== null) {
      objectStack.delete(original);
    }
    return;
  }
  compareStack.add(pairKey);
  const allKeys = /* @__PURE__ */ new Set([
    ...Object.keys(originalObj),
    ...Object.getOwnPropertySymbols(originalObj),
    ...Object.keys(modifiedObj),
    ...Object.getOwnPropertySymbols(modifiedObj)
  ]);
  for (const childKey of allKeys) {
    const originalValue = originalObj[childKey];
    const modifiedValue = modifiedObj[childKey];
    if (!(childKey in modifiedObj)) {
      continue;
    }
    if (!(childKey in originalObj)) {
      currentDiffNode.children[String(childKey)] = {
        type: "added",
        value: modifiedValue,
        children: {}
      };
      continue;
    }
    compareAndBuildDiff(
      originalValue,
      modifiedValue,
      currentDiffNode,
      childKey,
      compareStack,
      objectStack
    );
  }
  compareStack.delete(pairKey);
  if (typeof original === "object" && original !== null) {
    objectStack.delete(original);
  }
}
function createMergeCore(options = {}) {
  const { trackDiff = false } = options;
  return function mergeCore({
    target,
    source,
    customizer
  }) {
    const sources = Array.isArray(source) ? source : [source];
    const originalTarget = trackDiff ? structuredClone(target) : void 0;
    function handleCustomizer(targetValue, srcValue, sourceKey, target2, src, stack) {
      const customValue = customizer?.(
        targetValue,
        srcValue,
        sourceKey,
        target2,
        src,
        stack
      );
      if (customValue !== void 0) {
        Object.defineProperty(target2, sourceKey, {
          value: customValue,
          enumerable: true,
          writable: true,
          configurable: true
        });
        return true;
      }
      return false;
    }
    function createNewTarget(targetValue, srcValue) {
      if (targetValue === null || typeof targetValue === "undefined") {
        return srcValue;
      }
      if (!Array.isArray(targetValue) && typeof targetValue !== "object") {
        return srcValue;
      }
      if (targetValue && typeof targetValue === "object") {
        const isTargetClassInstance = isClassInstance(targetValue);
        const isSourceClassInstance = isClassInstance(srcValue);
        if (isTargetClassInstance || isSourceClassInstance) {
          if (isSourceClassInstance) {
            return srcValue;
          }
          return targetValue;
        }
        return Array.isArray(targetValue) ? [...targetValue] : { ...targetValue };
      }
      return Array.isArray(srcValue) ? [] : {};
    }
    function processSourceValue(target2, src, sourceKey, stack, pendingMerges) {
      const srcValue = src[sourceKey];
      const targetValue = target2[sourceKey];
      if (handleCustomizer(targetValue, srcValue, sourceKey, target2, src, stack)) {
        return;
      }
      if (srcValue && typeof srcValue === "object") {
        if (!stack.has(srcValue)) {
          const newTarget = createNewTarget(targetValue, srcValue);
          Object.defineProperty(target2, sourceKey, {
            value: newTarget,
            enumerable: true,
            writable: true,
            configurable: true
          });
          if (isClassInstance(newTarget)) return;
          pendingMerges.push({
            target: newTarget,
            source: srcValue,
            sourceKey,
            parentTarget: target2
          });
        }
      } else if (srcValue !== void 0) {
        target2[sourceKey] = srcValue;
      }
    }
    function processKeys(target2, source2, stack, pendingMerges) {
      const sourceKeys = [
        ...Object.keys(source2),
        ...Object.getOwnPropertySymbols(source2)
      ];
      for (const sourceKey of sourceKeys) {
        processSourceValue(target2, source2, sourceKey, stack, pendingMerges);
      }
    }
    function processPendingMerge({ target: target2, source: source2, sourceKey, parentTarget }, stack, pendingMerges) {
      if (stack.has(source2)) {
        if (trackDiff && sourceKey && parentTarget) {
          Object.defineProperty(parentTarget, sourceKey, {
            value: target2,
            enumerable: true,
            writable: true,
            configurable: true
          });
        }
        return;
      }
      if (!stack.has(target2) && !stack.has(source2)) {
        stack.add(target2);
        stack.add(source2);
        processKeys(target2, source2, stack, pendingMerges);
        stack.delete(source2);
        stack.delete(target2);
      }
    }
    function baseMerge(object, source2, stack = /* @__PURE__ */ new Set()) {
      if (stack.has(object) || stack.has(source2)) {
        return { ...object };
      }
      const result2 = { ...object };
      const pendingMerges = [];
      stack.add(result2);
      stack.add(source2);
      processKeys(result2, source2, stack, pendingMerges);
      while (pendingMerges.length) {
        processPendingMerge(pendingMerges.pop(), stack, pendingMerges);
      }
      stack.delete(source2);
      stack.delete(result2);
      return result2;
    }
    const result = sources.reduce((result2, source2) => baseMerge(result2, source2), {
      ...target
    });
    const diff = trackDiff && originalTarget ? generateDiff(originalTarget, result) : null;
    return { result, diff };
  };
}
function cleanupDiffTree(diffNode) {
  const cleanChildren = {};
  let hasChildren = false;
  for (const key in diffNode.children) {
    const childNode = cleanupDiffTree(diffNode.children[key]);
    if (childNode) {
      cleanChildren[key] = childNode;
      hasChildren = true;
    }
  }
  if (diffNode.type || hasChildren) {
    return {
      ...diffNode,
      children: cleanChildren
    };
  }
  return null;
}

// src/utils/mergeWith/mergeWith.ts
function mergeWith(target, source, customizer) {
  const mergeCore = createMergeCore();
  return mergeCore({ target, source, customizer }).result;
}

// src/utils/mergeWith/mergeWithDiff.ts
function mergeWithDiff(target, source, customizer) {
  const mergeCore = createMergeCore({ trackDiff: true });
  const { result, diff } = mergeCore({ target, source, customizer });
  return { result, diff: cleanupDiffTree(diff ?? { children: {} }) || { children: {} } };
}

// src/messageComposer/attachmentManager.ts
var initState2 = ({
  message
}) => ({
  attachments: (message?.attachments ?? [])?.filter(({ og_scrape_url }) => !og_scrape_url).map((att) => {
    const localMetadata = isUploadedAttachment(att) ? { id: generateUUIDv4(), uploadState: "finished" } : { id: generateUUIDv4() };
    return {
      ...att,
      localMetadata
    };
  })
});
var _AttachmentManager = class _AttachmentManager {
  constructor({ composer, message }) {
    this.setCustomUploadFn = (doUploadRequest) => {
      this.composer.updateConfig({ attachments: { doUploadRequest } });
    };
    this.cancelAttachmentUploads = (attachments = this.attachments) => {
      for (const { localMetadata } of attachments) {
        this.client.uploadManager.deleteUploadRecord(localMetadata.id);
      }
    };
    this.normalizeSnapshotAttachment = (attachment) => {
      if (attachment.localMetadata.uploadState !== "uploading") return attachment;
      this.client.uploadManager.deleteUploadRecord(attachment.localMetadata.id);
      return {
        ...attachment,
        localMetadata: {
          ...attachment.localMetadata,
          uploadConfirmationPending: false,
          uploadProgress: void 0,
          uploadState: "failed"
        }
      };
    };
    this.initState = ({ message } = {}) => {
      this.state.next(initState2({ message }));
    };
    this.getSnapshot = () => {
      const state = this.state.getLatestValue();
      let hasUpdates = false;
      const attachments = state.attachments.map(this.normalizeSnapshotAttachment);
      for (let i = 0; i < attachments.length; i++) {
        if (attachments[i] !== state.attachments[i]) {
          hasUpdates = true;
          break;
        }
      }
      return hasUpdates ? { ...state, attachments } : state;
    };
    this.restoreSnapshot = (snapshot) => {
      this.cancelAttachmentUploads(this.attachments);
      this.state.next(snapshot);
    };
    this.setAttachments = (attachments) => {
      this.state.partialNext({ attachments });
    };
    this.clearAttachments = () => {
      if (!this.attachments.length) return;
      this.removeAttachments(
        this.attachments.map((attachment) => attachment.localMetadata.id)
      );
    };
    this.getAttachmentIndex = (localId) => {
      const attachmentsById = this.attachmentsById;
      return this.attachments.indexOf(attachmentsById[localId]);
    };
    this.prepareAttachmentUpdate = (attachmentToUpdate) => {
      const stateAttachments = this.attachments;
      const attachments = [...this.attachments];
      const attachmentIndex = this.getAttachmentIndex(attachmentToUpdate.localMetadata.id);
      if (attachmentIndex === -1) return null;
      const merged = mergeWithDiff(
        stateAttachments[attachmentIndex],
        attachmentToUpdate
      );
      const updatesOnMerge = merged.diff && Object.keys(merged.diff.children).length;
      if (updatesOnMerge) {
        const localAttachment = ensureIsLocalAttachment(merged.result);
        if (localAttachment) {
          attachments.splice(attachmentIndex, 1, localAttachment);
          return attachments;
        }
      }
      return stateAttachments;
    };
    this.updateAttachment = (attachmentToUpdate) => {
      const updatedAttachments = this.prepareAttachmentUpdate(attachmentToUpdate);
      if (updatedAttachments && updatedAttachments !== this.attachments) {
        this.state.partialNext({ attachments: updatedAttachments });
      }
    };
    this.upsertAttachments = (attachmentsToUpsert) => {
      if (!attachmentsToUpsert.length) return;
      let attachments = [...this.attachments];
      let hasUpdates = false;
      attachmentsToUpsert.forEach((attachment) => {
        const updatedAttachments = this.prepareAttachmentUpdate(attachment);
        if (updatedAttachments === null) {
          const localAttachment = ensureIsLocalAttachment(attachment);
          if (localAttachment) {
            attachments.push(localAttachment);
            hasUpdates = true;
          }
        } else if (updatedAttachments !== this.attachments) {
          attachments = updatedAttachments;
          hasUpdates = true;
        }
      });
      if (hasUpdates) {
        this.state.partialNext({ attachments });
      }
    };
    /**
     * Releases a local preview URL the SDK minted, if it minted one.
     *
     * Only `blob:` URLs are ours to revoke: `toLocalUploadAttachment` puts a
     * `URL.createObjectURL` result in `previewUri` for a `File`, but for a React Native
     * `FileReference` it stores the caller's own native file URI, which must be left alone.
     */
    this.revokePreviewUri = (attachment) => {
      const previewUri = attachment.localMetadata?.previewUri;
      if (previewUri?.startsWith("blob:")) URL.revokeObjectURL?.(previewUri);
    };
    this.removeAttachments = (localAttachmentIds) => {
      if (!localAttachmentIds.length) return;
      const removedAttachments = this.attachments.filter(
        (attachment) => localAttachmentIds.includes(attachment.localMetadata?.id)
      );
      this.state.partialNext({
        attachments: this.attachments.filter(
          (attachment) => !localAttachmentIds.includes(attachment.localMetadata?.id)
        )
      });
      for (const id of localAttachmentIds) {
        this.client.uploadManager.deleteUploadRecord(id);
      }
      removedAttachments.forEach(this.revokePreviewUri);
    };
    this.getUploadConfigCheck = async (fileLike) => {
      const client = this.channel.getClient();
      let appSettings;
      if (!client.appSettingsPromise) {
        appSettings = await client.getAppSettings();
      } else {
        appSettings = await client.appSettingsPromise;
      }
      const uploadConfig = isImageFile(fileLike) ? appSettings?.app?.image_upload_config : appSettings?.app?.file_upload_config;
      if (!uploadConfig) return { uploadBlocked: false };
      const {
        allowed_file_extensions,
        allowed_mime_types,
        blocked_file_extensions,
        blocked_mime_types,
        size_limit
      } = uploadConfig;
      const sizeLimit = size_limit || DEFAULT_UPLOAD_SIZE_LIMIT_BYTES;
      const mimeType = fileLike.type;
      if (isFile(fileLike) || isFileReference(fileLike)) {
        if (allowed_file_extensions?.length && !allowed_file_extensions.some(
          (ext) => fileLike.name.toLowerCase().endsWith(ext.toLowerCase())
        )) {
          return { uploadBlocked: true, reason: "allowed_file_extensions" };
        }
        if (blocked_file_extensions?.length && blocked_file_extensions.some(
          (ext) => fileLike.name.toLowerCase().endsWith(ext.toLowerCase())
        )) {
          return { uploadBlocked: true, reason: "blocked_file_extensions" };
        }
      }
      if (allowed_mime_types?.length && !allowed_mime_types.some((type) => type.toLowerCase() === mimeType?.toLowerCase())) {
        return { uploadBlocked: true, reason: "allowed_mime_types" };
      }
      if (blocked_mime_types?.length && blocked_mime_types.some((type) => type.toLowerCase() === mimeType?.toLowerCase())) {
        return { uploadBlocked: true, reason: "blocked_mime_types" };
      }
      if (fileLike.size && fileLike.size > sizeLimit) {
        return { uploadBlocked: true, reason: "size_limit" };
      }
      return { uploadBlocked: false };
    };
    // @deprecated use AttachmentManager.toLocalUploadAttachment(file)
    this.fileToLocalUploadAttachment = async (fileLike) => {
      const localAttachment = _AttachmentManager.toLocalUploadAttachment(fileLike);
      const uploadPermissionCheck = await this.getUploadConfigCheck(
        localAttachment.localMetadata.file
      );
      localAttachment.localMetadata.uploadPermissionCheck = uploadPermissionCheck;
      localAttachment.localMetadata.uploadState = uploadPermissionCheck.uploadBlocked ? "blocked" : "pending";
      return localAttachment;
    };
    this.ensureLocalUploadAttachment = async (attachment) => {
      if (!attachment.localMetadata?.file) {
        this.client.notifications.addError({
          message: "File is required for upload attachment",
          origin: { emitter: "AttachmentManager", context: { attachment } },
          options: { type: "validation:attachment:file:missing" }
        });
        return;
      }
      if (!attachment.localMetadata.id) {
        this.client.notifications.addError({
          message: "Local upload attachment missing local id",
          origin: { emitter: "AttachmentManager", context: { attachment } },
          options: { type: "validation:attachment:id:missing" }
        });
        return;
      }
      if (!this.fileUploadFilter(attachment)) return;
      const newAttachment = await this.fileToLocalUploadAttachment(
        attachment.localMetadata.file
      );
      if (attachment.localMetadata.id) {
        newAttachment.localMetadata.id = attachment.localMetadata.id;
      }
      return newAttachment;
    };
    /**
     * Method to perform the default upload behavior without checking for custom upload functions
     * to prevent recursive calls
     */
    this.doDefaultUploadRequest = async (fileLike, options) => {
      const progressHandler = options?.onProgress ? (progressEvent) => {
        const percent = progressEvent.lengthComputable && progressEvent.total ? Math.round(progressEvent.loaded * 100 / progressEvent.total) : void 0;
        options.onProgress?.(percent);
      } : void 0;
      const axiosUploadConfig = progressHandler || options?.abortSignal ? {
        ...progressHandler ? { onUploadProgress: progressHandler } : {},
        ...options?.abortSignal ? { signal: options.abortSignal } : {}
      } : void 0;
      if (isFileReference(fileLike)) {
        return this.channel[isImageFile(fileLike) ? "sendImage" : "sendFile"](
          fileLike.uri,
          fileLike.name,
          fileLike.type,
          void 0,
          axiosUploadConfig
        );
      }
      const file = isFile(fileLike) ? fileLike : createFileFromBlobs({
        blobsArray: [fileLike],
        fileName: generateFileName(fileLike.type),
        mimeType: fileLike.type
      });
      const { duration, ...result } = await this.channel[isImageFile(fileLike) ? "sendImage" : "sendFile"](file, void 0, void 0, void 0, axiosUploadConfig);
      return result;
    };
    /**
     * todo: docs how to customize the image and file upload by overriding do
     */
    this.doUploadRequest = async (fileLike, options) => {
      const customUploadFn = this.config.doUploadRequest;
      if (customUploadFn) {
        return await customUploadFn(fileLike, options);
      }
      return this.doDefaultUploadRequest(fileLike, options);
    };
    // @deprecated use attachmentManager.uploadFile(file)
    this.uploadAttachment = async (attachment) => {
      if (!this.isUploadEnabled) return;
      const localAttachment = await this.ensureLocalUploadAttachment(attachment);
      if (typeof localAttachment === "undefined") return;
      if (localAttachment.localMetadata.uploadState === "blocked") {
        this.upsertAttachments([localAttachment]);
        this.client.notifications.addError({
          message: `The attachment upload was blocked`,
          origin: {
            emitter: "AttachmentManager",
            context: { attachment, blockedAttachment: localAttachment }
          },
          options: {
            type: "validation:attachment:upload:blocked",
            metadata: {
              reason: localAttachment.localMetadata.uploadPermissionCheck?.reason
            }
          }
        });
        return localAttachment;
      }
      let response;
      try {
        response = await this.upload(attachment);
      } catch (error) {
        const reason = error instanceof Error ? error.message : "unknown error";
        const failedAttachment = {
          ...attachment,
          localMetadata: {
            ...attachment.localMetadata,
            uploadState: "failed",
            uploadProgress: void 0,
            // `false`, not `undefined`: these updates go through `updateAttachment`, which merges
            // via `mergeWith` — and that skips `undefined` source values, so `undefined` would
            // leave a stale `true` in place.
            uploadConfirmationPending: false
          }
        };
        if (!isUploadCancellation(error)) {
          this.client.notifications.addError({
            message: "Error uploading attachment",
            origin: {
              emitter: "AttachmentManager",
              context: { attachment, failedAttachment }
            },
            options: {
              type: "api:attachment:upload:failed",
              metadata: { reason },
              originalError: error instanceof Error ? error : void 0
            }
          });
        }
        this.updateAttachment(failedAttachment);
        return failedAttachment;
      }
      if (!response) {
        this.removeAttachments([attachment.localMetadata.id]);
        return;
      }
      const uploadedAttachment = {
        ...attachment,
        localMetadata: {
          ...attachment.localMetadata,
          uploadState: "finished",
          uploadProgress: void 0,
          // See the note in the failure branch: `undefined` cannot clear through `mergeWith`.
          uploadConfirmationPending: false
        }
      };
      const previewUri = uploadedAttachment.localMetadata.previewUri;
      if (previewUri) {
        if (previewUri.startsWith("blob:")) URL.revokeObjectURL(previewUri);
        delete uploadedAttachment.localMetadata.previewUri;
      }
      if (isLocalImageAttachment(uploadedAttachment)) {
        uploadedAttachment.image_url = response.file;
      } else {
        uploadedAttachment.asset_url = response.file;
      }
      if (response.thumb_url) {
        uploadedAttachment.thumb_url = response.thumb_url;
      }
      this.updateAttachment(uploadedAttachment);
      return uploadedAttachment;
    };
    this.uploadFile = async (file) => {
      const preUpload = await this.preUploadMiddlewareExecutor.execute({
        eventName: "prepare",
        initialValue: {
          attachment: _AttachmentManager.toLocalUploadAttachment(file)
        },
        mode: "concurrent"
      });
      let attachment = preUpload.state.attachment;
      if (preUpload.status === "discard") return attachment;
      if (!this.fileUploadFilter(attachment)) return attachment;
      if (attachment.localMetadata.uploadState === "blocked") {
        this.upsertAttachments([attachment]);
        return preUpload.state.attachment;
      }
      let response;
      let error;
      try {
        response = await this.upload(attachment);
      } catch (err) {
        error = err instanceof Error ? err : void 0;
      }
      const postUpload = await this.postUploadMiddlewareExecutor.execute({
        eventName: "postProcess",
        initialValue: {
          attachment: {
            ...attachment,
            localMetadata: {
              ...attachment.localMetadata,
              uploadState: error ? "failed" : "finished",
              uploadProgress: void 0,
              // See the note in `uploadAttachment`: `undefined` cannot clear through `mergeWith`.
              uploadConfirmationPending: false
            }
          },
          error,
          response
        },
        mode: "concurrent"
      });
      attachment = postUpload.state.attachment;
      if (postUpload.status === "discard") {
        this.removeAttachments([attachment.localMetadata.id]);
        return attachment;
      }
      this.updateAttachment(attachment);
      return attachment;
    };
    this.uploadFiles = async (files) => {
      if (this.hasCustomDoUploadRequest && !this.hasAvailableUploadSlots || !this.hasCustomDoUploadRequest && !this.isUploadEnabled)
        return;
      const iterableFiles = isFileList(files) ? Array.from(files) : files;
      return await Promise.all(
        iterableFiles.slice(0, this.availableUploadSlots).map(this.uploadFile)
      );
    };
    this.composer = composer;
    this.state = new StateStore(initState2({ message }));
    this.attachmentsByIdGetterCache = { attachmentsById: {}, attachments: [] };
    this.preUploadMiddlewareExecutor = new AttachmentPreUploadMiddlewareExecutor({
      composer
    });
    this.postUploadMiddlewareExecutor = new AttachmentPostUploadMiddlewareExecutor({
      composer
    });
  }
  get attachmentsById() {
    const { attachments } = this.state.getLatestValue();
    if (attachments !== this.attachmentsByIdGetterCache.attachments) {
      this.attachmentsByIdGetterCache.attachments = attachments;
      this.attachmentsByIdGetterCache.attachmentsById = attachments.reduce((newAttachmentsById, attachment) => {
        var _a;
        if (!attachment.localMetadata.id) return newAttachmentsById;
        newAttachmentsById[_a = attachment.localMetadata.id] ?? (newAttachmentsById[_a] = attachment);
        return newAttachmentsById;
      }, {});
    }
    return this.attachmentsByIdGetterCache.attachmentsById;
  }
  get client() {
    return this.composer.client;
  }
  get channel() {
    return this.composer.channel;
  }
  get config() {
    return this.composer.config.attachments;
  }
  get acceptedFiles() {
    return this.config.acceptedFiles;
  }
  set acceptedFiles(acceptedFiles) {
    this.composer.updateConfig({ attachments: { acceptedFiles } });
  }
  /*
  @deprecated attachments can be filtered using injecting pre-upload middleware
   */
  get fileUploadFilter() {
    return this.config.fileUploadFilter;
  }
  /*
  @deprecated attachments can be filtered using injecting pre-upload middleware
   */
  set fileUploadFilter(fileUploadFilter) {
    this.composer.updateConfig({ attachments: { fileUploadFilter } });
  }
  get maxNumberOfFilesPerMessage() {
    return this.config.maxNumberOfFilesPerMessage;
  }
  set maxNumberOfFilesPerMessage(maxNumberOfFilesPerMessage) {
    if (maxNumberOfFilesPerMessage === this.maxNumberOfFilesPerMessage) return;
    this.composer.updateConfig({ attachments: { maxNumberOfFilesPerMessage } });
  }
  get attachments() {
    return this.state.getLatestValue().attachments;
  }
  get hasUploadPermission() {
    return !!this.channel.data?.own_capabilities?.includes("upload-file");
  }
  get hasCustomDoUploadRequest() {
    return typeof this.config.doUploadRequest === "function";
  }
  get hasAvailableUploadSlots() {
    return this.availableUploadSlots > 0;
  }
  get isUploadEnabled() {
    return this.hasUploadPermission && this.hasAvailableUploadSlots;
  }
  get successfulUploads() {
    return this.getUploadsByState("finished");
  }
  get successfulUploadsCount() {
    return this.successfulUploads.length;
  }
  get uploadsInProgressCount() {
    return this.getUploadsByState("uploading").length;
  }
  get failedUploadsCount() {
    return this.getUploadsByState("failed").length;
  }
  get blockedUploadsCount() {
    return this.getUploadsByState("blocked").length;
  }
  get pendingUploadsCount() {
    return this.getUploadsByState("pending").length;
  }
  get availableUploadSlots() {
    return this.config.maxNumberOfFilesPerMessage - this.successfulUploadsCount - this.uploadsInProgressCount;
  }
  getUploadsByState(state) {
    return Object.values(this.attachments).filter(
      ({ localMetadata }) => localMetadata.uploadState === state
    );
  }
  upload(attachment) {
    const localId = attachment.localMetadata.id;
    this.upsertAttachments([
      {
        ...attachment,
        localMetadata: {
          ...attachment.localMetadata,
          uploadState: "uploading",
          uploadProgress: this.config.trackUploadProgress ? 0 : void 0,
          uploadConfirmationPending: false
        }
      }
    ]);
    const unsubscribe = this.client.uploadManager.state.subscribeWithSelector(
      (s) => ({ upload: s.uploads[localId] }),
      ({ upload: nextUpload }) => {
        if (!nextUpload) return;
        this.updateAttachment({
          ...attachment,
          localMetadata: {
            ...attachment.localMetadata,
            uploadState: "uploading",
            uploadProgress: nextUpload.uploadProgress,
            uploadConfirmationPending: nextUpload.uploadConfirmationPending
          }
        });
      }
    );
    return this.client.uploadManager.upload({
      id: localId,
      channelCid: this.channel.cid,
      file: attachment.localMetadata.file
    }).finally(() => {
      unsubscribe();
    });
  }
};
_AttachmentManager.toLocalUploadAttachment = (fileLike) => {
  const file = isFileReference(fileLike) || isFile(fileLike) ? fileLike : createFileFromBlobs({
    blobsArray: [fileLike],
    fileName: generateFileName(fileLike.type),
    mimeType: fileLike.type
  });
  const localAttachment = {
    file_size: file.size,
    mime_type: file.type,
    localMetadata: {
      file,
      id: generateUUIDv4(),
      uploadState: "pending"
    },
    type: getAttachmentTypeFromMimeType(file.type)
  };
  localAttachment[isImageFile(file) ? "fallback" : "title"] = file.name;
  localAttachment.localMetadata.previewUri = isFileReference(fileLike) ? fileLike.uri : URL.createObjectURL?.(fileLike);
  if (isFileReference(fileLike) && fileLike.height && fileLike.width && isImageFile(file)) {
    localAttachment.original_height = fileLike.height;
    localAttachment.original_width = fileLike.width;
  }
  if (isFileReference(fileLike) && fileLike.thumb_url) {
    localAttachment.thumb_url = fileLike.thumb_url;
  }
  if (isFileReference(fileLike) && fileLike.duration) {
    localAttachment.duration = fileLike.duration;
  }
  return localAttachment;
};
var AttachmentManager = _AttachmentManager;

// src/messageComposer/configuration/configuration.ts
var import_linkifyjs = require("linkifyjs");

// src/messageComposer/middleware/messageComposer/attachments.ts
var localAttachmentToAttachment = (localAttachment) => {
  const { localMetadata, ...attachment } = localAttachment;
  return attachment;
};
var composeWithPendingUploads = ({
  composer,
  state
}) => {
  const composerIsKeptAsDraft = !!composer.pollId;
  const relevantAttachments = composer.attachmentManager.attachments.filter(
    (attachment) => isFinishedUpload(attachment) || !composerIsKeptAsDraft && isPendingUpload(attachment)
  );
  const localAttachments = (state.localMessage.attachments ?? []).concat(
    relevantAttachments.map(
      (attachment) => isPendingUpload(attachment) ? attachment : localAttachmentToAttachment(attachment)
    )
  );
  const messageAttachments = (state.message.attachments ?? []).concat(
    relevantAttachments.filter(isFinishedUpload).map(localAttachmentToAttachment)
  );
  if (!localAttachments.length && !messageAttachments.length) return state;
  return {
    ...state,
    localMessage: localAttachments.length ? {
      ...state.localMessage,
      attachments: localAttachments
    } : state.localMessage,
    message: messageAttachments.length ? {
      ...state.message,
      attachments: messageAttachments
    } : state.message
  };
};
var createSendWithPendingUploadsAttachmentsMiddleware = (composer) => ({
  allowsPendingUploads: true,
  id: "stream-io/message-composer-middleware/attachments",
  handlers: {
    compose: ({
      state,
      next,
      forward
    }) => {
      const { attachmentManager } = composer;
      if (!attachmentManager) return forward();
      return next(composeWithPendingUploads({ composer, state }));
    }
  }
});
var createAttachmentsCompositionMiddleware = (composer) => ({
  id: "stream-io/message-composer-middleware/attachments",
  handlers: {
    compose: ({
      state,
      next,
      discard,
      forward
    }) => {
      const { attachmentManager } = composer;
      if (!attachmentManager) return forward();
      if (attachmentManager.uploadsInProgressCount > 0) {
        composer.client.notifications.addWarning({
          message: "Wait until all attachments have uploaded",
          origin: {
            emitter: "MessageComposer",
            context: { composer }
          },
          options: {
            type: "validation:attachment:upload:in-progress"
          }
        });
        return discard();
      }
      const attachments = (state.message.attachments ?? []).concat(
        attachmentManager.successfulUploads.map(localAttachmentToAttachment)
      );
      if (!attachments.length) return forward();
      return next({
        ...state,
        localMessage: {
          ...state.localMessage,
          attachments
        },
        message: {
          ...state.message,
          attachments
        }
      });
    }
  }
});
var createDraftAttachmentsCompositionMiddleware = (composer) => ({
  id: "stream-io/message-composer-middleware/draft-attachments",
  handlers: {
    compose: ({
      state,
      next,
      forward
    }) => {
      const { attachmentManager } = composer;
      if (!attachmentManager) return forward();
      const successfulUploads = attachmentManager.successfulUploads;
      const attachments = successfulUploads.length ? (state.draft.attachments ?? []).concat(
        successfulUploads.map(localAttachmentToAttachment)
      ) : void 0;
      return next({
        ...state,
        draft: {
          ...state.draft,
          attachments
        }
      });
    }
  }
});

// src/messageComposer/middleware/messageComposer/cleanData.ts
var createCompositionDataCleanupMiddleware = (composer) => ({
  id: "stream-io/message-composer-middleware/data-cleanup",
  handlers: {
    compose: ({
      state,
      next
    }) => {
      const messageType = !composer.editedMessage?.type || composer.editedMessage.type === "error" ? "regular" : composer.editedMessage.type;
      const common = {
        type: messageType
      };
      const editedMessagePayloadToBeSent = composer.editedMessage ? toUpdatedMessagePayload(composer.editedMessage) : void 0;
      return next({
        ...state,
        localMessage: formatMessage({
          ...composer.editedMessage,
          ...state.localMessage,
          ...common
        }),
        message: {
          ...editedMessagePayloadToBeSent,
          ...state.message,
          ...common
        },
        sendOptions: composer.editedMessage && state.sendOptions?.skip_enrich_url ? { skip_enrich_url: state.sendOptions?.skip_enrich_url } : state.sendOptions
      });
    }
  }
});

// src/messageComposer/middleware/messageComposer/customData.ts
var createCustomDataCompositionMiddleware = (composer) => ({
  id: "stream-io/message-composer-middleware/custom-data",
  handlers: {
    compose: ({
      state,
      next,
      forward
    }) => {
      const data = composer.customDataManager.customMessageData;
      if (!data) return forward();
      return next({
        ...state,
        localMessage: {
          ...state.localMessage,
          ...data
        },
        message: {
          ...state.message,
          ...data
        }
      });
    }
  }
});
var createDraftCustomDataCompositionMiddleware = (composer) => ({
  id: "stream-io/message-composer-middleware/draft-custom-data",
  handlers: {
    compose: ({
      state,
      next,
      forward
    }) => {
      const data = composer.customDataManager.customMessageData;
      if (!data) return forward();
      return next({
        ...state,
        draft: {
          ...state.draft,
          ...data
        }
      });
    }
  }
});

// src/messageComposer/middleware/textComposer/mentionUtils.ts
var isUserMentionEntity = (entity) => entity.mentionType === "user";
var userResponseToMentionEntity = (user) => ({
  ...user,
  mentionType: "user"
});
var userResponsesToMentionEntities = (users) => users.map(userResponseToMentionEntity);
var mentionEntityToUserResponse = (entity) => {
  const { mentionType, ...user } = entity;
  return user;
};
var userSuggestionToUserResponse = (suggestion) => {
  const { mentionType, tokenizedDisplayName, ...userResponse } = suggestion;
  void mentionType;
  void tokenizedDisplayName;
  return userResponse;
};
var userSuggestionToMentionEntity = (suggestion) => userResponseToMentionEntity(userSuggestionToUserResponse(suggestion));

// src/messageComposer/textComposer.ts
var textIsEmpty = (text) => {
  const trimmedText = text.trim();
  return trimmedText === "" || trimmedText === ">" || trimmedText === "``````" || trimmedText === "``" || trimmedText === "**" || trimmedText === "____" || trimmedText === "__" || trimmedText === "****";
};
var getInitialMentions = (message) => {
  if (!message) return [];
  const mentions = (message.mentioned_users ?? []).map(
    (item) => typeof item === "string" ? { id: item, mentionType: "user" } : { ...item, mentionType: "user" }
  );
  if (message.mentioned_channel) {
    mentions.push({
      id: "channel",
      mentionType: "channel",
      name: "channel"
    });
  }
  if (message.mentioned_here) {
    mentions.push({
      id: "here",
      mentionType: "here",
      name: "here"
    });
  }
  if (message.mentioned_roles?.length) {
    mentions.push(
      ...message.mentioned_roles.map((role) => ({
        id: role,
        mentionType: "role",
        name: role
      }))
    );
  }
  if (message.mentioned_groups?.length) {
    mentions.push(
      ...message.mentioned_groups.map((group) => ({
        id: group.id,
        mentionType: "user_group",
        name: group.name
      }))
    );
  } else if (message.mentioned_group_ids?.length) {
    mentions.push(
      ...message.mentioned_group_ids.map((groupId) => ({
        id: groupId,
        mentionType: "user_group"
      }))
    );
  }
  return mentions;
};
var isSameMentionEntity = (first, second) => first.id === second.id && first.mentionType === second.mentionType;
var getMentionsFromState = (state) => state.mentions ?? state.mentionedUsers.map(userResponseToMentionEntity);
var initState3 = ({
  composer,
  message
}) => {
  if (!message) {
    const text2 = composer.config.text.defaultValue ?? "";
    return {
      command: null,
      mentionedUsers: [],
      mentions: [],
      text: text2,
      selection: { start: text2.length, end: text2.length }
    };
  }
  const text = message.text ?? "";
  const mentions = getInitialMentions(message);
  const mentionedUsers = (message.mentioned_users ?? []).map(
    (item) => typeof item === "string" ? { id: item } : item
  );
  return {
    mentionedUsers,
    mentions,
    text,
    selection: { start: text.length, end: text.length }
  };
};
var TextComposer = class {
  constructor({ composer, message }) {
    this.initState = ({ message } = {}) => {
      this.state.next(initState3({ composer: this.composer, message }));
    };
    this.getSnapshot = (state = this.state.getLatestValue()) => state;
    this.restoreSnapshot = (snapshot) => {
      this.state.next(snapshot);
    };
    /**
     * @deprecated Use `upsertMentionEntity({ ...user, mentionType: 'user' })` instead.
     */
    this.upsertMentionedUser = (user) => {
      const mentionedUsers = [...this.mentionedUsers];
      const existingUserIndex = mentionedUsers.findIndex((entity) => entity.id === user.id);
      if (existingUserIndex >= 0) {
        mentionedUsers.splice(existingUserIndex, 1, user);
        this.setMentionedUsers(mentionedUsers);
      } else {
        mentionedUsers.push(user);
        this.setMentionedUsers(mentionedUsers);
      }
    };
    /**
     * @deprecated Use `getMentionEntity('user', userId)` instead.
     */
    this.getMentionedUser = (userId) => this.mentionedUsers.find((user) => user.id === userId);
    /**
     * @deprecated Use `removeMentionEntity('user', userId)` instead.
     */
    this.removeMentionedUser = (userId) => {
      const existingUserIndex = this.mentionedUsers.findIndex(
        (entity) => entity.id === userId
      );
      if (existingUserIndex === -1) return;
      const mentionedUsers = [...this.mentionedUsers];
      mentionedUsers.splice(existingUserIndex, 1);
      this.setMentionedUsers(mentionedUsers);
    };
    this.upsertMentionEntity = (entity) => {
      const mentions = [...this.mentions];
      const existingEntityIndex = mentions.findIndex(
        (currentEntity) => isSameMentionEntity(currentEntity, entity)
      );
      if (existingEntityIndex >= 0) {
        mentions.splice(existingEntityIndex, 1, entity);
      } else {
        mentions.push(entity);
      }
      this.setMentions(mentions);
    };
    this.getMentionEntity = (mentionType, id) => this.mentions.find(
      (entity) => entity.mentionType === mentionType && entity.id === id
    );
    this.removeMentionEntity = (mentionType, id) => {
      const existingEntityIndex = this.mentions.findIndex(
        (entity) => entity.mentionType === mentionType && entity.id === id
      );
      if (existingEntityIndex === -1) return;
      const mentions = [...this.mentions];
      mentions.splice(existingEntityIndex, 1);
      this.setMentions(mentions);
    };
    this.setCommand = (command) => {
      if (!command) {
        this.clearCommand();
        return;
      }
      if (command.name === this.command?.name) return;
      if (this.composer.isCommandDisabled(command)) return;
      const stateToRestore = {
        command: null
      };
      this.commitState({
        ...this.state.getLatestValue(),
        command,
        effects: [
          {
            command,
            stateToRestore,
            type: "command.activate"
          }
        ],
        suggestions: void 0
      });
    };
    this.setText = (text) => {
      if (!this.enabled || text === this.text) return;
      this.state.partialNext({ text });
    };
    this.setSelection = (selection) => {
      const selectionChanged = selection.start !== this.selection.start || selection.end !== this.selection.end;
      if (!this.enabled || !selectionChanged) return;
      this.state.partialNext({ selection });
    };
    this.insertText = async ({
      text,
      selection
    }) => {
      if (!this.enabled) return;
      const normalizedText = text.replace(/\r\n/g, "\n");
      const finalSelection = selection ?? this.selection;
      const { maxLengthOnEdit } = this.composer.config.text ?? {};
      const currentText = this.text;
      const textBeforeTrim = [
        currentText.slice(0, finalSelection.start),
        normalizedText,
        currentText.slice(finalSelection.end)
      ].join("");
      const finalText = textBeforeTrim.slice(
        0,
        typeof maxLengthOnEdit === "number" ? maxLengthOnEdit : textBeforeTrim.length
      );
      const expectedCursorPosition = finalSelection.start + normalizedText.length;
      const cursorPosition = Math.min(expectedCursorPosition, finalText.length);
      await this.handleChange({
        text: finalText,
        selection: {
          start: cursorPosition,
          end: cursorPosition
        }
      });
    };
    this.wrapSelection = ({
      head = "",
      selection,
      tail = ""
    }) => {
      if (!this.enabled) return;
      const currentSelection = selection ?? this.selection;
      const prependedText = this.text.slice(0, currentSelection.start);
      const selectedText = this.text.slice(currentSelection.start, currentSelection.end);
      const appendedText = this.text.slice(currentSelection.end);
      const finalSelection = {
        start: prependedText.length + head.length,
        end: prependedText.length + head.length + selectedText.length
      };
      this.state.partialNext({
        text: [prependedText, head, selectedText, tail, appendedText].join(""),
        selection: finalSelection
      });
    };
    this.setSuggestions = (suggestions) => {
      this.state.partialNext({ suggestions });
    };
    this.closeSuggestions = () => {
      const { suggestions } = this.state.getLatestValue();
      if (!suggestions) return;
      this.state.partialNext({ suggestions: void 0 });
    };
    // --- END STATE API ---
    this.commitState = (state) => {
      const { change, effects, ...nextState } = state;
      void change;
      this.state.next(nextState);
      this.composer.applyEffects(effects);
    };
    // --- START TEXT PROCESSING ----
    this.handleChange = async ({
      text,
      selection
    }) => {
      if (!this.enabled) return;
      const output = await this.middlewareExecutor.execute({
        eventName: "onChange",
        initialValue: {
          ...this.state.getLatestValue(),
          text,
          selection
        }
      });
      if (output.status === "discard") return;
      this.commitState(output.state);
      if (this.config.publishTypingEvents && text) {
        logChatPromiseExecution(
          this.channel.keystroke(this.composer.threadId ?? void 0),
          "start typing event"
        );
      }
    };
    // todo: document how to register own middleware handler to simulate onSelectUser prop
    this.handleSelect = async (target) => {
      if (!this.enabled) return;
      const output = await this.middlewareExecutor.execute({
        eventName: "onSuggestionItemSelect",
        initialValue: {
          ...this.state.getLatestValue(),
          change: {
            selectedSuggestion: target
          }
        }
      });
      if (output?.status === "discard") return;
      this.commitState(output.state);
    };
    this.composer = composer;
    this.state = new StateStore(initState3({ composer, message }));
    this.middlewareExecutor = new TextComposerMiddlewareExecutor({ composer });
  }
  get channel() {
    return this.composer.channel;
  }
  get config() {
    return this.composer.config.text;
  }
  get enabled() {
    return this.composer.config.text.enabled;
  }
  set enabled(enabled) {
    if (enabled === this.enabled) return;
    this.composer.updateConfig({ text: { enabled } });
  }
  get defaultValue() {
    return this.composer.config.text.defaultValue;
  }
  set defaultValue(defaultValue) {
    if (defaultValue === this.defaultValue) return;
    this.composer.updateConfig({ text: { defaultValue } });
  }
  get maxLengthOnEdit() {
    return this.composer.config.text.maxLengthOnEdit;
  }
  set maxLengthOnEdit(maxLengthOnEdit) {
    if (maxLengthOnEdit === this.maxLengthOnEdit) return;
    this.composer.updateConfig({ text: { maxLengthOnEdit } });
  }
  get maxLengthOnSend() {
    return this.composer.config.text.maxLengthOnSend;
  }
  set maxLengthOnSend(maxLengthOnSend) {
    if (maxLengthOnSend === this.maxLengthOnSend) return;
    this.composer.updateConfig({ text: { maxLengthOnSend } });
  }
  get publishTypingEvents() {
    return this.composer.config.text.publishTypingEvents;
  }
  set publishTypingEvents(publishTypingEvents) {
    if (publishTypingEvents === this.publishTypingEvents) return;
    this.composer.updateConfig({ text: { publishTypingEvents } });
  }
  // --- START STATE API ---
  get command() {
    return this.state.getLatestValue().command;
  }
  get mentionedUsers() {
    return this.state.getLatestValue().mentionedUsers;
  }
  get mentions() {
    return getMentionsFromState(this.state.getLatestValue());
  }
  get selection() {
    return this.state.getLatestValue().selection;
  }
  get suggestions() {
    return this.state.getLatestValue().suggestions;
  }
  get text() {
    return this.state.getLatestValue().text;
  }
  get textIsEmpty() {
    return textIsEmpty(this.text);
  }
  setMentionedUsers(users) {
    const nonUserMentions = this.mentions.filter(
      (entity) => !isUserMentionEntity(entity)
    );
    this.state.partialNext({
      mentionedUsers: users,
      mentions: [...nonUserMentions, ...users.map(userResponseToMentionEntity)]
    });
  }
  setMentions(entities) {
    this.state.partialNext({
      mentionedUsers: entities.filter(isUserMentionEntity).map(mentionEntityToUserResponse),
      mentions: entities
    });
  }
  clearCommand() {
    if (!this.command) return;
    this.commitState({
      ...this.state.getLatestValue(),
      command: null,
      effects: [{ type: "command.clear" }]
    });
  }
  // --- END TEXT PROCESSING ----
};

// src/errors.ts
var APIErrorCodes = {
  "-1": { name: "InternalSystemError", retryable: true },
  "2": { name: "AccessKeyError", retryable: false },
  "3": { name: "AuthenticationFailedError", retryable: true },
  "4": { name: "InputError", retryable: false },
  "6": { name: "DuplicateUsernameError", retryable: false },
  "9": { name: "RateLimitError", retryable: true },
  "16": { name: "DoesNotExistError", retryable: false },
  "17": { name: "NotAllowedError", retryable: false },
  "18": { name: "EventNotSupportedError", retryable: false },
  "19": { name: "ChannelFeatureNotSupportedError", retryable: false },
  "20": { name: "MessageTooLongError", retryable: false },
  "21": { name: "MultipleNestingLevelError", retryable: false },
  "22": { name: "PayloadTooBigError", retryable: false },
  "23": { name: "RequestTimeoutError", retryable: true },
  "24": { name: "MaxHeaderSizeExceededError", retryable: false },
  "40": { name: "AuthErrorTokenExpired", retryable: false },
  "41": { name: "AuthErrorTokenNotValidYet", retryable: false },
  "42": { name: "AuthErrorTokenUsedBeforeIssuedAt", retryable: false },
  "43": { name: "AuthErrorTokenSignatureInvalid", retryable: false },
  "44": { name: "CustomCommandEndpointMissingError", retryable: false },
  "45": { name: "CustomCommandEndpointCallError", retryable: true },
  "46": { name: "ConnectionIDNotFoundError", retryable: false },
  "60": { name: "CoolDownError", retryable: true },
  "69": { name: "ErrWrongRegion", retryable: false },
  "70": { name: "ErrQueryChannelPermissions", retryable: false },
  "71": { name: "ErrTooManyConnections", retryable: true },
  "99": { name: "AppSuspendedError", retryable: false }
};
function isAPIError(error) {
  return error.code !== void 0;
}
function isErrorRetryable(error) {
  if (!error.code) return false;
  const err = APIErrorCodes[`${error.code}`];
  if (!err) return false;
  return err.retryable;
}
function isConnectionIDError(error) {
  return error.code === 46;
}
function isWSFailure(err) {
  if (typeof err.isWSFailure === "boolean") {
    return err.isWSFailure;
  }
  try {
    return JSON.parse(err.message).isWSFailure;
  } catch (_) {
    return false;
  }
}
function isErrorResponse(res) {
  return !res.status || res.status < 200 || 300 <= res.status;
}

// src/search/BaseSearchSource.ts
var DEFAULT_SEARCH_SOURCE_OPTIONS = {
  pageSize: 10,
  allowEmptySearchString: false,
  resetOnNewSearchQuery: true
};
var resolveDebounceOptions = ({
  debounceMs,
  shortQueryDebounceMs,
  longQueryDebounceMs,
  shortQueryMaxLength
}) => ({
  shortQueryDebounceMs: shortQueryDebounceMs ?? debounceMs ?? 500,
  longQueryDebounceMs: longQueryDebounceMs ?? debounceMs ?? 300,
  shortQueryMaxLength: shortQueryMaxLength ?? 2
});
var BaseSearchSourceBase = class {
  constructor(options) {
    this.setDebounceOptions = (options = {}) => {
      const resolved = resolveDebounceOptions(options);
      this.shortQueryDebounceMs = resolved.shortQueryDebounceMs;
      this.longQueryDebounceMs = resolved.longQueryDebounceMs;
      this.shortQueryMaxLength = resolved.shortQueryMaxLength;
      this.searchDebounced = debounce(
        this.executeQuery.bind(this),
        (searchString) => this.getDebounceMs(searchString)
      );
    };
    /**
     * Short queries match too much and are slow server-side, so they get a longer debounce.
     * Resolved per call, which is what lets the interval switch as the user keeps typing.
     */
    this.getDebounceMs = (searchString) => {
      const { length } = searchString ?? this.searchQuery;
      return length <= this.shortQueryMaxLength ? this.shortQueryDebounceMs : this.longQueryDebounceMs;
    };
    this.search = (searchQuery) => this.searchDebounced(searchQuery);
    this.activate = () => {
      if (this.isActive) return;
      this.state.partialNext({ isActive: true });
    };
    this.deactivate = () => {
      if (!this.isActive) return;
      this.state.partialNext({ isActive: false });
    };
    this.canExecuteQuery = (newSearchString) => {
      const hasNewSearchQuery = typeof newSearchString !== "undefined";
      const searchString = newSearchString ?? this.searchQuery;
      return !!(this.isActive && this.canDispatchQuery(hasNewSearchQuery) && (this.allowEmptySearchString || searchString));
    };
    const { pageSize, allowEmptySearchString, resetOnNewSearchQuery } = {
      ...DEFAULT_SEARCH_SOURCE_OPTIONS,
      ...options
    };
    this.pageSize = pageSize;
    this.allowEmptySearchString = allowEmptySearchString;
    this.resetOnNewSearchQuery = resetOnNewSearchQuery;
    this.state = new StateStore(this.initialState);
    this.setDebounceOptions(options ?? {});
  }
  /**
   * A new search query preempts an in-flight one; pagination waits for it to finish
   * and needs a next page to fetch.
   */
  canDispatchQuery(hasNewSearchQuery) {
    return hasNewSearchQuery || !this.isLoading && this.hasNext;
  }
  get lastQueryError() {
    return this.state.getLatestValue().lastQueryError;
  }
  get hasNext() {
    return this.state.getLatestValue().hasNext;
  }
  get hasResults() {
    return Array.isArray(this.state.getLatestValue().items);
  }
  get isActive() {
    return this.state.getLatestValue().isActive;
  }
  get isLoading() {
    return this.state.getLatestValue().isLoading;
  }
  get initialState() {
    return {
      hasNext: true,
      isActive: false,
      isLoading: false,
      items: void 0,
      lastQueryError: void 0,
      next: void 0,
      offset: 0,
      searchQuery: ""
    };
  }
  get items() {
    return this.state.getLatestValue().items;
  }
  get next() {
    return this.state.getLatestValue().next;
  }
  get offset() {
    return this.state.getLatestValue().offset;
  }
  get searchQuery() {
    return this.state.getLatestValue().searchQuery;
  }
  getStateBeforeFirstQuery(newSearchString) {
    const initialState = this.initialState;
    const oldItems = this.items;
    const items = this.resetOnNewSearchQuery ? initialState.items : oldItems;
    return {
      ...initialState,
      items,
      isActive: this.isActive,
      isLoading: this.resetOnNewSearchQuery ? true : !oldItems,
      searchQuery: newSearchString
    };
  }
  getStateAfterQuery(stateUpdate, isFirstPage) {
    const current = this.state.getLatestValue();
    return {
      ...current,
      lastQueryError: void 0,
      // reset lastQueryError that can be overridden by the stateUpdate
      ...stateUpdate,
      isLoading: false,
      items: isFirstPage ? stateUpdate.items : [...this.items ?? [], ...stateUpdate.items || []]
    };
  }
  prepareStateForQuery(newSearchString) {
    const hasNewSearchQuery = typeof newSearchString !== "undefined";
    const searchString = newSearchString ?? this.searchQuery;
    if (hasNewSearchQuery) {
      this.state.next(this.getStateBeforeFirstQuery(newSearchString ?? ""));
    } else {
      this.state.partialNext({ isLoading: true });
    }
    return { searchString, hasNewSearchQuery };
  }
  updatePaginationStateFromQuery(result) {
    const { items, next } = result;
    const stateUpdate = {};
    if (Object.prototype.hasOwnProperty.call(result, "next")) {
      stateUpdate.next = next;
      stateUpdate.hasNext = !!next;
    } else {
      stateUpdate.offset = (this.offset ?? 0) + items.length;
      stateUpdate.hasNext = items.length === this.pageSize;
    }
    return stateUpdate;
  }
  resetState() {
    this.state.next(this.initialState);
  }
  resetStateAndActivate() {
    this.resetState();
    this.activate();
  }
};
var BaseSearchSource = class extends BaseSearchSourceBase {
  constructor() {
    super(...arguments);
    /** Aborts the in-flight request, if any, once a newer query is dispatched. */
    this.queryAbortController = null;
  }
  /** Aborts the in-flight request, if any. Returns true when one was aborted. */
  abortInFlightQuery() {
    if (!this.queryAbortController) return false;
    this.queryAbortController.abort();
    this.queryAbortController = null;
    return true;
  }
  resetState() {
    this.abortInFlightQuery();
    super.resetState();
  }
  async executeQuery(newSearchString) {
    if (!this.canExecuteQuery(newSearchString)) return;
    this.abortInFlightQuery();
    const { signal } = this.queryAbortController = new AbortController();
    const { hasNewSearchQuery, searchString } = this.prepareStateForQuery(newSearchString);
    let stateUpdate = {};
    try {
      const results = await this.query(searchString, { signal });
      if (!results) return;
      const { items } = results;
      stateUpdate = this.updatePaginationStateFromQuery(results);
      stateUpdate.items = await this.filterQueryResults(items);
    } catch (e) {
      stateUpdate.lastQueryError = e;
      if (isAPIError(e) && !isErrorRetryable(e)) {
        stateUpdate.hasNext = false;
      }
    } finally {
      if (!signal.aborted) {
        this.state.next(this.getStateAfterQuery(stateUpdate, hasNewSearchQuery));
      }
    }
  }
  cancelScheduledQuery() {
    this.searchDebounced.cancel();
    if (this.abortInFlightQuery() && this.isLoading) {
      this.state.partialNext({ isLoading: false });
    }
  }
};
var BaseSearchSourceSync = class extends BaseSearchSourceBase {
  executeQuery(newSearchString) {
    if (!this.canExecuteQuery(newSearchString)) return;
    const { hasNewSearchQuery, searchString } = this.prepareStateForQuery(newSearchString);
    let stateUpdate = {};
    try {
      const results = this.query(searchString);
      if (!results) return;
      const { items } = results;
      stateUpdate = this.updatePaginationStateFromQuery(results);
      stateUpdate.items = this.filterQueryResults(items);
    } catch (e) {
      stateUpdate.lastQueryError = e;
      if (isAPIError(e) && !isErrorRetryable(e)) {
        stateUpdate.hasNext = false;
      }
    } finally {
      this.state.next(this.getStateAfterQuery(stateUpdate, hasNewSearchQuery));
    }
  }
  cancelScheduledQuery() {
    this.searchDebounced.cancel();
  }
};

// src/pagination/BasePaginator.ts
var DEFAULT_PAGINATION_OPTIONS = {
  debounceMs: 300,
  pageSize: 10
};
var BasePaginator = class {
  constructor(options) {
    this._isCursorPagination = false;
    this.setDebounceOptions = ({ debounceMs }) => {
      this._executeQueryDebounced = debounce(this.executeQuery.bind(this), debounceMs);
    };
    this.canExecuteQuery = (direction) => !this.isLoading && direction === "next" && this.hasNext || direction === "prev" && this.hasPrev;
    this.next = () => this.executeQuery({ direction: "next" });
    this.prev = () => this.executeQuery({ direction: "prev" });
    this.nextDebounced = () => {
      this._executeQueryDebounced({ direction: "next" });
    };
    this.prevDebounced = () => {
      this._executeQueryDebounced({ direction: "prev" });
    };
    const { debounceMs, pageSize } = { ...DEFAULT_PAGINATION_OPTIONS, ...options };
    this.pageSize = pageSize;
    this.state = new StateStore(this.initialState);
    this.setDebounceOptions({ debounceMs });
  }
  get lastQueryError() {
    return this.state.getLatestValue().lastQueryError;
  }
  get hasNext() {
    return this.state.getLatestValue().hasNext;
  }
  get hasPrev() {
    return this.state.getLatestValue().hasPrev;
  }
  get hasResults() {
    return Array.isArray(this.state.getLatestValue().items);
  }
  get isLoading() {
    return this.state.getLatestValue().isLoading;
  }
  get initialState() {
    return {
      hasNext: true,
      hasPrev: true,
      //todo: check if optimistic value does not cause problems in UI
      isLoading: false,
      items: void 0,
      lastQueryError: void 0,
      cursor: void 0,
      offset: 0
    };
  }
  get items() {
    return this.state.getLatestValue().items;
  }
  get cursor() {
    return this.state.getLatestValue().cursor;
  }
  get offset() {
    return this.state.getLatestValue().offset;
  }
  getStateBeforeFirstQuery() {
    return {
      ...this.initialState,
      isLoading: true
    };
  }
  getStateAfterQuery(stateUpdate, isFirstPage) {
    const current = this.state.getLatestValue();
    return {
      ...current,
      lastQueryError: void 0,
      // reset lastQueryError that can be overridden by the stateUpdate
      ...stateUpdate,
      isLoading: false,
      items: isFirstPage ? stateUpdate.items : [...this.items ?? [], ...stateUpdate.items || []]
    };
  }
  async executeQuery({ direction }) {
    if (!this.canExecuteQuery(direction)) return;
    const isFirstPage = typeof this.items === "undefined";
    if (isFirstPage) {
      this.state.next(this.getStateBeforeFirstQuery());
    } else {
      this.state.partialNext({ isLoading: true });
    }
    const stateUpdate = {};
    try {
      const results = await this.query({ direction });
      if (!results) return;
      const { items, next, prev } = results;
      if (isFirstPage && (next || prev)) {
        this._isCursorPagination = true;
      }
      if (this._isCursorPagination) {
        stateUpdate.cursor = { next: next || null, prev: prev || null };
        stateUpdate.hasNext = !!next;
        stateUpdate.hasPrev = !!prev;
      } else {
        stateUpdate.offset = (this.offset ?? 0) + items.length;
        stateUpdate.hasNext = items.length === this.pageSize;
      }
      stateUpdate.items = await this.filterQueryResults(items);
    } catch (e) {
      stateUpdate.lastQueryError = e;
    } finally {
      this.state.next(this.getStateAfterQuery(stateUpdate, isFirstPage));
    }
  }
  cancelScheduledQuery() {
    this._executeQueryDebounced.cancel();
  }
  resetState() {
    this.state.next(this.initialState);
  }
};

// src/pagination/FilterBuilder.ts
var FilterBuilder = class {
  constructor(params) {
    this.context = new StateStore(params?.initialContext ?? {});
    this.filterConfig = new StateStore(
      params?.initialFilterConfig ?? {}
    );
  }
  updateFilterConfig(config) {
    this.filterConfig.partialNext(config);
  }
  enableFilter(filterKey) {
    const config = this.filterConfig.getLatestValue();
    if (config[filterKey]) {
      this.filterConfig.partialNext({
        [filterKey]: {
          ...config[filterKey],
          enabled: true
        }
      });
    }
  }
  disableFilter(filterKey) {
    const config = this.filterConfig.getLatestValue();
    if (config[filterKey]) {
      this.filterConfig.partialNext({
        [filterKey]: {
          ...config[filterKey],
          enabled: false
        }
      });
    }
  }
  updateContext(newContext) {
    this.context.partialNext(newContext);
  }
  buildFilters(params) {
    const filters = {
      ...params?.baseFilters ?? {}
    };
    const filterConfig = this.filterConfig.getLatestValue();
    for (const key in filterConfig) {
      const configItem = filterConfig[key];
      if (!configItem?.enabled) continue;
      const generated = configItem.generate({
        ...this.context.getLatestValue(),
        ...params?.context ?? {}
      });
      if (generated) Object.assign(filters, generated);
    }
    return filters;
  }
};

// src/pagination/ReminderPaginator.ts
var ReminderPaginator = class extends BasePaginator {
  constructor(client, options) {
    super(options);
    this.query = async ({
      direction
    }) => {
      const cursor = this.cursor?.[direction];
      const {
        reminders: items,
        next,
        prev
      } = await this.client.queryReminders({
        filter: this.filters,
        sort: this.sort,
        limit: this.pageSize,
        [direction]: cursor
      });
      return { items, next, prev };
    };
    this.filterQueryResults = (items) => items;
    this.client = client;
  }
  get filters() {
    return this._filters;
  }
  get sort() {
    return this._sort;
  }
  set filters(filters) {
    this._filters = filters;
    this.resetState();
  }
  set sort(sort) {
    this._sort = sort;
    this.resetState();
  }
};

// src/pagination/UserGroupPaginator.ts
var isRecord = (value) => typeof value === "object" && value !== null;
var decodeCursor = (cursor) => {
  if (!cursor) return void 0;
  try {
    const parsed = JSON.parse(cursor);
    return isRecord(parsed) ? parsed : void 0;
  } catch {
    return void 0;
  }
};
var UserGroupPaginator = class extends BasePaginator {
  constructor(client, options) {
    super(options);
    this.buildNextCursor = (items) => {
      if (items.length < this.pageSize) return void 0;
      const lastItem = items[items.length - 1];
      if (!lastItem) return void 0;
      return JSON.stringify({
        created_at_gt: lastItem.created_at,
        id_gt: lastItem.id
      });
    };
    this.query = async ({
      direction
    }) => {
      if (direction === "prev") {
        return { items: [] };
      }
      const cursor = decodeCursor(this.cursor?.next);
      const options = {
        limit: this.pageSize,
        ...this.teamId ? { team_id: this.teamId } : {},
        ...cursor?.id_gt ? { id_gt: cursor.id_gt } : {},
        ...cursor?.created_at_gt ? { created_at_gt: cursor.created_at_gt } : {}
      };
      const { user_groups: items } = await this.client.queryUserGroups(options);
      return { items, next: this.buildNextCursor(items) };
    };
    this.filterQueryResults = (items) => items;
    this.client = client;
  }
  get initialState() {
    return {
      ...super.initialState,
      hasPrev: false
    };
  }
  get teamId() {
    return this._teamId;
  }
  set teamId(teamId) {
    if (teamId === this._teamId) return;
    this._teamId = teamId;
    this.resetState();
  }
};

// src/search/ChannelMemberSearchSource.ts
var ChannelMemberSearchSource = class extends BaseSearchSource {
  constructor(channel, options, filterBuilderOptions = {}) {
    const { filters, sort, searchOptions, ...restOptions } = options || {};
    super({ ...restOptions, allowEmptySearchString: true });
    this.type = "members";
    this.channel = channel;
    this.filters = filters;
    this.sort = sort;
    this.searchOptions = searchOptions;
    this.filterBuilder = new FilterBuilder({
      initialFilterConfig: {
        default: {
          enabled: true,
          generate: ({ searchQuery }) => searchQuery ? {
            $or: [
              { name: { $autocomplete: searchQuery } },
              { id: { $eq: searchQuery } }
            ]
          } : null
        }
      },
      ...filterBuilderOptions
    });
  }
  async query(searchQuery, queryOptions = {}) {
    const filters = this.filterBuilder.buildFilters({
      baseFilters: this.filters,
      context: {
        searchQuery
      }
    });
    const sort = this.sort ?? [];
    const options = { ...this.searchOptions, limit: this.pageSize, offset: this.offset };
    const { members } = await this.channel.queryMembers(
      filters ?? {},
      sort,
      options,
      queryOptions
    );
    return { items: members };
  }
  filterQueryResults(items) {
    return items;
  }
};

// src/search/SearchController.ts
var SearchController = class {
  constructor({ config, sources } = {}) {
    this.addSource = (source) => {
      this.state.partialNext({
        sources: [...this.sources, source]
      });
    };
    this.getSource = (sourceType) => this.sources.find((s) => s.type === sourceType);
    this.removeSource = (sourceType) => {
      const newSources = this.sources.filter((s) => s.type !== sourceType);
      if (newSources.length === this.sources.length) return;
      this.state.partialNext({ sources: newSources });
    };
    this.activateSource = (sourceType) => {
      const source = this.getSource(sourceType);
      if (!source || source.isActive) return;
      if (this.config.keepSingleActiveSource) {
        this.sources.forEach((s) => {
          if (s.type !== sourceType) {
            s.deactivate();
          }
        });
      }
      source.activate();
      this.state.partialNext({ sources: [...this.sources] });
    };
    this.deactivateSource = (sourceType) => {
      const source = this.getSource(sourceType);
      if (!source?.isActive) return;
      if (this.activeSources.length === 1) return;
      source.deactivate();
      this.state.partialNext({ sources: [...this.sources] });
    };
    this.activate = () => {
      if (!this.activeSources.length) {
        const sourcesToActivate = this.config.keepSingleActiveSource ? this.sources.slice(0, 1) : this.sources;
        sourcesToActivate.forEach((s) => s.activate());
      }
      if (this.isActive) return;
      this.state.partialNext({ isActive: true });
    };
    this.search = async (searchQuery) => {
      const searchedSources = this.activeSources;
      this.state.partialNext({
        searchQuery
      });
      await Promise.all(searchedSources.map((source) => source.search(searchQuery)));
    };
    this.cancelSearchQueries = () => {
      this.activeSources.forEach((s) => s.cancelScheduledQuery());
    };
    this.clear = () => {
      this.cancelSearchQueries();
      this.sources.forEach(
        (source) => source.state.next({ ...source.initialState, isActive: source.isActive })
      );
      this.state.next((current) => ({
        ...current,
        isActive: true,
        queriesInProgress: [],
        searchQuery: ""
      }));
    };
    this.exit = () => {
      this.cancelSearchQueries();
      this.sources.forEach(
        (source) => source.state.next({ ...source.initialState, isActive: source.isActive })
      );
      this.state.next((current) => ({
        ...current,
        isActive: false,
        queriesInProgress: [],
        searchQuery: ""
      }));
    };
    this.state = new StateStore({
      isActive: false,
      searchQuery: "",
      sources: sources ?? []
    });
    this._internalState = new StateStore({});
    this.config = { keepSingleActiveSource: true, ...config };
  }
  get hasNext() {
    return this.sources.some((source) => source.hasNext);
  }
  get sources() {
    return this.state.getLatestValue().sources;
  }
  get activeSources() {
    return this.state.getLatestValue().sources.filter((s) => s.isActive);
  }
  get isActive() {
    return this.state.getLatestValue().isActive;
  }
  get searchQuery() {
    return this.state.getLatestValue().searchQuery;
  }
  get searchSourceTypes() {
    return this.sources.map((s) => s.type);
  }
};

// src/search/UserSearchSource.ts
var UserSearchSource = class extends BaseSearchSource {
  constructor(client, options, filterBuilderOptions = {}) {
    const { filters, sort, searchOptions, ...restOptions } = options || {};
    super(restOptions);
    this.type = "users";
    this.client = client;
    this.filters = filters;
    this.sort = sort;
    this.searchOptions = searchOptions;
    this.filterBuilder = new FilterBuilder({
      initialFilterConfig: {
        $or: {
          enabled: true,
          generate: ({ searchQuery }) => searchQuery ? {
            $or: [
              { id: { $autocomplete: searchQuery } },
              { name: { $autocomplete: searchQuery } }
            ]
          } : null
        }
      },
      ...filterBuilderOptions
    });
  }
  async query(searchQuery, queryOptions = {}) {
    const filters = this.filterBuilder.buildFilters({
      baseFilters: this.filters,
      context: { searchQuery }
    });
    let sort;
    if (Array.isArray(this.sort)) {
      const hasIdSort = this.sort.some((entry) => "id" in entry);
      sort = hasIdSort ? this.sort : [...this.sort, { id: 1 }];
    } else {
      sort = { id: 1, ...this.sort };
    }
    const options = { ...this.searchOptions, limit: this.pageSize, offset: this.offset };
    const { users } = await this.client.queryUsers(filters, sort, options, queryOptions);
    return { items: users };
  }
  filterQueryResults(items) {
    return items.filter((u) => u.id !== this.client.user?.id);
  }
};

// src/search/ChannelSearchSource.ts
var ChannelSearchSource = class extends BaseSearchSource {
  constructor(client, options, filterBuilderOptions = {}) {
    const { filters, sort, searchOptions, ...restOptions } = options || {};
    super(restOptions);
    this.type = "channels";
    this.client = client;
    this.filters = filters;
    this.sort = sort;
    this.searchOptions = searchOptions;
    this.filterBuilder = new FilterBuilder({
      ...filterBuilderOptions,
      initialFilterConfig: {
        name: {
          enabled: true,
          generate: ({ searchQuery }) => searchQuery ? { name: { $autocomplete: searchQuery } } : null
        },
        ...filterBuilderOptions.initialFilterConfig
      }
    });
  }
  async query(searchQuery, queryOptions = {}) {
    const filters = this.filterBuilder.buildFilters({
      baseFilters: {
        ...this.client.userID ? { members: { $in: [this.client.userID] } } : {},
        ...this.filters
      },
      context: { searchQuery }
    });
    const sort = this.sort ?? {};
    const options = { ...this.searchOptions, limit: this.pageSize, offset: this.offset };
    const items = await this.client.queryChannels(filters, sort, options, queryOptions);
    return { items };
  }
  filterQueryResults(items) {
    return items;
  }
};

// src/search/MessageSearchSource.ts
var MessageSearchSource = class extends BaseSearchSource {
  constructor(client, options, filterBuilderOptions) {
    const {
      messageSearchChannelFilters,
      messageSearchFilters,
      messageSearchSort,
      channelQueryFilters,
      channelQuerySort,
      channelQueryOptions,
      ...restOptions
    } = options || {};
    super(restOptions);
    this.type = "messages";
    this.client = client;
    this.messageSearchChannelFilters = messageSearchChannelFilters;
    this.messageSearchFilters = messageSearchFilters;
    this.messageSearchSort = messageSearchSort;
    this.channelQueryFilters = channelQueryFilters;
    this.channelQuerySort = channelQuerySort;
    this.channelQueryOptions = channelQueryOptions;
    this.messageSearchChannelFilterBuilder = new FilterBuilder(filterBuilderOptions?.messageSearchChannel);
    this.messageSearchFilterBuilder = new FilterBuilder({
      ...filterBuilderOptions?.messageSearch,
      initialFilterConfig: {
        text: {
          enabled: true,
          generate: ({ searchQuery }) => searchQuery ? { text: searchQuery } : null
        },
        ...filterBuilderOptions?.messageSearch?.initialFilterConfig
      }
    });
    this.channelQueryFilterBuilder = new FilterBuilder({
      ...filterBuilderOptions?.channelQuery,
      initialFilterConfig: {
        cid: {
          enabled: true,
          generate: ({ cids }) => cids ? { cid: { $in: cids } } : null
        },
        ...filterBuilderOptions?.channelQuery?.initialFilterConfig
      }
    });
  }
  async query(searchQuery, queryOptions = {}) {
    if (!this.client.userID || this.next === null) return { items: [] };
    const channelFilters = this.messageSearchChannelFilterBuilder.buildFilters({
      baseFilters: {
        ...this.client.userID ? { members: { $in: [this.client.userID] } } : {},
        ...this.messageSearchChannelFilters
      },
      context: { searchQuery }
    });
    const messageFilters = this.messageSearchFilterBuilder.buildFilters({
      baseFilters: {
        type: "regular",
        ...this.messageSearchFilters
      },
      context: { searchQuery }
    });
    const sort = {
      created_at: -1,
      ...this.messageSearchSort
    };
    const options = {
      limit: this.pageSize,
      next: this.next,
      sort
    };
    const { next, results } = await this.client.search(
      channelFilters,
      messageFilters,
      options,
      queryOptions
    );
    const items = results.map(({ message }) => message);
    if (queryOptions.signal?.aborted) return { items, next };
    const cids = Array.from(
      items.reduce((acc, message) => {
        if (message.cid && !this.client.activeChannels[message.cid]) acc.add(message.cid);
        return acc;
      }, /* @__PURE__ */ new Set())
    );
    if (cids.length > 0) {
      const channelQueryFilters = this.channelQueryFilterBuilder.buildFilters({
        baseFilters: this.channelQueryFilters,
        context: { cids }
      });
      await this.client.queryChannels(
        channelQueryFilters,
        {
          last_message_at: -1,
          ...this.channelQuerySort
        },
        this.channelQueryOptions,
        queryOptions
      );
    }
    return { items, next };
  }
  filterQueryResults(items) {
    return items;
  }
};

// src/messageComposer/middleware/textComposer/commandUtils.ts
function escapeCommandRegExp(text) {
  return text.replace(/[-[\]{}()*+?.,/\\^$|#]/g, "\\$&");
}
var getRawCommandName = (text) => text?.match(/^\/(\S+)(?:\s.*)?$/)?.[1];
var getCompleteCommandInString = (text) => {
  const match = text.match(/^\/(\S+)\s+.*/);
  const commandName = match && match[1];
  return commandName;
};
var stripCommandFromText = (text, commandName) => text.replace(new RegExp(`^${escapeCommandRegExp(`/${commandName}`)}\\s*`), "");
var stripMentionTokens = (text, mentionedUsersInText, trigger = "@") => mentionedUsersInText.reduce((value, user) => {
  let next = value.replace(`${trigger}${user.id}`, "");
  if (user.name) {
    next = next.replace(`${trigger}${user.name}`, "");
  }
  return next.trim();
}, text.trim());
var getMentionedUsersInText = (text, mentionedUsers) => Array.from(
  new Set(
    mentionedUsers.filter(
      ({ id, name }) => text.includes(`@${id}`) || !!name && text.includes(`@${name}`)
    )
  )
);
var getCommandByName = (searchSource, commandName) => {
  if (!commandName) return;
  const normalizedCommandName = commandName.toLowerCase();
  return searchSource.query(normalizedCommandName).items.find((command) => command.name?.toLowerCase() === normalizedCommandName);
};
var notifyCommandDisabled = (composer, command) => {
  const disabledReason = composer.getCommandDisabledReason(command);
  if (!disabledReason) return;
  composer.client.notifications.addWarning({
    message: disabledReason === "editing" ? "Command not available while editing" : "Command not available while replying",
    origin: {
      emitter: "MessageComposer",
      context: { command, composer }
    },
    options: {
      type: "validation:command:disabled",
      metadata: {
        command: command.name,
        reason: disabledReason
      }
    }
  });
  return true;
};
var notifyCommandNotReady = ({
  composer,
  sendability
}) => {
  if (sendability.ready) return;
  composer.client.notifications.addWarning({
    message: "Command not ready to be sent",
    origin: {
      emitter: "MessageComposer",
      context: { command: sendability.command, composer }
    },
    options: {
      type: "validation:command:not-ready",
      metadata: {
        command: sendability.command.name,
        ...sendability.reason ? { reason: sendability.reason } : {},
        ...sendability.metadata ?? {}
      }
    }
  });
  return true;
};

// src/messageComposer/middleware/textComposer/textMiddlewareUtils.ts
var getTriggerCharWithToken = ({
  trigger,
  text,
  isCommand = false,
  acceptTrailingSpaces = true
}) => {
  const escapedTrigger = escapeCommandRegExp(trigger);
  const triggerNorWhitespace = `[^\\s${escapedTrigger}]*`;
  const match = text.match(
    new RegExp(
      isCommand ? `^[${escapedTrigger}]${triggerNorWhitespace}$` : acceptTrailingSpaces ? `(?!^|\\W)?[${escapedTrigger}]${triggerNorWhitespace}\\s?${triggerNorWhitespace}$` : `(?!^|\\W)?[${escapedTrigger}]${triggerNorWhitespace}$`,
      "g"
    )
  );
  return match && match[match.length - 1].trim();
};
var insertItemWithTrigger = ({
  insertText,
  selection,
  text,
  trigger
}) => {
  const beforeCursor = text.slice(0, selection.end);
  const afterCursor = text.slice(selection.end);
  const lastIndex = beforeCursor.lastIndexOf(trigger);
  const newText = beforeCursor.slice(0, lastIndex) + insertText + afterCursor;
  return {
    text: newText,
    selection: {
      start: lastIndex + insertText.length,
      end: lastIndex + insertText.length
    }
  };
};
var replaceWordWithEntity = async ({
  caretPosition,
  getEntityString,
  text
}) => {
  const lastWordRegex = /([^\s]+)(\s*)$/;
  const match = lastWordRegex.exec(text.slice(0, caretPosition));
  if (!match) return text;
  const lastWord = match[1];
  if (!lastWord) return text;
  const spaces = match[2];
  const newWord = await getEntityString(lastWord);
  if (newWord == null) return text;
  const textBeforeWord = text.slice(0, caretPosition - match[0].length);
  const textAfterCaret = text.slice(caretPosition, -1);
  return textBeforeWord + newWord + spaces + textAfterCaret;
};
var getTokenizedSuggestionDisplayName = ({
  displayName,
  searchToken
}) => ({
  tokenizedDisplayName: {
    token: searchToken,
    parts: searchToken ? displayName.split(new RegExp(`(${escapeCommandRegExp(searchToken)})`, "gi")).filter(Boolean) : [displayName]
  }
});

// src/messageComposer/middleware/textComposer/commands.ts
var CommandSearchSource = class extends BaseSearchSourceSync {
  constructor(channel, options) {
    super(options);
    this.type = "commands";
    this.canExecuteQuery = (newSearchString) => {
      const hasNewSearchQuery = typeof newSearchString !== "undefined";
      return this.isActive && !this.isLoading && (this.hasNext || hasNewSearchQuery);
    };
    this.channel = channel;
  }
  getStateBeforeFirstQuery(newSearchString) {
    const newState = super.getStateBeforeFirstQuery(newSearchString);
    const { items } = this.state.getLatestValue();
    return {
      ...newState,
      items
      // preserve items to avoid flickering
    };
  }
  query(searchQuery) {
    const channelConfig = this.channel.getConfig();
    const commands = channelConfig?.commands || [];
    const selectedCommands = commands.filter(
      (command) => !!(command.name && command.name.toLowerCase().indexOf(searchQuery.toLowerCase()) !== -1)
    );
    if (searchQuery) {
      selectedCommands.sort((a, b) => {
        let nameA = a.name?.toLowerCase();
        let nameB = b.name?.toLowerCase();
        if (nameA?.indexOf(searchQuery) === 0) {
          nameA = `0${nameA}`;
        }
        if (nameB?.indexOf(searchQuery) === 0) {
          nameB = `0${nameB}`;
        }
        if (nameA != null && nameB != null) {
          if (nameA < nameB) {
            return -1;
          }
          if (nameA > nameB) {
            return 1;
          }
        }
        return 0;
      });
    }
    return {
      items: selectedCommands.map((command) => ({
        ...command,
        id: command.name
      })),
      next: null
    };
  }
  filterQueryResults(items) {
    return items;
  }
};
var DEFAULT_OPTIONS = { minChars: 1, trigger: "/" };
var createCommandsMiddleware = (channel, options) => {
  const finalOptions = mergeWith(DEFAULT_OPTIONS, options ?? {});
  let searchSource = new CommandSearchSource(channel);
  if (options?.searchSource) {
    searchSource = options.searchSource;
    searchSource.resetState();
  }
  searchSource.activate();
  return {
    id: "stream-io/text-composer/commands-middleware",
    handlers: {
      onChange: ({ state, next, complete, forward }) => {
        if (!state.selection) return forward();
        const finalText = state.text.slice(0, state.selection.end);
        const commandName = getCompleteCommandInString(finalText);
        if (commandName) {
          const command = getCommandByName(searchSource, commandName);
          const composer = options?.composer;
          if (command && !composer?.isCommandDisabled(command)) {
            return next({
              ...state,
              command,
              suggestions: void 0
            });
          }
        }
        const triggerWithToken = getTriggerCharWithToken({
          trigger: finalOptions.trigger,
          text: finalText,
          acceptTrailingSpaces: false,
          isCommand: true
        });
        const newSearchTriggerred = triggerWithToken && triggerWithToken.length === finalOptions.minChars;
        if (newSearchTriggerred) {
          searchSource.resetStateAndActivate();
        }
        const triggerWasRemoved = !triggerWithToken || triggerWithToken.length < finalOptions.minChars;
        if (triggerWasRemoved) {
          const hasStaleSuggestions = state.suggestions?.trigger === finalOptions.trigger;
          const newState = { ...state };
          if (hasStaleSuggestions) {
            delete newState.suggestions;
          }
          return next(newState);
        }
        return complete({
          ...state,
          command: null,
          suggestions: {
            query: triggerWithToken.slice(1),
            searchSource,
            trigger: finalOptions.trigger
          }
        });
      },
      onSuggestionItemSelect: ({ state, next, forward }) => {
        const { selectedSuggestion } = state.change ?? {};
        if (!selectedSuggestion || state.suggestions?.trigger !== finalOptions.trigger)
          return forward();
        const composer = options?.composer;
        if (composer && notifyCommandDisabled(composer, selectedSuggestion)) {
          return forward();
        }
        searchSource.resetStateAndActivate();
        return next({
          ...state,
          ...insertItemWithTrigger({
            insertText: `/${selectedSuggestion.name} `,
            selection: state.selection,
            text: state.text,
            trigger: finalOptions.trigger
          }),
          command: selectedSuggestion,
          suggestions: void 0
        });
      }
    }
  };
};

// src/messageComposer/middleware/messageComposer/compositionValidation.ts
var getDisabledRawCommand = (composer, searchSource, text) => {
  const rawCommand = getCommandByName(searchSource, getRawCommandName(text));
  if (rawCommand && composer.isCommandDisabled(rawCommand)) {
    return rawCommand;
  }
};
var createCompositionValidationMiddleware = (composer, commandSearchSource) => {
  const effectiveCommandSearchSource = commandSearchSource ?? new CommandSearchSource(composer.channel);
  return {
    id: "stream-io/message-composer-middleware/data-validation",
    handlers: {
      compose: async ({
        state,
        discard,
        forward
      }) => {
        const { maxLengthOnSend } = composer.config.text ?? {};
        const inputText = state.message.text ?? "";
        const disabledRawCommand = getDisabledRawCommand(
          composer,
          effectiveCommandSearchSource,
          inputText
        );
        if (disabledRawCommand) {
          notifyCommandDisabled(composer, disabledRawCommand);
          return await discard();
        }
        const currentCommand = composer.textComposer.command ?? getCommandByName(effectiveCommandSearchSource, getRawCommandName(inputText));
        if (currentCommand && notifyCommandNotReady({
          composer,
          sendability: composer.validateCommandSendability(currentCommand, inputText)
        })) {
          return await discard();
        }
        const hasExceededMaxLength = typeof maxLengthOnSend === "number" && inputText.length > maxLengthOnSend;
        if (composer.compositionIsEmpty || hasExceededMaxLength) {
          return await discard();
        }
        return await forward();
      }
    }
  };
};
var createDraftCompositionValidationMiddleware = (composer) => ({
  id: "stream-io/message-composer-middleware/draft-data-validation",
  handlers: {
    compose: async ({
      state,
      discard,
      forward
    }) => {
      const hasData = !textIsEmpty(state.draft.text ?? "") || state.draft.attachments?.length || state.draft.poll_id || state.draft.quoted_message_id;
      const shouldCreateDraft = composer.lastChangeOriginIsLocal && hasData;
      if (!shouldCreateDraft) {
        return await discard();
      }
      return await forward();
    }
  }
});

// src/messageComposer/middleware/messageComposer/linkPreviews.ts
var createLinkPreviewsCompositionMiddleware = (composer) => ({
  id: "stream-io/message-composer-middleware/link-previews",
  handlers: {
    compose: ({
      state,
      next,
      forward
    }) => {
      const { linkPreviewsManager } = composer;
      if (!linkPreviewsManager) return forward();
      linkPreviewsManager.cancelURLEnrichment();
      const someLinkPreviewsLoading = linkPreviewsManager.loadingPreviews.length > 0;
      const someLinkPreviewsDismissed = linkPreviewsManager.dismissedPreviews.length > 0;
      const linkPreviews = linkPreviewsManager.loadingPreviews.length > 0 ? [] : linkPreviewsManager.loadedPreviews.map(
        (preview) => LinkPreviewsManager.getPreviewData(preview)
      );
      const attachments = (state.message.attachments ?? []).concat(
        linkPreviews
      );
      if (!attachments.length) return forward();
      const sendOptions = { ...state.sendOptions };
      const skip_enrich_url = !someLinkPreviewsLoading && linkPreviews.length > 0 || someLinkPreviewsDismissed;
      if (skip_enrich_url) {
        sendOptions.skip_enrich_url = true;
      }
      return next({
        ...state,
        message: {
          ...state.message,
          attachments
        },
        localMessage: {
          ...state.localMessage,
          attachments
        },
        sendOptions
      });
    }
  }
});
var createDraftLinkPreviewsCompositionMiddleware = (composer) => ({
  id: "stream-io/message-composer-middleware/draft-link-previews",
  handlers: {
    compose: ({
      state,
      next,
      forward
    }) => {
      const { linkPreviewsManager } = composer;
      if (!linkPreviewsManager) return forward();
      linkPreviewsManager.cancelURLEnrichment();
      const linkPreviews = linkPreviewsManager.loadedPreviews.map(
        (preview) => LinkPreviewsManager.getPreviewData(preview)
      );
      if (!linkPreviews.length) return forward();
      return next({
        ...state,
        draft: {
          ...state.draft,
          attachments: (state.draft.attachments ?? []).concat(linkPreviews)
        }
      });
    }
  }
});

// src/messageComposer/middleware/messageComposer/textComposer.ts
var textIncludesMentionToken = (text, token) => text.includes(`@${token}`);
var isDefined = (value) => value !== void 0;
var getMentionEntityTextCandidates = (entity) => {
  if (entity.mentionType === "channel") return ["channel"];
  if (entity.mentionType === "here") return ["here"];
  if (entity.mentionType === "user") return [entity.id, entity.name].filter(isDefined);
  if (entity.mentionType === "role") return [entity.name, entity.id].filter(isDefined);
  if (entity.mentionType === "user_group") {
    return entity.name ? [entity.name, entity.id].filter(isDefined) : [];
  }
  return [];
};
var isMentionEntityPresentInText = (entity, text) => {
  const textCandidates = getMentionEntityTextCandidates(entity);
  if (!textCandidates.length) return true;
  return textCandidates.some((candidate) => textIncludesMentionToken(text, candidate));
};
var dedupeBy = (items, getKey) => {
  const uniqueItems = /* @__PURE__ */ new Map();
  items.forEach((item) => {
    uniqueItems.set(getKey(item), item);
  });
  return [...uniqueItems.values()];
};
var buildMentionCompositionMetadata = ({
  mentions,
  text
}) => {
  const presentMentions = dedupeBy(
    mentions.filter((entity) => isMentionEntityPresentInText(entity, text)),
    (entity) => `${entity.mentionType}:${entity.id}`
  );
  return presentMentions.reduce(
    (acc, entity) => {
      if (entity.mentionType === "user") {
        acc.mentioned_users.push(mentionEntityToUserResponse(entity));
      } else if (entity.mentionType === "channel") {
        acc.mentioned_channel = true;
      } else if (entity.mentionType === "here") {
        acc.mentioned_here = true;
      } else if (entity.mentionType === "role") {
        acc.mentioned_roles.push(entity.id);
      } else if (entity.mentionType === "user_group") {
        acc.mentioned_group_ids.push(entity.id);
      }
      return acc;
    },
    {
      mentioned_channel: false,
      mentioned_group_ids: [],
      mentioned_here: false,
      mentioned_roles: [],
      mentioned_users: []
    }
  );
};
var createTextComposerCompositionMiddleware = (composer) => ({
  id: "stream-io/message-composer-middleware/text-composition",
  handlers: {
    compose: ({
      state,
      next,
      forward
    }) => {
      if (!composer.textComposer) return forward();
      const { mentions, text } = composer.textComposer;
      const {
        mentioned_channel,
        mentioned_group_ids,
        mentioned_here,
        mentioned_roles,
        mentioned_users
      } = buildMentionCompositionMetadata({ mentions, text });
      if (!text && !mentioned_channel && !mentioned_here && mentioned_group_ids.length === 0 && mentioned_roles.length === 0 && mentioned_users.length === 0) {
        return forward();
      }
      return next({
        ...state,
        localMessage: {
          ...state.localMessage,
          mentioned_channel,
          mentioned_group_ids,
          mentioned_here,
          mentioned_roles,
          mentioned_users,
          text
        },
        message: {
          ...state.message,
          mentioned_channel,
          mentioned_group_ids,
          mentioned_here,
          mentioned_roles,
          mentioned_users: mentioned_users.map((u) => u.id),
          text
        }
      });
    }
  }
});
var createDraftTextComposerCompositionMiddleware = (composer) => ({
  id: "stream-io/message-composer-middleware/draft-text-composition",
  handlers: {
    compose: ({
      state,
      next,
      forward
    }) => {
      if (!composer.textComposer) return forward();
      const { maxLengthOnSend } = composer.config.text ?? {};
      const { mentions, text: inputText } = composer.textComposer;
      const text = typeof maxLengthOnSend === "number" && inputText.length > maxLengthOnSend ? inputText.slice(0, maxLengthOnSend) : inputText;
      const {
        mentioned_channel,
        mentioned_group_ids,
        mentioned_here,
        mentioned_roles,
        mentioned_users
      } = buildMentionCompositionMetadata({ mentions, text });
      const draftMentionPayload = {
        ...mentioned_channel ? { mentioned_channel: true } : {},
        ...mentioned_group_ids.length ? { mentioned_group_ids } : {},
        ...mentioned_here ? { mentioned_here: true } : {},
        ...mentioned_roles.length ? { mentioned_roles } : {},
        ...mentioned_users.length ? { mentioned_users: mentioned_users.map((u) => u.id) } : {}
      };
      return next({
        ...state,
        draft: {
          ...state.draft,
          ...draftMentionPayload,
          text
        }
      });
    }
  }
});

// src/messageComposer/middleware/messageComposer/messageComposerState.ts
var createMessageComposerStateCompositionMiddleware = (composer) => ({
  id: "stream-io/message-composer-middleware/own-state",
  handlers: {
    compose: ({
      state,
      next
    }) => {
      const payload = {};
      if (composer.quotedMessage) {
        payload.quoted_message_id = composer.quotedMessage.id;
      }
      if (composer.pollId) {
        payload.poll_id = composer.pollId;
      }
      if (composer.showReplyInChannel) {
        payload.show_in_channel = true;
      }
      return next({
        ...state,
        localMessage: {
          ...state.localMessage,
          ...payload,
          quoted_message: composer.quotedMessage ?? void 0
        },
        message: {
          ...state.message,
          ...payload
        }
      });
    }
  }
});
var createDraftMessageComposerStateCompositionMiddleware = (composer) => ({
  id: "stream-io/message-composer-middleware/draft-own-state",
  handlers: {
    compose: ({
      state,
      next
    }) => {
      const payload = {};
      if (composer.quotedMessage) {
        payload.quoted_message_id = composer.quotedMessage.id;
      }
      if (composer.pollId) {
        payload.poll_id = composer.pollId;
      }
      if (composer.showReplyInChannel) {
        payload.show_in_channel = true;
      }
      return next({
        ...state,
        draft: {
          ...state.draft,
          ...payload
        }
      });
    }
  }
});

// src/messageComposer/middleware/messageComposer/userDataInjection.ts
var createUserDataInjectionMiddleware = (composer) => ({
  id: "stream-io/message-composer-middleware/user-data-injection",
  handlers: {
    compose: ({
      state,
      next,
      forward
    }) => {
      if (!composer.client.user) {
        return forward();
      }
      const { channel_mutes, devices, mutes, ...messageUser } = composer.client.user;
      return next({
        ...state,
        localMessage: {
          ...state.localMessage,
          user: messageUser,
          user_id: messageUser.id
        }
      });
    }
  }
});

// src/messageComposer/middleware/messageComposer/pollOnly.ts
var pollLocalMessageNullifiedFields = {
  attachments: [],
  mentioned_users: [],
  parent_id: void 0,
  quoted_message: void 0,
  text: ""
};
var createPollOnlyCompositionMiddleware = (composer) => ({
  id: "stream-io/message-composer-middleware/poll-only",
  handlers: {
    compose: ({
      state,
      complete,
      forward
    }) => {
      const pollId = composer.pollId;
      const isEditingMessage = !!composer.editedMessage;
      const isComposingThreadReply = !!composer.threadId;
      if (!pollId || isComposingThreadReply || isEditingMessage) return forward();
      return complete({
        ...state,
        localMessage: {
          ...state.localMessage,
          ...pollLocalMessageNullifiedFields,
          poll_id: pollId
        },
        message: {
          id: state.localMessage.id,
          poll_id: pollId
        }
      });
    }
  }
});

// src/messageComposer/middleware/messageComposer/sharedLocation.ts
var createSharedLocationCompositionMiddleware = (composer) => ({
  id: "stream-io/message-composer-middleware/shared-location",
  handlers: {
    compose: ({
      state,
      next,
      forward
    }) => {
      const { locationComposer } = composer;
      const location = locationComposer.validLocation;
      if (!locationComposer || !location || !composer.client.user) return forward();
      const timestamp = (/* @__PURE__ */ new Date()).toISOString();
      return next({
        ...state,
        localMessage: {
          ...state.localMessage,
          shared_location: {
            ...location,
            channel_cid: composer.channel.cid,
            created_at: timestamp,
            updated_at: timestamp,
            user_id: composer.client.user.id
          }
        },
        message: {
          ...state.message,
          shared_location: location
        }
      });
    }
  }
});

// src/messageComposer/middleware/messageComposer/MessageComposerMiddlewareExecutor.ts
var MessageComposerMiddlewareExecutor = class extends MiddlewareExecutor {
  constructor({ composer }) {
    super();
    this.use([
      createUserDataInjectionMiddleware(composer),
      createPollOnlyCompositionMiddleware(composer),
      createTextComposerCompositionMiddleware(composer),
      createAttachmentsCompositionMiddleware(composer),
      createLinkPreviewsCompositionMiddleware(composer),
      createSharedLocationCompositionMiddleware(composer),
      createMessageComposerStateCompositionMiddleware(composer),
      createCustomDataCompositionMiddleware(composer),
      createCompositionValidationMiddleware(composer),
      createCompositionDataCleanupMiddleware(composer)
    ]);
  }
  /**
   * Narrows the base accessor to the composition middleware type, so callers can read the
   * declarations it carries (`allowsPendingUploads`) without casting.
   */
  get installedMiddleware() {
    return super.installedMiddleware;
  }
};
var MessageDraftComposerMiddlewareExecutor = class extends MiddlewareExecutor {
  constructor({ composer }) {
    super();
    this.use([
      createDraftTextComposerCompositionMiddleware(composer),
      createDraftAttachmentsCompositionMiddleware(composer),
      createDraftLinkPreviewsCompositionMiddleware(composer),
      createDraftMessageComposerStateCompositionMiddleware(composer),
      createDraftCustomDataCompositionMiddleware(composer),
      createDraftCompositionValidationMiddleware(composer)
    ]);
  }
};

// src/messageComposer/middleware/messageComposer/commandInjection.ts
var createCommandInjectionMiddleware = (composer) => ({
  handlers: {
    compose: ({
      forward,
      next,
      state
    }) => {
      const command = composer.textComposer.command;
      if (!command) {
        return forward();
      }
      const { text } = state.localMessage;
      const injection = `/${command?.name}`;
      const enrichedText = `${injection} ${text}`;
      return next({
        ...state,
        localMessage: {
          ...state.localMessage,
          text: enrichedText
        },
        message: {
          ...state.message,
          text: enrichedText
        }
      });
    }
  },
  id: "stream-io/message-composer-middleware/command-string-injection"
});
var createDraftCommandInjectionMiddleware = (composer) => ({
  handlers: {
    compose: ({
      forward,
      next,
      state
    }) => {
      const command = composer.textComposer.command;
      if (!command) {
        return forward();
      }
      const { text } = state.draft;
      const injection = `/${command?.name}`;
      const enrichedText = `${injection} ${text}`;
      return next({
        ...state,
        draft: {
          ...state.draft,
          text: enrichedText
        }
      });
    }
  },
  id: "stream-io/message-composer-middleware/draft-command-string-injection"
});

// src/messageComposer/middleware/pollComposer/state.ts
var VALID_MAX_VOTES_VALUE_REGEX = /^([2-9]|10)$/;
var MAX_POLL_OPTIONS = 100;
var textFieldIsEmpty = (text) => !text.trim();
var pollStateChangeValidators = {
  enforce_unique_vote: () => ({ max_votes_allowed: void 0 }),
  max_votes_allowed: ({ data, value }) => {
    if (data.enforce_unique_vote && value)
      return { max_votes_allowed: "Enforce unique vote is enabled" };
    const numericMatch = value.match(/^[0-9]+$/);
    if (!numericMatch && value) {
      return { max_votes_allowed: "Only numbers are allowed" };
    }
    if (value?.length > 1 && !value.match(VALID_MAX_VOTES_VALUE_REGEX))
      return { max_votes_allowed: "Type a number from 2 to 10" };
    return { max_votes_allowed: void 0 };
  },
  options: ({ value: options }) => {
    const errors = {};
    const seenOptions = /* @__PURE__ */ new Set();
    options.forEach((option) => {
      if (seenOptions.has(option.text) && option.text.length) {
        errors[option.id] = "Option already exists";
      } else {
        seenOptions.add(option.text);
      }
    });
    return Object.keys(errors).length > 0 ? { options: errors } : { options: void 0 };
  }
};
var defaultPollFieldChangeEventValidators = {
  name: ({ currentError, value }) => value && currentError ? { name: void 0 } : { name: typeof currentError === "string" ? currentError : void 0 }
};
var defaultPollFieldBlurEventValidators = {
  max_votes_allowed: ({ value }) => {
    if (value && !value.match(VALID_MAX_VOTES_VALUE_REGEX))
      return { max_votes_allowed: "Type a number from 2 to 10" };
    return { max_votes_allowed: void 0 };
  },
  name: ({ value }) => {
    if (textFieldIsEmpty(value)) return { name: "Question is required" };
    return { name: void 0 };
  },
  options: (params) => {
    const defaultResult = pollStateChangeValidators.options?.(params);
    const errors = defaultResult?.options ?? {};
    params.value.forEach((option, index) => {
      const isTheLastOption = index === params.value.length - 1;
      if (textFieldIsEmpty(option.text) && !isTheLastOption) {
        errors[option.id] = "Option is empty";
      }
    });
    return Object.keys(errors).length > 0 ? { options: errors } : { options: void 0 };
  }
};
var isTargetedOptionTextUpdate = (value) => !Array.isArray(value) && typeof value?.index === "number" && typeof value?.text === "string";
var clampMaxVotesAllowed = (value) => {
  if (value === "" || value == null) return "";
  const num = typeof value === "string" ? parseInt(value, 10) : Number(value);
  if (!Number.isInteger(num) || Number.isNaN(num)) return "";
  return String(Math.min(10, Math.max(2, num)));
};
var pollCompositionStateProcessors = {
  enforce_unique_vote: ({ value }) => ({
    enforce_unique_vote: value,
    max_votes_allowed: ""
  }),
  max_votes_allowed: ({ value }) => ({
    max_votes_allowed: clampMaxVotesAllowed(value)
  }),
  options: ({ value, data }) => {
    if (Array.isArray(value)) {
      return {
        options: value.map((option) => ({
          id: option.id,
          text: option.text.trim()
        }))
      };
    }
    const { index, text } = value;
    const prevOptions = data.options || [];
    const shouldRemoveOption = prevOptions && prevOptions.slice(index + 1).length > 0 && !text;
    const optionListHead = prevOptions.slice(0, index);
    const optionListTail = prevOptions.slice(index + 1);
    const newOptions = [
      ...optionListHead,
      ...shouldRemoveOption ? [] : [{ ...prevOptions[index], text }],
      ...optionListTail
    ];
    const shouldAddNewOption = prevOptions.length < MAX_POLL_OPTIONS && !newOptions.some((option) => !option.text.trim());
    if (shouldAddNewOption) {
      newOptions.push({ id: generateUUIDv4(), text: "" });
    }
    return { options: newOptions };
  }
};
var createPollComposerStateMiddleware = ({
  processors: customProcessors,
  validators: customValidators
} = {}) => {
  const universalHandler = ({
    state,
    validators,
    processors
  }) => {
    const { previousState, targetFields } = state;
    let newData;
    if (!processors && isTargetedOptionTextUpdate(targetFields.options)) {
      const options = [...previousState.data.options];
      const targetOption = previousState.data.options[targetFields.options.index];
      if (targetOption) {
        targetOption.text = targetFields.options.text;
        options.splice(targetFields.options.index, 1, targetOption);
      }
      newData = { ...targetFields, options };
    } else if (!processors) {
      newData = targetFields;
    } else {
      newData = Object.entries(targetFields).reduce(
        (acc, [key, value]) => {
          const processor = processors[key];
          acc = {
            ...acc,
            ...processor ? processor({ data: previousState.data, value }) : { [key]: value }
          };
          return acc;
        },
        {}
      );
    }
    const newErrors = Object.keys(targetFields).reduce((acc, key) => {
      const validator = validators[key];
      if (validator) {
        const error = validator({
          currentError: previousState.errors[key],
          data: previousState.data,
          value: newData[key]
        });
        acc = { ...acc, ...error };
      }
      return acc;
    }, {});
    return { newData, newErrors };
  };
  return {
    id: "stream-io/poll-composer-state-processing",
    handlers: {
      handleFieldChange: ({
        state,
        next,
        forward
      }) => {
        if (!state.targetFields) return forward();
        const { previousState, injectedFieldErrors } = state;
        const { newData, newErrors } = universalHandler({
          processors: {
            ...pollCompositionStateProcessors,
            ...customProcessors?.handleFieldChange
          },
          state,
          validators: {
            ...pollStateChangeValidators,
            ...defaultPollFieldChangeEventValidators,
            ...customValidators?.handleFieldChange
          }
        });
        return next({
          ...state,
          nextState: {
            ...previousState,
            data: { ...previousState.data, ...newData },
            errors: { ...previousState.errors, ...newErrors, ...injectedFieldErrors }
          }
        });
      },
      handleFieldBlur: ({
        state,
        next,
        forward
      }) => {
        if (!state.targetFields) return forward();
        const { previousState } = state;
        const { newData, newErrors } = universalHandler({
          processors: customProcessors?.handleFieldBlur,
          state,
          validators: {
            ...pollStateChangeValidators,
            ...defaultPollFieldBlurEventValidators,
            ...customValidators?.handleFieldBlur
          }
        });
        return next({
          ...state,
          nextState: {
            ...previousState,
            data: { ...previousState.data, ...newData },
            errors: {
              ...previousState.errors,
              ...newErrors,
              ...state.injectedFieldErrors
            }
          }
        });
      }
    }
  };
};

// src/messageComposer/middleware/pollComposer/composition.ts
var createPollCompositionValidationMiddleware = (composer) => ({
  id: "stream-io/poll-composer-composition",
  handlers: {
    compose: ({
      discard,
      forward
    }) => {
      if (composer.pollComposer.canCreatePoll) return forward();
      return discard();
    }
  }
});

// src/messageComposer/middleware/pollComposer/PollComposerMiddlewareExecutor.ts
var PollComposerCompositionMiddlewareExecutor = class extends MiddlewareExecutor {
  constructor({ composer }) {
    super();
    this.use([createPollCompositionValidationMiddleware(composer)]);
  }
};
var PollComposerStateMiddlewareExecutor = class extends MiddlewareExecutor {
  constructor() {
    super();
    this.use([createPollComposerStateMiddleware()]);
  }
};

// src/messageComposer/middleware/textComposer/activeCommandGuard.ts
var createActiveCommandGuardMiddleware = () => ({
  handlers: {
    onChange: ({ complete, forward, state }) => {
      if (state.command) {
        return complete(state);
      }
      return forward();
    },
    onSuggestionItemSelect: ({ forward }) => forward()
  },
  id: "stream-io/text-composer/active-command-guard"
});

// src/messageComposer/middleware/textComposer/commandEffects.ts
var emptyCommandStateToRestore = {
  selection: { start: 0, end: 0 },
  text: ""
};
var createCommandActivationEffect = (command) => ({
  command,
  stateToRestore: emptyCommandStateToRestore,
  type: "command.activate"
});
var isCommandResponse = (suggestion) => typeof suggestion?.name === "string";
var createCommandEffectsMiddleware = () => ({
  handlers: {
    onChange: ({ forward }) => forward(),
    onSuggestionItemSelect: ({ state, next, forward }) => {
      const { selectedSuggestion } = state.change ?? {};
      if (!isCommandResponse(selectedSuggestion) || !state.command || state.command.name !== selectedSuggestion.name) {
        return forward();
      }
      return next({
        ...state,
        effects: [...state.effects ?? [], createCommandActivationEffect(state.command)]
      });
    }
  },
  id: "stream-io/text-composer/command-effects-middleware"
});

// src/messageComposer/middleware/textComposer/commandStringExtraction.ts
var createCommandStringExtractionMiddleware = () => ({
  handlers: {
    onChange: ({ complete, forward, state }) => {
      const { command } = state;
      if (!command?.name) {
        return forward();
      }
      const newText = stripCommandFromText(state.text, command.name);
      return complete({
        ...state,
        selection: {
          end: newText.length,
          start: newText.length
        },
        text: newText
      });
    },
    onSuggestionItemSelect: ({ next, forward, state }) => {
      const { command } = state;
      if (!command) {
        return forward();
      }
      const triggerWithCommand = `/${command?.name} `;
      const newText = state.text.slice(triggerWithCommand.length);
      return next({
        ...state,
        selection: {
          end: newText.length,
          start: newText.length
        },
        text: newText
      });
    }
  },
  id: "stream-io/text-composer/command-string-extraction"
});

// src/messageComposer/middleware/textComposer/mentions.ts
var accentsMap = {
  a: "\xE1|\xE0|\xE3|\xE2|\xC0|\xC1|\xC3|\xC2",
  c: "\xE7|\xC7",
  e: "\xE9|\xE8|\xEA|\xC9|\xC8|\xCA",
  i: "\xED|\xEC|\xEE|\xCD|\xCC|\xCE",
  n: "\xF1|\xD1",
  o: "\xF3|\xF2|\xF4|\u0151|\xF5|\xD3|\xD2|\xD4|\xD5",
  u: "\xFA|\xF9|\xFB|\xFC|\xDA|\xD9|\xDB|\xDC"
};
var removeDiacritics = (text) => {
  if (!text) return "";
  return Object.keys(accentsMap).reduce(
    (acc, current) => acc.replace(new RegExp(accentsMap[current], "g"), current),
    text
  );
};
var calculateLevenshtein = (query, name) => {
  if (query.length === 0) return name.length;
  if (name.length === 0) return query.length;
  const matrix = [];
  let i;
  for (i = 0; i <= name.length; i++) {
    matrix[i] = [i];
  }
  let j;
  for (j = 0; j <= query.length; j++) {
    matrix[0][j] = j;
  }
  for (i = 1; i <= name.length; i++) {
    for (j = 1; j <= query.length; j++) {
      if (name.charAt(i - 1) === query.charAt(j - 1)) {
        matrix[i][j] = matrix[i - 1][j - 1];
      } else {
        matrix[i][j] = Math.min(
          matrix[i - 1][j - 1] + 1,
          // substitution
          Math.min(
            matrix[i][j - 1] + 1,
            // insertion
            matrix[i - 1][j] + 1
          )
        );
      }
    }
  }
  return matrix[name.length][query.length];
};
var hasOwnCapability = (ownCapabilities, capability) => ownCapabilities?.includes(capability) ?? false;
var getAllowedMentionTypesFromCapabilities = (ownCapabilities) => ({
  channel: hasOwnCapability(ownCapabilities, "notify-channel"),
  here: hasOwnCapability(ownCapabilities, "notify-here"),
  role: hasOwnCapability(ownCapabilities, "notify-role"),
  user: true,
  user_group: hasOwnCapability(ownCapabilities, "notify-group")
});
var decodeUserGroupCursor = (cursor) => {
  if (!cursor) return void 0;
  try {
    return JSON.parse(cursor);
  } catch {
    return void 0;
  }
};
var upsertUserResponse = (users, user) => {
  const existingIndex = users.findIndex((currentUser) => currentUser.id === user.id);
  if (existingIndex === -1) return users.concat(user);
  const nextUsers = [...users];
  nextUsers.splice(existingIndex, 1, user);
  return nextUsers;
};
var upsertMentionEntity = (mentions, entity) => {
  const existingIndex = mentions.findIndex(
    (currentEntity) => currentEntity.id === entity.id && currentEntity.mentionType === entity.mentionType
  );
  if (existingIndex === -1) return mentions.concat(entity);
  const nextMentions = [...mentions];
  nextMentions.splice(existingIndex, 1, entity);
  return nextMentions;
};
var mentionSuggestionToEntity = (suggestion) => {
  if (suggestion.mentionType === "user") {
    return userSuggestionToMentionEntity(suggestion);
  } else if (suggestion.mentionType === "channel") {
    return {
      id: "channel",
      mentionType: "channel",
      name: "channel"
    };
  } else if (suggestion.mentionType === "here") {
    return {
      id: "here",
      mentionType: "here",
      name: "here"
    };
  } else if (suggestion.mentionType === "role") {
    return {
      id: suggestion.id,
      mentionType: "role",
      name: suggestion.name
    };
  } else if (suggestion.mentionType === "user_group") {
    return {
      id: suggestion.id,
      mentionType: "user_group",
      name: suggestion.name
    };
  }
  throw new Error(`Unsupported mention suggestion type: ${JSON.stringify(suggestion)}`);
};
var mentionSuggestionToInsertText = (suggestion) => `@${suggestion.name || suggestion.id} `;
var DEFAULT_SUGGESTION_FACTORY_MAPPERS = {
  channel: (value, { searchToken }) => {
    const name = String(value);
    return {
      id: name,
      mentionType: "channel",
      name: "channel",
      ...getTokenizedSuggestionDisplayName({
        displayName: name,
        searchToken
      })
    };
  },
  here: (value, { searchToken }) => {
    const name = String(value);
    return {
      id: name,
      mentionType: "here",
      name: "here",
      ...getTokenizedSuggestionDisplayName({
        displayName: name,
        searchToken
      })
    };
  },
  role: (value, { searchToken }) => {
    const role = String(value);
    return {
      id: role,
      mentionType: "role",
      name: role,
      ...getTokenizedSuggestionDisplayName({
        displayName: role,
        searchToken
      })
    };
  },
  user: (value, { searchToken }) => {
    const user = value;
    return {
      ...user,
      mentionType: "user",
      ...getTokenizedSuggestionDisplayName({
        displayName: user.name || user.id,
        searchToken
      })
    };
  },
  user_group: (value, { searchToken }) => {
    const userGroup = value;
    return {
      description: userGroup.description,
      id: userGroup.id,
      /*
      Currently, all members of the group are always returned. Groups are limited to 100 members.
      The memberCount == len(members) will always be true unless we add pagination here in the future
       */
      memberCount: userGroup.members?.length,
      mentionType: "user_group",
      name: userGroup.name,
      ...getTokenizedSuggestionDisplayName({
        displayName: userGroup.name || userGroup.id,
        searchToken
      })
    };
  }
};
var MentionsSearchSource = class extends BaseSearchSource {
  constructor(channel, options) {
    const {
      mentionAllAppUsers,
      memberFilters,
      memberSort,
      searchOptions,
      suggestionFactoryMappers,
      textComposerText,
      transliterate,
      trigger,
      userFilters,
      userSort,
      ...restOptions
    } = options || {};
    super({ ...restOptions, allowEmptySearchString: true });
    this.type = "mentions";
    this.normalizeSearchValue = (value) => this.transliterate(removeDiacritics(value)).toLowerCase();
    this.matchesSearchQuery = (value, searchQuery) => {
      if (!searchQuery) return true;
      return this.normalizeSearchValue(value).includes(
        this.normalizeSearchValue(searchQuery)
      );
    };
    this.matchesPrefixSearchQuery = (value, searchQuery) => {
      if (!searchQuery) return true;
      return this.normalizeSearchValue(value).startsWith(
        this.normalizeSearchValue(searchQuery)
      );
    };
    this.matchesUserNameSearchQuery = (value, searchQuery) => {
      if (!searchQuery) return true;
      const normalizedValueWords = this.normalizeSearchValue(value).split(/\s+/).filter(Boolean);
      const normalizedQueryWords = this.normalizeSearchValue(searchQuery).split(/\s+/).filter(Boolean);
      if (!normalizedValueWords.length || !normalizedQueryWords.length) return false;
      const fullMatchWords = normalizedQueryWords.slice(0, -1);
      const finalQueryWord = normalizedQueryWords[normalizedQueryWords.length - 1];
      return fullMatchWords.every((queryWord) => normalizedValueWords.includes(queryWord)) && normalizedValueWords.some((valueWord) => valueWord.startsWith(finalQueryWord));
    };
    this.isMentionTypeAllowed = (mentionType) => getAllowedMentionTypesFromCapabilities(this.channel.data?.own_capabilities)[mentionType];
    this.mapMentionSuggestion = (mentionType, value, searchToken = this.searchQuery) => {
      const mapper = this.config.suggestionFactoryMappers?.[mentionType] ?? DEFAULT_SUGGESTION_FACTORY_MAPPERS[mentionType];
      return mapper(value, {
        searchToken,
        source: this
      });
    };
    this.getChannelTeam = () => this.channel.data?.team;
    this.toUserSuggestion = (user, searchToken = this.searchQuery) => this.mapMentionSuggestion("user", user, searchToken);
    this.toChannelMentionSuggestion = (searchToken = this.searchQuery) => this.mapMentionSuggestion("channel", "channel", searchToken);
    this.toHereMentionSuggestion = (searchToken = this.searchQuery) => this.mapMentionSuggestion("here", "here", searchToken);
    this.toRoleMentionSuggestion = (role, searchToken = this.searchQuery) => this.mapMentionSuggestion("role", role, searchToken);
    this.toUserGroupMentionSuggestion = (userGroup, searchToken = this.searchQuery) => this.mapMentionSuggestion("user_group", userGroup, searchToken);
    this.transliterate = (text) => text;
    this.getMembersAndWatchers = () => {
      const memberUsers = Object.values(this.channel.state.members ?? {}).map(
        ({ user }) => user
      );
      const watcherUsers = Object.values(this.channel.state.watchers ?? {});
      const users = [...memberUsers, ...watcherUsers];
      const uniqueUsers = {};
      users.forEach((user) => {
        if (user && !uniqueUsers[user.id]) {
          uniqueUsers[user.id] = user;
        }
      });
      return Object.values(uniqueUsers);
    };
    this.getBuiltinMentionSuggestions = (searchQuery) => [
      ...this.isMentionTypeAllowed("channel") ? [this.toChannelMentionSuggestion(searchQuery)] : [],
      ...this.isMentionTypeAllowed("here") ? [this.toHereMentionSuggestion(searchQuery)] : []
    ].filter(({ name }) => this.matchesPrefixSearchQuery(name, searchQuery));
    this.getRoleMentionSuggestions = async (query, queryOptions = {}) => {
      if (!this.isMentionTypeAllowed("role")) return [];
      if (!query) return [];
      const { roles } = await this.client.searchRoles({ query }, queryOptions);
      return [...roles?.map((role) => role.name) ?? []].sort((left, right) => left.localeCompare(right)).map((role) => this.toRoleMentionSuggestion(role, query));
    };
    this.searchMembersLocally = (searchQuery) => {
      const { textComposerText } = this.config;
      if (!textComposerText) return [];
      return this.getMembersAndWatchers().filter((user) => {
        if (user.id === this.client.userID) return false;
        if (!searchQuery) return true;
        const updatedId = this.transliterate(removeDiacritics(user.id)).toLowerCase();
        const updatedQuery = this.transliterate(
          removeDiacritics(searchQuery)
        ).toLowerCase();
        const maxDistance = 3;
        const trigger = this.config.trigger ?? "@";
        const lastDigits = textComposerText.slice(-(maxDistance + 1)).includes(trigger);
        if (this.matchesUserNameSearchQuery(user.name, updatedQuery)) {
          return true;
        }
        const levenshtein = calculateLevenshtein(updatedQuery, updatedId);
        return updatedId.includes(updatedQuery) || levenshtein <= maxDistance && lastDigits;
      }).sort((a, b) => {
        if (!this.memberSort) return (a.name || "").localeCompare(b.name || "");
        for (const [field, direction] of Object.entries(this.memberSort)) {
          const aValue = a[field];
          const bValue = b[field];
          if (aValue === bValue) continue;
          return direction === 1 ? String(aValue || "").localeCompare(String(bValue || "")) : String(bValue || "").localeCompare(String(aValue || ""));
        }
        return 0;
      });
    };
    this.prepareQueryUsersParams = (searchQuery, offset = 0) => ({
      filters: {
        $or: [
          { id: { $autocomplete: searchQuery } },
          { name: { $autocomplete: searchQuery } }
        ],
        ...this.userFilters
      },
      sort: this.userSort ?? [{ name: 1 }, { id: 1 }],
      // todo: document the change - the sort is overridden, not merged
      options: { ...this.searchOptions, limit: this.pageSize, offset }
    });
    this.prepareQueryMembersParams = (searchQuery, offset = 0) => {
      const maxSortParamsCount = 1;
      let sort = [{ user_id: 1 }];
      if (!this.memberSort) {
        sort = [{ user_id: 1 }];
      } else if (Array.isArray(this.memberSort)) {
        sort = this.memberSort[0];
      } else if (Object.keys(this.memberSort).length === maxSortParamsCount) {
        sort = this.memberSort;
      }
      return {
        // todo: document the change - the filter is overridden, not merged
        filters: this.memberFilters ?? { name: { $autocomplete: searchQuery } },
        // autocomplete possible only for name
        sort,
        options: { ...this.searchOptions, limit: this.pageSize, offset }
      };
    };
    this.queryUsers = async (searchQuery, offset = 0, queryOptions = {}) => {
      const { filters, sort, options } = this.prepareQueryUsersParams(searchQuery, offset);
      const { users } = await this.client.queryUsers(filters, sort, options, queryOptions);
      return users;
    };
    this.queryMembers = async (searchQuery, offset = 0, queryOptions = {}) => {
      const { filters, sort, options } = this.prepareQueryMembersParams(
        searchQuery,
        offset
      );
      const response = await this.channel.queryMembers(
        filters,
        sort,
        options,
        queryOptions
      );
      return response.members.map((member) => member.user);
    };
    this.getUserSuggestionsPage = async (searchQuery, userOffset = 0, queryOptions = {}) => {
      if (!this.isMentionTypeAllowed("user")) {
        return {
          items: [],
          nextOffset: void 0
        };
      }
      let users;
      const shouldSearchLocally = this.allMembersLoadedWithInitialChannelQuery || !searchQuery;
      if (this.config.mentionAllAppUsers) {
        users = await this.queryUsers(searchQuery, userOffset, queryOptions);
      } else if (shouldSearchLocally) {
        const localUsers = this.searchMembersLocally(searchQuery);
        const items2 = localUsers.slice(userOffset, userOffset + this.pageSize).map((user) => this.toUserSuggestion(user, searchQuery));
        return {
          items: items2,
          nextOffset: localUsers.length > userOffset + this.pageSize ? userOffset + items2.length : void 0
        };
      } else {
        users = await this.queryMembers(searchQuery, userOffset, queryOptions);
      }
      const items = users.map((user) => this.toUserSuggestion(user, searchQuery));
      return {
        items,
        nextOffset: users.length === this.pageSize ? userOffset + users.length : void 0
      };
    };
    this.buildUserGroupSearchCursor = (items) => {
      if (items.length < this.pageSize) return void 0;
      const lastItem = items[items.length - 1];
      if (!lastItem?.name) return void 0;
      return JSON.stringify({
        id_gt: lastItem.id,
        name_gt: lastItem.name
      });
    };
    this.getUserGroupSuggestionsPage = async (searchQuery, cursor, queryOptions = {}) => {
      if (!this.isMentionTypeAllowed("user_group")) {
        return {
          items: [],
          next: void 0
        };
      }
      if (!searchQuery) {
        return {
          items: [],
          next: void 0
        };
      }
      const teamId = this.getChannelTeam();
      const userGroupCursor = decodeUserGroupCursor(cursor);
      const options = {
        query: searchQuery,
        limit: this.pageSize,
        ...teamId ? { team_id: teamId } : {},
        ...userGroupCursor?.id_gt ? { id_gt: userGroupCursor.id_gt } : {},
        ...userGroupCursor?.name_gt ? { name_gt: userGroupCursor.name_gt } : {}
      };
      const { user_groups } = await this.client.searchUserGroups(options, queryOptions);
      return {
        items: user_groups.map(
          (userGroup) => this.toUserGroupMentionSuggestion(userGroup, searchQuery)
        ),
        next: this.buildUserGroupSearchCursor(user_groups)
      };
    };
    this.client = channel.getClient();
    this.channel = channel;
    this.userFilters = userFilters;
    this.userSort = userSort;
    this.memberFilters = memberFilters;
    this.memberSort = memberSort;
    this.searchOptions = searchOptions;
    this.config = {
      mentionAllAppUsers,
      suggestionFactoryMappers,
      textComposerText,
      trigger
    };
    if (transliterate) {
      this.transliterate = transliterate;
    }
  }
  get allMembersLoadedWithInitialChannelQuery() {
    const countLoadedMembers = Object.keys(this.channel.state.members || {}).length;
    return countLoadedMembers < MAX_CHANNEL_MEMBER_COUNT_IN_CHANNEL_QUERY;
  }
  getStateBeforeFirstQuery(newSearchString) {
    this.userGroupCursor = void 0;
    const newState = super.getStateBeforeFirstQuery(newSearchString);
    const { items } = this.state.getLatestValue();
    return {
      ...newState,
      items
      // preserve items to avoid flickering
    };
  }
  updatePaginationStateFromQuery() {
    const userPaginationState = this.latestUserPaginationState ?? { itemCount: 0 };
    return {
      hasNext: typeof userPaginationState.nextOffset !== "undefined" || typeof this.userGroupCursor !== "undefined",
      next: void 0,
      offset: (this.offset ?? 0) + userPaginationState.itemCount
    };
  }
  async query(searchQuery, queryOptions = {}) {
    const userOffset = this.offset ?? 0;
    const isFirstPage = userOffset === 0 && typeof this.userGroupCursor === "undefined";
    const previousUserPaginationState = this.latestUserPaginationState;
    const previousUserGroupCursor = this.userGroupCursor;
    const [userResultsState, userGroupResultsState, roleSuggestionsState] = await Promise.allSettled([
      this.getUserSuggestionsPage(searchQuery, userOffset, queryOptions),
      this.getUserGroupSuggestionsPage(
        searchQuery,
        previousUserGroupCursor,
        queryOptions
      ),
      isFirstPage ? this.getRoleMentionSuggestions(searchQuery, queryOptions) : Promise.resolve([])
    ]);
    if (queryOptions.signal?.aborted) return { items: [] };
    const userResults = userResultsState.status === "fulfilled" ? userResultsState.value : {
      items: [],
      nextOffset: isFirstPage ? void 0 : previousUserPaginationState?.nextOffset
    };
    const userGroupResults = userGroupResultsState.status === "fulfilled" ? userGroupResultsState.value : {
      items: [],
      next: isFirstPage ? void 0 : previousUserGroupCursor
    };
    const roleSuggestions = roleSuggestionsState.status === "fulfilled" ? roleSuggestionsState.value : [];
    const items = [
      ...isFirstPage ? this.getBuiltinMentionSuggestions(searchQuery) : [],
      ...roleSuggestions,
      ...userGroupResults.items,
      ...userResults.items
    ];
    this.latestUserPaginationState = {
      itemCount: userResults.items.length,
      nextOffset: userResults.nextOffset
    };
    this.userGroupCursor = userGroupResults.next;
    return {
      items
    };
  }
  filterMutes(data) {
    const { textComposerText } = this.config;
    if (!textComposerText) return [];
    const { mutedUsers } = this.client;
    if (textComposerText.includes("/unmute") && !mutedUsers.length) {
      return [];
    }
    if (!mutedUsers.length) return data;
    if (textComposerText.includes("/unmute")) {
      return data.filter(
        (suggestion) => suggestion.mentionType === "user" && mutedUsers.some((mute) => mute.target.id === suggestion.id)
      );
    }
    return data.filter(
      (suggestion) => suggestion.mentionType !== "user" || mutedUsers.every((mute) => mute.target.id !== suggestion.id)
    );
  }
  filterQueryResults(items) {
    return this.filterMutes(items);
  }
  resetState() {
    this.latestUserPaginationState = void 0;
    this.userGroupCursor = void 0;
    super.resetState();
  }
};
var DEFAULT_OPTIONS2 = { minChars: 1, trigger: "@" };
var createMentionsMiddleware = (channel, options) => {
  const finalOptions = mergeWith(DEFAULT_OPTIONS2, options ?? {});
  let searchSource;
  if (options?.searchSource) {
    searchSource = options.searchSource;
    searchSource.resetState();
  } else {
    searchSource = new MentionsSearchSource(channel, { trigger: finalOptions.trigger });
  }
  searchSource.activate();
  let lastInsertedMentionEndOffset;
  return {
    id: "stream-io/text-composer/mentions-middleware",
    handlers: {
      onChange: ({ state, next, complete, forward }) => {
        if (!state.selection) return forward();
        const cursorJustInsertedAMention = lastInsertedMentionEndOffset !== void 0 && state.selection.end === lastInsertedMentionEndOffset;
        lastInsertedMentionEndOffset = void 0;
        const currentMentions = state.command || state.text.trimStart().startsWith("/") ? state.mentionedUsers : getMentionedUsersInText(state.text, state.mentionedUsers);
        const mentionedUsersChanged = currentMentions.length !== state.mentionedUsers.length || currentMentions.some(
          (user, index) => user.id !== state.mentionedUsers[index]?.id
        );
        const stateWithMentions = mentionedUsersChanged ? { ...state, mentionedUsers: currentMentions } : state;
        const textBeforeCursor = stateWithMentions.text.slice(
          0,
          stateWithMentions.selection.end
        );
        const triggerWithToken = getTriggerCharWithToken({
          trigger: finalOptions.trigger,
          text: textBeforeCursor
        });
        const newSearchTriggered = triggerWithToken && triggerWithToken.length === finalOptions.minChars;
        if (newSearchTriggered) {
          searchSource.resetStateAndActivate();
        }
        const triggerMatchesCommittedMention = !!triggerWithToken && /\s$/.test(textBeforeCursor) && (cursorJustInsertedAMention || (stateWithMentions.mentions ?? []).some((entity) => {
          const insertedToken = entity.name ?? entity.id;
          if (!insertedToken) return false;
          const insertedForm = `@${insertedToken} `;
          if (!textBeforeCursor.endsWith(insertedForm)) return false;
          const escapedInsertedForm = insertedForm.replace(
            /[.*+?^${}()|[\]\\]/g,
            "\\$&"
          );
          const occurrences = textBeforeCursor.match(
            new RegExp(escapedInsertedForm, "g")
          );
          return (occurrences?.length ?? 0) === 1;
        }));
        const triggerWasRemoved = !triggerWithToken || triggerWithToken.length < finalOptions.minChars;
        if (triggerWasRemoved || triggerMatchesCommittedMention) {
          const hasStaleSuggestions = stateWithMentions.suggestions?.trigger === finalOptions.trigger;
          const newState = { ...stateWithMentions };
          if (hasStaleSuggestions) {
            delete newState.suggestions;
          }
          return next(newState);
        }
        searchSource.config.textComposerText = stateWithMentions.text;
        return complete({
          ...stateWithMentions,
          suggestions: {
            query: triggerWithToken.slice(1),
            searchSource,
            trigger: finalOptions.trigger
          }
        });
      },
      onSuggestionItemSelect: ({ state, complete, forward }) => {
        const { selectedSuggestion } = state.change ?? {};
        if (!selectedSuggestion || state.suggestions?.trigger !== finalOptions.trigger)
          return forward();
        searchSource.resetStateAndActivate();
        const mentionEntity = mentionSuggestionToEntity(selectedSuggestion);
        const mentions = upsertMentionEntity(
          state.mentions ?? userResponsesToMentionEntities(state.mentionedUsers),
          mentionEntity
        );
        const insertResult = insertItemWithTrigger({
          insertText: mentionSuggestionToInsertText(selectedSuggestion),
          selection: state.selection,
          text: state.text,
          trigger: finalOptions.trigger
        });
        lastInsertedMentionEndOffset = insertResult.selection.end;
        return complete({
          ...state,
          ...insertResult,
          mentionedUsers: selectedSuggestion.mentionType === "user" ? upsertUserResponse(
            state.mentionedUsers,
            userSuggestionToUserResponse(selectedSuggestion)
          ) : state.mentionedUsers,
          mentions,
          suggestions: void 0
        });
      }
    }
  };
};

// src/messageComposer/middleware/textComposer/validation.ts
var createTextComposerPreValidationMiddleware = (composer) => ({
  id: "stream-io/text-composer/pre-validation-middleware",
  handlers: {
    onChange: ({ state, next, forward }) => {
      const { maxLengthOnEdit } = composer.config.text ?? {};
      if (typeof maxLengthOnEdit === "number" && state.text.length > maxLengthOnEdit) {
        state.text = state.text.slice(0, maxLengthOnEdit);
        return next({
          ...state,
          text: state.text
        });
      }
      return forward();
    },
    onSuggestionItemSelect: ({ forward }) => forward()
  }
});

// src/messageComposer/middleware/textComposer/TextComposerMiddlewareExecutor.ts
var TextComposerMiddlewareExecutor = class extends MiddlewareExecutor {
  constructor({ composer }) {
    super();
    this.use([
      createTextComposerPreValidationMiddleware(composer),
      createMentionsMiddleware(composer.channel),
      createCommandsMiddleware(composer.channel, {
        composer
      }),
      createCommandEffectsMiddleware()
    ]);
  }
  async execute({
    eventName,
    initialValue: initialState
  }) {
    const result = await this.executeMiddlewareChain({
      eventName,
      initialValue: initialState
    });
    const { query, searchSource } = result.state.suggestions ?? {};
    searchSource?.search(query)?.catch(console.error);
    return result;
  }
};

// src/messageComposer/configuration/commands.configuration.ts
var MENTION_ONLY_COMMANDS = /* @__PURE__ */ new Set(["mute", "unmute", "unban"]);
var defaultCommandSendabilityValidator = ({
  command,
  commandArgsText,
  mentionedUsersInText
}) => {
  if (command.name !== "ban" && !MENTION_ONLY_COMMANDS.has(command.name ?? "")) return;
  if (mentionedUsersInText.length === 0) {
    return { command, ready: false, reason: "missing-mention" };
  }
  if (command.name !== "ban") {
    return { command, ready: true };
  }
  const banReason = stripMentionTokens(commandArgsText, mentionedUsersInText);
  if (!banReason.length) {
    return { command, ready: false, reason: "missing-ban-reason" };
  }
  return { command, ready: true };
};
var DEFAULT_COMMANDS_CONFIG = {
  sendValidator: defaultCommandSendabilityValidator
};
var applyCommandValidatorOverride = (targetConfig, sourceConfig) => {
  const overrideValidator = sourceConfig?.commands?.sendValidator;
  if (typeof overrideValidator === "undefined") {
    return targetConfig;
  }
  return {
    ...targetConfig,
    commands: {
      ...targetConfig.commands,
      sendValidator: overrideValidator
    }
  };
};

// src/messageComposer/configuration/configuration.ts
var DEFAULT_LINK_PREVIEW_MANAGER_CONFIG = {
  debounceURLEnrichmentMs: 1500,
  enabled: false,
  findURLFn: (text) => (0, import_linkifyjs.find)(text, "url", { defaultProtocol: "https" }).reduce((acc, link) => {
    try {
      const url = new URL(link.href);
      if (link.isLink && /^[a-zA-Z0-9-.]+\.[a-zA-Z]{2,}$/.test(url.hostname)) {
        acc.push(link.href);
      }
    } catch {
    }
    return acc;
  }, [])
};
var DEFAULT_ATTACHMENT_MANAGER_CONFIG = {
  acceptedFiles: [],
  // an empty array means all files are accepted
  fileUploadFilter: () => true,
  maxNumberOfFilesPerMessage: API_MAX_FILES_ALLOWED_PER_MESSAGE,
  trackUploadProgress: true
};
var DEFAULT_TEXT_COMPOSER_CONFIG = {
  enabled: true,
  publishTypingEvents: true
};
var DEFAULT_LOCATION_COMPOSER_CONFIG = {
  enabled: true,
  getDeviceId: () => generateUUIDv4()
};
var DEFAULT_COMPOSER_CONFIG = {
  attachments: DEFAULT_ATTACHMENT_MANAGER_CONFIG,
  commands: DEFAULT_COMMANDS_CONFIG,
  drafts: { enabled: false },
  linkPreviews: DEFAULT_LINK_PREVIEW_MANAGER_CONFIG,
  location: DEFAULT_LOCATION_COMPOSER_CONFIG,
  text: DEFAULT_TEXT_COMPOSER_CONFIG
};

// src/messageComposer/CustomDataManager.ts
var initState4 = (options) => {
  if (!options)
    return { message: {}, custom: {} };
  return { message: {}, custom: {} };
};
var CustomDataManager = class {
  constructor({ composer, message }) {
    this.isMessageDataEqual = (nextState, previousState) => JSON.stringify(nextState.message) === JSON.stringify(previousState?.message);
    this.initState = ({ message } = {}) => {
      this.state.next(initState4({ composer: this.composer, message }));
    };
    this.getSnapshot = () => this.state.getLatestValue();
    this.restoreSnapshot = (snapshot) => {
      this.state.next(snapshot);
    };
    this.composer = composer;
    this.state = new StateStore(initState4({ composer, message }));
  }
  get customMessageData() {
    return this.state.getLatestValue().message;
  }
  get customComposerData() {
    return this.state.getLatestValue().custom;
  }
  setMessageData(data) {
    this.state.partialNext({
      message: {
        ...this.state.getLatestValue().message,
        ...data
      }
    });
  }
  setCustomData(data) {
    this.state.partialNext({
      custom: {
        ...this.state.getLatestValue().custom,
        ...data
      }
    });
  }
};

// src/messageComposer/linkPreviewsManager.ts
var LinkPreviewStatus = /* @__PURE__ */ ((LinkPreviewStatus2) => {
  LinkPreviewStatus2["DISMISSED"] = "dismissed";
  LinkPreviewStatus2["FAILED"] = "failed";
  LinkPreviewStatus2["LOADED"] = "loaded";
  LinkPreviewStatus2["LOADING"] = "loading";
  LinkPreviewStatus2["PENDING"] = "pending";
  return LinkPreviewStatus2;
})(LinkPreviewStatus || {});
var linkPreviewArrayToMap = (linkPreviews) => new Map(linkPreviews.map((linkPreview) => [linkPreview.og_scrape_url, linkPreview]));
var initState5 = ({
  message
}) => message ? {
  previews: message.attachments?.reduce((acc, attachment) => {
    if (!attachment.og_scrape_url) return acc;
    acc.set(attachment.og_scrape_url, {
      ...attachment,
      status: "loaded" /* LOADED */
    });
    return acc;
  }, /* @__PURE__ */ new Map()) ?? /* @__PURE__ */ new Map()
} : {
  previews: /* @__PURE__ */ new Map()
};
var _LinkPreviewsManager = class _LinkPreviewsManager {
  constructor({ composer, message }) {
    this.shouldDiscardEnrichQueries = false;
    this.initState = ({ message } = {}) => {
      this.cancelURLEnrichment();
      this.state.next(initState5({ message: this.enabled ? message : void 0 }));
    };
    this.getSnapshot = () => this.state.getLatestValue();
    this.restoreSnapshot = (snapshot) => {
      this.state.next(snapshot);
    };
    this._findAndEnrichUrls = async (text) => {
      if (!this.enabled) return;
      const urls = this.config.findURLFn(text);
      this.shouldDiscardEnrichQueries = !urls.length;
      if (this.shouldDiscardEnrichQueries) {
        this.state.next({ previews: /* @__PURE__ */ new Map() });
        return;
      }
      const keptPreviews = new Map(
        Array.from(this.previews).filter(
          ([previewUrl]) => urls.includes(previewUrl) || urls.includes(previewUrl + "/")
        )
      );
      const newLinkPreviews = urls.filter((url) => {
        const existingPreviews = this.previews;
        const existingPreviewLink = existingPreviews.get(url) || existingPreviews.get(url + "/");
        return !existingPreviewLink;
      }).map(
        (url) => ({
          og_scrape_url: url.trim(),
          status: "loading" /* LOADING */
        })
      );
      if (!newLinkPreviews.length) return;
      this.state.partialNext({
        previews: new Map([...keptPreviews, ...linkPreviewArrayToMap(newLinkPreviews)])
      });
      await Promise.all(
        newLinkPreviews.map(async (linkPreview) => {
          try {
            const { duration, ...ogAttachment } = await this.client.enrichURL(
              linkPreview.og_scrape_url
            );
            if (this.shouldDiscardEnrichQueries) return;
            if (this.previews.has(linkPreview.og_scrape_url)) {
              this.updatePreview(linkPreview.og_scrape_url, {
                status: "loaded" /* LOADED */,
                ...ogAttachment
              });
            }
          } catch (error) {
            if (this.previews.has(linkPreview.og_scrape_url)) {
              this.updatePreview(linkPreview.og_scrape_url, {
                status: "failed" /* FAILED */
              });
            }
          }
          return linkPreview;
        })
      );
    };
    this.cancelURLEnrichment = () => {
      this.findAndEnrichUrls.cancel?.();
      this.findAndEnrichUrls.flush?.();
    };
    /**
     * Clears all non-dismissed previews when the text composer is cleared.
     * This ensures that dismissed previews are not re-enriched in the future.
     */
    this.clearPreviews = () => {
      const currentPreviews = this.previews;
      const newPreviews = /* @__PURE__ */ new Map();
      currentPreviews.forEach((preview, url) => {
        if (_LinkPreviewsManager.previewIsDismissed(preview)) {
          newPreviews.set(url, preview);
        }
      });
      this.state.partialNext({ previews: newPreviews });
    };
    this.updatePreview = (url, preview) => {
      if (!url) return;
      const existingPreview = this.previews.get(url);
      const status = preview.status ?? this.previews.get(url)?.status ?? "pending" /* PENDING */;
      let finalPreview = preview;
      if (existingPreview) {
        const merged = mergeWithDiff(existingPreview, preview);
        const isSame = !merged.diff || Object.keys(merged.diff).length === 0;
        if (isSame) return;
        finalPreview = merged.result;
      }
      this.state.partialNext({
        previews: new Map(this.previews).set(url, {
          ...finalPreview,
          og_scrape_url: url,
          status
        })
      });
    };
    this.dismissPreview = (url) => {
      const preview = this.previews.get(url);
      if (preview) {
        this.onLinkPreviewDismissed?.(preview);
        this.updatePreview(url, { status: "dismissed" /* DISMISSED */ });
      }
    };
    this.composer = composer;
    this.state = new StateStore(
      initState5({ message: this.enabled ? message : void 0 })
    );
    this.findAndEnrichUrls = debounce(
      this._findAndEnrichUrls.bind(this),
      this.config.debounceURLEnrichmentMs
    );
  }
  get client() {
    return this.composer.client;
  }
  get channel() {
    return this.composer.channel;
  }
  get previews() {
    return this.state.getLatestValue().previews;
  }
  get loadingPreviews() {
    return Array.from(this.previews.values()).filter(
      (linkPreview) => _LinkPreviewsManager.previewIsLoading(linkPreview)
    );
  }
  get loadedPreviews() {
    return Array.from(this.previews.values()).filter(
      (linkPreview) => _LinkPreviewsManager.previewIsLoaded(linkPreview)
    );
  }
  get dismissedPreviews() {
    return Array.from(this.previews.values()).filter(
      (linkPreview) => _LinkPreviewsManager.previewIsDismissed(linkPreview)
    );
  }
  get failedPreviews() {
    return Array.from(this.previews.values()).filter(
      (linkPreview) => _LinkPreviewsManager.previewIsFailed(linkPreview)
    );
  }
  get pendingPreviews() {
    return Array.from(this.previews.values()).filter(
      (linkPreview) => _LinkPreviewsManager.previewIsPending(linkPreview)
    );
  }
  get config() {
    return this.composer.config.linkPreviews;
  }
  get debounceURLEnrichmentMs() {
    return this.config.debounceURLEnrichmentMs;
  }
  set debounceURLEnrichmentMs(debounceURLEnrichmentMs) {
    this.cancelURLEnrichment();
    this.findAndEnrichUrls = debounce(
      this._findAndEnrichUrls.bind(this),
      this.config.debounceURLEnrichmentMs
    );
    this.composer.updateConfig({ linkPreviews: { debounceURLEnrichmentMs } });
  }
  get enabled() {
    return !!this.channel.getConfig()?.url_enrichment && this.composer.config.linkPreviews.enabled;
  }
  set enabled(enabled) {
    if (enabled === this.enabled) return;
    this.composer.updateConfig({ linkPreviews: { enabled } });
  }
  get findURLFn() {
    return this.config.findURLFn;
  }
  set findURLFn(fn) {
    this.composer.updateConfig({ linkPreviews: { findURLFn: fn } });
  }
  get onLinkPreviewDismissed() {
    return this.config.onLinkPreviewDismissed;
  }
  set onLinkPreviewDismissed(fn) {
    this.composer.updateConfig({ linkPreviews: { onLinkPreviewDismissed: fn } });
  }
};
_LinkPreviewsManager.previewIsLoading = (preview) => preview.status === "loading" /* LOADING */;
_LinkPreviewsManager.previewIsLoaded = (preview) => preview.status === "loaded" /* LOADED */;
_LinkPreviewsManager.previewIsDismissed = (preview) => preview.status === "dismissed" /* DISMISSED */;
_LinkPreviewsManager.previewIsFailed = (preview) => preview.status === "failed" /* FAILED */;
_LinkPreviewsManager.previewIsPending = (preview) => preview.status === "pending" /* PENDING */;
_LinkPreviewsManager.getPreviewData = (preview) => {
  const { status, ...data } = preview;
  return data;
};
var LinkPreviewsManager = _LinkPreviewsManager;

// src/messageComposer/LocationComposer.ts
var MIN_LIVE_LOCATION_SHARE_DURATION = 60 * 1e3;
var initState6 = ({
  message
}) => ({
  location: message?.shared_location ?? null
});
var LocationComposer = class {
  constructor({ composer, message }) {
    this.initState = ({ message } = {}) => {
      this.state.next(initState6({ message }));
    };
    this.getSnapshot = () => this.state.getLatestValue();
    this.restoreSnapshot = (snapshot) => {
      this.state.next(snapshot);
    };
    this.setData = (data) => {
      if (!this.config.enabled) return;
      if (!data.latitude || !data.longitude) return;
      this.state.partialNext({
        location: {
          ...data,
          message_id: this.composer.id,
          created_by_device_id: this.deviceId
        }
      });
    };
    this.composer = composer;
    this.state = new StateStore(initState6({ message }));
    this._deviceId = this.config.getDeviceId();
  }
  get config() {
    return this.composer.config.location;
  }
  get deviceId() {
    return this._deviceId;
  }
  get location() {
    return this.state.getLatestValue().location;
  }
  get validLocation() {
    const { durationMs, ...location } = this.location ?? {};
    if (!!location?.created_by_device_id && location.message_id && location.latitude && location.longitude && (typeof durationMs === "undefined" || durationMs >= MIN_LIVE_LOCATION_SHARE_DURATION)) {
      return {
        ...location,
        end_at: durationMs && new Date(Date.now() + durationMs).toISOString()
      };
    }
    return null;
  }
};

// src/messageComposer/MessageComposerEffectHandlers.ts
var applyCommandActivationEffect = (effect, composer) => {
  const snapshot = composer.getSnapshot();
  if (effect.stateToRestore) {
    snapshot.textComposer = {
      ...snapshot.textComposer,
      ...effect.stateToRestore,
      command: null
    };
  }
  composer.captureSnapshot(snapshot);
  composer.textComposer.state.next({
    command: effect.command,
    mentionedUsers: [],
    mentions: [],
    suggestions: void 0,
    selection: { start: 0, end: 0 },
    text: ""
  });
  const attachmentsToCancel = composer.attachmentManager.attachments;
  composer.attachmentManager.initState();
  composer.attachmentManager.cancelAttachmentUploads(attachmentsToCancel);
  composer.linkPreviewsManager.initState();
  composer.locationComposer.initState();
  composer.pollComposer.initState();
  composer.customDataManager.initState();
};
var applyCommandClearEffect = (_, composer) => {
  const snapshot = composer.popSnapshot();
  if (!snapshot) return;
  composer.restoreSnapshot(snapshot);
};
var MessageComposerEffectHandlers = class {
  constructor(options) {
    this.options = options;
    this.handlers = /* @__PURE__ */ new Map();
    this.registerDefaultHandlers = () => {
      this.registerEffectHandler(
        "command.activate",
        applyCommandActivationEffect
      );
      this.registerEffectHandler(
        "command.clear",
        applyCommandClearEffect
      );
    };
    this.registerEffectHandler = (type, handler) => {
      this.handlers.set(type, handler);
    };
    this.applyEffects = (effects = []) => {
      effects.forEach((effect) => this.applyEffect(effect));
    };
    this.applyEffect = (effect) => {
      const handler = this.handlers.get(effect.type);
      handler?.(effect, this.options.composer);
    };
    this.registerDefaultHandlers();
  }
};

// src/types.ts
var Product = /* @__PURE__ */ ((Product2) => {
  Product2["Chat"] = "chat";
  Product2["Video"] = "video";
  Product2["Moderation"] = "moderation";
  Product2["Feeds"] = "feeds";
  return Product2;
})(Product || {});
var ErrorFromResponse = class extends Error {
  constructor(message, {
    code,
    status,
    response
  }) {
    super(message);
    this.name = "ErrorFromResponse";
    this.code = code;
    this.response = response;
    this.status = status;
  }
  // Vitest helper (serialized errors are too large to read)
  // https://github.com/vitest-dev/vitest/blob/v3.1.3/packages/utils/src/error.ts#L60-L62
  toJSON() {
    const extra = [
      ["status", this.status],
      ["code", this.code]
    ];
    const joinable = [];
    for (const [key, value] of extra) {
      if (typeof value !== "undefined" && value !== null) {
        joinable.push(`${key}: ${value}`);
      }
    }
    return {
      message: `(${joinable.join(", ")}) - ${this.message}`,
      stack: this.stack,
      name: this.name,
      code: this.code,
      status: this.status
    };
  }
};
var VotingVisibility = /* @__PURE__ */ ((VotingVisibility2) => {
  VotingVisibility2["anonymous"] = "anonymous";
  VotingVisibility2["public"] = "public";
  return VotingVisibility2;
})(VotingVisibility || {});

// src/messageComposer/pollComposer.ts
var PollComposer = class {
  constructor({ composer }) {
    this.initState = () => {
      this.state.next(this.initialState);
    };
    this.getSnapshot = () => this.state.getLatestValue();
    this.restoreSnapshot = (snapshot) => {
      this.state.next(snapshot);
    };
    /**
     * Updates specified fields and generates relevant errors
     * @param data
     * @param injectedFieldErrors - errors produced externally that will take precedence over the errors generated in the middleware chaing
     */
    // FIXME: change method params to a single object with the next major release
    this.updateFields = async (data, injectedFieldErrors) => {
      const { state, status } = await this.stateMiddlewareExecutor.execute({
        eventName: "handleFieldChange",
        initialValue: {
          nextState: { ...this.state.getLatestValue() },
          previousState: { ...this.state.getLatestValue() },
          targetFields: data,
          injectedFieldErrors
        }
      });
      if (status === "discard") return;
      this.state.next(state.nextState);
    };
    this.handleFieldBlur = async (field) => {
      const result = await this.stateMiddlewareExecutor.execute({
        eventName: "handleFieldBlur",
        initialValue: {
          nextState: { ...this.state.getLatestValue() },
          previousState: { ...this.state.getLatestValue() },
          targetFields: { [field]: this.state.getLatestValue().data[field] }
        }
      });
      if (result.status === "discard") return;
      this.state.next(result.state.nextState);
    };
    this.compose = async () => {
      const { data, errors } = this.state.getLatestValue();
      const result = await this.compositionMiddlewareExecutor.execute({
        eventName: "compose",
        initialValue: {
          data: {
            ...data,
            max_votes_allowed: data.max_votes_allowed ? parseInt(data.max_votes_allowed) : void 0,
            options: data.options?.filter((o) => o.text.trim()).map((o) => ({ text: o.text }))
          },
          errors
        }
      });
      if (result.status === "discard") return;
      return result.state;
    };
    this.composer = composer;
    this.state = new StateStore(this.initialState);
    this.compositionMiddlewareExecutor = new PollComposerCompositionMiddlewareExecutor({
      composer
    });
    this.stateMiddlewareExecutor = new PollComposerStateMiddlewareExecutor();
  }
  get initialState() {
    return {
      data: {
        allow_answers: false,
        allow_user_suggested_options: false,
        description: "",
        enforce_unique_vote: true,
        id: generateUUIDv4(),
        max_votes_allowed: "",
        name: "",
        options: [{ id: generateUUIDv4(), text: "" }],
        user_id: this.composer.client.user?.id,
        voting_visibility: "public" /* public */
      },
      errors: {}
    };
  }
  get allow_answers() {
    return this.state.getLatestValue().data.allow_answers;
  }
  get allow_user_suggested_options() {
    return this.state.getLatestValue().data.allow_user_suggested_options;
  }
  get description() {
    return this.state.getLatestValue().data.description;
  }
  get enforce_unique_vote() {
    return this.state.getLatestValue().data.enforce_unique_vote;
  }
  get id() {
    return this.state.getLatestValue().data.id;
  }
  get max_votes_allowed() {
    return this.state.getLatestValue().data.max_votes_allowed;
  }
  get name() {
    return this.state.getLatestValue().data.name;
  }
  get options() {
    return this.state.getLatestValue().data.options;
  }
  get user_id() {
    return this.state.getLatestValue().data.user_id;
  }
  get voting_visibility() {
    return this.state.getLatestValue().data.voting_visibility;
  }
  get canCreatePoll() {
    const { data, errors } = this.state.getLatestValue();
    const hasAtLeastOneNonEmptyOption = data.options.filter((o) => !!o.text.trim()).length > 0;
    const hasName = !!data.name;
    const maxVotesAllowedNumber = parseInt(
      data.max_votes_allowed?.match(VALID_MAX_VOTES_VALUE_REGEX)?.[0] || ""
    );
    const validMaxVotesAllowed = data.max_votes_allowed === "" || !!maxVotesAllowedNumber && (2 <= maxVotesAllowedNumber || maxVotesAllowedNumber <= 10);
    return hasAtLeastOneNonEmptyOption && hasName && validMaxVotesAllowed && Object.values(errors).filter((errorText) => !!errorText).length === 0;
  }
};

// src/thread.ts
var DEFAULT_PAGE_LIMIT = 50;
var DEFAULT_SORT = [{ created_at: -1 }];
var MARK_AS_READ_THROTTLE_TIMEOUT = 1e3;
var THREAD_RESPONSE_RESERVED_KEYS = {
  active_participant_count: true,
  channel: true,
  channel_cid: true,
  created_at: true,
  created_by: true,
  created_by_user_id: true,
  deleted_at: true,
  draft: true,
  last_message_at: true,
  latest_replies: true,
  parent_message: true,
  parent_message_id: true,
  participant_count: true,
  read: true,
  reply_count: true,
  thread_participants: true,
  title: true,
  updated_at: true
};
var constructCustomDataObject = (threadData) => {
  const custom = {};
  for (const key in threadData) {
    if (THREAD_RESPONSE_RESERVED_KEYS[key]) {
      continue;
    }
    const customKey = key;
    custom[customKey] = threadData[customKey];
  }
  return custom;
};
var Thread = class extends WithSubscriptions {
  constructor({
    client,
    threadData
  }) {
    super();
    this.failedRepliesMap = /* @__PURE__ */ new Map();
    this.activate = () => {
      this.state.partialNext({ active: true });
    };
    this.deactivate = () => {
      this.state.partialNext({ active: false });
    };
    this.reload = async () => {
      if (this.state.getLatestValue().isLoading) {
        return;
      }
      this.state.partialNext({ isLoading: true });
      try {
        const thread = await this.client.getThread(this.id, { watch: true });
        this.hydrateState(thread);
      } finally {
        this.state.partialNext({ isLoading: false });
      }
    };
    this.hydrateState = (thread) => {
      if (thread === this) {
        return;
      }
      if (thread.id !== this.id) {
        throw new Error(
          "Cannot hydrate thread's state using thread with different threadId"
        );
      }
      const {
        createdAt,
        custom,
        title,
        deletedAt,
        parentMessage,
        participants,
        read,
        replyCount,
        replies,
        updatedAt
      } = thread.state.getLatestValue();
      const pendingReplies = Array.from(this.failedRepliesMap.values());
      this.state.partialNext({
        title,
        createdAt,
        custom,
        deletedAt,
        parentMessage,
        participants,
        read,
        replyCount,
        replies: pendingReplies.length ? replies.concat(pendingReplies) : replies,
        updatedAt,
        isStateStale: false
      });
    };
    this.registerSubscriptions = () => {
      if (this.hasSubscriptions) {
        return;
      }
      this.addUnsubscribeFunction(this.subscribeThreadUpdated());
      this.addUnsubscribeFunction(this.subscribeMarkActiveThreadRead());
      this.addUnsubscribeFunction(this.subscribeReloadActiveStaleThread());
      this.addUnsubscribeFunction(this.subscribeMarkThreadStale());
      this.addUnsubscribeFunction(this.subscribeNewReplies());
      this.addUnsubscribeFunction(this.subscribeRepliesRead());
      this.addUnsubscribeFunction(this.subscribeRepliesUnread());
      this.addUnsubscribeFunction(this.subscribeMessageDeleted());
      this.addUnsubscribeFunction(this.subscribeMessageUpdated());
    };
    this.subscribeThreadUpdated = () => this.client.on("thread.updated", (event) => {
      if (!event.thread || event.thread.parent_message_id !== this.id) {
        return;
      }
      const threadData = event.thread;
      this.state.partialNext({
        title: threadData.title,
        updatedAt: new Date(threadData.updated_at),
        deletedAt: threadData.deleted_at ? new Date(threadData.deleted_at) : null,
        // TODO: use threadData.custom once we move to API v2
        custom: constructCustomDataObject(threadData)
      });
    }).unsubscribe;
    this.subscribeMarkActiveThreadRead = () => this.state.subscribeWithSelector(
      (nextValue) => ({
        active: nextValue.active,
        unreadMessageCount: ownUnreadCountSelector(this.client.userID)(nextValue)
      }),
      ({ active, unreadMessageCount }) => {
        if (!active || !unreadMessageCount) return;
        this.throttledMarkAsRead();
      }
    );
    this.subscribeReloadActiveStaleThread = () => this.state.subscribeWithSelector(
      (nextValue) => ({ active: nextValue.active, isStateStale: nextValue.isStateStale }),
      ({ active, isStateStale }) => {
        if (active && isStateStale) {
          this.reload();
        }
      }
    );
    this.subscribeMarkThreadStale = () => this.client.on("user.watching.stop", (event) => {
      const { channel } = this.state.getLatestValue();
      if (!this.client.userID || this.client.userID !== event.user?.id || event.channel?.cid !== channel.cid) {
        return;
      }
      this.state.partialNext({ isStateStale: true });
    }).unsubscribe;
    this.subscribeRepliesUnread = () => this.client.on("notification.mark_unread", (event) => {
      if (!event.user || !event.created_at || !event.thread_id) return;
      if (event.thread_id !== this.id) return;
      const userId = event.user.id;
      const createdAt = event.created_at;
      const user = event.user;
      this.state.next((current) => ({
        ...current,
        read: {
          ...current.read,
          [userId]: {
            ...current.read[userId],
            lastReadAt: typeof event.last_read_at !== "undefined" ? new Date(event.last_read_at) : new Date(createdAt),
            user,
            firstUnreadMessageId: event.first_unread_message_id,
            unreadMessageCount: event.unread_messages ?? 0
          }
        }
      }));
    }).unsubscribe;
    this.subscribeNewReplies = () => this.client.on("message.new", (event) => {
      if (!this.client.userID || event.message?.parent_id !== this.id) {
        return;
      }
      const isOwnMessage = event.message.user?.id === this.client.userID;
      const { active, read } = this.state.getLatestValue();
      this.upsertReplyLocally({
        message: event.message,
        // Message from current user could have been added optimistically,
        // so the actual timestamp might differ in the event
        timestampChanged: isOwnMessage
      });
      if (active) {
        this.throttledMarkAsRead();
      }
      const nextRead = {};
      for (const userId of Object.keys(read)) {
        const userRead = read[userId];
        if (userRead) {
          let nextUserRead = userRead;
          if (userId === event.user?.id) {
            nextUserRead = {
              ...nextUserRead,
              lastReadAt: event.created_at ? new Date(event.created_at) : /* @__PURE__ */ new Date(),
              user: event.user,
              unreadMessageCount: 0
            };
          } else if (active && userId === this.client.userID) {
          } else {
            nextUserRead = {
              ...nextUserRead,
              unreadMessageCount: userRead.unreadMessageCount + 1
            };
          }
          nextRead[userId] = nextUserRead;
        }
      }
      this.state.partialNext({ read: nextRead });
    }).unsubscribe;
    this.subscribeRepliesRead = () => this.client.on("message.read", (event) => {
      if (!event.user || !event.created_at || !event.thread) return;
      if (event.thread.parent_message_id !== this.id) return;
      const userId = event.user.id;
      const createdAt = event.created_at;
      const user = event.user;
      this.state.next((current) => ({
        ...current,
        read: {
          ...current.read,
          [userId]: {
            lastReadAt: new Date(createdAt),
            user,
            lastReadMessageId: event.last_read_message_id,
            unreadMessageCount: 0
          }
        }
      }));
    }).unsubscribe;
    this.subscribeMessageDeleted = () => this.client.on("message.deleted", (event) => {
      if (!event.message) return;
      if (event.message.parent_id === this.id) {
        if (event.hard_delete) {
          this.deleteReplyLocally({ message: event.message });
        } else {
          this.upsertReplyLocally({ message: event.message });
        }
      }
      if (event.message.id === this.id) {
        this.updateParentMessageLocally({ message: event.message });
      }
    }).unsubscribe;
    this.subscribeMessageUpdated = () => {
      const eventTypes = [
        "message.updated",
        "reaction.new",
        "reaction.deleted",
        "reaction.updated"
      ];
      const unsubscribeFunctions = eventTypes.map(
        (eventType) => this.client.on(eventType, (event) => {
          if (event.message) {
            this.updateParentMessageOrReplyLocally(event.message);
          }
        }).unsubscribe
      );
      return () => unsubscribeFunctions.forEach((unsubscribe) => unsubscribe());
    };
    this.unregisterSubscriptions = () => {
      const symbol = super.unregisterSubscriptions();
      this.state.partialNext({ isStateStale: true });
      return symbol;
    };
    this.deleteReplyLocally = ({ message }) => {
      const { replies } = this.state.getLatestValue();
      const index = findIndexInSortedArray({
        needle: formatMessage(message),
        sortedArray: replies,
        sortDirection: "ascending",
        selectValueToCompare: (reply) => reply.created_at.getTime(),
        selectKey: (reply) => reply.id
      });
      if (replies[index]?.id !== message.id) {
        return;
      }
      const updatedReplies = [...replies];
      updatedReplies.splice(index, 1);
      this.state.partialNext({
        replies: updatedReplies
      });
    };
    this.upsertReplyLocally = ({
      message,
      timestampChanged = false
    }) => {
      if (message.parent_id !== this.id) {
        throw new Error("Reply does not belong to this thread");
      }
      const formattedMessage = formatMessage(message);
      if (message.status === "failed") {
        this.failedRepliesMap.set(formattedMessage.id, formattedMessage);
      } else if (this.failedRepliesMap.has(message.id)) {
        this.failedRepliesMap.delete(message.id);
      }
      this.state.next((current) => ({
        ...current,
        replies: addToMessageList(current.replies, formattedMessage, timestampChanged)
      }));
    };
    this.updateParentMessageLocally = ({ message }) => {
      if (message.id !== this.id) {
        throw new Error("Message does not belong to this thread");
      }
      this.state.next((current) => {
        const formattedMessage = formatMessage(message);
        return {
          ...current,
          deletedAt: formattedMessage.deleted_at,
          parentMessage: formattedMessage,
          replyCount: message.reply_count ?? current.replyCount
        };
      });
    };
    this.updateParentMessageOrReplyLocally = (message) => {
      if (message.parent_id === this.id) {
        this.upsertReplyLocally({ message });
      }
      if (!message.parent_id && message.id === this.id) {
        this.updateParentMessageLocally({ message });
      }
    };
    this.markAsRead = async ({ force = false } = {}) => {
      if (this.ownUnreadCount === 0 && !force) {
        return null;
      }
      return await this.client.messageDeliveryReporter.markRead(this);
    };
    this.throttledMarkAsRead = throttle(
      () => this.markAsRead(),
      MARK_AS_READ_THROTTLE_TIMEOUT,
      { trailing: true }
    );
    this.queryReplies = ({
      limit = DEFAULT_PAGE_LIMIT,
      sort = DEFAULT_SORT,
      ...otherOptions
    } = {}) => this.channel.getReplies(this.id, { limit, ...otherOptions }, sort);
    this.loadNextPage = ({ limit = DEFAULT_PAGE_LIMIT } = {}) => this.loadPage(limit);
    this.loadPrevPage = ({ limit = DEFAULT_PAGE_LIMIT } = {}) => this.loadPage(-limit);
    this.loadPage = async (count) => {
      const { pagination } = this.state.getLatestValue();
      const [loadingKey, cursorKey, insertionMethodKey] = count > 0 ? ["isLoadingNext", "nextCursor", "push"] : ["isLoadingPrev", "prevCursor", "unshift"];
      if (pagination[loadingKey] || pagination[cursorKey] === null) return;
      const queryOptions = { [count > 0 ? "id_gt" : "id_lt"]: pagination[cursorKey] };
      const limit = Math.abs(count);
      this.state.partialNext({ pagination: { ...pagination, [loadingKey]: true } });
      try {
        const data = await this.queryReplies({ ...queryOptions, limit });
        const replies = data.messages.map(formatMessage);
        const maybeNextCursor = replies.at(count > 0 ? -1 : 0)?.id ?? null;
        this.state.next((current) => {
          let nextReplies = current.replies;
          if (replies.length > 0) {
            nextReplies = [...current.replies];
            nextReplies[insertionMethodKey](...replies);
          }
          return {
            ...current,
            replies: nextReplies,
            pagination: {
              ...current.pagination,
              [cursorKey]: data.messages.length < limit ? null : maybeNextCursor,
              [loadingKey]: false
            }
          };
        });
      } catch (error) {
        this.client.logger("error", error.message);
        this.state.next((current) => ({
          ...current,
          pagination: {
            ...current.pagination,
            [loadingKey]: false
          }
        }));
      }
    };
    const channel = client.channel(threadData.channel.type, threadData.channel.id, {
      // @ts-expect-error name is a "custom" property
      name: threadData.channel.name
    });
    channel._hydrateMembers({
      members: threadData.channel.members ?? [],
      overrideCurrentState: false
    });
    const placeholderReadResponse = client.userID ? [
      {
        user: { id: client.userID },
        unread_messages: 0,
        last_read: (/* @__PURE__ */ new Date()).toISOString()
      }
    ] : [];
    this.state = new StateStore({
      // local only
      active: false,
      isLoading: false,
      isStateStale: false,
      // 99.9% should never change
      channel,
      createdAt: new Date(threadData.created_at),
      // rest
      deletedAt: threadData.deleted_at ? new Date(threadData.deleted_at) : null,
      pagination: repliesPaginationFromInitialThread(threadData),
      parentMessage: formatMessage(threadData.parent_message),
      participants: threadData.thread_participants,
      read: formatReadState(
        !threadData.read || threadData.read.length === 0 ? placeholderReadResponse : threadData.read
      ),
      replies: threadData.latest_replies.map(formatMessage),
      replyCount: threadData.reply_count ?? 0,
      updatedAt: threadData.updated_at ? new Date(threadData.updated_at) : null,
      title: threadData.title,
      custom: constructCustomDataObject(threadData)
    });
    this.id = threadData.parent_message_id;
    this.client = client;
    this.messageComposer = new MessageComposer({
      client,
      composition: threadData.draft,
      compositionContext: this
    });
  }
  get channel() {
    return this.state.getLatestValue().channel;
  }
  get hasStaleState() {
    return this.state.getLatestValue().isStateStale;
  }
  get ownUnreadCount() {
    return ownUnreadCountSelector(this.client.userID)(this.state.getLatestValue());
  }
};
var formatReadState = (read) => read.reduce((state, userRead) => {
  state[userRead.user.id] = {
    user: userRead.user,
    lastReadMessageId: userRead.last_read_message_id,
    unreadMessageCount: userRead.unread_messages ?? 0,
    lastReadAt: new Date(userRead.last_read)
  };
  return state;
}, {});
var repliesPaginationFromInitialThread = (thread) => {
  const latestRepliesContainsAllReplies = thread.latest_replies.length === thread.reply_count;
  return {
    nextCursor: null,
    prevCursor: latestRepliesContainsAllReplies ? null : thread.latest_replies.at(0)?.id ?? null,
    isLoadingNext: false,
    isLoadingPrev: false
  };
};
var ownUnreadCountSelector = (currentUserId) => (state) => currentUserId && state.read[currentUserId]?.unreadMessageCount || 0;

// src/messageComposer/messageComposer.ts
var compositionIsDraftResponse = (composition) => !!composition?.message;
var initEditingAuditState = (composition) => {
  let draftUpdate = null;
  let stateUpdate = (/* @__PURE__ */ new Date()).getTime();
  if (compositionIsDraftResponse(composition)) {
    stateUpdate = draftUpdate = new Date(composition.created_at).getTime();
  } else if (composition && isLocalMessage(composition)) {
    stateUpdate = new Date(composition.updated_at).getTime();
  }
  return {
    lastChange: {
      draftUpdate,
      stateUpdate
    }
  };
};
var initState7 = (composition) => {
  if (!composition) {
    return {
      draftId: null,
      id: MessageComposer.generateId(),
      pollId: null,
      quotedMessage: null,
      showReplyInChannel: false,
      editedMessage: null
    };
  }
  const quotedMessage = composition.quoted_message;
  const editedMessage = compositionIsDraftResponse(composition) ? null : formatMessage(composition);
  let message;
  let draftId = null;
  let id = MessageComposer.generateId();
  if (compositionIsDraftResponse(composition)) {
    message = composition.message;
    draftId = composition.message.id;
  } else {
    message = composition;
    id = composition.id;
  }
  return {
    draftId,
    id,
    pollId: message.poll_id ?? null,
    quotedMessage: quotedMessage ? formatMessage(quotedMessage) : null,
    showReplyInChannel: false,
    editedMessage
  };
};
var _MessageComposer = class _MessageComposer extends WithSubscriptions {
  // todo: mediaRecorder: MediaRecorderController;
  constructor({
    composition,
    config,
    compositionContext,
    client
  }) {
    super();
    this.snapshots = [];
    this.setEditedMessage = (editedMessage) => {
      this.state.partialNext({ editedMessage: editedMessage ?? null });
      if (editedMessage) {
        this.textComposer.clearCommand();
      }
    };
    this.getCommandDisabledReason = (command) => {
      if (this.editedMessage) return "editing";
      if (this.quotedMessage && (command.set === "moderation_set" || command.name === "moderation_set")) {
        return "quoted_message";
      }
      return void 0;
    };
    this.isCommandDisabled = (command) => !!this.getCommandDisabledReason(command);
    this.validateCommandSendability = (command, text = this.textComposer.text) => {
      const currentMentionedUsers = this.textComposer.mentionedUsers;
      const mentionedUsersInText = getMentionedUsersInText(text, currentMentionedUsers);
      const validationContext = {
        command,
        commandArgsText: command.name ? stripCommandFromText(text, command.name).trim() : text.trim(),
        composer: this,
        mentionedUsersInText,
        rawText: text
      };
      const result = this.config.commands.sendValidator(validationContext);
      if (result && !result.ready) {
        return result;
      }
      return { command, ready: true };
    };
    this.refreshId = () => {
      this.state.partialNext({ id: _MessageComposer.generateId() });
    };
    this.initState = ({
      composition
    } = {}) => {
      this.clearSnapshots();
      this.editingAuditState.partialNext(this.initEditingAuditState(composition));
      const message = typeof composition === "undefined" ? composition : compositionIsDraftResponse(composition) ? composition.message : formatMessage(composition);
      this.attachmentManager.initState({ message });
      this.linkPreviewsManager.initState({ message });
      this.locationComposer.initState({ message });
      this.textComposer.initState({ message });
      this.pollComposer.initState();
      this.customDataManager.initState({ message });
      this.state.next(initState7(composition));
    };
    this.initStateFromChannelResponse = (channelApiResponse) => {
      if (this.channel.cid !== channelApiResponse.channel.cid) {
        return;
      }
      if (channelApiResponse.draft) {
        this.initState({ composition: channelApiResponse.draft });
      } else if (this.state.getLatestValue().draftId) {
        this.clear();
        this.client.offlineDb?.executeQuerySafely(
          (db) => db.deleteDraft({
            cid: this.channel.cid,
            parent_id: void 0
            // makes sure that we don't delete thread drafts while upserting channels
          }),
          { method: "deleteDraft" }
        );
      }
    };
    this.initEditingAuditState = (composition) => initEditingAuditState(composition);
    this.clearSnapshots = () => {
      this.snapshots = [];
    };
    this.getSnapshot = () => ({
      attachmentManager: this.attachmentManager.getSnapshot(),
      customDataManager: this.customDataManager.getSnapshot(),
      linkPreviewsManager: this.linkPreviewsManager.getSnapshot(),
      locationComposer: this.locationComposer.getSnapshot(),
      pollComposer: this.pollComposer.getSnapshot(),
      textComposer: this.textComposer.getSnapshot()
    });
    this.restoreSnapshot = (snapshot) => {
      this.attachmentManager.restoreSnapshot(snapshot.attachmentManager);
      this.linkPreviewsManager.restoreSnapshot(snapshot.linkPreviewsManager);
      this.locationComposer.restoreSnapshot(snapshot.locationComposer);
      this.pollComposer.restoreSnapshot(snapshot.pollComposer);
      this.customDataManager.restoreSnapshot(snapshot.customDataManager);
      this.textComposer.restoreSnapshot(snapshot.textComposer);
    };
    this.captureSnapshot = (snapshot = this.getSnapshot()) => {
      if (this.snapshots.length) return;
      this.snapshots.push(snapshot);
    };
    this.popSnapshot = () => this.snapshots.pop();
    this.registerEffectHandler = (type, handler) => {
      this.effectHandlers.registerEffectHandler(type, handler);
    };
    this.applyEffects = (effects = []) => {
      this.effectHandlers.applyEffects(effects);
    };
    this.registerDraftEventSubscriptions = () => {
      const unsubscribeDraftUpdated = this.subscribeDraftUpdated();
      const unsubscribeDraftDeleted = this.subscribeDraftDeleted();
      return () => {
        unsubscribeDraftUpdated();
        unsubscribeDraftDeleted();
      };
    };
    this.registerSubscriptions = () => {
      if (!this.hasSubscriptions) {
        this.addUnsubscribeFunction(this.subscribeMessageComposerSetupStateChange());
        this.addUnsubscribeFunction(this.subscribeMessageUpdated());
        this.addUnsubscribeFunction(this.subscribeMessageDeleted());
        this.addUnsubscribeFunction(this.subscribeTextComposerStateChanged());
        this.addUnsubscribeFunction(this.subscribeAttachmentManagerStateChanged());
        this.addUnsubscribeFunction(this.subscribeLinkPreviewsManagerStateChanged());
        this.addUnsubscribeFunction(this.subscribeLocationComposerStateChanged());
        this.addUnsubscribeFunction(this.subscribePollComposerStateChanged());
        this.addUnsubscribeFunction(this.subscribeCustomDataManagerStateChanged());
        this.addUnsubscribeFunction(this.subscribeMessageComposerStateChanged());
        this.addUnsubscribeFunction(this.subscribeMessageComposerConfigStateChanged());
      }
      this.incrementRefCount();
      return () => this.unregisterSubscriptions();
    };
    this.subscribeMessageUpdated = () => {
      const eventTypes = [
        "message.updated",
        "reaction.new",
        "reaction.deleted",
        // todo: do we need to subscribe to this especially when the whole state is overriden?
        "reaction.updated"
        // todo: do we need to subscribe to this especially when the whole state is overriden?
      ];
      const unsubscribeFunctions = eventTypes.map(
        (eventType) => this.client.on(eventType, (event) => {
          if (!event.message) return;
          if (event.message.id === this.id) {
            this.initState({ composition: event.message });
          }
          if (this.quotedMessage?.id && event.message.id === this.quotedMessage.id) {
            this.setQuotedMessage(formatMessage(event.message));
          }
        }).unsubscribe
      );
      return () => unsubscribeFunctions.forEach((unsubscribe) => unsubscribe());
    };
    this.subscribeMessageComposerSetupStateChange = () => {
      let tearDown = null;
      const unsubscribe = this.client._messageComposerSetupState.subscribeWithSelector(
        ({ setupFunction: setup }) => ({
          setup
        }),
        ({ setup }) => {
          tearDown?.();
          tearDown = setup?.({ composer: this }) ?? null;
        }
      );
      return () => {
        tearDown?.();
        unsubscribe();
      };
    };
    this.subscribeMessageDeleted = () => this.client.on("message.deleted", (event) => {
      if (!event.message) return;
      if (event.message.id === this.id) {
        this.clear();
      } else if (this.quotedMessage && event.message.id === this.quotedMessage.id) {
        this.setQuotedMessage(null);
      }
    }).unsubscribe;
    this.subscribeDraftUpdated = () => this.client.on("draft.updated", (event) => {
      const draft = event.draft;
      if (!draft || (draft.parent_id ?? null) !== (this.threadId ?? null) || draft.channel_cid !== this.channel.cid)
        return;
      if (this.editedMessage) return;
      this.initState({ composition: draft });
    }).unsubscribe;
    this.subscribeDraftDeleted = () => this.client.on("draft.deleted", (event) => {
      const draft = event.draft;
      if (!draft || (draft.parent_id ?? null) !== (this.threadId ?? null) || draft.channel_cid !== this.channel.cid) {
        return;
      }
      if (this.editedMessage) return;
      this.logDraftUpdateTimestamp();
      if (this.compositionIsEmpty) {
        return;
      }
      this.clear();
    }).unsubscribe;
    this.subscribeTextComposerStateChanged = () => this.textComposer.state.subscribeWithSelector(
      ({ text }) => [text],
      ([currentText], previousSelection) => {
        if (typeof previousSelection === "undefined") return;
        this.logStateUpdateTimestamp();
        if (this.compositionIsEmpty) {
          this.deleteDraft();
          return;
        }
        if (!this.linkPreviewsManager.enabled) return;
        if (!currentText) {
          this.linkPreviewsManager.clearPreviews();
        } else {
          this.linkPreviewsManager.findAndEnrichUrls(currentText);
        }
      }
    );
    this.subscribeAttachmentManagerStateChanged = () => this.attachmentManager.state.subscribe((_, previousValue) => {
      if (typeof previousValue === "undefined") return;
      this.logStateUpdateTimestamp();
      if (this.compositionIsEmpty) {
        this.deleteDraft();
        return;
      }
    });
    this.subscribeLocationComposerStateChanged = () => this.locationComposer.state.subscribe((_, previousValue) => {
      if (typeof previousValue === "undefined") return;
      this.logStateUpdateTimestamp();
      if (this.compositionIsEmpty) {
        this.deleteDraft();
        return;
      }
    });
    this.subscribeLinkPreviewsManagerStateChanged = () => this.linkPreviewsManager.state.subscribe((_, previousValue) => {
      if (typeof previousValue === "undefined") return;
      this.logStateUpdateTimestamp();
      if (this.compositionIsEmpty) {
        this.deleteDraft();
        return;
      }
    });
    this.subscribePollComposerStateChanged = () => this.pollComposer.state.subscribe((_, previousValue) => {
      if (typeof previousValue === "undefined") return;
      this.logStateUpdateTimestamp();
      if (this.compositionIsEmpty) {
        this.deleteDraft();
        return;
      }
    });
    this.subscribeCustomDataManagerStateChanged = () => this.customDataManager.state.subscribe((nextValue, previousValue) => {
      if (typeof previousValue !== "undefined" && // FIXME: is this check really necessary?
      !this.customDataManager.isMessageDataEqual(nextValue, previousValue)) {
        this.logStateUpdateTimestamp();
      }
    });
    this.subscribeMessageComposerStateChanged = () => this.state.subscribe((_, previousValue) => {
      if (typeof previousValue === "undefined") return;
      this.logStateUpdateTimestamp();
      if (this.compositionIsEmpty) {
        this.deleteDraft();
      }
    });
    this.subscribeMessageComposerConfigStateChanged = () => {
      let draftUnsubscribeFunction;
      const unsubscribe = this.configState.subscribeWithSelector(
        (currentValue) => ({
          textDefaultValue: currentValue.text.defaultValue,
          draftsEnabled: currentValue.drafts.enabled
        }),
        ({ textDefaultValue, draftsEnabled }) => {
          if (this.textComposer.text === "" && textDefaultValue) {
            this.textComposer.insertText({
              text: textDefaultValue,
              selection: { start: 0, end: 0 }
            });
          }
          if (draftsEnabled && !draftUnsubscribeFunction) {
            draftUnsubscribeFunction = this.registerDraftEventSubscriptions();
          } else if (!draftsEnabled && draftUnsubscribeFunction) {
            draftUnsubscribeFunction();
            draftUnsubscribeFunction = null;
          }
        }
      );
      return () => {
        draftUnsubscribeFunction?.();
        unsubscribe();
      };
    };
    this.setQuotedMessage = (quotedMessage) => {
      this.state.partialNext({ quotedMessage });
      const activeCommand = this.textComposer.command;
      if (quotedMessage && activeCommand && this.isCommandDisabled(activeCommand)) {
        this.textComposer.clearCommand();
      }
    };
    this.toggleShowReplyInChannel = () => {
      this.state.partialNext({ showReplyInChannel: !this.showReplyInChannel });
    };
    this.clear = () => {
      this.setQuotedMessage(null);
      this.initState();
    };
    this.restore = () => {
      const { editedMessage } = this;
      if (editedMessage) {
        this.initState({ composition: editedMessage });
        return;
      }
      this.clear();
    };
    this.compose = async () => {
      const created_at = this.editedMessage?.created_at ?? /* @__PURE__ */ new Date();
      const text = "";
      const result = await this.compositionMiddlewareExecutor.execute({
        eventName: "compose",
        initialValue: {
          message: {
            id: this.id,
            parent_id: this.threadId ?? void 0,
            type: "regular"
          },
          localMessage: {
            attachments: [],
            created_at,
            // only assigned to localMessage as this is used for optimistic update
            deleted_at: null,
            error: void 0,
            id: this.id,
            mentioned_users: [],
            parent_id: this.threadId ?? void 0,
            pinned_at: this.editedMessage?.pinned_at || null,
            reaction_groups: null,
            status: this.editedMessage ? this.editedMessage.status : "sending",
            text,
            type: "regular",
            updated_at: created_at
          },
          sendOptions: {}
        }
      });
      if (result.status === "discard") return;
      return result.state;
    };
    this.composeDraft = async () => {
      const { state, status } = await this.draftCompositionMiddlewareExecutor.execute({
        eventName: "compose",
        initialValue: {
          draft: { id: this.id, parent_id: this.threadId ?? void 0, text: "" }
        }
      });
      if (status === "discard") return;
      return state;
    };
    this.createDraft = async () => {
      if (this.editedMessage || !this.config.drafts.enabled) return;
      const composition = await this.composeDraft();
      if (!composition) return;
      const { draft } = composition;
      this.state.partialNext({ draftId: draft.id });
      if (this.client.offlineDb) {
        try {
          const optimisticDraftResponse = {
            channel_cid: this.channel.cid,
            created_at: (/* @__PURE__ */ new Date()).toISOString(),
            message: draft,
            parent_id: draft.parent_id,
            quoted_message: this.quotedMessage ? unformatMessage(this.quotedMessage) : void 0
          };
          await this.client.offlineDb.upsertDraft({ draft: optimisticDraftResponse });
        } catch (error) {
          this.client.logger("error", `offlineDb:upsertDraft`, {
            tags: ["channel", "offlineDb"],
            error
          });
        }
      }
      this.logDraftUpdateTimestamp();
      await this.channel.createDraft(draft);
    };
    this.deleteDraft = async () => {
      if (this.editedMessage || !this.config.drafts.enabled || !this.draftId) return;
      this.state.partialNext({ draftId: null });
      const parentId = this.threadId ?? void 0;
      if (this.client.offlineDb) {
        try {
          await this.client.offlineDb.deleteDraft({
            cid: this.channel.cid,
            parent_id: parentId
          });
        } catch (error) {
          this.client.logger("error", `offlineDb:deleteDraft`, {
            tags: ["channel", "offlineDb"],
            error
          });
        }
      }
      this.logDraftUpdateTimestamp();
      await this.channel.deleteDraft({ parent_id: parentId });
    };
    this.getDraft = async () => {
      if (this.editedMessage || !this.config.drafts.enabled || !this.client.userID) return;
      const draftFromOfflineDB = await this.client.offlineDb?.getDraft({
        cid: this.channel.cid,
        userId: this.client.userID,
        parent_id: this.threadId ?? void 0
      });
      if (draftFromOfflineDB) {
        this.initState({ composition: draftFromOfflineDB });
      }
      try {
        const response = await this.channel.getDraft({
          parent_id: this.threadId ?? void 0
        });
        const { draft } = response;
        if (!draft) return;
        this.client.offlineDb?.executeQuerySafely(
          (db) => db.upsertDraft({
            draft
          }),
          { method: "upsertDraft" }
        );
        this.initState({ composition: draft });
      } catch (error) {
        this.client.logger("error", `messageComposer:getDraft`, {
          tags: ["channel", "messageComposer"],
          error
        });
      }
    };
    this.createPoll = async () => {
      const composition = await this.pollComposer.compose();
      if (!composition || !composition.data.id) return;
      try {
        const poll = await this.client.polls.createPoll(composition.data);
        this.state.partialNext({ pollId: poll?.id });
      } catch (error) {
        this.client.notifications.addError({
          message: "Failed to create the poll",
          origin: {
            emitter: "MessageComposer",
            context: { composer: this }
          },
          options: {
            type: "api:poll:create:failed",
            metadata: {
              reason: error.message
            },
            originalError: error instanceof Error ? error : void 0
          }
        });
        throw error;
      }
    };
    this.sendLocation = async () => {
      const location = this.locationComposer.validLocation;
      if (this.threadId || !location) return;
      try {
        await this.channel.sendSharedLocation(location);
        this.refreshId();
        this.locationComposer.initState();
      } catch (error) {
        this.client.notifications.addError({
          message: "Failed to share the location",
          origin: {
            emitter: "MessageComposer",
            context: { composer: this }
          },
          options: {
            type: "api:location:create:failed",
            metadata: {
              reason: error.message
            },
            originalError: error instanceof Error ? error : void 0
          }
        });
        throw error;
      }
    };
    this.compositionContext = compositionContext;
    if (compositionContext instanceof Channel) {
      this.channel = compositionContext;
    } else if (compositionContext instanceof Thread) {
      this.channel = compositionContext.channel;
    } else if (compositionContext.cid) {
      const [type, id] = compositionContext.cid.split(":");
      this.channel = client.channel(type, id);
    } else {
      throw new Error(
        "MessageComposer requires composition context pointing to channel (channel or context.cid)"
      );
    }
    const mergeMessageComposerConfigCustomizer = (originalVal, channelConfigVal, key) => typeof originalVal === "object" ? void 0 : originalVal === false && key === "enabled" ? false : ["string", "number", "bigint", "boolean", "symbol"].includes(
      // prevent enabling features that are disabled server-side
      typeof channelConfigVal
    ) ? channelConfigVal : originalVal;
    this.configState = new StateStore(
      applyCommandValidatorOverride(
        mergeWith(
          mergeWith(DEFAULT_COMPOSER_CONFIG, config ?? {}),
          {
            location: {
              enabled: this.channel.getConfig()?.shared_locations
            }
          },
          mergeMessageComposerConfigCustomizer
        ),
        config
      )
    );
    let message = void 0;
    if (compositionIsDraftResponse(composition)) {
      message = composition.message;
    } else if (composition) {
      message = formatMessage(composition);
    }
    this.attachmentManager = new AttachmentManager({ composer: this, message });
    this.linkPreviewsManager = new LinkPreviewsManager({ composer: this, message });
    this.locationComposer = new LocationComposer({ composer: this, message });
    this.textComposer = new TextComposer({ composer: this, message });
    this.pollComposer = new PollComposer({ composer: this });
    this.customDataManager = new CustomDataManager({ composer: this, message });
    this.editingAuditState = new StateStore(
      this.initEditingAuditState(composition)
    );
    this.state = new StateStore(initState7(composition));
    this.compositionMiddlewareExecutor = new MessageComposerMiddlewareExecutor({
      composer: this
    });
    this.draftCompositionMiddlewareExecutor = new MessageDraftComposerMiddlewareExecutor({
      composer: this
    });
    this.effectHandlers = new MessageComposerEffectHandlers({ composer: this });
  }
  static evaluateContextType(compositionContext) {
    if (compositionContext instanceof Channel) {
      return "channel";
    }
    if (compositionContext instanceof Thread) {
      return "thread";
    }
    if (typeof compositionContext.legacyThreadId === "string") {
      return "legacy_thread";
    }
    return "message";
  }
  static constructTag(compositionContext) {
    return `${this.evaluateContextType(compositionContext)}_${compositionContext.id}`;
  }
  get config() {
    return this.configState.getLatestValue();
  }
  get editedMessage() {
    return this.state.getLatestValue().editedMessage ?? void 0;
  }
  set editedMessage(editedMessage) {
    this.state.partialNext({ editedMessage: editedMessage ?? null });
  }
  get contextType() {
    return _MessageComposer.evaluateContextType(this.compositionContext);
  }
  get tag() {
    return _MessageComposer.constructTag(this.compositionContext);
  }
  get threadId() {
    if (this.compositionContext instanceof Channel) {
      return null;
    }
    if (this.compositionContext instanceof Thread) {
      return this.compositionContext.id;
    }
    if (typeof this.compositionContext.legacyThreadId === "string") {
      return this.compositionContext.legacyThreadId;
    }
    if (typeof this.compositionContext.parent_id === "string") {
      return this.compositionContext.parent_id;
    }
    return null;
  }
  get client() {
    return this.channel.getClient();
  }
  get id() {
    return this.state.getLatestValue().id;
  }
  get draftId() {
    return this.state.getLatestValue().draftId;
  }
  get lastChange() {
    return this.editingAuditState.getLatestValue().lastChange;
  }
  get quotedMessage() {
    return this.state.getLatestValue().quotedMessage;
  }
  get pollId() {
    return this.state.getLatestValue().pollId;
  }
  get showReplyInChannel() {
    return this.state.getLatestValue().showReplyInChannel;
  }
  get isCommandSendable() {
    const currentCommand = this.textComposer.command;
    return !currentCommand || this.validateCommandSendability(currentCommand).ready;
  }
  /**
   * Whether the installed composition middleware composes messages whose uploads are still in
   * flight — see `MessageCompositionMiddleware.allowsPendingUploads`.
   *
   * Derived from the chain rather than from config, so there is exactly one switch: the UI SDK
   * that implements the rest of the flow installs the middleware, and everything that has to
   * follow from that (starting with {@link hasSendableData}) follows on its own.
   *
   * **Temporary API.** In v10 sending with pending uploads becomes part of the composer
   * configuration (`MessageComposerConfig`, set through `updateConfig`), and this getter,
   * together with the middleware declaration it reads, is replaced by that config field.
   */
  get allowsPendingUploads() {
    return this.compositionMiddlewareExecutor.installedMiddleware.some(
      (middleware) => middleware.allowsPendingUploads
    );
  }
  get hasSendableData() {
    if (!this.isCommandSendable) return false;
    if (this.allowsPendingUploads) {
      const hasSendableAttachment = this.attachmentManager.attachments.some(
        (attachment) => isFinishedUpload(attachment) || isPendingUpload(attachment)
      );
      return !!(!this.textComposer.textIsEmpty || hasSendableAttachment || this.pollId || this.locationComposer.validLocation);
    }
    return !!(!this.attachmentManager.uploadsInProgressCount && (!this.textComposer.textIsEmpty || this.attachmentManager.successfulUploadsCount > 0) || this.pollId || !!this.locationComposer.validLocation);
  }
  get compositionIsEmpty() {
    return !this.quotedMessage && this.contentIsEmpty;
  }
  get contentIsEmpty() {
    return this.textComposer.textIsEmpty && !this.attachmentManager.attachments.length && !this.pollId && !this.locationComposer.validLocation;
  }
  get lastChangeOriginIsLocal() {
    const initiatedWithoutDraft = this.lastChange.draftUpdate === null;
    const composingMessageFromScratch = initiatedWithoutDraft && !this.editedMessage;
    const editedMessageWasUpdated = !!this.editedMessage?.updated_at && new Date(this.editedMessage.updated_at).getTime() < this.lastChange.stateUpdate;
    const draftWasChanged = !!this.lastChange.draftUpdate && this.lastChange.draftUpdate < this.lastChange.stateUpdate;
    return editedMessageWasUpdated || draftWasChanged || composingMessageFromScratch;
  }
  updateConfig(config) {
    this.configState.partialNext(
      applyCommandValidatorOverride(mergeWith(this.config, config), config)
    );
  }
  logStateUpdateTimestamp() {
    this.editingAuditState.partialNext({
      lastChange: { ...this.lastChange, stateUpdate: (/* @__PURE__ */ new Date()).getTime() }
    });
  }
  logDraftUpdateTimestamp() {
    if (!this.config.drafts.enabled) return;
    const timestamp = (/* @__PURE__ */ new Date()).getTime();
    this.editingAuditState.partialNext({
      lastChange: { draftUpdate: timestamp, stateUpdate: timestamp }
    });
  }
};
_MessageComposer.generateId = generateUUIDv4;
var MessageComposer = _MessageComposer;

// src/messageDelivery/MessageDeliveryReporter.ts
var MAX_DELIVERED_MESSAGE_COUNT_IN_PAYLOAD = 100;
var MARK_AS_DELIVERED_BUFFER_TIMEOUT = 1e3;
var MARK_AS_READ_THROTTLE_TIMEOUT2 = 1e3;
var RETRY_COUNT_LIMIT_FOR_TIMEOUT_INCREASE = 3;
var isChannel = (item) => item instanceof Channel;
var isThread = (item) => item instanceof Thread;
var MessageDeliveryReporter = class _MessageDeliveryReporter {
  constructor({ client }) {
    this.deliveryReportCandidates = /* @__PURE__ */ new Map();
    this.nextDeliveryReportCandidates = /* @__PURE__ */ new Map();
    this.markDeliveredRequestPromise = null;
    this.markDeliveredTimeout = null;
    this.requestTimeoutMs = MARK_AS_DELIVERED_BUFFER_TIMEOUT;
    // increased up to RETRY_COUNT_LIMIT_FOR_TIMEOUT_INCREASE
    this.requestRetryCount = 0;
    /**
     * Retrieve the reference to the latest message in the state that is nor read neither reported as delivered
     * @param collection
     */
    this.getNextDeliveryReportCandidate = (collection) => {
      const ownUserId = this.client.user?.id;
      if (!ownUserId) return;
      let latestMessages = [];
      let lastDeliveredAt;
      let lastReadAt;
      let key = void 0;
      if (isChannel(collection)) {
        latestMessages = collection.state.latestMessages;
        const ownReadState = collection.state.read[ownUserId] ?? {};
        lastReadAt = ownReadState?.last_read;
        lastDeliveredAt = ownReadState?.last_delivered_at;
        key = collection.cid;
      } else if (isThread(collection)) {
        latestMessages = collection.state.getLatestValue().replies;
        const ownReadState = collection.state.getLatestValue().read[ownUserId] ?? {};
        lastReadAt = ownReadState?.lastReadAt;
        lastDeliveredAt = ownReadState?.lastDeliveredAt;
        key = `${collection.channel.cid}:${collection.id}`;
        return;
      } else {
        return;
      }
      if (!key) return;
      const [latestMessage] = latestMessages.slice(-1);
      const wholeCollectionIsRead = !latestMessage || lastReadAt >= latestMessage.created_at;
      if (wholeCollectionIsRead) return { key, id: null };
      const wholeCollectionIsMarkedDelivered = !latestMessage || (lastDeliveredAt ?? 0) >= latestMessage.created_at;
      if (wholeCollectionIsMarkedDelivered) return { key, id: null };
      return { key, id: latestMessage.id || null };
    };
    /**
     * Fires delivery announcement request followed by immediate delivery candidate buffer reset.
     * @param options
     */
    this.announceDelivery = (options) => {
      if (!this.canExecuteRequest) return;
      const { latest_delivered_messages, sendBuffer } = this.confirmationsFromDeliveryReportCandidates();
      if (!latest_delivered_messages.length) return;
      const payload = { ...options, latest_delivered_messages };
      const postFlightReconcile = ({
        preventSchedulingRetry
      } = {}) => {
        this.markDeliveredRequestPromise = null;
        for (const [k, v] of this.nextDeliveryReportCandidates.entries()) {
          this.deliveryReportCandidates.set(k, v);
        }
        this.nextDeliveryReportCandidates = /* @__PURE__ */ new Map();
        if (preventSchedulingRetry) return;
        this.announceDeliveryBuffered(options);
      };
      const handleSuccess = () => {
        this.resetBackOff();
        postFlightReconcile();
      };
      const handleError = (error) => {
        const newDeliveryReportCandidates = new Map(sendBuffer);
        for (const [k, v] of this.deliveryReportCandidates.entries()) {
          newDeliveryReportCandidates.set(k, v);
        }
        this.deliveryReportCandidates = newDeliveryReportCandidates;
        if (isAPIError(error) && isErrorRetryable(error) || error.status >= 500) {
          this.increaseBackOff();
          postFlightReconcile();
        } else {
          postFlightReconcile({ preventSchedulingRetry: true });
        }
      };
      this.markDeliveredRequestPromise = this.client.markChannelsDelivered(payload).then(handleSuccess, handleError);
    };
    this.announceDeliveryBuffered = (options) => {
      if (this.hasTimer || !this.canExecuteRequest) return;
      this.markDeliveredTimeout = setTimeout(() => {
        this.markDeliveredTimeout = null;
        this.announceDelivery(options);
      }, this.requestTimeoutMs);
    };
    /**
     * Delegates the mark-read call to the Channel or Thread instance
     * @param collection
     * @param options
     */
    this.markRead = async (collection, options) => {
      if (!userHasReadReceipts(this.client)) return null;
      let result = null;
      if (isChannel(collection)) {
        result = await collection.markAsReadRequest(options);
      } else if (isThread(collection)) {
        result = await collection.channel.markAsReadRequest({
          ...options,
          thread_id: collection.id
        });
      }
      this.removeCandidateFor(collection);
      return result;
    };
    /**
     * Throttles the MessageDeliveryReporter.markRead call
     * @param collection
     * @param options
     */
    this.throttledMarkRead = throttle(this.markRead, MARK_AS_READ_THROTTLE_TIMEOUT2, {
      leading: false,
      trailing: true
    });
    this.client = client;
  }
  get markDeliveredRequestInFlight() {
    return this.markDeliveredRequestPromise !== null;
  }
  get hasTimer() {
    return this.markDeliveredTimeout !== null;
  }
  get hasDeliveryCandidates() {
    return this.deliveryReportCandidates.size > 0;
  }
  get canExecuteRequest() {
    return !this.markDeliveredRequestInFlight && this.hasDeliveryCandidates;
  }
  static hasPermissionToReportDeliveryFor(collection) {
    if (isChannel(collection)) return !!collection.getConfig()?.delivery_events;
    if (isThread(collection)) return !!collection.channel.getConfig()?.delivery_events;
  }
  increaseBackOff() {
    if (this.requestRetryCount >= RETRY_COUNT_LIMIT_FOR_TIMEOUT_INCREASE) return;
    this.requestRetryCount = this.requestRetryCount + 1;
    this.requestTimeoutMs = this.requestTimeoutMs * 2;
  }
  resetBackOff() {
    this.requestTimeoutMs = MARK_AS_DELIVERED_BUFFER_TIMEOUT;
    this.requestRetryCount = 0;
  }
  /**
   * Build latest_delivered_messages payload from an arbitrary buffer (deliveryReportCandidates / nextDeliveryReportCandidates)
   */
  confirmationsFrom(map2) {
    return Array.from(map2.entries()).map(([key, messageId]) => {
      const [type, id, parent_id] = key.split(":");
      return parent_id ? { cid: `${type}:${id}`, id: messageId, parent_id } : { cid: key, id: messageId };
    });
  }
  confirmationsFromDeliveryReportCandidates() {
    const entries = Array.from(this.deliveryReportCandidates);
    const sendBuffer = new Map(entries.slice(0, MAX_DELIVERED_MESSAGE_COUNT_IN_PAYLOAD));
    this.deliveryReportCandidates = new Map(
      entries.slice(MAX_DELIVERED_MESSAGE_COUNT_IN_PAYLOAD)
    );
    return { latest_delivered_messages: this.confirmationsFrom(sendBuffer), sendBuffer };
  }
  /**
   * Generate candidate key for storing in the candidates buffer
   * @param collection
   * @private
   */
  candidateKeyFor(collection) {
    if (isChannel(collection)) return collection.cid;
    if (isThread(collection)) return `${collection.channel.cid}:${collection.id}`;
  }
  /**
   * Updates the delivery candidates buffer with the latest delivery candidates
   * @param collection
   */
  trackDeliveredCandidate(collection) {
    if (!_MessageDeliveryReporter.hasPermissionToReportDeliveryFor(collection)) return;
    const candidate = this.getNextDeliveryReportCandidate(collection);
    if (!candidate?.key) return;
    const buffer = this.markDeliveredRequestInFlight ? this.nextDeliveryReportCandidates : this.deliveryReportCandidates;
    if (candidate.id === null) buffer.delete(candidate.key);
    else buffer.set(candidate.key, candidate.id);
  }
  /**
   * Removes candidate from the delivery report buffer
   * @param collection
   * @private
   */
  removeCandidateFor(collection) {
    const candidateKey = this.candidateKeyFor(collection);
    if (!candidateKey) return;
    this.deliveryReportCandidates.delete(candidateKey);
    this.nextDeliveryReportCandidates.delete(candidateKey);
  }
  /**
   * Records the latest message delivered for Channel or Thread instances and schedules the next report
   * if not already scheduled and candidates exist.
   * Should be used for WS handling (message.new) as well as for ingesting HTTP channel query results.
   * @param collections
   */
  syncDeliveredCandidates(collections) {
    if (this.client.user?.privacy_settings?.delivery_receipts?.enabled === false) return;
    for (const c of collections) this.trackDeliveredCandidate(c);
    this.announceDeliveryBuffered();
  }
};

// src/messageDelivery/MessageReceiptsTracker.ts
var MIN_REF = { timestampMs: Number.NEGATIVE_INFINITY, msgId: "" };
var compareRefsAsc = (a, b) => a.timestampMs !== b.timestampMs ? a.timestampMs - b.timestampMs : 0;
var findIndex = (arr, target, keyOf) => {
  let lo = 0, hi = arr.length;
  while (lo < hi) {
    const mid = lo + hi >>> 1;
    if (compareRefsAsc(keyOf(arr[mid]), target) >= 0) hi = mid;
    else lo = mid + 1;
  }
  return lo;
};
var findUpperIndex = (arr, target, keyOf) => {
  let lo = 0, hi = arr.length;
  while (lo < hi) {
    const mid = lo + hi >>> 1;
    if (compareRefsAsc(keyOf(arr[mid]), target) > 0) hi = mid;
    else lo = mid + 1;
  }
  return lo;
};
var insertByKey = (arr, item, keyOf) => arr.splice(findUpperIndex(arr, keyOf(item), keyOf), 0, item);
var removeByOldKey = (arr, item, oldKey, keyOf) => {
  let i = findIndex(arr, oldKey, keyOf);
  while (i < arr.length && compareRefsAsc(keyOf(arr[i]), oldKey) === 0) {
    if (arr[i].user.id === item.user.id) {
      arr.splice(i, 1);
      return;
    }
    i++;
  }
};
var MessageReceiptsTracker = class {
  constructor({ locateMessage }) {
    this.byUser = /* @__PURE__ */ new Map();
    this.readSorted = [];
    // asc by lastReadRef
    this.deliveredSorted = [];
    this.locateMessage = locateMessage;
  }
  /** Build initial state from server snapshots (single pass + sort). */
  ingestInitial(responses) {
    this.byUser.clear();
    this.readSorted = [];
    this.deliveredSorted = [];
    for (const r of responses) {
      const lastReadTimestamp = r.last_read ? new Date(r.last_read).getTime() : null;
      const lastDeliveredTimestamp = r.last_delivered_at ? new Date(r.last_delivered_at).getTime() : null;
      const lastReadRef = lastReadTimestamp ? this.locateMessage(lastReadTimestamp) ?? MIN_REF : MIN_REF;
      let lastDeliveredRef = lastDeliveredTimestamp ? this.locateMessage(lastDeliveredTimestamp) ?? MIN_REF : MIN_REF;
      const isReadAfterDelivered = compareRefsAsc(lastDeliveredRef, lastReadRef) < 0;
      if (isReadAfterDelivered) lastDeliveredRef = lastReadRef;
      const userProgress = { user: r.user, lastReadRef, lastDeliveredRef };
      this.byUser.set(r.user.id, userProgress);
      this.readSorted.splice(
        findIndex(this.readSorted, lastReadRef, (up) => up.lastReadRef),
        0,
        userProgress
      );
      this.deliveredSorted.splice(
        findIndex(this.deliveredSorted, lastDeliveredRef, (up) => up.lastDeliveredRef),
        0,
        userProgress
      );
    }
  }
  /** message.delivered — user device confirmed delivery up to and including messageId. */
  onMessageDelivered({
    user,
    deliveredAt,
    lastDeliveredMessageId
  }) {
    const timestampMs = new Date(deliveredAt).getTime();
    const msgRef = lastDeliveredMessageId ? { timestampMs, msgId: lastDeliveredMessageId } : this.locateMessage(new Date(deliveredAt).getTime());
    if (!msgRef) return;
    const userProgress = this.ensureUser(user);
    const newDelivered = compareRefsAsc(msgRef, userProgress.lastReadRef) < 0 ? userProgress.lastReadRef : msgRef;
    if (compareRefsAsc(newDelivered, userProgress.lastDeliveredRef) <= 0) return;
    removeByOldKey(
      this.deliveredSorted,
      userProgress,
      userProgress.lastDeliveredRef,
      (x) => x.lastDeliveredRef
    );
    userProgress.lastDeliveredRef = newDelivered;
    insertByKey(this.deliveredSorted, userProgress, (x) => x.lastDeliveredRef);
  }
  /** message.read — user read up to and including messageId. */
  onMessageRead({
    user,
    readAt,
    lastReadMessageId
  }) {
    const timestampMs = new Date(readAt).getTime();
    const msgRef = lastReadMessageId ? { timestampMs, msgId: lastReadMessageId } : this.locateMessage(timestampMs);
    if (!msgRef) return;
    const userProgress = this.ensureUser(user);
    if (compareRefsAsc(msgRef, userProgress.lastReadRef) <= 0) return;
    removeByOldKey(
      this.readSorted,
      userProgress,
      userProgress.lastReadRef,
      (x) => x.lastReadRef
    );
    userProgress.lastReadRef = msgRef;
    insertByKey(this.readSorted, userProgress, (x) => x.lastReadRef);
    if (compareRefsAsc(userProgress.lastDeliveredRef, userProgress.lastReadRef) < 0) {
      removeByOldKey(
        this.deliveredSorted,
        userProgress,
        userProgress.lastDeliveredRef,
        (x) => x.lastDeliveredRef
      );
      userProgress.lastDeliveredRef = userProgress.lastReadRef;
      insertByKey(this.deliveredSorted, userProgress, (x) => x.lastDeliveredRef);
    }
  }
  /** notification.mark_unread — user marked messages unread starting at `first_unread_message_id`.
   * Sets lastReadRef to the event’s last_read_* values. Delivery never moves backward.
   * The event is sent only to the user that triggered the action (own user), so we will never adjust read ref
   * for other users - we will not see changes in the UI for other users. However, this implementation does not
   * take into consideration this fact and is ready to handle the mark-unread event for any user.
   */
  onNotificationMarkUnread({
    user,
    lastReadAt,
    lastReadMessageId
  }) {
    const userProgress = this.ensureUser(user);
    const newReadRef = lastReadAt ? { timestampMs: new Date(lastReadAt).getTime(), msgId: lastReadMessageId ?? "" } : { ...MIN_REF };
    if (compareRefsAsc(newReadRef, userProgress.lastReadRef) === 0 && newReadRef.msgId === userProgress.lastReadRef.msgId) {
      return;
    }
    removeByOldKey(
      this.readSorted,
      userProgress,
      userProgress.lastReadRef,
      (x) => x.lastReadRef
    );
    userProgress.lastReadRef = newReadRef;
    insertByKey(this.readSorted, userProgress, (x) => x.lastReadRef);
    if (compareRefsAsc(userProgress.lastDeliveredRef, userProgress.lastReadRef) < 0) {
      removeByOldKey(
        this.deliveredSorted,
        userProgress,
        userProgress.lastDeliveredRef,
        (x) => x.lastDeliveredRef
      );
      userProgress.lastDeliveredRef = userProgress.lastReadRef;
      insertByKey(this.deliveredSorted, userProgress, (x) => x.lastDeliveredRef);
    }
  }
  /** All users who READ this message. */
  readersForMessage(msgRef) {
    const index = findIndex(this.readSorted, msgRef, ({ lastReadRef }) => lastReadRef);
    return this.readSorted.slice(index).map((x) => x.user);
  }
  /** All users who have it DELIVERED (includes readers). */
  deliveredForMessage(msgRef) {
    const pos = findIndex(
      this.deliveredSorted,
      msgRef,
      ({ lastDeliveredRef }) => lastDeliveredRef
    );
    return this.deliveredSorted.slice(pos).map((x) => x.user);
  }
  /** Users who delivered but have NOT read. */
  deliveredNotReadForMessage(msgRef) {
    const pos = findIndex(
      this.deliveredSorted,
      msgRef,
      ({ lastDeliveredRef }) => lastDeliveredRef
    );
    const usersDeliveredNotRead = [];
    for (let i = pos; i < this.deliveredSorted.length; i++) {
      const userProgress = this.deliveredSorted[i];
      if (compareRefsAsc(userProgress.lastReadRef, msgRef) < 0)
        usersDeliveredNotRead.push(userProgress.user);
    }
    return usersDeliveredNotRead;
  }
  /** Users for whom `msgRef` is their *last read* (exact match). */
  usersWhoseLastReadIs(msgRef) {
    if (!msgRef.msgId) return [];
    const start = findIndex(this.readSorted, msgRef, (x) => x.lastReadRef);
    const end = findUpperIndex(this.readSorted, msgRef, (x) => x.lastReadRef);
    const users = [];
    for (let i = start; i < end; i++) {
      const up = this.readSorted[i];
      if (up.lastReadRef.msgId === msgRef.msgId) users.push(up.user);
    }
    return users;
  }
  /** Users for whom `msgRef` is their *last delivered* (exact match). */
  usersWhoseLastDeliveredIs(msgRef) {
    if (!msgRef.msgId) return [];
    const start = findIndex(this.deliveredSorted, msgRef, (x) => x.lastDeliveredRef);
    const end = findUpperIndex(this.deliveredSorted, msgRef, (x) => x.lastDeliveredRef);
    const users = [];
    for (let i = start; i < end; i++) {
      const up = this.deliveredSorted[i];
      if (up.lastDeliveredRef.msgId === msgRef.msgId) users.push(up.user);
    }
    return users;
  }
  // ---- queries: per-user status ----
  hasUserRead(msgRef, userId) {
    const up = this.byUser.get(userId);
    return !!up && compareRefsAsc(up.lastReadRef, msgRef) >= 0;
  }
  hasUserDelivered(msgRef, userId) {
    const up = this.byUser.get(userId);
    return !!up && compareRefsAsc(up.lastDeliveredRef, msgRef) >= 0;
  }
  getUserProgress(userId) {
    const userProgress = this.byUser.get(userId);
    if (!userProgress) return null;
    return userProgress;
  }
  groupUsersByLastReadMessage() {
    return Array.from(this.byUser.values()).reduce(
      (acc, userProgress) => {
        const msgId = userProgress.lastReadRef.msgId;
        if (!msgId) return acc;
        if (!acc[msgId]) acc[msgId] = [];
        acc[msgId].push(userProgress.user);
        return acc;
      },
      {}
    );
  }
  groupUsersByLastDeliveredMessage() {
    return Array.from(this.byUser.values()).reduce(
      (acc, userProgress) => {
        const msgId = userProgress.lastDeliveredRef.msgId;
        if (!msgId) return acc;
        if (!acc[msgId]) acc[msgId] = [];
        acc[msgId].push(userProgress.user);
        return acc;
      },
      {}
    );
  }
  ensureUser(user) {
    let up = this.byUser.get(user.id);
    if (!up) {
      up = { user, lastReadRef: MIN_REF, lastDeliveredRef: MIN_REF };
      this.byUser.set(user.id, up);
      insertByKey(this.readSorted, up, (x) => x.lastReadRef);
      insertByKey(this.deliveredSorted, up, (x) => x.lastDeliveredRef);
    }
    return up;
  }
};

// src/channel.ts
var Channel = class {
  /**
   * constructor - Create a channel
   *
   * @param {StreamChat} client the chat client
   * @param {string} type  the type of channel
   * @param {string} [id]  the id of the chat
   * @param {ChannelData} data any additional custom params
   *
   * @return {Channel} Returns a new uninitialized channel
   */
  constructor(client, type, id, data) {
    /**
     * create - Creates a new channel
     *
     * @return {Promise<QueryChannelAPIResponse>} The Server Response
     *
     */
    this.create = async (options) => {
      const defaultOptions = {
        ...options,
        watch: false,
        state: false,
        presence: false
      };
      return await this.query(defaultOptions, "latest");
    };
    this._callChannelListeners = (event) => {
      const channel = this;
      const listeners = [];
      if (channel.listeners.all) {
        listeners.push(...channel.listeners.all);
      }
      if (channel.listeners[event.type]) {
        listeners.push(...channel.listeners[event.type]);
      }
      for (const listener of listeners) {
        if (typeof listener !== "string") {
          listener(event);
        }
      }
    };
    /**
     * _channelURL - Returns the channel url
     *
     * @return {string} The channel url
     */
    this._channelURL = () => {
      if (!this.id) {
        throw new Error("channel id is not defined");
      }
      return `${this.getClient().baseURL}/channels/${encodeURIComponent(
        this.type
      )}/${encodeURIComponent(this.id)}`;
    };
    const validTypeRe = /^[\w_-]+$/;
    const validIDRe = /^[\w!_-]+$/;
    if (!validTypeRe.test(type)) {
      throw new Error(`Invalid chat type ${type}, letters, numbers and "_-" are allowed`);
    }
    if (typeof id === "string" && !validIDRe.test(id)) {
      throw new Error(`Invalid chat id ${id}, letters, numbers and "!-_" are allowed`);
    }
    this._client = client;
    this.type = type;
    this.id = id;
    this.data = data;
    this._data = { ...data };
    this.cid = `${type}:${id}`;
    this.listeners = {};
    this.state = new ChannelState(this);
    this.initialized = false;
    this.offlineMode = false;
    this.lastTypingEvent = null;
    this.isTyping = false;
    this.disconnected = false;
    this.messageComposer = new MessageComposer({
      client: this._client,
      compositionContext: this
    });
    this.messageReceiptsTracker = new MessageReceiptsTracker({
      locateMessage: (timestampMs) => {
        const msg = this.state.findMessageByTimestamp(timestampMs);
        return msg && { timestampMs, msgId: msg.id };
      }
    });
    this.cooldownTimer = new CooldownTimer({ channel: this });
  }
  /**
   * getClient - Get the chat client for this channel. If client.disconnect() was called, this function will error
   *
   * @return {StreamChat}
   */
  getClient() {
    if (this.disconnected === true) {
      throw Error(`You can't use a channel after client.disconnect() was called`);
    }
    return this._client;
  }
  /**
   * getConfig - Get the config for this channel id (cid)
   *
   * @return {Record<string, unknown>}
   */
  getConfig() {
    const client = this.getClient();
    return client.configs[this.cid];
  }
  /**
   * sendMessage - Send a message to this channel
   *
   * @param {Message} message The Message object
   * @param {boolean} [options.skip_enrich_url] Do not try to enrich the URLs within message
   * @param {boolean} [options.skip_push] Skip sending push notifications
   * @param {boolean} [options.is_pending_message] DEPRECATED, please use `pending` instead.
   * @param {boolean} [options.pending] Make this message pending
   * @param {Record<string,string>} [options.pending_message_metadata] Metadata for the pending message
   * @param {boolean} [options.force_moderation] Apply force moderation for server-side requests
   *
   * @return {Promise<SendMessageAPIResponse>} The Server Response
   */
  async _sendMessage(rawMessage, options) {
    const client = this.getClient();
    const message = sanitizeOutgoingAttachments({
      client,
      message: rawMessage
    });
    return await client.post(this._channelURL() + "/message", {
      message,
      ...options
    });
  }
  async sendMessage(message, options) {
    try {
      const offlineDb = this.getClient().offlineDb;
      if (offlineDb) {
        const messageId = message.id;
        if (messageId) {
          return await offlineDb.queueTask({
            task: {
              channelId: this.id,
              channelType: this.type,
              messageId,
              payload: [message, options],
              type: "send-message"
            }
          });
        }
      }
    } catch (error) {
      this._client.logger("error", `offlineDb:send-message`, {
        tags: ["channel", "offlineDb"],
        error
      });
    }
    return await this._sendMessage(message, options);
  }
  /**
   * Upload a file to this channel’s file endpoint (multipart). Forwards to the client’s `sendFile` implementation.
   *
   * @param uri File source: URL string, `File`, `Buffer`, or readable stream (Node).
   * @param name File name sent in the multipart body.
   * @param contentType MIME type; defaults are applied when omitted.
   * @param user Optional user payload appended to the form as JSON.
   * @param axiosRequestConfig Optional Axios per-request config, merged after upload defaults (e.g. `onUploadProgress`, `signal` from `AbortController`).
   * @return Promise resolving to `{ file: string, ... }` with the CDN URL.
   */
  sendFile(uri, name, contentType, user, axiosRequestConfig) {
    return this.getClient().sendFile(
      `${this._channelURL()}/file`,
      uri,
      name,
      contentType,
      user,
      axiosRequestConfig
    );
  }
  /**
   * Upload an image to this channel’s image endpoint (multipart). Uses the same transport as `sendFile`.
   *
   * @param uri Image source: URL string, `File`, or readable stream (Node). For `Buffer` uploads, use `sendFile` toward the channel file endpoint instead.
   * @param name File name sent in the multipart body.
   * @param contentType MIME type.
   * @param user Optional user payload appended to the form as JSON.
   * @param axiosRequestConfig Optional Axios per-request config, merged after upload defaults (e.g. `onUploadProgress`, `signal`).
   * @return Promise resolving to `{ file: string, ... }` with the CDN URL.
   */
  sendImage(uri, name, contentType, user, axiosRequestConfig) {
    return this.getClient().sendFile(
      `${this._channelURL()}/image`,
      uri,
      name,
      contentType,
      user,
      axiosRequestConfig
    );
  }
  deleteFile(url) {
    return this.getClient().delete(`${this._channelURL()}/file`, { url });
  }
  deleteImage(url) {
    return this.getClient().delete(`${this._channelURL()}/image`, { url });
  }
  /**
   * sendEvent - Send an event on this channel
   *
   * @param {Event} event for example {type: 'message.read'}
   *
   * @return {Promise<EventAPIResponse>} The Server Response
   */
  async sendEvent(event) {
    this._checkInitialized();
    return await this.getClient().post(this._channelURL() + "/event", {
      event
    });
  }
  /**
   * search - Query messages
   *
   * @param {MessageFilters | string}  query search query or object MongoDB style filters
   * @param {{client_id?: string; connection_id?: string; query?: string; message_filter_conditions?: MessageFilters}} options Option object, {user_id: 'tommaso'}
   *
   * @return {Promise<SearchAPIResponse>} search messages response
   */
  async search(query, options = {}) {
    if (options.offset && options.next) {
      throw Error(`Cannot specify offset with next`);
    }
    const payload = {
      filter_conditions: { cid: this.cid },
      ...options,
      sort: options.sort ? normalizeQuerySort(options.sort) : void 0
    };
    if (typeof query === "string") {
      payload.query = query;
    } else if (typeof query === "object") {
      payload.message_filter_conditions = query;
    } else {
      throw Error(`Invalid type ${typeof query} for query parameter`);
    }
    await this.getClient().wsPromise;
    return await this.getClient().get(
      this.getClient().baseURL + "/search",
      {
        payload
      }
    );
  }
  /**
   * queryMembers - Query Members
   *
   * @param {MemberFilters}  filterConditions object MongoDB style filters
   * @param {MemberSort} [sort] Sort options, for instance [{created_at: -1}].
   * When using multiple fields, make sure you use array of objects to guarantee field order, for instance [{name: -1}, {created_at: 1}]
   * @param {{ limit?: number; offset?: number }} [options] Option object, {limit: 10, offset:10}
   * @param {RequestOptions} [requestOptions] Carries an abort signal. Not sent in the request.
   *
   * @return {Promise<ChannelMemberAPIResponse>} Query Members response
   */
  async queryMembers(filterConditions, sort = [], options = {}, requestOptions = {}) {
    let id;
    const type = this.type;
    let members;
    if (this.id) {
      id = this.id;
    } else if (this.data?.members && Array.isArray(this.data.members)) {
      members = this.data.members;
    }
    return await this.getClient().get(
      this.getClient().baseURL + "/members",
      {
        payload: {
          type,
          id,
          members,
          sort: normalizeQuerySort(sort),
          filter_conditions: filterConditions,
          ...options
        }
      },
      requestOptions
    );
  }
  /**
     * updateMemberPartial - Partial update a member
     *
     * @param {PartialUpdateMember}  updates
     * @param {{ user_id?: string }} [options] Option object, {user_id: 'jane'} to optionally specify the user id
  
     * @return {Promise<ChannelMemberResponse>} Updated member
     */
  async updateMemberPartial(updates, options) {
    const url = new URL(`${this._channelURL()}/member`);
    if (options?.userId) {
      url.searchParams.append("user_id", options.userId);
    }
    return await this.getClient().patch(
      url.toString(),
      updates
    );
  }
  /**
   * @deprecated Use `updateMemberPartial` instead
   * partialUpdateMember - Partial update a member
   *
   * @param {string} user_id member user id
   * @param {PartialUpdateMember}  updates
   *
   * @return {Promise<ChannelMemberResponse>} Updated member
   */
  async partialUpdateMember(user_id, updates) {
    if (!user_id) {
      throw Error("Please specify the user id");
    }
    return await this.getClient().patch(
      this._channelURL() + `/member/${encodeURIComponent(user_id)}`,
      updates
    );
  }
  /**
   * sendReaction - Sends a reaction to a message. If offline support is enabled, it will make sure
   * that sending the reaction is queued up if it fails due to bad internet conditions and executed
   * later.
   *
   * @param {string} messageID the message id
   * @param {Reaction} reaction the reaction object for instance {type: 'love'}
   * @param {{ enforce_unique?: boolean, skip_push?: boolean }} [options] Option object, {enforce_unique: true, skip_push: true} to override any existing reaction or skip sending push notifications
   *
   * @return {Promise<ReactionAPIResponse>} The Server Response
   */
  async sendReaction(messageID, reaction, options) {
    if (!messageID) {
      throw Error(`Message id is missing`);
    }
    if (!reaction || Object.keys(reaction).length === 0) {
      throw Error(`Reaction object is missing`);
    }
    try {
      const offlineDb = this.getClient().offlineDb;
      if (offlineDb) {
        return await offlineDb.queueTask({
          task: {
            channelId: this.id,
            channelType: this.type,
            messageId: messageID,
            payload: [messageID, reaction, options],
            type: "send-reaction"
          }
        });
      }
    } catch (error) {
      this._client.logger("error", `offlineDb:send-reaction`, {
        tags: ["channel", "offlineDb"],
        error
      });
    }
    return this._sendReaction(messageID, reaction, options);
  }
  /**
   * sendReaction - Send a reaction about a message
   *
   * @param {string} messageID the message id
   * @param {Reaction} reaction the reaction object for instance {type: 'love'}
   * @param {{ enforce_unique?: boolean, skip_push?: boolean }} [options] Option object, {enforce_unique: true, skip_push: true} to override any existing reaction or skip sending push notifications
   *
   * @return {Promise<ReactionAPIResponse>} The Server Response
   */
  async _sendReaction(messageID, reaction, options) {
    if (!messageID) {
      throw Error(`Message id is missing`);
    }
    if (!reaction || Object.keys(reaction).length === 0) {
      throw Error(`Reaction object is missing`);
    }
    return await this.getClient().post(
      this.getClient().baseURL + `/messages/${encodeURIComponent(messageID)}/reaction`,
      {
        reaction,
        ...options
      }
    );
  }
  async deleteReaction(messageID, reactionType, user_id) {
    this._checkInitialized();
    if (!reactionType || !messageID) {
      throw Error(
        "Deleting a reaction requires specifying both the message and reaction type"
      );
    }
    try {
      const offlineDb = this.getClient().offlineDb;
      if (offlineDb) {
        const message = this.state.messages.find(({ id }) => id === messageID);
        const reaction = {
          created_at: "",
          updated_at: "",
          message_id: messageID,
          type: reactionType,
          user_id: this.getClient().userID ?? user_id
        };
        if (message) {
          await offlineDb.deleteReaction({
            message,
            reaction
          });
        }
        return await offlineDb.queueTask({
          task: {
            channelId: this.id,
            channelType: this.type,
            messageId: messageID,
            payload: [messageID, reactionType],
            type: "delete-reaction"
          }
        });
      }
    } catch (error) {
      this._client.logger("error", `offlineDb:delete-reaction`, {
        tags: ["channel", "offlineDb"],
        error
      });
    }
    return await this._deleteReaction(messageID, reactionType, user_id);
  }
  /**
   * deleteReaction - Delete a reaction by user and type
   *
   * @param {string} messageID the id of the message from which te remove the reaction
   * @param {string} reactionType the type of reaction that should be removed
   * @param {string} [user_id] the id of the user (used only for server side request) default null
   *
   * @return {Promise<ReactionAPIResponse>} The Server Response
   */
  async _deleteReaction(messageID, reactionType, user_id) {
    this._checkInitialized();
    if (!reactionType || !messageID) {
      throw Error(
        "Deleting a reaction requires specifying both the message and reaction type"
      );
    }
    const url = this.getClient().baseURL + `/messages/${encodeURIComponent(messageID)}/reaction/${encodeURIComponent(
      reactionType
    )}`;
    if (user_id) {
      return await this.getClient().delete(url, { user_id });
    }
    return await this.getClient().delete(url, {});
  }
  /**
   * update - Edit the channel's custom properties
   *
   * @param {ChannelData} channelData The object to update the custom properties of this channel with
   * @param {Message} [updateMessage] Optional message object for channel members notification
   * @param {ChannelUpdateOptions} [options] Option object, configuration to control the behavior while updating
   * @return {Promise<UpdateChannelAPIResponse>} The server response
   */
  async update(channelData = {}, updateMessage, options) {
    const reserved = [
      "config",
      "cid",
      "created_by",
      "id",
      "member_count",
      "type",
      "created_at",
      "updated_at",
      "last_message_at",
      "own_capabilities"
    ];
    reserved.forEach((key) => {
      delete channelData[key];
    });
    return await this._update({
      message: updateMessage,
      data: channelData,
      ...options
    });
  }
  /**
   * updatePartial - partial update channel properties
   *
   * @param {PartialUpdateChannel} partial update request
   *
   * @return {Promise<PartialUpdateChannelAPIResponse>}
   */
  async updatePartial(update) {
    const data = await this.getClient().patch(
      this._channelURL(),
      update
    );
    const areCapabilitiesChanged = [...data.channel.own_capabilities || []].sort().join() !== [
      ...Array.isArray(this.data?.own_capabilities) ? this.data?.own_capabilities : []
    ].sort().join();
    this.data = data.channel;
    if (areCapabilitiesChanged) {
      this.getClient().dispatchEvent({
        type: "capabilities.changed",
        cid: this.cid,
        own_capabilities: data.channel.own_capabilities
      });
    }
    return data;
  }
  /**
   * enableSlowMode - enable slow mode
   *
   * @param {number} coolDownInterval the cooldown interval in seconds
   * @return {Promise<UpdateChannelAPIResponse>} The server response
   */
  async enableSlowMode(coolDownInterval) {
    const data = await this.getClient().post(
      this._channelURL(),
      {
        cooldown: coolDownInterval
      }
    );
    this.data = data.channel;
    return data;
  }
  /**
   * disableSlowMode - disable slow mode
   *
   * @return {Promise<UpdateChannelAPIResponse>} The server response
   */
  async disableSlowMode() {
    const data = await this.getClient().post(
      this._channelURL(),
      {
        cooldown: 0
      }
    );
    this.data = data.channel;
    return data;
  }
  async sendSharedLocation(location, userId) {
    const result = await this.sendMessage({
      id: location.message_id,
      shared_location: location,
      user: userId ? { id: userId } : void 0
    });
    if (location.end_at) {
      this.getClient().dispatchEvent({
        message: result.message,
        type: "live_location_sharing.started"
      });
    }
    return result;
  }
  async stopLiveLocationSharing(payload) {
    const location = await this.getClient().updateLocation({
      ...payload,
      end_at: (/* @__PURE__ */ new Date()).toISOString()
    });
    this.getClient().dispatchEvent({
      live_location: location,
      type: "live_location_sharing.stopped"
    });
  }
  /**
   * delete - Delete the channel. Messages are permanently removed.
   *
   * @param {boolean} [options.hard_delete] Defines if the channel is hard deleted or not
   *
   * @return {Promise<DeleteChannelAPIResponse>} The server response
   */
  async delete(options = {}) {
    return await this.getClient().delete(this._channelURL(), {
      ...options
    });
  }
  /**
   * truncate - Removes all messages from the channel
   * @param {TruncateOptions} [options] Defines truncation options
   * @return {Promise<TruncateChannelAPIResponse>} The server response
   */
  async truncate(options = {}) {
    return await this.getClient().post(
      this._channelURL() + "/truncate",
      options
    );
  }
  /**
   * acceptInvite - accept invitation to the channel
   *
   * @param {UpdateChannelOptions} [options] The object to update the custom properties of this channel with
   *
   * @return {Promise<UpdateChannelAPIResponse>} The server response
   */
  async acceptInvite(options = {}) {
    return await this._update({ accept_invite: true, ...options });
  }
  /**
   * rejectInvite - reject invitation to the channel
   *
   * @param {UpdateChannelOptions} [options] The object to update the custom properties of this channel with
   *
   * @return {Promise<UpdateChannelAPIResponse>} The server response
   */
  async rejectInvite(options = {}) {
    return await this._update({ reject_invite: true, ...options });
  }
  /**
   * addMembers - add members to the channel
   *
   * @param {string[] | Array<NewMemberPayload>} members An array of members to add to the channel
   * @param {Message} [message] Optional message object for channel members notification
   * @param {ChannelUpdateOptions} [options] Option object, configuration to control the behavior while updating
   * @return {Promise<UpdateChannelAPIResponse>} The server response
   */
  async addMembers(members, message, options = {}) {
    return await this._update({ add_members: members, message, ...options });
  }
  /**
   * addFilterTags - add filter tags to the channel
   *
   * @param {string[]} tags An array of tags to add to the channel
   * @param {Message} [message] Optional message object for channel members notification
   * @param {ChannelUpdateOptions} [options] Option object, configuration to control the behavior while updating
   * @return {Promise<UpdateChannelAPIResponse>} The server response
   */
  async addFilterTags(tags, message, options = {}) {
    return await this._update({ add_filter_tags: tags, message, ...options });
  }
  /**
   * removeFilterTags - remove filter tags from the channel
   *
   * @param {string[]} tags An array of tags to remove from the channel
   * @param {Message} [message] Optional message object for channel members notification
   * @param {ChannelUpdateOptions} [options] Option object, configuration to control the behavior while updating
   * @return {Promise<UpdateChannelAPIResponse>} The server response
   */
  async removeFilterTags(tags, message, options = {}) {
    return await this._update({ remove_filter_tags: tags, message, ...options });
  }
  /**
   * addModerators - add moderators to the channel
   *
   * @param {string[]} members An array of member identifiers
   * @param {Message} [message] Optional message object for channel members notification
   * @param {ChannelUpdateOptions} [options] Option object, configuration to control the behavior while updating
   * @return {Promise<UpdateChannelAPIResponse>} The server response
   */
  async addModerators(members, message, options = {}) {
    return await this._update({ add_moderators: members, message, ...options });
  }
  /**
   * assignRoles - sets member roles in a channel
   *
   * @param {{channel_role: Role, user_id: string}[]} roles List of role assignments
   * @param {Message} [message] Optional message object for channel members notification
   * @param {ChannelUpdateOptions} [options] Option object, configuration to control the behavior while updating
   * @return {Promise<UpdateChannelAPIResponse>} The server response
   */
  async assignRoles(roles, message, options = {}) {
    return await this._update({ assign_roles: roles, message, ...options });
  }
  /**
   * inviteMembers - invite members to the channel
   *
   * @param {string[] | Array<NewMemberPayload>} members An array of members to invite to the channel
   * @param {Message} [message] Optional message object for channel members notification
   * @param {ChannelUpdateOptions} [options] Option object, configuration to control the behavior while updating
   * @return {Promise<UpdateChannelAPIResponse>} The server response
   */
  async inviteMembers(members, message, options = {}) {
    return await this._update({ invites: members, message, ...options });
  }
  /**
   * removeMembers - remove members from channel
   *
   * @param {string[]} members An array of member identifiers
   * @param {Message} [message] Optional message object for channel members notification
   * @param {ChannelUpdateOptions} [options] Option object, configuration to control the behavior while updating
   * @return {Promise<UpdateChannelAPIResponse>} The server response
   */
  async removeMembers(members, message, options = {}) {
    return await this._update({ remove_members: members, message, ...options });
  }
  /**
   * demoteModerators - remove moderator role from channel members
   *
   * @param {string[]} members An array of member identifiers
   * @param {Message} [message] Optional message object for channel members notification
   * @param {ChannelUpdateOptions} [options] Option object, configuration to control the behavior while updating
   * @return {Promise<UpdateChannelAPIResponse>} The server response
   */
  async demoteModerators(members, message, options = {}) {
    return await this._update({ demote_moderators: members, message, ...options });
  }
  /**
   * _update - executes channel update request
   * @param payload Object Update Channel payload
   * @return {Promise<UpdateChannelAPIResponse>} The server response
   * TODO: introduce new type instead of Object in the next major update
   */
  async _update(payload) {
    const data = await this.getClient().post(
      this._channelURL(),
      payload
    );
    this.data = data.channel;
    return data;
  }
  /**
   * mute - mutes the current channel
   * @param {{ user_id?: string, expiration?: string }} opts expiration in minutes or user_id
   * @return {Promise<MuteChannelAPIResponse>} The server response
   *
   * example with expiration:
   * await channel.mute({expiration: moment.duration(2, 'weeks')});
   *
   * example server side:
   * await channel.mute({user_id: userId});
   *
   */
  async mute(opts = {}) {
    return await this.getClient().post(
      this.getClient().baseURL + "/moderation/mute/channel",
      {
        channel_cid: this.cid,
        ...opts
      }
    );
  }
  /**
   * unmute - mutes the current channel
   * @param {{ user_id?: string}} opts user_id
   * @return {Promise<APIResponse>} The server response
   *
   * example server side:
   * await channel.unmute({user_id: userId});
   */
  async unmute(opts = {}) {
    return await this.getClient().post(
      this.getClient().baseURL + "/moderation/unmute/channel",
      {
        channel_cid: this.cid,
        ...opts
      }
    );
  }
  /**
   * archive - archives the current channel
   * @param {{ user_id?: string }} opts user_id if called server side
   * @return {Promise<ChannelMemberResponse>} The server response
   *
   * example:
   * await channel.archives();
   *
   * example server side:
   * await channel.archive({user_id: userId});
   *
   */
  async archive(opts = {}) {
    const cli = this.getClient();
    const uid = opts.user_id || cli.userID;
    if (!uid) {
      throw Error("A user_id is required for archiving a channel");
    }
    const resp = await this.partialUpdateMember(uid, { set: { archived: true } });
    return resp.channel_member;
  }
  /**
   * unarchive - unarchives the current channel
   * @param {{ user_id?: string }} opts user_id if called server side
   * @return {Promise<ChannelMemberResponse>} The server response
   *
   * example:
   * await channel.unarchive();
   *
   * example server side:
   * await channel.unarchive({user_id: userId});
   *
   */
  async unarchive(opts = {}) {
    const cli = this.getClient();
    const uid = opts.user_id || cli.userID;
    if (!uid) {
      throw Error("A user_id is required for unarchiving a channel");
    }
    const resp = await this.partialUpdateMember(uid, { set: { archived: false } });
    return resp.channel_member;
  }
  /**
   * pin - pins the current channel
   * @param {{ user_id?: string }} opts user_id if called server side
   * @return {Promise<ChannelMemberResponse>} The server response
   *
   * example:
   * await channel.pin();
   *
   * example server side:
   * await channel.pin({user_id: userId});
   *
   */
  async pin(opts = {}) {
    const cli = this.getClient();
    const uid = opts.user_id || cli.userID;
    if (!uid) {
      throw new Error("A user_id is required for pinning a channel");
    }
    const resp = await this.partialUpdateMember(uid, { set: { pinned: true } });
    return resp.channel_member;
  }
  /**
   * unpin - unpins the current channel
   * @param {{ user_id?: string }} opts user_id if called server side
   * @return {Promise<ChannelMemberResponse>} The server response
   *
   * example:
   * await channel.unpin();
   *
   * example server side:
   * await channel.unpin({user_id: userId});
   *
   */
  async unpin(opts = {}) {
    const cli = this.getClient();
    const uid = opts.user_id || cli.userID;
    if (!uid) {
      throw new Error("A user_id is required for unpinning a channel");
    }
    const resp = await this.partialUpdateMember(uid, { set: { pinned: false } });
    return resp.channel_member;
  }
  /**
   * muteStatus - returns the mute status for the current channel
   * @return {{ muted: boolean; createdAt: Date | null; expiresAt: Date | null }} { muted: true | false, createdAt: Date | null, expiresAt: Date | null}
   */
  muteStatus() {
    this._checkInitialized();
    return this.getClient()._muteStatus(this.cid);
  }
  sendAction(messageID, formData) {
    this._checkInitialized();
    if (!messageID) {
      throw Error(`Message id is missing`);
    }
    return this.getClient().post(
      this.getClient().baseURL + `/messages/${encodeURIComponent(messageID)}/action`,
      {
        message_id: messageID,
        form_data: formData,
        id: this.id,
        type: this.type
      }
    );
  }
  /**
   * keystroke - First of the typing.start and typing.stop events based on the users keystrokes.
   * Call this on every keystroke
   * @see {@link https://getstream.io/chat/docs/typing_indicators/?language=js|Docs}
   * @param {string} [parent_id] set this field to `message.id` to indicate that typing event is happening in a thread
   */
  async keystroke(parent_id, options) {
    if (!this._isTypingIndicatorsEnabled()) {
      return;
    }
    const now = /* @__PURE__ */ new Date();
    const diff = this.lastTypingEvent && now.getTime() - this.lastTypingEvent.getTime();
    this.lastKeyStroke = now;
    this.isTyping = true;
    if (diff === null || diff > 2e3) {
      this.lastTypingEvent = /* @__PURE__ */ new Date();
      await this.sendEvent({
        type: "typing.start",
        parent_id,
        ...options || {}
      });
    }
  }
  /**
   * Sends an event to update the AI state for a specific message.
   * Typically used by the server connected to the AI service to notify clients of state changes.
   *
   * @param messageId - The ID of the message associated with the AI state.
   * @param state - The new state of the AI process (e.g., thinking, generating).
   * @param options - Optional parameters, such as `ai_message`, to include additional details in the event.
   */
  async updateAIState(messageId, state, options = {}) {
    await this.sendEvent({
      ...options,
      type: "ai_indicator.update",
      message_id: messageId,
      ai_state: state
    });
  }
  /**
   * Sends an event to notify watchers to clear the typing/thinking UI when the AI response starts streaming.
   * Typically used by the server connected to the AI service to inform clients that the AI response has started.
   */
  async clearAIIndicator() {
    await this.sendEvent({
      type: "ai_indicator.clear"
    });
  }
  /**
   * Sends an event to stop AI response generation, leaving the message in its current state.
   * Triggered by the user to halt the AI response process.
   */
  async stopAIResponse() {
    await this.sendEvent({
      type: "ai_indicator.stop"
    });
  }
  /**
   * stopTyping - Sets last typing to null and sends the typing.stop event
   * @see {@link https://getstream.io/chat/docs/typing_indicators/?language=js|Docs}
   * @param {string} [parent_id] set this field to `message.id` to indicate that typing event is happening in a thread
   */
  async stopTyping(parent_id, options) {
    if (!this._isTypingIndicatorsEnabled()) {
      return;
    }
    this.lastTypingEvent = null;
    this.isTyping = false;
    await this.sendEvent({
      type: "typing.stop",
      parent_id,
      ...options || {}
    });
  }
  _isTypingIndicatorsEnabled() {
    if (!this.getConfig()?.typing_events || !this.getClient().wsConnection?.isHealthy) {
      return false;
    }
    return this.getClient().user?.privacy_settings?.typing_indicators?.enabled ?? true;
  }
  /**
   * lastMessage - return the last message, takes into account that last few messages might not be perfectly sorted
   *
   * @return {ReturnType<ChannelState['formatMessage']> | undefined} Description
   */
  lastMessage() {
    let min = this.state.latestMessages.length - 5;
    if (min < 0) {
      min = 0;
    }
    const max = this.state.latestMessages.length + 1;
    const messageSlice = this.state.latestMessages.slice(min, max);
    messageSlice.sort((a, b) => b.created_at.getTime() - a.created_at.getTime());
    return messageSlice[0];
  }
  /**
   * markRead - Send the mark read event for this user, only works if the `read_events` setting is enabled. Syncs the message delivery report candidates local state.
   *
   * @param {MarkReadOptions} data
   * @return {Promise<EventAPIResponse | null>} Description
   */
  async markRead(data = {}) {
    return await this.getClient().messageDeliveryReporter.markRead(this, data);
  }
  /**
   * markReadRequest - Send the mark read event for this user, only works if the `read_events` setting is enabled
   *
   * @param {MarkReadOptions} data
   * @return {Promise<EventAPIResponse | null>} Description
   */
  async markAsReadRequest(data = {}) {
    this._checkInitialized();
    if (!this.getConfig()?.read_events && !this.getClient()._isUsingServerAuth()) {
      return null;
    }
    return await this.getClient().post(this._channelURL() + "/read", {
      ...data
    });
  }
  /**
   * markUnread - Mark the channel as unread from messageID, only works if the `read_events` setting is enabled
   *
   * @param {MarkUnreadOptions} data
   * @return {APIResponse} An API response
   */
  async markUnread(data) {
    this._checkInitialized();
    if (!this.getConfig()?.read_events && !this.getClient()._isUsingServerAuth()) {
      return Promise.resolve(null);
    }
    return await this.getClient().post(this._channelURL() + "/unread", {
      ...data
    });
  }
  /**
   * markReadLocally - Resets this user's unread count locally, without any backend call. Intended for
   * channels that have read events disabled (e.g. livestreams) when the client is created with the
   * `isLocalUnreadCountEnabled` option. Dispatches a dedicated, client-only `message.read_locally` event
   * that runs through the same `_handleChannelEvent` read logic as a real `message.read` (minus the
   * delivery-report network sync), so the read-state update lives in one place. When offline support
   * is enabled, the offline DB persists the reset for read-events-disabled channels, so the local
   * count stays consistent across app restarts.
   *
   * @return {Event | undefined} The dispatched `message.read_locally` event, or `undefined` if there is no connected user.
   */
  markReadLocally() {
    const client = this.getClient();
    if (!client.userID) return;
    const event = {
      channel_id: this.id,
      channel_type: this.type,
      cid: this.cid,
      created_at: (/* @__PURE__ */ new Date()).toISOString(),
      last_read_message_id: this.lastMessage()?.id,
      team: this.data?.team,
      type: "message.read_locally",
      user: client.user
    };
    client.dispatchEvent(event);
    return event;
  }
  /**
   * clean - Cleans the channel state and fires stop typing if needed
   */
  clean() {
    if (this.lastKeyStroke) {
      const now = /* @__PURE__ */ new Date();
      const diff = now.getTime() - this.lastKeyStroke.getTime();
      if (diff > 1e3 && this.isTyping) {
        logChatPromiseExecution(this.stopTyping(), "stop typing event");
      }
    }
    this.state.clean();
  }
  /**
   * watch - Loads the initial channel state and watches for changes
   *
   * @param {ChannelQueryOptions} options additional options for the query endpoint
   *
   * @return {Promise<QueryChannelAPIResponse>} The server response
   */
  async watch(options) {
    const defaultOptions = {
      state: true,
      watch: true,
      presence: false
    };
    await this.getClient().wsPromise;
    if (!this.getClient()._hasConnectionID()) {
      defaultOptions.watch = false;
    }
    const combined = { ...defaultOptions, ...options };
    const state = await this.query(combined, "latest");
    this.initialized = true;
    this.data = state.channel;
    this._client.logger(
      "info",
      `channel:watch() - started watching channel ${this.cid}`,
      {
        tags: ["channel"],
        channel: this
      }
    );
    return state;
  }
  /**
   * stopWatching - Stops watching the channel
   *
   * @return {Promise<APIResponse>} The server response
   */
  async stopWatching() {
    const response = await this.getClient().post(
      this._channelURL() + "/stop-watching",
      {}
    );
    this._client.logger(
      "info",
      `channel:watch() - stopped watching channel ${this.cid}`,
      {
        tags: ["channel"],
        channel: this
      }
    );
    return response;
  }
  /**
   * getReplies - List the message replies for a parent message.
   *
   * The recommended way of working with threads is to use the Thread class.
   *
   * @param {string} parent_id The message parent id, ie the top of the thread
   * @param {MessagePaginationOptions & { user?: UserResponse; user_id?: string }} options Pagination params, ie {limit:10, id_lte: 10}
   *
   * @return {Promise<GetRepliesAPIResponse>} A response with a list of messages
   */
  async getReplies(parent_id, options, sort) {
    const normalizedSort = sort ? normalizeQuerySort(sort) : void 0;
    const data = await this.getClient().get(
      this.getClient().baseURL + `/messages/${encodeURIComponent(parent_id)}/replies`,
      {
        sort: normalizedSort,
        ...options
      }
    );
    if (data.messages) {
      this.state.addMessagesSorted(data.messages);
    }
    return data;
  }
  /**
   * getPinnedMessages - List list pinned messages of the channel
   *
   * @param {PinnedMessagePaginationOptions & { user?: UserResponse; user_id?: string }} options Pagination params, ie {limit:10, id_lte: 10}
   * @param {PinnedMessagesSort} sort defines sorting direction of pinned messages
   *
   * @return {Promise<GetRepliesAPIResponse>} A response with a list of messages
   */
  async getPinnedMessages(options, sort = []) {
    return await this.getClient().get(
      this._channelURL() + "/pinned_messages",
      {
        payload: {
          ...options,
          sort: normalizeQuerySort(sort)
        }
      }
    );
  }
  /**
   * getReactions - List the reactions, supports pagination
   *
   * @param {string} message_id The message id
   * @param {{ limit?: number; offset?: number }} options The pagination options
   *
   * @return {Promise<GetReactionsAPIResponse>} Server response
   */
  getReactions(message_id, options) {
    return this.getClient().get(
      this.getClient().baseURL + `/messages/${encodeURIComponent(message_id)}/reactions`,
      {
        ...options
      }
    );
  }
  /**
   * getMessagesById - Retrieves a list of messages by ID
   *
   * @param {string[]} messageIds The ids of the messages to retrieve from this channel
   *
   * @return {Promise<GetMultipleMessagesAPIResponse>} Server response
   */
  getMessagesById(messageIds) {
    return this.getClient().get(
      this._channelURL() + "/messages",
      {
        ids: messageIds.join(",")
      }
    );
  }
  /**
   * lastRead - returns the last time the user marked the channel as read if the user never marked the channel as read, this will return null
   * @return {Date | null | undefined}
   */
  lastRead() {
    const { userID } = this.getClient();
    if (userID) {
      return this.state.read[userID] ? this.state.read[userID].last_read : null;
    }
  }
  _countMessageAsUnread(message) {
    if (message.shadowed) return false;
    if (message.silent) return false;
    if (message.parent_id && !message.show_in_channel) return false;
    if (message.user?.id === this.getClient().userID) return false;
    if (message.user?.id && this.getClient().userMuteStatus(message.user.id))
      return false;
    if (!this.getClient().options.isLocalUnreadCountEnabled && !channelHasReadEvents(this)) {
      return false;
    }
    if (this.getClient()._muteStatus(this.cid).muted) return false;
    return true;
  }
  /**
   * countUnread - Count of unread messages
   *
   * @param {Date | null} [lastRead] lastRead the time that the user read a message, defaults to current user's read state
   *
   * @return {number} Unread count
   */
  countUnread(lastRead) {
    if (!lastRead) return this.state.unreadCount;
    let count = 0;
    for (let i = 0; i < this.state.latestMessages.length; i += 1) {
      const message = this.state.latestMessages[i];
      if (message.created_at > lastRead && this._countMessageAsUnread(message)) {
        count++;
      }
    }
    return count;
  }
  /**
   * countUnreadMentions - Count the number of unread messages mentioning the current user
   *
   * @return {number} Unread mentions count
   */
  countUnreadMentions() {
    const lastRead = this.lastRead();
    const userID = this.getClient().userID;
    let count = 0;
    for (let i = 0; i < this.state.latestMessages.length; i += 1) {
      const message = this.state.latestMessages[i];
      if (this._countMessageAsUnread(message) && (!lastRead || message.created_at > lastRead) && message.mentioned_users?.some((user) => user.id === userID)) {
        count++;
      }
    }
    return count;
  }
  /**
   * query - Query the API, get messages, members or other channel fields
   *
   * @param {ChannelQueryOptions} options The query options
   * @param {MessageSetType} messageSetToAddToIfDoesNotExist It's possible to load disjunct sets of a channel's messages into state, use `current` to load the initial channel state or if you want to extend the currently displayed messages, use `latest` if you want to load/extend the latest messages, `new` is used for loading a specific message and it's surroundings
   *
   * @return {Promise<QueryChannelAPIResponse>} Returns a query response
   */
  async query(options = {}, messageSetToAddToIfDoesNotExist = "current") {
    await this.getClient().wsPromise;
    const createdById = options.created_by?.id ?? options.created_by_id ?? this._data?.created_by?.id ?? this._data?.created_by_id;
    if (this.getClient()._isUsingServerAuth() && typeof createdById !== "string") {
      this.getClient().logger(
        "warn",
        "Either `created_by` (with `id` property) or `created_by_id` are missing from both `Channel._data` and `options` parameter"
      );
    }
    let queryURL = `${this.getClient().baseURL}/channels/${encodeURIComponent(
      this.type
    )}`;
    if (this.id) {
      queryURL += `/${encodeURIComponent(this.id)}`;
    }
    const state = await this.getClient().post(
      queryURL + "/query",
      {
        data: this._data,
        state: true,
        ...options
      }
    );
    if (!this.id) {
      this.id = state.channel.id;
      this.cid = state.channel.cid;
      const tempChannelCid = generateChannelTempCid(
        this.type,
        state.members.map((member) => member.user_id || member.user?.id || "")
      );
      if (tempChannelCid && tempChannelCid in this.getClient().activeChannels) {
        delete this.getClient().activeChannels[tempChannelCid];
      }
      if (!(this.cid in this.getClient().activeChannels) && this.getClient()._cacheEnabled()) {
        this.getClient().activeChannels[this.cid] = this;
      }
    }
    this.getClient()._addChannelConfig(state.channel);
    if (typeof state.channel.config?.shared_locations !== "undefined") {
      this.messageComposer.updateConfig({
        location: { enabled: state.channel.config.shared_locations }
      });
    }
    const { messageSet, filteredMessageIds } = this._initializeState(
      state,
      messageSetToAddToIfDoesNotExist
    );
    messageSet.pagination = {
      ...messageSet.pagination,
      ...messageSetPagination({
        parentSet: messageSet,
        messagePaginationOptions: options?.messages,
        requestedPageSize: options?.messages?.limit ?? DEFAULT_QUERY_CHANNEL_MESSAGE_LIST_PAGE_SIZE,
        returnedPage: state.messages,
        filteredReturnedPage: state.messages.filter(
          (m) => !filteredMessageIds.includes(m.id)
        ),
        logger: this.getClient().logger
      })
    };
    this.getClient().polls.hydratePollCache(state.messages, true);
    this.getClient().reminders.hydrateState(state.messages);
    this.messageComposer.initStateFromChannelResponse(state);
    const areCapabilitiesChanged = [...state.channel.own_capabilities || []].sort().join() !== [
      ...this.data && Array.isArray(this.data?.own_capabilities) ? this.data.own_capabilities : []
    ].sort().join();
    this.data = state.channel;
    this.offlineMode = false;
    this.cooldownTimer.refresh();
    if (areCapabilitiesChanged) {
      this.getClient().dispatchEvent({
        type: "capabilities.changed",
        cid: this.cid,
        own_capabilities: state.channel.own_capabilities
      });
    }
    this.getClient().dispatchEvent({
      type: "channels.queried",
      queriedChannels: {
        channels: [state],
        isLatestMessageSet: messageSet.isLatest
      }
    });
    this.getClient().offlineDb?.executeQuerySafely(
      (db) => db.upsertChannels?.({
        channels: [state],
        isLatestMessagesSet: messageSet.isLatest
      }),
      { method: "upsertChannels" }
    );
    this.getClient().syncDeliveredCandidates([this]);
    return state;
  }
  /**
   * banUser - Bans a user from a channel
   *
   * @param {string} targetUserID
   * @param {BanUserOptions} options
   * @returns {Promise<APIResponse>}
   */
  async banUser(targetUserID, options) {
    this._checkInitialized();
    return await this.getClient().banUser(targetUserID, {
      ...options,
      type: this.type,
      id: this.id
    });
  }
  /**
   * hides the channel from queryChannels for the user until a message is added
   * If clearHistory is set to true - all messages will be removed for the user
   *
   * @param {string | null} userId
   * @param {boolean} clearHistory
   * @returns {Promise<APIResponse>}
   */
  async hide(userId = null, clearHistory = false) {
    this._checkInitialized();
    return await this.getClient().post(`${this._channelURL()}/hide`, {
      user_id: userId,
      clear_history: clearHistory
    });
  }
  /**
   * removes the hidden status for a channel
   *
   * @param {string | null} userId
   * @returns {Promise<APIResponse>}
   */
  async show(userId = null) {
    this._checkInitialized();
    return await this.getClient().post(`${this._channelURL()}/show`, {
      user_id: userId
    });
  }
  /**
   * unbanUser - Removes the bans for a user on a channel
   *
   * @param {string} targetUserID
   * @param {UnBanUserOptions} options
   * @returns {Promise<APIResponse>}
   */
  async unbanUser(targetUserID, options) {
    this._checkInitialized();
    return await this.getClient().unbanUser(targetUserID, {
      ...options,
      type: this.type,
      id: this.id
    });
  }
  /**
   * shadowBan - Shadow bans a user from a channel
   *
   * @param {string} targetUserID
   * @param {BanUserOptions} options
   * @returns {Promise<APIResponse>}
   */
  async shadowBan(targetUserID, options) {
    this._checkInitialized();
    return await this.getClient().shadowBan(targetUserID, {
      ...options,
      type: this.type,
      id: this.id
    });
  }
  /**
   * removeShadowBan - Removes the shadow ban for a user on a channel
   *
   * @param {string} targetUserID
   * @returns {Promise<APIResponse>}
   */
  async removeShadowBan(targetUserID) {
    this._checkInitialized();
    return await this.getClient().removeShadowBan(targetUserID, {
      type: this.type,
      id: this.id
    });
  }
  /**
   * Cast or cancel one or more votes on a poll
   * @param pollId string The poll id
   * @param votes PollVoteData[] The votes that will be casted (or canceled in case of an empty array)
   * @returns {APIResponse & PollVoteResponse} The poll votes
   */
  async vote(messageId, pollId, vote) {
    return await this.getClient().castPollVote(messageId, pollId, vote);
  }
  async removeVote(messageId, pollId, voteId) {
    return await this.getClient().removePollVote(messageId, pollId, voteId);
  }
  /**
   * createDraft - Creates or updates a draft message in a channel
   *
   * @param {DraftMessagePayload} message The draft message to create or update
   *
   * @return {Promise<CreateDraftResponse>} Response containing the created draft
   */
  async _createDraft(message) {
    return await this.getClient().post(
      this._channelURL() + "/draft",
      {
        message
      }
    );
  }
  /**
   * createDraft - Creates or updates a draft message in a channel. If offline support is
   * enabled, it will make sure that creating the draft is queued up if it fails due to
   * bad internet conditions and executed later.
   *
   * @param {DraftMessagePayload} message The draft message to create or update
   *
   * @return {Promise<CreateDraftResponse>} Response containing the created draft
   */
  async createDraft(message) {
    try {
      const offlineDb = this.getClient().offlineDb;
      if (offlineDb) {
        return await offlineDb.queueTask({
          task: {
            channelId: this.id,
            channelType: this.type,
            threadId: message.parent_id,
            payload: [message],
            type: "create-draft"
          }
        });
      }
    } catch (error) {
      this._client.logger("error", `offlineDb:create-draft`, {
        tags: ["channel", "offlineDb"],
        error
      });
    }
    return this._createDraft(message);
  }
  /**
   * deleteDraft - Deletes a draft message from a channel or a thread.
   *
   * @param {Object} options
   * @param {string} options.parent_id Optional parent message ID for drafts in threads
   *
   * @return {Promise<APIResponse>} API response
   */
  async _deleteDraft({ parent_id } = {}) {
    return await this.getClient().delete(this._channelURL() + "/draft", {
      parent_id
    });
  }
  /**
   * deleteDraft - Deletes a draft message from a channel or a thread. If offline support is
   * enabled, it will make sure that deleting the draft is queued up if it fails due to
   * bad internet conditions and executed later.
   *
   * @param {Object} options
   * @param {string} options.parent_id Optional parent message ID for drafts in threads
   *
   * @return {Promise<APIResponse>} API response
   */
  async deleteDraft(options = {}) {
    const { parent_id } = options;
    try {
      const offlineDb = this.getClient().offlineDb;
      if (offlineDb) {
        return await offlineDb.queueTask({
          task: {
            channelId: this.id,
            channelType: this.type,
            threadId: parent_id,
            payload: [options],
            type: "delete-draft"
          }
        });
      }
    } catch (error) {
      this._client.logger("error", `offlineDb:delete-draft`, {
        tags: ["channel", "offlineDb"],
        error
      });
    }
    return this._deleteDraft(options);
  }
  /**
   * getDraft - Retrieves a draft message from a channel
   *
   * @param {Object} options
   * @param {string} options.parent_id Optional parent message ID for drafts in threads
   *
   * @return {Promise<GetDraftResponse>} Response containing the draft
   */
  async getDraft({ parent_id } = {}) {
    return await this.getClient().get(this._channelURL() + "/draft", {
      parent_id
    });
  }
  on(callbackOrString, callbackOrNothing) {
    const key = callbackOrNothing ? callbackOrString : "all";
    const callback = callbackOrNothing ? callbackOrNothing : callbackOrString;
    if (!(key in this.listeners)) {
      this.listeners[key] = [];
    }
    this._client.logger(
      "info",
      `Attaching listener for ${key} event on channel ${this.cid}`,
      {
        tags: ["event", "channel"],
        channel: this
      }
    );
    this.listeners[key].push(callback);
    return {
      unsubscribe: () => {
        this._client.logger(
          "info",
          `Removing listener for ${key} event from channel ${this.cid}`,
          {
            tags: ["event", "channel"],
            channel: this
          }
        );
        this.listeners[key] = this.listeners[key].filter((el) => el !== callback);
      }
    };
  }
  off(callbackOrString, callbackOrNothing) {
    const key = callbackOrNothing ? callbackOrString : "all";
    const callback = callbackOrNothing ? callbackOrNothing : callbackOrString;
    if (!(key in this.listeners)) {
      this.listeners[key] = [];
    }
    this._client.logger(
      "info",
      `Removing listener for ${key} event from channel ${this.cid}`,
      {
        tags: ["event", "channel"],
        channel: this
      }
    );
    this.listeners[key] = this.listeners[key].filter((value) => value !== callback);
  }
  _handleChannelEvent(event) {
    const channel = this;
    this._client.logger(
      "info",
      `channel:_handleChannelEvent - Received event of type { ${event.type} } on ${this.cid}`,
      {
        tags: ["event", "channel"],
        channel: this
      }
    );
    const channelState = channel.state;
    switch (event.type) {
      case "typing.start":
        if (event.user?.id) {
          channelState.typing[event.user.id] = event;
        }
        break;
      case "typing.stop":
        if (event.user?.id) {
          delete channelState.typing[event.user.id];
        }
        break;
      // `message.read_locally` is the client-only event dispatched by `markReadLocally()` when read
      // events are disabled (e.g. livestreams with `isLocalUnreadCountEnabled`). It reuses the exact
      // `message.read` state logic so the read-state update lives in one place — only the
      // delivery-report network sync below is skipped for it.
      case "message.read_locally":
      case "message.read":
        if (event.thread) break;
        if (event.user?.id && event.created_at) {
          const previousReadState = channelState.read[event.user.id];
          channelState.read[event.user.id] = {
            // in case we already have delivery information
            ...previousReadState,
            last_read: new Date(event.created_at),
            last_read_message_id: event.last_read_message_id,
            user: event.user,
            unread_messages: 0
          };
          this.messageReceiptsTracker.onMessageRead({
            user: event.user,
            readAt: event.created_at,
            lastReadMessageId: event.last_read_message_id
          });
          const client = this.getClient();
          const isOwnEvent = event.user?.id === client.user?.id;
          if (isOwnEvent) {
            channelState.unreadCount = 0;
            if (event.type === "message.read") {
              client.syncDeliveredCandidates([this]);
            }
          }
        }
        break;
      case "message.delivered":
        if (event.user?.id && event.created_at) {
          const previousReadState = channelState.read[event.user.id];
          channelState.read[event.user.id] = {
            ...previousReadState,
            last_delivered_at: event.last_delivered_at ? new Date(event.last_delivered_at) : void 0,
            last_delivered_message_id: event.last_delivered_message_id,
            user: event.user
          };
          this.messageReceiptsTracker.onMessageDelivered({
            user: event.user,
            deliveredAt: event.created_at,
            lastDeliveredMessageId: event.last_delivered_message_id
          });
          const client = this.getClient();
          const isOwnEvent = event.user?.id === client.user?.id;
          if (isOwnEvent) {
            client.syncDeliveredCandidates([this]);
          }
        }
        break;
      case "user.watching.start":
      case "user.updated":
        if (event.user?.id) {
          channelState.watchers[event.user.id] = event.user;
        }
        break;
      case "user.watching.stop":
        if (event.user?.id) {
          delete channelState.watchers[event.user.id];
        }
        break;
      case "message.deleted":
        if (event.message) {
          this._extendEventWithOwnReactions(event);
          if (event.hard_delete) channelState.removeMessage(event.message);
          else channelState.addMessageSorted(event.message, false, false);
          channelState.removeQuotedMessageReferences(event.message);
          if (event.message.pinned) {
            channelState.removePinnedMessage(event.message);
          }
        }
        break;
      case "user.messages.deleted":
        if (event.user) {
          this.state.deleteUserMessages(
            event.user,
            !!event.hard_delete,
            new Date(event.created_at ?? Date.now())
          );
        }
        break;
      case "message.new":
        if (event.message) {
          const client = this.getClient();
          const ownMessage = event.user?.id === client.user?.id;
          const isThreadMessage = event.message.parent_id && !event.message.show_in_channel;
          if (this.state.isUpToDate || isThreadMessage) {
            channelState.addMessageSorted(event.message, ownMessage);
          }
          if (event.message.pinned) {
            channelState.addPinnedMessage(event.message);
          }
          const preventUnreadCountUpdate = ownMessage || isThreadMessage;
          if (ownMessage) {
            this.cooldownTimer.refresh();
          }
          if (preventUnreadCountUpdate) break;
          if (event.user?.id) {
            for (const userId in channelState.read) {
              if (userId === event.user.id) {
                channelState.read[event.user.id] = {
                  last_read: new Date(event.created_at),
                  user: event.user,
                  unread_messages: 0,
                  last_delivered_at: new Date(event.created_at),
                  last_delivered_message_id: event.message.id
                };
              } else {
                channelState.read[userId].unread_messages += 1;
              }
            }
          }
          if (this._countMessageAsUnread(event.message)) {
            channelState.unreadCount = channelState.unreadCount + 1;
          }
          client.syncDeliveredCandidates([this]);
        }
        break;
      case "message.updated":
      case "message.undeleted":
        if (event.message) {
          this._extendEventWithOwnReactions(event);
          channelState.addMessageSorted(event.message, false, false);
          channelState._updateQuotedMessageReferences({ message: event.message });
          if (event.message.pinned) {
            channelState.addPinnedMessage(event.message);
          } else {
            channelState.removePinnedMessage(event.message);
          }
        }
        break;
      case "channel.truncated":
        if (event.channel?.truncated_at) {
          const truncatedAt = +new Date(event.channel.truncated_at);
          channelState.messageSets.forEach((messageSet, messageSetIndex) => {
            messageSet.messages.forEach(({ created_at: createdAt, id }) => {
              if (truncatedAt > +createdAt)
                channelState.removeMessage({ id, messageSetIndex });
            });
          });
          channelState.pinnedMessages.forEach(({ id, created_at: createdAt }) => {
            if (truncatedAt > +createdAt)
              channelState.removePinnedMessage({ id });
          });
          channelState.unreadCount = this.countUnread(
            new Date(event.channel.truncated_at)
          );
        } else {
          channelState.clearMessages();
          channelState.unreadCount = 0;
        }
        if (event.message) {
          channelState.addMessageSorted(event.message);
          if (event.message.pinned) {
            channelState.addPinnedMessage(event.message);
          }
        }
        break;
      case "member.added":
      case "member.updated": {
        const memberCopy = {
          ...event.member
        };
        if (memberCopy.pinned_at === null) {
          delete memberCopy.pinned_at;
        }
        if (memberCopy.archived_at === null) {
          delete memberCopy.archived_at;
        }
        if (memberCopy?.user) {
          channelState.members = {
            ...channelState.members,
            [memberCopy.user.id]: memberCopy
          };
        }
        const currentUserId = this.getClient().userID;
        if (typeof currentUserId === "string" && typeof memberCopy?.user?.id === "string" && memberCopy.user.id === currentUserId) {
          channelState.membership = memberCopy;
        }
        break;
      }
      case "member.removed":
        if (event.user?.id) {
          const newMembers = {
            ...channelState.members
          };
          delete newMembers[event.user.id];
          channelState.members = newMembers;
        }
        break;
      case "notification.mark_unread": {
        const ownMessage = event.user?.id === this.getClient().user?.id;
        if (!ownMessage || !event.user) break;
        const unreadCount = event.unread_messages ?? 0;
        const currentState = channelState.read[event.user.id];
        channelState.read[event.user.id] = {
          // keep the message delivery info
          ...currentState,
          first_unread_message_id: event.first_unread_message_id,
          last_read: new Date(event.last_read_at),
          last_read_message_id: event.last_read_message_id,
          user: event.user,
          unread_messages: unreadCount
        };
        channelState.unreadCount = unreadCount;
        this.messageReceiptsTracker.onNotificationMarkUnread({
          user: event.user,
          lastReadAt: event.last_read_at,
          lastReadMessageId: event.last_read_message_id
        });
        break;
      }
      case "channel.updated":
        if (event.channel) {
          const isFrozenChanged = event.channel?.frozen !== void 0 && event.channel.frozen !== channel.data?.frozen;
          if (isFrozenChanged) {
            this.query({ state: false, messages: { limit: 0 }, watchers: { limit: 0 } });
          }
          const newChannelData = {
            ...event.channel,
            hidden: event.channel?.hidden ?? channel.data?.hidden,
            own_capabilities: event.channel?.own_capabilities ?? channel.data?.own_capabilities
          };
          channel.data = newChannelData;
          this.cooldownTimer.refresh();
        }
        break;
      case "reaction.new":
        if (event.message && event.reaction) {
          const { message, reaction } = event;
          event.message = channelState.addReaction(reaction, message);
        }
        break;
      case "reaction.deleted":
        if (event.message && event.reaction) {
          const { message, reaction } = event;
          event.message = channelState.removeReaction(reaction, message);
        }
        break;
      case "reaction.updated":
        if (event.message && event.reaction) {
          const { message, reaction } = event;
          event.message = channelState.addReaction(
            reaction,
            message,
            true
          );
        }
        break;
      case "channel.hidden":
        channel.data = {
          ...channel.data,
          blocked: !!event.channel?.blocked,
          hidden: true
        };
        if (event.clear_history) {
          channelState.clearMessages();
        }
        break;
      case "channel.visible":
        channel.data = {
          ...channel.data,
          blocked: !!event.channel?.blocked,
          hidden: false
        };
        this.getClient().offlineDb?.handleChannelVisibilityEvent({ event });
        break;
      case "user.banned":
        if (!event.user?.id) break;
        channelState.members[event.user.id] = {
          ...channelState.members[event.user.id] || {},
          shadow_banned: !!event.shadow,
          banned: !event.shadow,
          user: { ...channelState.members[event.user.id]?.user || {}, ...event.user }
        };
        break;
      case "user.unbanned":
        if (!event.user?.id) break;
        channelState.members[event.user.id] = {
          ...channelState.members[event.user.id] || {},
          shadow_banned: false,
          banned: false,
          user: { ...channelState.members[event.user.id]?.user || {}, ...event.user }
        };
        break;
      default:
    }
    if (event.watcher_count !== void 0) {
      channel.state.watcher_count = event.watcher_count;
    }
  }
  _checkInitialized() {
    if (!this.initialized && !this.offlineMode && !this.getClient()._isUsingServerAuth()) {
      throw Error(
        `Channel ${this.cid} hasn't been initialized yet. Make sure to call .watch() and wait for it to resolve`
      );
    }
  }
  _initializeState(state, messageSetToAddToIfDoesNotExist = "latest") {
    const { state: clientState, user, userID } = this.getClient();
    if (state.members) {
      this._hydrateMembers({ members: state.members });
      for (const member of state.members) {
        if (member.user) {
          clientState.updateUserReference(member.user, this.cid);
        }
      }
    }
    this.state.membership = state.membership || {};
    const messages = state.messages || [];
    if (!this.state.messages) {
      this.state.initMessages();
    }
    const { messageSet, filteredMessageIds } = this.state.addMessagesSorted(
      messages,
      false,
      true,
      true,
      messageSetToAddToIfDoesNotExist
    );
    if (!this.state.pinnedMessages) {
      this.state.pinnedMessages = [];
    }
    this.state.addPinnedMessages(state.pinned_messages || []);
    if (state.pending_messages) {
      this.state.pending_messages = state.pending_messages;
    }
    if (state.watcher_count !== void 0) {
      this.state.watcher_count = state.watcher_count;
    }
    if (state.watchers) {
      for (const watcher of state.watchers) {
        if (watcher) {
          clientState.updateUserReference(watcher, this.cid);
          this.state.watchers[watcher.id] = watcher;
        }
      }
    }
    if (userID != null) {
      const last_read = this.state.last_message_at || /* @__PURE__ */ new Date();
      if (user) {
        this.state.read[user.id] = {
          user,
          last_read,
          unread_messages: 0
        };
      }
    }
    if (state.read) {
      for (const read of state.read) {
        this.state.read[read.user.id] = {
          last_delivered_at: read.last_delivered_at ? new Date(read.last_delivered_at) : void 0,
          last_delivered_message_id: read.last_delivered_message_id,
          last_read: new Date(read.last_read),
          last_read_message_id: read.last_read_message_id,
          unread_messages: read.unread_messages ?? 0,
          user: read.user
        };
        if (read.user.id === user?.id) {
          this.state.unreadCount = this.state.read[read.user.id].unread_messages;
        }
      }
      this.messageReceiptsTracker.ingestInitial(state.read);
    }
    return {
      messageSet,
      filteredMessageIds
    };
  }
  _extendEventWithOwnReactions(event) {
    if (!event.message) {
      return;
    }
    const message = this.state.findMessage(event.message.id, event.message.parent_id);
    if (message) {
      event.message.own_reactions = message.own_reactions;
    }
  }
  _hydrateMembers({
    members,
    overrideCurrentState = true
  }) {
    const newMembersById = members.reduce(
      (membersById, member) => {
        if (member.user) {
          membersById[member.user.id] = member;
        }
        return membersById;
      },
      {}
    );
    if (overrideCurrentState) {
      this.state.members = newMembersById;
    } else if (!overrideCurrentState && members.length) {
      this.state.members = {
        ...this.state.members,
        ...newMembersById
      };
    }
  }
  _disconnect() {
    this._client.logger(
      "info",
      `channel:disconnect() - Disconnecting the channel ${this.cid}`,
      {
        tags: ["connection", "channel"],
        channel: this
      }
    );
    this.disconnected = true;
    this.cooldownTimer.clearTimeout();
    this.state.setIsUpToDate(false);
  }
};

// src/client_state.ts
var ClientState = class {
  constructor({ client }) {
    this.client = client;
    this.users = {};
    this.userChannelReferences = {};
  }
  updateUsers(users) {
    for (const user of users) {
      this.updateUser(user);
    }
  }
  updateUser(user) {
    if (user != null && this.client._cacheEnabled()) {
      this.users[user.id] = user;
    }
  }
  updateUserReference(user, channelID) {
    if (user == null || !this.client._cacheEnabled()) {
      return;
    }
    this.updateUser(user);
    if (!this.userChannelReferences[user.id]) {
      this.userChannelReferences[user.id] = {};
    }
    this.userChannelReferences[user.id][channelID] = true;
  }
  deleteAllChannelReference(channelID) {
    for (const userID in this.userChannelReferences) {
      delete this.userChannelReferences[userID][channelID];
    }
  }
};

// src/connection.ts
var import_isomorphic_ws = __toESM(require("isomorphic-ws"));

// src/insights.ts
var import_axios2 = __toESM(require("axios"));
var InsightMetrics = class {
  constructor() {
    this.connectionStartTimestamp = null;
    this.wsTotalFailures = 0;
    this.wsConsecutiveFailures = 0;
    this.instanceClientId = randomId();
  }
};
var postInsights = async (insightType, insights) => {
  const maxAttempts = 3;
  for (let i = 0; i < maxAttempts; i++) {
    try {
      await import_axios2.default.post(
        `https://chat-insights.getstream.io/insights/${insightType}`,
        insights
      );
    } catch (e) {
      await sleep((i + 1) * 3e3);
      continue;
    }
    break;
  }
};
function buildWsFatalInsight(connection, event) {
  return {
    ...event,
    ...buildWsBaseInsight(connection)
  };
}
function buildWsBaseInsight(connection) {
  const { client } = connection;
  return {
    ready_state: connection.ws?.readyState,
    url: connection._buildUrl(),
    api_key: client.key,
    start_ts: client.insightMetrics.connectionStartTimestamp,
    end_ts: (/* @__PURE__ */ new Date()).getTime(),
    auth_type: client.getAuthType(),
    token: client.tokenManager.token,
    user_id: client.userID,
    user_details: client._user,
    device: client.options.device,
    client_id: connection.connectionID,
    ws_details: connection.ws,
    ws_consecutive_failures: client.insightMetrics.wsConsecutiveFailures,
    ws_total_failures: client.insightMetrics.wsTotalFailures,
    request_id: connection.requestID,
    online: typeof navigator !== "undefined" ? navigator?.onLine : null,
    user_agent: typeof navigator !== "undefined" ? navigator?.userAgent : null,
    instance_client_id: client.insightMetrics.instanceClientId
  };
}
function buildWsSuccessAfterFailureInsight(connection) {
  return buildWsBaseInsight(connection);
}

// src/connection.ts
var isCloseEvent = (res) => res.code !== void 0;
var isErrorEvent = (res) => res.error !== void 0;
var StableWSConnection = class {
  constructor({ client }) {
    /**
     * Builds and returns the url for websocket.
     * @private
     * @returns url string
     */
    this._buildUrl = () => {
      const qs = this.client._buildWSPayload(this.requestID);
      const token = this.client.tokenManager.getToken();
      const wsUrlParams = this.client.options.wsUrlParams;
      const params = new URLSearchParams(wsUrlParams);
      params.set("json", qs);
      params.set("api_key", this.client.key);
      params.set("authorization", `${token}`);
      params.set("stream-auth-type", this.client.getAuthType());
      params.set("X-Stream-Client", this.client.getUserAgent());
      return `${this.client.wsBaseURL}/connect?${params.toString()}`;
    };
    /**
     * onlineStatusChanged - this function is called when the browser connects or disconnects from the internet.
     *
     * @param {Event} event Event with type online or offline
     *
     */
    this.onlineStatusChanged = (event) => {
      if (event.type === "offline") {
        this._log("onlineStatusChanged() - Status changing to offline");
        this._setHealth(false);
      } else if (event.type === "online") {
        this._log(
          `onlineStatusChanged() - Status changing to online. isHealthy: ${this.isHealthy}`
        );
        if (!this.isHealthy) {
          this._reconnect({ interval: 10 });
        }
      }
    };
    this.onopen = (wsID) => {
      if (this.wsID !== wsID) return;
      this._log("onopen() - onopen callback", { wsID });
    };
    this.onmessage = (wsID, event) => {
      if (this.wsID !== wsID) return;
      this._log("onmessage() - onmessage callback", { event, wsID });
      if (typeof event.data !== "string") return;
      const data = JSON.parse(event.data);
      if (!this.isResolved) {
        this.isResolved = true;
        if (data.error) {
          this.rejectPromise?.(this._errorFromWSEvent(data, false));
          return;
        }
        this.resolvePromise?.(data);
        this._setHealth(true);
      }
      this.lastEvent = /* @__PURE__ */ new Date();
      if (data.type === "health.check") {
        this.scheduleNextPing();
      }
      this.client.dispatchEvent(data);
      this.scheduleConnectionCheck();
    };
    this.onclose = (wsID, event) => {
      if (this.wsID !== wsID) return;
      this._log("onclose() - onclose callback - " + event.code, { event, wsID });
      if (event.code === chatCodes.WS_CLOSED_SUCCESS) {
        const error = new Error(
          `WS connection reject with error ${event.reason}`
        );
        error.reason = event.reason;
        error.code = event.code;
        error.wasClean = event.wasClean;
        error.target = event.target;
        this.rejectPromise?.(error);
        this._log(`onclose() - WS connection reject with error ${event.reason}`, { event });
      } else {
        this.consecutiveFailures += 1;
        this.totalFailures += 1;
        this._setHealth(false);
        this.isConnecting = false;
        this.rejectPromise?.(this._errorFromWSEvent(event));
        this._log(`onclose() - WS connection closed. Calling reconnect ...`, { event });
        this._reconnect();
      }
    };
    this.onerror = (wsID, event) => {
      if (this.wsID !== wsID) return;
      this.consecutiveFailures += 1;
      this.totalFailures += 1;
      this._setHealth(false);
      this.isConnecting = false;
      this.rejectPromise?.(this._errorFromWSEvent(event));
      this._log(`onerror() - WS connection resulted into error`, { event });
      this._reconnect();
    };
    /**
     * _setHealth - Sets the connection to healthy or unhealthy.
     * Broadcasts an event in case the connection status changed.
     *
     * @param {boolean} healthy boolean indicating if the connection is healthy or not
     *
     */
    this._setHealth = (healthy) => {
      if (healthy === this.isHealthy) return;
      this.isHealthy = healthy;
      if (this.isHealthy) {
        this.client.dispatchEvent({ type: "connection.changed", online: this.isHealthy });
        return;
      }
      setTimeout(() => {
        if (this.isHealthy) return;
        this.client.dispatchEvent({ type: "connection.changed", online: this.isHealthy });
      }, 5e3);
    };
    /**
     * _errorFromWSEvent - Creates an error object for the WS event
     *
     */
    this._errorFromWSEvent = (event, isWSFailure2 = true) => {
      let code;
      let statusCode;
      let message;
      if (isCloseEvent(event)) {
        code = event.code;
        statusCode = "unknown";
        message = event.reason;
      }
      if (isErrorEvent(event)) {
        code = event.error.code;
        statusCode = event.error.StatusCode;
        message = event.error.message;
      }
      this._log(`_errorFromWSEvent() - WS failed with code ${code}`, { event }, "warn");
      const error = new Error(
        `WS failed with code ${code} and reason - ${message}`
      );
      error.code = code;
      error.StatusCode = statusCode;
      error.isWSFailure = isWSFailure2;
      return error;
    };
    /**
     * _setupPromise - sets up the this.connectOpen promise
     */
    this._setupConnectionPromise = () => {
      this.isResolved = false;
      this.connectionOpen = new Promise((resolve, reject) => {
        this.resolvePromise = resolve;
        this.rejectPromise = reject;
      });
    };
    /**
     * Schedules a next health check ping for websocket.
     */
    this.scheduleNextPing = () => {
      if (this.healthCheckTimeoutRef) {
        clearTimeout(this.healthCheckTimeoutRef);
      }
      this.healthCheckTimeoutRef = setTimeout(() => {
        const data = [{ type: "health.check", client_id: this.client.clientID }];
        try {
          this.ws?.send(JSON.stringify(data));
        } catch (e) {
        }
      }, this.pingInterval);
    };
    /**
     * scheduleConnectionCheck - schedules a check for time difference between last received event and now.
     * If the difference is more than 35 seconds, it means our health check logic has failed and websocket needs
     * to be reconnected.
     */
    this.scheduleConnectionCheck = () => {
      if (this.connectionCheckTimeoutRef) {
        clearTimeout(this.connectionCheckTimeoutRef);
      }
      this.connectionCheckTimeoutRef = setTimeout(() => {
        const now = /* @__PURE__ */ new Date();
        if (this.lastEvent && now.getTime() - this.lastEvent.getTime() > this.connectionCheckTimeout) {
          this._log("scheduleConnectionCheck - going to reconnect");
          this._setHealth(false);
          this._reconnect();
        }
      }, this.connectionCheckTimeout);
    };
    this.client = client;
    this.consecutiveFailures = 0;
    this.totalFailures = 0;
    this.isConnecting = false;
    this.isDisconnected = false;
    this.isResolved = false;
    this.isHealthy = false;
    this.wsID = 1;
    this.lastEvent = null;
    this.pingInterval = 25 * 1e3;
    this.connectionCheckTimeout = this.pingInterval + 10 * 1e3;
    addConnectionEventListeners(this.onlineStatusChanged);
  }
  _log(msg, extra = {}, level = "info") {
    this.client.logger(level, "connection:" + msg, { tags: ["connection"], ...extra });
  }
  setClient(client) {
    this.client = client;
  }
  /**
   * connect - Connect to the WS URL
   * the default 15s timeout allows between 2~3 tries
   * @return {ConnectAPIResponse<ChannelType, CommandType, UserType>} Promise that completes once the first health check message is received
   */
  async connect(timeout = 15e3) {
    if (this.isConnecting) {
      throw Error(
        `You've called connect twice, can only attempt 1 connection at the time`
      );
    }
    this.isDisconnected = false;
    try {
      const healthCheck = await this._connect();
      this.consecutiveFailures = 0;
      this._log(`connect() - Established ws connection with healthcheck: ${healthCheck}`);
    } catch (error) {
      this.isHealthy = false;
      this.consecutiveFailures += 1;
      const e = error;
      if (e.code === chatCodes.TOKEN_EXPIRED && !this.client.tokenManager.isStatic()) {
        this._log(
          "connect() - WS failure due to expired token, so going to try to reload token and reconnect"
        );
        this._reconnect({ refreshToken: true });
      } else if (!e.isWSFailure) {
        throw new Error(
          JSON.stringify({
            code: e.code,
            StatusCode: e.StatusCode,
            message: e.message,
            isWSFailure: e.isWSFailure
          })
        );
      }
    }
    return await this._waitForHealthy(timeout);
  }
  /**
   * _waitForHealthy polls the promise connection to see if its resolved until it times out
   * the default 15s timeout allows between 2~3 tries
   * @param timeout duration(ms)
   */
  _waitForHealthy(timeout = 15e3) {
    return Promise.race([
      (async () => {
        const interval = 50;
        for (let i = 0; i <= timeout; i += interval) {
          try {
            return await this.connectionOpen;
          } catch (error) {
            if (i === timeout) {
              throw new Error(
                JSON.stringify({
                  code: error.code,
                  StatusCode: error.StatusCode,
                  message: error.message,
                  isWSFailure: error.isWSFailure
                })
              );
            }
            await sleep(interval);
          }
        }
      })(),
      (async () => {
        await sleep(timeout);
        this.isConnecting = false;
        throw new Error(
          JSON.stringify({
            code: "",
            StatusCode: "",
            message: "initial WS connection could not be established",
            isWSFailure: true
          })
        );
      })()
    ]);
  }
  /**
   * disconnect - Disconnect the connection and doesn't recover...
   *
   */
  disconnect(timeout) {
    this._log(`disconnect() - Closing the websocket connection for wsID ${this.wsID}`);
    this.wsID += 1;
    this.isConnecting = false;
    this.isDisconnected = true;
    if (this.healthCheckTimeoutRef) {
      clearInterval(this.healthCheckTimeoutRef);
    }
    if (this.connectionCheckTimeoutRef) {
      clearInterval(this.connectionCheckTimeoutRef);
    }
    removeConnectionEventListeners(this.onlineStatusChanged);
    this.isHealthy = false;
    if (this.ws && this.ws.removeAllListeners) {
      this.ws.removeAllListeners();
    }
    let isClosedPromise;
    const { ws } = this;
    if (ws && ws.close && ws.readyState === ws.OPEN) {
      isClosedPromise = new Promise((resolve) => {
        const onclose = (event) => {
          this._log(
            `disconnect() - resolving isClosedPromise ${event ? "with" : "without"} close frame`,
            { event }
          );
          resolve();
        };
        ws.onclose = onclose;
        setTimeout(onclose, timeout != null ? timeout : 1e3);
      });
      this._log(
        `disconnect() - Manually closed connection by calling client.disconnect()`
      );
      ws.close(
        chatCodes.WS_CLOSED_SUCCESS,
        "Manually closed connection by calling client.disconnect()"
      );
    } else {
      this._log(`disconnect() - ws connection doesn't exist or it is already closed.`);
      isClosedPromise = Promise.resolve();
    }
    delete this.ws;
    return isClosedPromise;
  }
  /**
   * _connect - Connect to the WS endpoint
   *
   * @return {ConnectAPIResponse<ChannelType, CommandType, UserType>} Promise that completes once the first health check message is received
   */
  async _connect() {
    if (this.isConnecting || this.isDisconnected && this.client.options.enableWSFallback)
      return;
    this.isConnecting = true;
    this.requestID = randomId();
    this.client.insightMetrics.connectionStartTimestamp = (/* @__PURE__ */ new Date()).getTime();
    let isTokenReady = false;
    try {
      this._log(`_connect() - waiting for token`);
      await this.client.tokenManager.tokenReady();
      isTokenReady = true;
    } catch (e) {
    }
    try {
      if (!isTokenReady) {
        this._log(`_connect() - tokenProvider failed before, so going to retry`);
        await this.client.tokenManager.loadToken();
      }
      this._setupConnectionPromise();
      const wsURL = this._buildUrl();
      this._log(`_connect() - Connecting to ${wsURL}`, {
        wsURL,
        requestID: this.requestID
      });
      this.ws = new import_isomorphic_ws.default(wsURL);
      this.ws.onopen = this.onopen.bind(this, this.wsID);
      this.ws.onclose = this.onclose.bind(this, this.wsID);
      this.ws.onerror = this.onerror.bind(this, this.wsID);
      this.ws.onmessage = this.onmessage.bind(this, this.wsID);
      const response = await this.connectionOpen;
      this.isConnecting = false;
      if (response) {
        this.connectionID = response.connection_id;
        if (this.client.insightMetrics.wsConsecutiveFailures > 0 && this.client.options.enableInsights) {
          postInsights(
            "ws_success_after_failure",
            buildWsSuccessAfterFailureInsight(this)
          );
          this.client.insightMetrics.wsConsecutiveFailures = 0;
        }
        return response;
      }
    } catch (error) {
      this.isConnecting = false;
      this._log(`_connect() - Error - `, error);
      if (this.client.options.enableInsights) {
        this.client.insightMetrics.wsConsecutiveFailures++;
        this.client.insightMetrics.wsTotalFailures++;
        const insights = buildWsFatalInsight(
          this,
          convertErrorToJson(error)
        );
        postInsights?.("ws_fatal", insights);
      }
      throw error;
    }
  }
  /**
   * _reconnect - Retry the connection to WS endpoint
   *
   * @param {{ interval?: number; refreshToken?: boolean }} options Following options are available
   *
   * - `interval`	{int}			number of ms that function should wait before reconnecting
   * - `refreshToken` {boolean}	reload/refresh user token be refreshed before attempting reconnection.
   */
  async _reconnect(options = {}) {
    this._log("_reconnect() - Initiating the reconnect");
    if (this.isConnecting || this.isHealthy) {
      this._log("_reconnect() - Abort (1) since already connecting or healthy");
      return;
    }
    let interval = options.interval;
    if (!interval) {
      interval = retryInterval(this.consecutiveFailures);
    }
    await sleep(interval);
    if (this.isConnecting || this.isHealthy) {
      this._log("_reconnect() - Abort (2) since already connecting or healthy");
      return;
    }
    if (this.isDisconnected && this.client.options.enableWSFallback) {
      this._log("_reconnect() - Abort (3) since disconnect() is called");
      return;
    }
    this._log("_reconnect() - Destroying current WS connection");
    this._destroyCurrentWSConnection();
    if (options.refreshToken) {
      await this.client.tokenManager.loadToken();
    }
    try {
      await this._connect();
      this._log("_reconnect() - Waiting for recoverCallBack");
      await this.client.recoverState();
      this._log("_reconnect() - Finished recoverCallBack");
      this.consecutiveFailures = 0;
    } catch (error) {
      this.isHealthy = false;
      this.consecutiveFailures += 1;
      if (error.code === chatCodes.TOKEN_EXPIRED && !this.client.tokenManager.isStatic()) {
        this._log(
          "_reconnect() - WS failure due to expired token, so going to try to reload token and reconnect"
        );
        return this._reconnect({ refreshToken: true });
      }
      if (error.isWSFailure) {
        this._log("_reconnect() - WS failure, so going to try to reconnect");
        this._reconnect();
      }
    }
    this._log("_reconnect() - == END ==");
  }
  /**
   * _destroyCurrentWSConnection - Removes the current WS connection
   *
   */
  _destroyCurrentWSConnection() {
    this.wsID += 1;
    try {
      this?.ws?.removeAllListeners();
      this?.ws?.close();
    } catch (e) {
    }
  }
};

// src/signing.ts
var import_jsonwebtoken = __toESM(require("jsonwebtoken"));
var import_crypto = __toESM(require("crypto"));
var import_zlib = __toESM(require("zlib"));
function JWTUserToken(apiSecret, userId, extraData = {}, jwtOptions = {}) {
  if (typeof userId !== "string") {
    throw new TypeError("userId should be a string");
  }
  const payload = {
    user_id: userId,
    ...extraData
  };
  if (import_jsonwebtoken.default == null || import_jsonwebtoken.default.sign == null) {
    throw Error(
      `Unable to find jwt crypto, if you are getting this error is probably because you are trying to generate tokens on browser or React Native (or other environment where crypto functions are not available). Please Note: token should only be generated server-side.`
    );
  }
  const opts = Object.assign(
    { algorithm: "HS256", noTimestamp: true },
    jwtOptions
  );
  if (payload.iat) {
    opts.noTimestamp = false;
  }
  return import_jsonwebtoken.default.sign(payload, apiSecret, opts);
}
function JWTServerToken(apiSecret, jwtOptions = {}) {
  const payload = {
    server: true
  };
  const opts = Object.assign(
    { algorithm: "HS256", noTimestamp: true },
    jwtOptions
  );
  return import_jsonwebtoken.default.sign(payload, apiSecret, opts);
}
function UserFromToken(token) {
  const fragments = token.split(".");
  if (fragments.length !== 3) {
    return "";
  }
  const b64Payload = fragments[1];
  const payload = decodeBase64(b64Payload);
  const data = JSON.parse(payload);
  return data.user_id;
}
function DevToken(userId) {
  return [
    "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
    //{"alg": "HS256", "typ": "JWT"}
    encodeBase64(JSON.stringify({ user_id: userId })),
    "devtoken"
    // hardcoded signature
  ].join(".");
}
function verifySignature(body, signature, secret) {
  const key = Buffer.from(secret, "utf8");
  const hash = import_crypto.default.createHmac("sha256", key).update(body).digest("hex");
  try {
    return import_crypto.default.timingSafeEqual(Buffer.from(hash), Buffer.from(signature));
  } catch {
    return false;
  }
}
function CheckSignature(body, secret, signature) {
  return verifySignature(body, signature, secret);
}
var InvalidWebhookErrorMessages = {
  signatureMismatch: "signature mismatch",
  invalidBase64: "invalid base64 encoding",
  gzipFailed: "gzip decompression failed",
  invalidJson: "invalid JSON payload"
};
var InvalidWebhookError = class extends Error {
  constructor(message = InvalidWebhookErrorMessages.signatureMismatch) {
    super(message);
    this.name = "InvalidWebhookError";
  }
};
function gunzipPayload(rawBody) {
  const GZIP_MAGIC = Buffer.from([31, 139]);
  const body = Buffer.isBuffer(rawBody) ? rawBody : Buffer.from(rawBody);
  if (body.length >= 2 && body.subarray(0, 2).equals(GZIP_MAGIC)) {
    try {
      return import_zlib.default.gunzipSync(body);
    } catch {
      throw new InvalidWebhookError(InvalidWebhookErrorMessages.gzipFailed);
    }
  }
  return body;
}
function decodeSqsPayload(body) {
  if (!/^[A-Za-z0-9+/]*={0,2}$/.test(body) || body.length % 4 !== 0) {
    throw new InvalidWebhookError(InvalidWebhookErrorMessages.invalidBase64);
  }
  const decoded = Buffer.from(body, "base64");
  if (decoded.toString("base64").length !== body.length) {
    throw new InvalidWebhookError(InvalidWebhookErrorMessages.invalidBase64);
  }
  return gunzipPayload(decoded);
}
function decodeSnsPayload(notificationBody) {
  const inner = extractSnsMessage(notificationBody);
  return decodeSqsPayload(inner ?? notificationBody);
}
function extractSnsMessage(notificationBody) {
  const trimmed = notificationBody.replace(/^[\s\uFEFF]+/, "");
  if (!trimmed.startsWith("{")) {
    return null;
  }
  let parsed;
  try {
    parsed = JSON.parse(trimmed);
  } catch {
    return null;
  }
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed) || typeof parsed.Message !== "string") {
    return null;
  }
  return parsed.Message;
}
function parseEvent(payload) {
  const text = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
  try {
    return JSON.parse(text);
  } catch {
    throw new InvalidWebhookError(InvalidWebhookErrorMessages.invalidJson);
  }
}
function verifyAndParse(payload, signature, secret) {
  if (!verifySignature(payload, signature, secret)) {
    throw new InvalidWebhookError(InvalidWebhookErrorMessages.signatureMismatch);
  }
  return parseEvent(payload);
}
function verifyAndParseWebhook(rawBody, signature, secret) {
  return verifyAndParse(gunzipPayload(rawBody), signature, secret);
}
function parseSqs(messageBody) {
  return parseEvent(decodeSqsPayload(messageBody));
}
function parseSns(notificationBody) {
  return parseEvent(decodeSnsPayload(notificationBody));
}

// src/token_manager.ts
var TokenManager = class {
  /**
   * Constructor
   *
   * @param {Secret} secret
   */
  constructor(secret) {
    /**
     * Set the static string token or token provider.
     * Token provider should return a token string or a promise which resolves to string token.
     *
     * @param {TokenOrProvider} tokenOrProvider
     * @param {UserResponse} user
     */
    this.setTokenOrProvider = async (tokenOrProvider, user) => {
      this.validateToken(tokenOrProvider, user);
      this.user = user;
      if (isFunction(tokenOrProvider)) {
        this.tokenProvider = tokenOrProvider;
        this.type = "provider";
      }
      if (typeof tokenOrProvider === "string") {
        this.token = tokenOrProvider;
        this.type = "static";
      }
      if (!tokenOrProvider && this.user && this.secret) {
        this.token = JWTUserToken(this.secret, user.id, {}, {});
        this.type = "static";
      }
      await this.loadToken();
    };
    /**
     * Resets the token manager.
     * Useful for client disconnection or switching user.
     */
    this.reset = () => {
      this.token = void 0;
      this.tokenProvider = void 0;
      this.type = "static";
      this.user = void 0;
      this.loadTokenPromise = null;
    };
    // Validates the user token.
    this.validateToken = (tokenOrProvider, user) => {
      if (user && user.anon && !tokenOrProvider) return;
      if (!this.secret && !tokenOrProvider) {
        throw new Error("User token can not be empty");
      }
      if (tokenOrProvider && typeof tokenOrProvider !== "string" && !isFunction(tokenOrProvider)) {
        throw new Error("user token should either be a string or a function");
      }
      if (typeof tokenOrProvider === "string") {
        if (user.anon && tokenOrProvider === "") return;
        const tokenUserId = UserFromToken(tokenOrProvider);
        if (tokenOrProvider != null && (tokenUserId == null || tokenUserId === "" || tokenUserId !== user.id)) {
          throw new Error(
            "userToken does not have a user_id or is not matching with user.id"
          );
        }
      }
    };
    // Resolves when token is ready. This function is simply to check if loadToken is in progress, in which
    // case a function should wait.
    this.tokenReady = () => this.loadTokenPromise;
    // Fetches a token from tokenProvider function and sets in tokenManager.
    // In case of static token, it will simply resolve to static token.
    this.loadToken = () => {
      this.loadTokenPromise = new Promise(async (resolve, reject) => {
        if (this.type === "static") {
          return resolve(this.token);
        }
        if (this.tokenProvider && typeof this.tokenProvider !== "string") {
          try {
            this.token = await this.tokenProvider();
          } catch (e) {
            return reject(
              new Error(`Call to tokenProvider failed with message: ${e}`, { cause: e })
            );
          }
          resolve(this.token);
        }
      });
      return this.loadTokenPromise;
    };
    // Returns a current token
    this.getToken = () => {
      if (this.token) {
        return this.token;
      }
      if (this.user && this.user.anon && !this.token) {
        return this.token;
      }
      if (this.secret) {
        return JWTServerToken(this.secret);
      }
      throw new Error(
        `Both secret and user tokens are not set. Either client.connectUser wasn't called or client.disconnect was called`
      );
    };
    this.isStatic = () => this.type === "static";
    this.loadTokenPromise = null;
    if (secret) {
      this.secret = secret;
    }
    this.type = "static";
    if (this.secret) {
      this.token = JWTServerToken(this.secret);
    }
  }
};

// src/connection_fallback.ts
var import_axios3 = __toESM(require("axios"));
var WSConnectionFallback = class {
  constructor({ client }) {
    /** @private */
    this._onlineStatusChanged = (event) => {
      this._log(`_onlineStatusChanged() - ${event.type}`);
      if (event.type === "offline") {
        this._setState("CLOSED" /* Closed */);
        this.cancelToken?.cancel("disconnect() is called");
        this.cancelToken = void 0;
        return;
      }
      if (event.type === "online" && this.state === "CLOSED" /* Closed */) {
        this.connect(true);
      }
    };
    /** @private */
    this._req = async (params, config, retry) => {
      if (!this.cancelToken && !params.close) {
        this.cancelToken = import_axios3.default.CancelToken.source();
      }
      try {
        const res = await this.client.doAxiosRequest(
          "get",
          this.client.baseURL.replace(":3030", ":8900") + "/longpoll",
          // replace port if present for testing with local API
          void 0,
          {
            config: { ...config, cancelToken: this.cancelToken?.token },
            params
          }
        );
        this.consecutiveFailures = 0;
        return res;
      } catch (error) {
        this.consecutiveFailures += 1;
        if (retry && isErrorRetryable(error)) {
          this._log(`_req() - Retryable error, retrying request`);
          await sleep(retryInterval(this.consecutiveFailures));
          return this._req(params, config, retry);
        }
        throw error;
      }
    };
    /** @private */
    this._poll = async () => {
      while (this.state === "CONNECTED" /* Connected */) {
        try {
          const data = await this._req({}, { timeout: 3e4 }, true);
          if (data.events?.length) {
            for (let i = 0; i < data.events.length; i++) {
              this.client.dispatchEvent(data.events[i]);
            }
          }
        } catch (error) {
          if (import_axios3.default.isCancel(error)) {
            this._log(`_poll() - axios canceled request`);
            return;
          }
          if (isConnectionIDError(error)) {
            this._log(`_poll() - ConnectionID error, connecting without ID...`);
            this._setState("DISCONNECTED" /* Disconnected */);
            this.connect(true);
            return;
          }
          if (isAPIError(error) && !isErrorRetryable(error)) {
            this._setState("CLOSED" /* Closed */);
            return;
          }
          await sleep(retryInterval(this.consecutiveFailures));
        }
      }
    };
    /**
     * connect try to open a longpoll request
     * @param reconnect should be false for first call and true for subsequent calls to keep the connection alive and call recoverState
     */
    this.connect = async (reconnect = false) => {
      if (this.state === "CONNECTING" /* Connecting */) {
        this._log("connect() - connecting already in progress", { reconnect }, "warn");
        return;
      }
      if (this.state === "CONNECTED" /* Connected */) {
        this._log("connect() - already connected and polling", { reconnect }, "warn");
        return;
      }
      this._setState("CONNECTING" /* Connecting */);
      this.connectionID = void 0;
      try {
        const { event } = await this._req(
          { json: this.client._buildWSPayload() },
          { timeout: 8e3 },
          // 8s
          reconnect
        );
        this._setState("CONNECTED" /* Connected */);
        this.connectionID = event.connection_id;
        this.client.dispatchEvent(event);
        this._poll();
        if (reconnect) {
          this.client.recoverState();
        }
        return event;
      } catch (err) {
        this._setState("CLOSED" /* Closed */);
        throw err;
      }
    };
    /**
     * isHealthy checks if there is a connectionID and connection is in Connected state
     */
    this.isHealthy = () => !!this.connectionID && this.state === "CONNECTED" /* Connected */;
    this.disconnect = async (timeout = 2e3) => {
      removeConnectionEventListeners(this._onlineStatusChanged);
      this._setState("DISCONNECTED" /* Disconnected */);
      this.cancelToken?.cancel("disconnect() is called");
      this.cancelToken = void 0;
      const connection_id = this.connectionID;
      this.connectionID = void 0;
      try {
        await this._req({ close: true, connection_id }, { timeout }, false);
        this._log(`disconnect() - Closed connectionID`);
      } catch (err) {
        this._log(`disconnect() - Failed`, { err }, "error");
      }
    };
    this.client = client;
    this.state = "INIT" /* Init */;
    this.consecutiveFailures = 0;
    addConnectionEventListeners(this._onlineStatusChanged);
  }
  _log(msg, extra = {}, level = "info") {
    this.client.logger(level, "WSConnectionFallback:" + msg, {
      tags: ["connection_fallback", "connection"],
      ...extra
    });
  }
  _setState(state) {
    this._log(`_setState() - ${state}`);
    if (this.state === "CONNECTING" /* Connecting */ && state === "CONNECTED" /* Connected */) {
      this.client.dispatchEvent({ type: "connection.changed", online: true });
    }
    if (state === "CLOSED" /* Closed */ || state === "DISCONNECTED" /* Disconnected */) {
      this.client.dispatchEvent({ type: "connection.changed", online: false });
    }
    this.state = state;
  }
};

// src/segment.ts
var Segment = class {
  constructor(client, type, id, data) {
    this.client = client;
    this.type = type;
    this.id = id;
    this.data = data;
  }
  create() {
    const body = {
      name: this.data?.name,
      filter: this.data?.filter,
      description: this.data?.description,
      all_sender_channels: this.data?.all_sender_channels,
      all_users: this.data?.all_users
    };
    return this.client.createSegment(this.type, this.id, body);
  }
  verifySegmentId() {
    if (!this.id) {
      throw new Error(
        "Segment id is missing. Either create the segment using segment.create() or set the id during instantiation - const segment = client.segment(id)"
      );
    }
  }
  get() {
    this.verifySegmentId();
    return this.client.getSegment(this.id);
  }
  update(data) {
    this.verifySegmentId();
    return this.client.updateSegment(this.id, data);
  }
  addTargets(targets) {
    this.verifySegmentId();
    return this.client.addSegmentTargets(this.id, targets);
  }
  removeTargets(targets) {
    this.verifySegmentId();
    return this.client.removeSegmentTargets(this.id, targets);
  }
  delete() {
    this.verifySegmentId();
    return this.client.deleteSegment(this.id);
  }
  targetExists(targetId) {
    this.verifySegmentId();
    return this.client.segmentTargetExists(this.id, targetId);
  }
  queryTargets(filter = {}, sort = [], options = {}) {
    this.verifySegmentId();
    return this.client.querySegmentTargets(this.id, filter, sort, options);
  }
};

// src/moderation.ts
var MODERATION_ENTITY_TYPES = {
  user: "stream:user",
  message: "stream:chat:v1:message",
  userprofile: "stream:v1:user_profile"
};
var Moderation = class {
  constructor(client) {
    this.client = client;
  }
  /**
   * Flag a user
   *
   * @param {string} flaggedUserID User ID to be flagged
   * @param {string} reason Reason for flagging the user
   * @param {Object} options Additional options for flagging the user
   * @param {string} options.user_id (For server side usage) User ID of the user who is flagging the target user
   * @param {Object} options.custom Additional data to be stored with the flag
   * @returns
   */
  flagUser(flaggedUserID, reason, options = {}) {
    return this.flag(MODERATION_ENTITY_TYPES.user, flaggedUserID, "", reason, options);
  }
  /**
   * Flag a message
   *
   * @param {string} messageID Message ID to be flagged
   * @param {string} reason Reason for flagging the message
   * @param {Object} options Additional options for flagging the message
   * @param {string} options.user_id (For server side usage) User ID of the user who is flagging the target message
   * @param {Object} options.custom Additional data to be stored with the flag
   * @returns
   */
  flagMessage(messageID, reason, options = {}) {
    return this.flag(MODERATION_ENTITY_TYPES.message, messageID, "", reason, options);
  }
  /**
   * Flag a user
   *
   * @param {string} entityType Entity type to be flagged
   * @param {string} entityId Entity ID to be flagged
   * @param {string} entityCreatorID User ID of the entity creator
   * @param {string} reason Reason for flagging the entity
   * @param {Object} options Additional options for flagging the entity
   * @param {string} options.user_id (For server side usage) User ID of the user who is flagging the target entity
   * @param {Object} options.moderation_payload Content to be flagged e.g., { texts: ['text1', 'text2'], images: ['image1', 'image2']}
   * @param {Object} options.custom Additional data to be stored with the flag
   * @returns
   */
  async flag(entityType, entityId, entityCreatorID, reason, options = {}) {
    return await this.client.post(
      this.client.baseURL + "/api/v2/moderation/flag",
      {
        entity_type: entityType,
        entity_id: entityId,
        entity_creator_id: entityCreatorID,
        reason,
        ...options
      }
    );
  }
  /**
   * Mute a user
   * @param {string} targetID  User ID to be muted
   * @param {Object} options Additional options for muting the user
   * @param {string} options.user_id (For server side usage) User ID of the user who is muting the target user
   * @param {number} options.timeout Timeout for the mute in minutes
   * @returns
   */
  async muteUser(targetID, options = {}) {
    return await this.client.post(
      this.client.baseURL + "/api/v2/moderation/mute",
      {
        target_ids: [targetID],
        ...options
      }
    );
  }
  /**
   * Unmute a user
   * @param {string} targetID  User ID to be unmuted
   * @param {Object} options Additional options for unmuting the user
   * @param {string} options.user_id (For server side usage) User ID of the user who is unmuting the target user
   * @returns
   */
  async unmuteUser(targetID, options) {
    return await this.client.post(
      this.client.baseURL + "/api/v2/moderation/unmute",
      {
        target_ids: [targetID],
        ...options
      }
    );
  }
  /**
   * Get moderation report for a user
   * @param {string} userID User ID for which moderation report is to be fetched
   * @param {Object} options Additional options for fetching the moderation report
   * @param {boolean} options.create_user_if_not_exists Create user if not exists
   * @param {boolean} options.include_user_blocks Include user blocks
   * @param {boolean} options.include_user_mutes Include user mutes
   */
  async getUserModerationReport(userID, options = {}) {
    return await this.client.get(
      this.client.baseURL + `/api/v2/moderation/user_report`,
      {
        user_id: userID,
        ...options
      }
    );
  }
  /**
   * Query review queue
   * @param {Object} filterConditions Filter conditions for querying review queue
   * @param {Object} sort Sort conditions for querying review queue
   * @param {Object} options Pagination options for querying review queue
   */
  async queryReviewQueue(filterConditions = {}, sort = [], options = {}) {
    return await this.client.post(
      this.client.baseURL + "/api/v2/moderation/review_queue",
      {
        filter: filterConditions,
        sort: normalizeQuerySort(sort),
        ...options
      }
    );
  }
  /**
   * Upsert moderation config
   * @param {Object} config Moderation config to be upserted
   */
  async upsertConfig(config) {
    return await this.client.post(
      this.client.baseURL + "/api/v2/moderation/config",
      config
    );
  }
  /**
   * Get moderation config
   * @param {string} key Key for which moderation config is to be fetched
   */
  async getConfig(key, data) {
    return await this.client.get(
      this.client.baseURL + "/api/v2/moderation/config/" + key,
      data
    );
  }
  async deleteConfig(key, data) {
    return await this.client.delete(
      this.client.baseURL + "/api/v2/moderation/config/" + key,
      data
    );
  }
  /**
   * Query moderation configs
   * @param {Object} filterConditions Filter conditions for querying moderation configs
   * @param {Object} sort Sort conditions for querying moderation configs
   * @param {Object} options Additional options for querying moderation configs
   */
  async queryConfigs(filterConditions, sort, options = {}) {
    return await this.client.post(
      this.client.baseURL + "/api/v2/moderation/configs",
      {
        filter: filterConditions,
        sort,
        ...options
      }
    );
  }
  async submitAction(actionType, itemID, options = {}) {
    return await this.client.post(
      this.client.baseURL + "/api/v2/moderation/submit_action",
      {
        action_type: actionType,
        item_id: itemID,
        ...options
      }
    );
  }
  /**
   *
   * @param {string} entityType string Type of entity to be checked E.g., stream:user, stream:chat:v1:message, or any custom string
   * @param {string} entityID string ID of the entity to be checked. This is mainly for tracking purposes
   * @param {string} entityCreatorID string ID of the entity creator
   * @param {object} moderationPayload object Content to be checked for moderation. E.g., { texts: ['text1', 'text2'], images: ['image1', 'image2']}
   * @param {Array} moderationPayload.texts array Array of texts to be checked for moderation
   * @param {Array} moderationPayload.images array Array of images to be checked for moderation
   * @param {Array} moderationPayload.videos array Array of videos to be checked for moderation
   * @param configKey
   * @param options
   * @returns
   */
  async check(entityType, entityID, entityCreatorID, moderationPayload, configKey, options, testMode) {
    return await this.client.post(
      this.client.baseURL + `/api/v2/moderation/check`,
      {
        entity_type: entityType,
        entity_id: entityID,
        entity_creator_id: entityCreatorID,
        moderation_payload: moderationPayload,
        config_key: configKey,
        options,
        test_mode: testMode
      }
    );
  }
  /**
   * Experimental: Check user profile
   *
   * Warning: This is an experimental feature and the API is subject to change.
   *
   * This function is used to check a user profile for moderation.
   * This will not create any review queue items for the user profile.
   * You can just use this to check whether to allow a certain user profile to be created or not.
   *
   * Example:
   *
   * ```ts
   * const res = await client.moderation.checkUserProfile(userId, { username: "fuck_boy_001", image: "https://example.com/profile.jpg" });
   * if (res.recommended_action === "remove") {
   *   // Block the user profile from being created
   * } else {
   *   // Allow the user profile to be created
   * }
   * ```
   *
   * @param userId
   * @param profile.username
   * @param profile.image
   * @returns
   */
  async checkUserProfile(userId, profile) {
    if (!profile.username && !profile.image) {
      throw new Error("Either username or image must be provided");
    }
    const moderationPayload = {};
    if (profile.username) {
      moderationPayload.texts = [profile.username];
    }
    if (profile.image) {
      moderationPayload.images = [profile.image];
    }
    return await this.check(
      MODERATION_ENTITY_TYPES.userprofile,
      userId,
      userId,
      moderationPayload,
      "user_profile:default",
      {
        force_sync: true
      },
      true
    );
  }
  /**
   *
   * @param {string} entityType string Type of entity to be checked E.g., stream:user, stream:chat:v1:message, or any custom string
   * @param {string} entityID string ID of the entity to be checked. This is mainly for tracking purposes
   * @param {string} entityCreatorID string ID of the entity creator
   * @param {object} moderationPayload object Content to be checked for moderation. E.g., { texts: ['text1', 'text2'], images: ['image1', 'image2']}
   * @param {Array} moderationPayload.texts array Array of texts to be checked for moderation
   * @param {Array} moderationPayload.images array Array of images to be checked for moderation
   * @param {Array} moderationPayload.videos array Array of videos to be checked for moderation
   * @param {object} moderationPayload.custom object Additional custom data to attach to the moderation review queue item
   * @param {Array<CustomCheckFlag>} flags Array of CustomCheckFlag to be passed to flag the entity
   * @returns
   */
  async addCustomFlags(entityType, entityID, entityCreatorID, moderationPayload, flags) {
    return await this.client.post(
      this.client.baseURL + `/api/v2/moderation/custom_check`,
      {
        entity_type: entityType,
        entity_id: entityID,
        entity_creator_id: entityCreatorID,
        moderation_payload: moderationPayload,
        flags
      }
    );
  }
  /**
   * Add custom flags to a message
   * @param {string} messageID Message ID to be flagged
   * @param {Array<CustomCheckFlag>} flags Array of CustomCheckFlag to be passed to flag the message
   * @returns
   */
  async addCustomMessageFlags(messageID, flags) {
    return await this.addCustomFlags(
      MODERATION_ENTITY_TYPES.message,
      messageID,
      "",
      {},
      flags
    );
  }
  /**
   * Create or update a moderation rule
   * @param {ModerationRuleRequest} rule Rule configuration to be upserted
   * @returns
   */
  async upsertModerationRule(rule) {
    return await this.client.post(
      this.client.baseURL + "/api/v2/moderation/moderation_rule",
      rule
    );
  }
  /**
   * Query moderation rules
   * @param {QueryModerationRulesFilters} filterConditions Filter conditions for querying moderation rules
   * @param {QueryModerationRulesSort} sort Sort conditions for querying moderation rules
   * @param {Pager} options Pagination options for querying moderation rules
   * @returns
   */
  async queryModerationRules(filterConditions = {}, sort = [], options = {}) {
    return await this.client.post(
      this.client.baseURL + "/api/v2/moderation/moderation_rules",
      {
        filter: filterConditions,
        sort,
        ...options
      }
    );
  }
  /**
   * Get a specific moderation rule by ID
   * @param {string} id ID of the moderation rule to fetch
   * @returns
   */
  async getModerationRule(id) {
    return await this.client.get(
      this.client.baseURL + "/api/v2/moderation/moderation_rule/" + id
    );
  }
  /**
   * Delete a moderation rule by ID
   * @param {string} id ID of the moderation rule to delete
   * @returns
   */
  async deleteModerationRule(id) {
    return await this.client.delete(
      this.client.baseURL + "/api/v2/moderation/moderation_rule/" + id
    );
  }
};

// src/thread_manager.ts
var DEFAULT_CONNECTION_RECOVERY_THROTTLE_DURATION = 1e3;
var MAX_QUERY_THREADS_LIMIT = 25;
var THREAD_MANAGER_INITIAL_STATE = {
  active: false,
  wasActivatedAtLeastOnce: false,
  isThreadOrderStale: false,
  threads: [],
  unreadThreadCount: 0,
  unseenThreadIds: [],
  lastConnectionDropAt: null,
  pagination: {
    isLoading: false,
    isLoadingNext: false,
    nextCursor: null
  },
  ready: false
};
var ThreadManager = class extends WithSubscriptions {
  // cache used in combination with threadsById
  // used for threads which are not stored in the list
  // private threadCache: Record<string, Thread | undefined> = {};
  constructor({ client }) {
    super();
    this.resetState = () => {
      this.state.next(THREAD_MANAGER_INITIAL_STATE);
    };
    this.activate = () => {
      this.state.partialNext({ active: true, wasActivatedAtLeastOnce: true });
    };
    this.deactivate = () => {
      this.state.partialNext({ active: false });
    };
    this.registerSubscriptions = () => {
      if (this.hasSubscriptions) return;
      this.addUnsubscribeFunction(this.subscribeUnreadThreadsCountChange());
      this.addUnsubscribeFunction(this.subscribeManageThreadSubscriptions());
      this.addUnsubscribeFunction(this.subscribeReloadOnActivation());
      this.addUnsubscribeFunction(this.subscribeNewReplies());
      this.addUnsubscribeFunction(this.subscribeRecoverAfterConnectionDrop());
      this.addUnsubscribeFunction(this.subscribeChannelDeleted());
    };
    this.subscribeUnreadThreadsCountChange = () => {
      const { unread_threads: unreadThreadCount = 0 } = this.client.user ?? {};
      this.state.partialNext({ unreadThreadCount });
      const unsubscribeFunctions = [
        "health.check",
        "notification.mark_read",
        "notification.mark_unread",
        "notification.thread_message_new",
        "notification.channel_deleted"
      ].map(
        (eventType) => this.client.on(eventType, (event) => {
          const { unread_threads: unreadThreadCount2 } = event.me ?? event;
          if (typeof unreadThreadCount2 === "number") {
            this.state.partialNext({ unreadThreadCount: unreadThreadCount2 });
          }
        }).unsubscribe
      );
      return () => unsubscribeFunctions.forEach((unsubscribe) => unsubscribe());
    };
    this.subscribeChannelDeleted = () => this.client.on("notification.channel_deleted", (event) => {
      const { cid } = event;
      const { threads } = this.state.getLatestValue();
      const newThreads = threads.filter((thread) => thread.channel.cid !== cid);
      this.state.partialNext({ threads: newThreads });
    }).unsubscribe;
    this.subscribeManageThreadSubscriptions = () => this.state.subscribeWithSelector(
      (nextValue) => ({ threads: nextValue.threads }),
      ({ threads: nextThreads }, prev) => {
        const { threads: prevThreads = [] } = prev ?? {};
        const removedThreads = prevThreads.filter(
          (thread) => thread !== this.threadsById[thread.id]
        );
        nextThreads.forEach((thread) => thread.registerSubscriptions());
        removedThreads.forEach((thread) => thread.unregisterSubscriptions());
      }
    );
    this.subscribeReloadOnActivation = () => this.state.subscribeWithSelector(
      (nextValue) => ({ active: nextValue.active }),
      ({ active }) => {
        if (active) this.reload();
      }
    );
    this.subscribeNewReplies = () => this.client.on("notification.thread_message_new", (event) => {
      const parentId = event.message?.parent_id;
      if (!parentId) return;
      const { unseenThreadIds, ready } = this.state.getLatestValue();
      if (!ready) return;
      if (this.threadsById[parentId]) {
        this.state.partialNext({ isThreadOrderStale: true });
      } else if (!unseenThreadIds.includes(parentId)) {
        this.state.partialNext({ unseenThreadIds: unseenThreadIds.concat(parentId) });
      }
    }).unsubscribe;
    this.subscribeRecoverAfterConnectionDrop = () => {
      const unsubscribeConnectionDropped = this.client.on("connection.changed", (event) => {
        if (event.online === false) {
          this.state.next(
            (current) => current.lastConnectionDropAt ? current : {
              ...current,
              lastConnectionDropAt: /* @__PURE__ */ new Date()
            }
          );
        }
      }).unsubscribe;
      const throttledHandleConnectionRecovered = throttle(
        () => {
          const { lastConnectionDropAt, wasActivatedAtLeastOnce } = this.state.getLatestValue();
          if (!lastConnectionDropAt || !wasActivatedAtLeastOnce) return;
          this.reload({ force: true });
        },
        DEFAULT_CONNECTION_RECOVERY_THROTTLE_DURATION,
        { trailing: true }
      );
      const unsubscribeConnectionRecovered = this.client.on(
        "connection.recovered",
        throttledHandleConnectionRecovered
      ).unsubscribe;
      return () => {
        unsubscribeConnectionDropped();
        unsubscribeConnectionRecovered();
      };
    };
    this.unregisterSubscriptions = () => {
      this.state.getLatestValue().threads.forEach((thread) => thread.unregisterSubscriptions());
      return super.unregisterSubscriptions();
    };
    this.reload = async ({ force = false } = {}) => {
      const { threads, unseenThreadIds, isThreadOrderStale, pagination, ready } = this.state.getLatestValue();
      if (pagination.isLoading) return;
      if (!force && ready && !unseenThreadIds.length && !isThreadOrderStale) return;
      const limit = threads.length + unseenThreadIds.length;
      try {
        this.state.next((current) => ({
          ...current,
          pagination: {
            ...current.pagination,
            isLoading: true
          }
        }));
        const response = await this.queryThreads({
          limit: Math.min(limit, MAX_QUERY_THREADS_LIMIT) || MAX_QUERY_THREADS_LIMIT
        });
        const nextThreads = [];
        for (const incomingThread of response.threads) {
          const existingThread = this.threadsById[incomingThread.id];
          if (existingThread) {
            nextThreads.push(existingThread);
            if (existingThread.hasStaleState) {
              existingThread.hydrateState(incomingThread);
            }
          } else {
            nextThreads.push(incomingThread);
          }
        }
        this.state.next((current) => ({
          ...current,
          threads: nextThreads,
          unseenThreadIds: [],
          isThreadOrderStale: false,
          pagination: {
            ...current.pagination,
            isLoading: false,
            nextCursor: response.next ?? null
          },
          ready: true
        }));
      } catch (error) {
        this.client.logger("error", error.message);
        this.state.next((current) => ({
          ...current,
          pagination: {
            ...current.pagination,
            isLoading: false
          }
        }));
      }
    };
    this.queryThreads = (options = {}) => this.client.queryThreads({
      limit: 25,
      participant_limit: 10,
      reply_limit: 10,
      watch: true,
      ...options
    });
    this.loadNextPage = async (options = {}) => {
      const { pagination } = this.state.getLatestValue();
      if (pagination.isLoadingNext || !pagination.nextCursor) return;
      try {
        this.state.partialNext({ pagination: { ...pagination, isLoadingNext: true } });
        const response = await this.queryThreads({
          ...options,
          next: pagination.nextCursor
        });
        this.state.next((current) => ({
          ...current,
          threads: response.threads.length ? current.threads.concat(response.threads) : current.threads,
          pagination: {
            ...current.pagination,
            nextCursor: response.next ?? null,
            isLoadingNext: false
          }
        }));
      } catch (error) {
        this.client.logger("error", error.message);
        this.state.next((current) => ({
          ...current,
          pagination: {
            ...current.pagination,
            isLoadingNext: false
          }
        }));
      }
    };
    this.client = client;
    this.state = new StateStore(THREAD_MANAGER_INITIAL_STATE);
    this.threadsByIdGetterCache = { threads: [], threadsById: {} };
  }
  get threadsById() {
    const { threads } = this.state.getLatestValue();
    if (threads === this.threadsByIdGetterCache.threads) {
      return this.threadsByIdGetterCache.threadsById;
    }
    const threadsById = threads.reduce(
      (newThreadsById, thread) => {
        newThreadsById[thread.id] = thread;
        return newThreadsById;
      },
      {}
    );
    this.threadsByIdGetterCache.threads = threads;
    this.threadsByIdGetterCache.threadsById = threadsById;
    return threadsById;
  }
};

// src/poll.ts
var isPollUpdatedEvent = (e) => e.type === "poll.updated";
var isPollClosedEventEvent = (e) => e.type === "poll.closed";
var isPollVoteCastedEvent = (e) => e.type === "poll.vote_casted";
var isPollVoteChangedEvent = (e) => e.type === "poll.vote_changed";
var isPollVoteRemovedEvent = (e) => e.type === "poll.vote_removed";
var isVoteAnswer = (vote) => !!vote?.answer_text;
var Poll = class {
  constructor({ client, poll }) {
    this.getInitialStateFromPollResponse = (poll) => {
      const { own_votes, id, ...pollResponseForState } = poll;
      const { ownAnswer, ownVotes } = own_votes?.reduce(
        (acc, voteOrAnswer) => {
          if (isVoteAnswer(voteOrAnswer)) {
            acc.ownAnswer = voteOrAnswer;
          } else {
            acc.ownVotes.push(voteOrAnswer);
          }
          return acc;
        },
        { ownVotes: [] }
      ) ?? { ownVotes: [] };
      return {
        ...pollResponseForState,
        lastActivityAt: /* @__PURE__ */ new Date(),
        maxVotedOptionIds: getMaxVotedOptionIds(
          pollResponseForState.vote_counts_by_option
        ),
        ownAnswer,
        ownVotesByOptionId: getOwnVotesByOptionId(ownVotes)
      };
    };
    this.upsertOfflineDb = () => {
      this.client.offlineDb?.executeQuerySafely(
        (db) => db.upsertPoll({ poll: mapPollStateToResponse(this) }),
        { method: "upsertPoll" }
      );
    };
    this.reinitializeState = (poll) => {
      this.state.partialNext(this.getInitialStateFromPollResponse(poll));
    };
    this.handlePollUpdated = (event) => {
      if (event.poll?.id && event.poll.id !== this.id) return;
      if (!isPollUpdatedEvent(event)) return;
      const { id, ...pollData } = extractPollData(event.poll);
      this.state.partialNext({ ...pollData, lastActivityAt: new Date(event.created_at) });
      this.upsertOfflineDb();
    };
    this.handlePollClosed = (event) => {
      if (event.poll?.id && event.poll.id !== this.id) return;
      if (!isPollClosedEventEvent(event)) return;
      this.state.partialNext({
        is_closed: true,
        lastActivityAt: new Date(event.created_at)
      });
      this.upsertOfflineDb();
    };
    this.handleVoteCasted = (event) => {
      if (event.poll?.id && event.poll.id !== this.id) return;
      if (!isPollVoteCastedEvent(event)) return;
      const currentState = this.data;
      const isOwnVote = event.poll_vote.user_id === this.client.userID;
      let latestAnswers = [...currentState.latest_answers];
      let ownAnswer = currentState.ownAnswer;
      const ownVotesByOptionId = currentState.ownVotesByOptionId;
      let maxVotedOptionIds = currentState.maxVotedOptionIds;
      if (isOwnVote) {
        if (isVoteAnswer(event.poll_vote)) {
          ownAnswer = event.poll_vote;
        } else if (event.poll_vote.option_id) {
          ownVotesByOptionId[event.poll_vote.option_id] = event.poll_vote;
        }
      }
      if (isVoteAnswer(event.poll_vote)) {
        latestAnswers = [event.poll_vote, ...latestAnswers];
      } else {
        maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
      }
      const pollEnrichData = extractPollEnrichedData(event.poll);
      this.state.partialNext({
        ...pollEnrichData,
        latest_answers: latestAnswers,
        lastActivityAt: new Date(event.created_at),
        ownAnswer,
        ownVotesByOptionId,
        maxVotedOptionIds
      });
      this.upsertOfflineDb();
    };
    this.handleVoteChanged = (event) => {
      if (event.poll?.id && event.poll.id !== this.id) return;
      if (!isPollVoteChangedEvent(event)) return;
      const currentState = this.data;
      const isOwnVote = event.poll_vote.user_id === this.client.userID;
      let latestAnswers = [...currentState.latest_answers];
      let ownAnswer = currentState.ownAnswer;
      let ownVotesByOptionId = currentState.ownVotesByOptionId;
      let maxVotedOptionIds = currentState.maxVotedOptionIds;
      if (isOwnVote) {
        if (isVoteAnswer(event.poll_vote)) {
          latestAnswers = [
            event.poll_vote,
            ...latestAnswers.filter((answer) => answer.id !== event.poll_vote.id)
          ];
          ownAnswer = event.poll_vote;
        } else if (event.poll_vote.option_id) {
          if (event.poll.enforce_unique_vote) {
            ownVotesByOptionId = { [event.poll_vote.option_id]: event.poll_vote };
          } else {
            ownVotesByOptionId = Object.entries(ownVotesByOptionId).reduce((acc, [optionId, vote]) => {
              if (optionId !== event.poll_vote.option_id && vote.id === event.poll_vote.id) {
                return acc;
              }
              acc[optionId] = vote;
              return acc;
            }, {});
            ownVotesByOptionId[event.poll_vote.option_id] = event.poll_vote;
          }
          if (ownAnswer?.id === event.poll_vote.id) {
            ownAnswer = void 0;
          }
          maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
        }
      } else if (isVoteAnswer(event.poll_vote)) {
        latestAnswers = [event.poll_vote, ...latestAnswers];
      } else {
        maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
      }
      const pollEnrichData = extractPollEnrichedData(event.poll);
      this.state.partialNext({
        ...pollEnrichData,
        latest_answers: latestAnswers,
        lastActivityAt: new Date(event.created_at),
        ownAnswer,
        ownVotesByOptionId,
        maxVotedOptionIds
      });
      this.upsertOfflineDb();
    };
    this.handleVoteRemoved = (event) => {
      if (event.poll?.id && event.poll.id !== this.id) return;
      if (!isPollVoteRemovedEvent(event)) return;
      const currentState = this.data;
      const isOwnVote = event.poll_vote.user_id === this.client.userID;
      let latestAnswers = [...currentState.latest_answers];
      let ownAnswer = currentState.ownAnswer;
      const ownVotesByOptionId = { ...currentState.ownVotesByOptionId };
      let maxVotedOptionIds = currentState.maxVotedOptionIds;
      if (isVoteAnswer(event.poll_vote)) {
        latestAnswers = latestAnswers.filter((answer) => answer.id !== event.poll_vote.id);
        if (isOwnVote) {
          ownAnswer = void 0;
        }
      } else {
        maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
        if (isOwnVote && event.poll_vote.option_id) {
          delete ownVotesByOptionId[event.poll_vote.option_id];
        }
      }
      const pollEnrichData = extractPollEnrichedData(event.poll);
      this.state.partialNext({
        ...pollEnrichData,
        latest_answers: latestAnswers,
        lastActivityAt: new Date(event.created_at),
        ownAnswer,
        ownVotesByOptionId,
        maxVotedOptionIds
      });
      this.upsertOfflineDb();
    };
    this.query = async (id) => {
      const { poll } = await this.client.getPoll(id);
      this.state.partialNext({ ...poll, lastActivityAt: /* @__PURE__ */ new Date() });
      return poll;
    };
    this.update = async (data) => await this.client.updatePoll({ ...data, id: this.id });
    this.partialUpdate = async (partialPollObject) => await this.client.partialUpdatePoll(this.id, partialPollObject);
    this.close = async () => await this.client.closePoll(this.id);
    this.delete = async () => await this.client.deletePoll(this.id);
    this.createOption = async (option) => await this.client.createPollOption(this.id, option);
    this.updateOption = async (option) => await this.client.updatePollOption(this.id, option);
    this.deleteOption = async (optionId) => await this.client.deletePollOption(this.id, optionId);
    this.castVote = async (optionId, messageId) => {
      const { max_votes_allowed, ownVotesByOptionId } = this.data;
      const reachedVoteLimit = max_votes_allowed && max_votes_allowed === Object.keys(ownVotesByOptionId).length;
      if (reachedVoteLimit) {
        this.client.notifications.addInfo({
          message: "Reached the vote limit. Remove an existing vote first.",
          origin: {
            emitter: "Poll",
            context: { messageId, optionId }
          },
          options: {
            type: "validation:poll:castVote:limit"
          }
        });
        return;
      }
      return await this.client.castPollVote(messageId, this.id, {
        option_id: optionId
      });
    };
    this.removeVote = async (voteId, messageId) => await this.client.removePollVote(messageId, this.id, voteId);
    this.addAnswer = async (answerText, messageId) => await this.client.addPollAnswer(messageId, this.id, answerText);
    this.removeAnswer = async (answerId, messageId) => await this.client.removePollVote(messageId, this.id, answerId);
    this.queryAnswers = async (params) => await this.client.queryPollAnswers(
      this.id,
      params.filter,
      params.sort,
      params.options
    );
    this.queryOptionVotes = async (params) => await this.client.queryPollVotes(
      this.id,
      params.filter,
      params.sort,
      params.options
    );
    this.client = client;
    this.id = poll.id;
    this.state = new StateStore(this.getInitialStateFromPollResponse(poll));
  }
  get data() {
    return this.state.getLatestValue();
  }
};
function getMaxVotedOptionIds(voteCountsByOption) {
  let maxVotes = 0;
  let winningOptions = [];
  for (const [id, count] of Object.entries(voteCountsByOption ?? {})) {
    if (count > maxVotes) {
      winningOptions = [id];
      maxVotes = count;
    } else if (count === maxVotes) {
      winningOptions.push(id);
    }
  }
  return winningOptions;
}
function getOwnVotesByOptionId(ownVotes) {
  return !ownVotes ? {} : ownVotes.reduce((acc, vote) => {
    if (isVoteAnswer(vote) || !vote.option_id) return acc;
    acc[vote.option_id] = vote;
    return acc;
  }, {});
}
function extractPollData(pollResponse) {
  return {
    allow_answers: pollResponse.allow_answers,
    allow_user_suggested_options: pollResponse.allow_user_suggested_options,
    description: pollResponse.description,
    enforce_unique_vote: pollResponse.enforce_unique_vote,
    id: pollResponse.id,
    is_closed: pollResponse.is_closed,
    max_votes_allowed: pollResponse.max_votes_allowed,
    name: pollResponse.name,
    options: pollResponse.options,
    voting_visibility: pollResponse.voting_visibility
  };
}
function mapPollStateToResponse(poll) {
  const {
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    lastActivityAt,
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    maxVotedOptionIds,
    ownVotesByOptionId,
    ownAnswer,
    ...restState
  } = poll.data;
  const ownVotes = [
    ...Object.values(ownVotesByOptionId),
    ...ownAnswer ? [ownAnswer] : []
  ].sort((a, b) => Date.parse(a.created_at) - Date.parse(b.created_at));
  return {
    ...restState,
    own_votes: ownVotes,
    id: poll.id
  };
}
function extractPollEnrichedData(pollResponse) {
  return {
    answers_count: pollResponse.answers_count,
    latest_votes_by_option: pollResponse.latest_votes_by_option,
    vote_count: pollResponse.vote_count,
    vote_counts_by_option: pollResponse.vote_counts_by_option
  };
}

// src/poll_manager.ts
var PollManager = class extends WithSubscriptions {
  constructor({ client }) {
    super();
    // The pollCache contains only polls that have been created and sent as messages
    // (i.e only polls that are coupled with a message, can be voted on and require a
    // reactive state). It shall work as a basic look-up table for our SDK to be able
    // to quickly consume poll state that will be reactive even without the polls being
    // rendered within the UI.
    this.pollCache = /* @__PURE__ */ new Map();
    this.fromState = (id) => this.pollCache.get(id);
    this.registerSubscriptions = () => {
      if (this.hasSubscriptions) {
        return;
      }
      this.addUnsubscribeFunction(this.subscribeMessageNew());
      this.addUnsubscribeFunction(this.subscribePollUpdated());
      this.addUnsubscribeFunction(this.subscribePollClosed());
      this.addUnsubscribeFunction(this.subscribeVoteCasted());
      this.addUnsubscribeFunction(this.subscribeVoteChanged());
      this.addUnsubscribeFunction(this.subscribeVoteRemoved());
    };
    this.createPoll = async (poll) => {
      const { poll: createdPoll } = await this.client.createPoll(poll);
      if (!createdPoll.vote_counts_by_option) {
        createdPoll.vote_counts_by_option = {};
      }
      this.setOrOverwriteInCache(createdPoll);
      return this.fromState(createdPoll.id);
    };
    this.getPoll = async (id) => {
      const cachedPoll = this.fromState(id);
      if (cachedPoll) {
        this.client.getPoll(id).then(({ poll: poll2 }) => this.setOrOverwriteInCache(poll2, true));
        return cachedPoll;
      }
      const { poll } = await this.client.getPoll(id);
      this.setOrOverwriteInCache(poll);
      return this.fromState(id);
    };
    this.queryPolls = async (filter, sort = [], options = {}) => {
      const { polls, next } = await this.client.queryPolls(filter, sort, options);
      const pollInstances = polls.map((poll) => {
        this.setOrOverwriteInCache(poll, true);
        return this.fromState(poll.id);
      });
      return {
        polls: pollInstances,
        next
      };
    };
    this.hydratePollCache = (messages, overwriteState) => {
      for (const message of messages) {
        if (!message.poll) {
          continue;
        }
        const pollResponse = message.poll;
        this.setOrOverwriteInCache(pollResponse, overwriteState);
      }
    };
    this.setOrOverwriteInCache = (pollResponse, overwriteState) => {
      if (!this.client._cacheEnabled()) {
        return;
      }
      const pollFromCache = this.fromState(pollResponse.id);
      if (!pollFromCache) {
        const poll = new Poll({ client: this.client, poll: pollResponse });
        this.pollCache.set(poll.id, poll);
      } else if (overwriteState) {
        pollFromCache.reinitializeState(pollResponse);
      }
    };
    this.subscribePollUpdated = () => this.client.on("poll.updated", (event) => {
      if (event.poll?.id) {
        this.fromState(event.poll.id)?.handlePollUpdated(event);
      }
    }).unsubscribe;
    this.subscribePollClosed = () => this.client.on("poll.closed", (event) => {
      if (event.poll?.id) {
        this.fromState(event.poll.id)?.handlePollClosed(event);
      }
    }).unsubscribe;
    this.subscribeVoteCasted = () => this.client.on("poll.vote_casted", (event) => {
      if (event.poll?.id) {
        this.fromState(event.poll.id)?.handleVoteCasted(event);
      }
    }).unsubscribe;
    this.subscribeVoteChanged = () => this.client.on("poll.vote_changed", (event) => {
      if (event.poll?.id) {
        this.fromState(event.poll.id)?.handleVoteChanged(event);
      }
    }).unsubscribe;
    this.subscribeVoteRemoved = () => this.client.on("poll.vote_removed", (event) => {
      if (event.poll?.id) {
        this.fromState(event.poll.id)?.handleVoteRemoved(event);
      }
    }).unsubscribe;
    this.subscribeMessageNew = () => this.client.on("message.new", (event) => {
      const { message } = event;
      if (message) {
        const formattedMessage = formatMessage(message);
        this.hydratePollCache([formattedMessage]);
      }
    }).unsubscribe;
    this.client = client;
  }
  get data() {
    return this.pollCache;
  }
};

// src/channel_manager.ts
var channelManagerEventToHandlerMapping = {
  "channel.deleted": "channelDeletedHandler",
  "channel.hidden": "channelHiddenHandler",
  "channel.truncated": "channelTruncatedHandler",
  "channel.updated": "channelUpdatedHandler",
  "channel.visible": "channelVisibleHandler",
  "message.new": "newMessageHandler",
  "member.updated": "memberUpdatedHandler",
  "notification.added_to_channel": "notificationAddedToChannelHandler",
  "notification.message_new": "notificationNewMessageHandler",
  "notification.removed_from_channel": "notificationRemovedFromChannelHandler"
};
var DEFAULT_CHANNEL_MANAGER_OPTIONS = {
  abortInFlightQuery: false,
  allowNotLoadedChannelPromotionForEvent: {
    "channel.visible": true,
    "message.new": true,
    "notification.added_to_channel": true,
    "notification.message_new": true
  },
  lockChannelOrder: false
};
var DEFAULT_CHANNEL_MANAGER_PAGINATION_OPTIONS = {
  offset: 0
};
var mapPredefinedFilterSortToChannelSort = (sort) => (sort ?? []).map(({ direction = 1, field }) => ({
  [field]: direction
}));
var getResponsePaginationParams = ({
  queryChannelsResponse,
  sort
}) => {
  const predefinedFilter = queryChannelsResponse?.predefined_filter;
  if (!predefinedFilter) {
    return {};
  }
  return {
    responseFilters: predefinedFilter.filter,
    responseSort: predefinedFilter.sort !== void 0 ? mapPredefinedFilterSortToChannelSort(predefinedFilter.sort) : sort
  };
};
var getResponseFiltersAndSort = (pagination) => ({
  filters: pagination.responseFilters ?? pagination.filters,
  sort: pagination.responseSort ?? pagination.sort
});
var omitResponsePaginationParams = (pagination) => {
  const paginationWithoutResponseParams = { ...pagination };
  delete paginationWithoutResponseParams.responseFilters;
  delete paginationWithoutResponseParams.responseSort;
  return paginationWithoutResponseParams;
};
var isQueryChannelsResponseWithChannels = (response) => !Array.isArray(response);
var ChannelManager = class extends WithSubscriptions {
  constructor({
    client,
    eventHandlerOverrides = {},
    options = {},
    queryChannelsOverride
  }) {
    super();
    this.eventHandlers = /* @__PURE__ */ new Map();
    this.eventHandlerOverrides = /* @__PURE__ */ new Map();
    this.options = {};
    this.stateOptions = {};
    this.setChannels = (valueOrFactory) => {
      this.state.next((current) => {
        const { channels: currentChannels } = current;
        const newChannels = isPatch(valueOrFactory) ? valueOrFactory(currentChannels) : valueOrFactory;
        if (currentChannels === newChannels) {
          return current;
        }
        return { ...current, channels: newChannels };
      });
      const {
        channels,
        pagination: { filters, options, sort }
      } = this.state.getLatestValue();
      this.client.offlineDb?.executeQuerySafely(
        (db) => db.upsertCidsForQuery({
          cids: channels.map((channel) => channel.cid),
          filters,
          options,
          sort
        }),
        { method: "upsertCidsForQuery" }
      );
    };
    this.setEventHandlerOverrides = (eventHandlerOverrides = {}) => {
      const truthyEventHandlerOverrides = Object.entries(eventHandlerOverrides).reduce((acc, [key, value]) => {
        if (value) {
          acc[key] = value;
        }
        return acc;
      }, {});
      this.eventHandlerOverrides = new Map(
        Object.entries(truthyEventHandlerOverrides)
      );
    };
    this.setQueryChannelsRequest = (queryChannelsRequest) => {
      this.queryChannelsRequest = queryChannelsRequest;
    };
    this.setOptions = (options = {}) => {
      this.options = { ...DEFAULT_CHANNEL_MANAGER_OPTIONS, ...options };
    };
    this.executeChannelsQuery = async (payload, retryCount = 0) => {
      const { filters, sort, options, stateOptions } = payload;
      const { offset, limit } = {
        ...DEFAULT_CHANNEL_MANAGER_PAGINATION_OPTIONS,
        ...options
      };
      try {
        const queryChannelsResponse = await this.queryChannelsRequest(
          filters,
          sort,
          options,
          { ...stateOptions, withResponse: true }
        );
        const channels = isQueryChannelsResponseWithChannels(queryChannelsResponse) ? queryChannelsResponse.channels : queryChannelsResponse;
        const newOffset = offset + (channels?.length ?? 0);
        const newOptions = { ...options, offset: newOffset };
        const { pagination } = this.state.getLatestValue();
        const responsePaginationParams = getResponsePaginationParams({
          queryChannelsResponse: isQueryChannelsResponseWithChannels(queryChannelsResponse) ? queryChannelsResponse : void 0,
          sort
        });
        const paginationWithoutResponseParams = omitResponsePaginationParams(pagination);
        this.state.partialNext({
          channels,
          pagination: {
            // Drop response derived filter/sort from the previous query before applying
            // the current response. Non predefined queries do not return this metadata,
            // so keeping the old values would make later WS mutations use stale
            // predefined filter semantics. Also the predefined_filter might change, producing
            // a different combination as well so we always need to first clean up.
            ...paginationWithoutResponseParams,
            hasNext: (channels?.length ?? 0) >= (limit ?? 1),
            isLoading: false,
            options: newOptions,
            ...responsePaginationParams
          },
          initialized: true,
          error: void 0
        });
        this.client.offlineDb?.executeQuerySafely(
          (db) => db.upsertCidsForQuery({
            cids: channels.map((channel) => channel.cid),
            filters: pagination.filters,
            options,
            sort: pagination.sort
          }),
          { method: "upsertCidsForQuery" }
        );
      } catch (err) {
        if (retryCount >= DEFAULT_QUERY_CHANNELS_RETRY_COUNT) {
          console.warn(err);
          const wrappedError = new Error(
            `Maximum number of retries reached in queryChannels. Last error message is: ${err}`
          );
          const state = this.state.getLatestValue();
          const isOfflineSupportEnabledWithChannels = this.client.offlineDb && state.channels.length > 0;
          this.state.partialNext({
            error: isOfflineSupportEnabledWithChannels ? void 0 : wrappedError,
            pagination: {
              ...state.pagination,
              isLoading: false,
              isLoadingNext: false
            }
          });
          return;
        }
        await sleep(DEFAULT_QUERY_CHANNELS_MS_BETWEEN_RETRIES);
        return this.executeChannelsQuery(payload, retryCount + 1);
      }
    };
    this.queryChannels = async (filters, sort = [], options = {}, stateOptions = {}) => {
      const {
        pagination: { isLoading, filters: filtersFromState },
        initialized
      } = this.state.getLatestValue();
      if (isLoading && !this.options.abortInFlightQuery && // TODO: Figure a proper way to either deeply compare these or
      //       create hashes from each.
      JSON.stringify(filtersFromState) === JSON.stringify(filters)) {
        return;
      }
      const executeChannelsQueryPayload = { filters, sort, options, stateOptions };
      try {
        this.stateOptions = stateOptions;
        this.state.next((currentState) => ({
          ...currentState,
          pagination: {
            ...omitResponsePaginationParams(currentState.pagination),
            isLoading: true,
            isLoadingNext: false,
            filters,
            sort,
            options
          },
          error: void 0
        }));
        if (this.client.offlineDb?.getChannelsForQuery && this.client.user?.id) {
          if (!initialized) {
            const channelsFromDB = await this.client.offlineDb.getChannelsForQuery({
              userId: this.client.user.id,
              filters,
              options,
              sort
            });
            if (channelsFromDB) {
              const offlineChannels = this.client.hydrateActiveChannels(channelsFromDB, {
                offlineMode: true,
                skipInitialization: []
                // passing empty array will clear out the existing messages from channel state, this removes the possibility of duplicate messages
              });
              this.state.partialNext({ channels: offlineChannels });
            }
          }
          if (!this.client.offlineDb.syncManager.syncStatus) {
            this.client.offlineDb.syncManager.scheduleSyncStatusChangeCallback(
              this.id,
              async () => {
                await this.executeChannelsQuery(executeChannelsQueryPayload);
              }
            );
            return;
          }
        }
        await this.executeChannelsQuery(executeChannelsQueryPayload);
      } catch (error) {
        this.client.logger("error", error.message);
        this.state.next((currentState) => ({
          ...currentState,
          pagination: { ...currentState.pagination, isLoading: false }
        }));
        throw error;
      }
    };
    this.loadNext = async () => {
      const { pagination, initialized } = this.state.getLatestValue();
      const { filters, sort, options, isLoadingNext, hasNext } = pagination;
      if (!initialized || isLoadingNext || !hasNext) {
        return;
      }
      try {
        const { offset, limit } = {
          ...DEFAULT_CHANNEL_MANAGER_PAGINATION_OPTIONS,
          ...options
        };
        this.state.partialNext({
          pagination: { ...pagination, isLoading: false, isLoadingNext: true }
        });
        const queryChannelsResponse = await this.queryChannelsRequest(
          filters,
          sort,
          options,
          this.stateOptions
        );
        const nextChannels = isQueryChannelsResponseWithChannels(queryChannelsResponse) ? queryChannelsResponse.channels : queryChannelsResponse;
        const { channels } = this.state.getLatestValue();
        const newOffset = offset + (nextChannels?.length ?? 0);
        const newOptions = { ...options, offset: newOffset };
        this.state.partialNext({
          channels: uniqBy([...channels || [], ...nextChannels], "cid"),
          pagination: {
            ...pagination,
            hasNext: (nextChannels?.length ?? 0) >= (limit ?? 1),
            isLoading: false,
            isLoadingNext: false,
            options: newOptions
          }
        });
      } catch (error) {
        this.client.logger("error", error.message);
        this.state.next((currentState) => ({
          ...currentState,
          pagination: {
            ...currentState.pagination,
            isLoadingNext: false,
            isLoading: false
          }
        }));
        throw error;
      }
    };
    this.notificationAddedToChannelHandler = async (event) => {
      const { id, type, members } = event?.channel ?? {};
      if (!type || !this.options.allowNotLoadedChannelPromotionForEvent?.["notification.added_to_channel"]) {
        return;
      }
      const channel = await getAndWatchChannel({
        client: this.client,
        id,
        members: members?.reduce((acc, { user, user_id }) => {
          const userId = user_id || user?.id;
          if (userId) {
            acc.push(userId);
          }
          return acc;
        }, []),
        type
      });
      const { pagination, channels } = this.state.getLatestValue();
      if (!channels) {
        return;
      }
      const { sort } = getResponseFiltersAndSort(pagination);
      this.setChannels(
        promoteChannel({
          channels,
          channelToMove: channel,
          sort
        })
      );
    };
    this.channelDeletedHandler = (event) => {
      const { channels } = this.state.getLatestValue();
      if (!channels) {
        return;
      }
      const newChannels = [...channels];
      const channelIndex = newChannels.findIndex(
        (channel) => channel.cid === (event.cid || event.channel?.cid)
      );
      if (channelIndex < 0) {
        return;
      }
      newChannels.splice(channelIndex, 1);
      this.setChannels(newChannels);
    };
    this.channelHiddenHandler = this.channelDeletedHandler;
    this.newMessageHandler = (event) => {
      const { pagination, channels } = this.state.getLatestValue();
      if (!channels) {
        return;
      }
      const { filters, sort } = getResponseFiltersAndSort(pagination);
      const channelType = event.channel_type;
      const channelId = event.channel_id;
      if (!channelType || !channelId) {
        return;
      }
      const targetChannel = this.client.channel(channelType, channelId);
      const targetChannelIndex = channels.indexOf(targetChannel);
      const targetChannelExistsWithinList = targetChannelIndex >= 0;
      const isTargetChannelPinned = isChannelPinned(targetChannel);
      const isTargetChannelArchived = isChannelArchived(targetChannel);
      const considerArchivedChannels = shouldConsiderArchivedChannels(filters);
      const considerPinnedChannels = shouldConsiderPinnedChannels(sort);
      if (
        // filter is defined, target channel is archived and filter option is set to false
        considerArchivedChannels && isTargetChannelArchived && !filters.archived || // filter is defined, target channel isn't archived and filter option is set to true
        considerArchivedChannels && !isTargetChannelArchived && filters.archived || // sort option is defined, target channel is pinned
        considerPinnedChannels && isTargetChannelPinned || // list order is locked
        this.options.lockChannelOrder || // target channel is not within the loaded list and loading from cache is disallowed
        !targetChannelExistsWithinList && !this.options.allowNotLoadedChannelPromotionForEvent?.["message.new"]
      ) {
        return;
      }
      this.setChannels(
        promoteChannel({
          channels,
          channelToMove: targetChannel,
          channelToMoveIndexWithinChannels: targetChannelIndex,
          sort
        })
      );
    };
    this.notificationNewMessageHandler = async (event) => {
      const { id, type } = event?.channel ?? {};
      if (!id || !type) {
        return;
      }
      const channel = await getAndWatchChannel({
        client: this.client,
        id,
        type
      });
      const { channels, pagination } = this.state.getLatestValue();
      const { filters, sort } = getResponseFiltersAndSort(pagination);
      const considerArchivedChannels = shouldConsiderArchivedChannels(filters);
      const isTargetChannelArchived = isChannelArchived(channel);
      if (!channels || considerArchivedChannels && isTargetChannelArchived && !filters.archived || considerArchivedChannels && !isTargetChannelArchived && filters.archived || !this.options.allowNotLoadedChannelPromotionForEvent?.["notification.message_new"]) {
        return;
      }
      this.setChannels(
        promoteChannel({
          channels,
          channelToMove: channel,
          sort
        })
      );
    };
    this.channelVisibleHandler = async (event) => {
      const { channel_type: channelType, channel_id: channelId } = event;
      if (!channelType || !channelId) {
        return;
      }
      const channel = await getAndWatchChannel({
        client: this.client,
        id: event.channel_id,
        type: event.channel_type
      });
      const { channels, pagination } = this.state.getLatestValue();
      const { filters, sort } = getResponseFiltersAndSort(pagination);
      const considerArchivedChannels = shouldConsiderArchivedChannels(filters);
      const isTargetChannelArchived = isChannelArchived(channel);
      if (!channels || considerArchivedChannels && isTargetChannelArchived && !filters.archived || considerArchivedChannels && !isTargetChannelArchived && filters.archived || !this.options.allowNotLoadedChannelPromotionForEvent?.["channel.visible"]) {
        return;
      }
      this.setChannels(
        promoteChannel({
          channels,
          channelToMove: channel,
          sort
        })
      );
    };
    this.notificationRemovedFromChannelHandler = this.channelDeletedHandler;
    this.memberUpdatedHandler = (event) => {
      const { pagination, channels } = this.state.getLatestValue();
      const { filters, sort } = getResponseFiltersAndSort(pagination);
      if (!event.member?.user || event.member.user.id !== this.client.userID || !event.channel_type || !event.channel_id) {
        return;
      }
      const channelType = event.channel_type;
      const channelId = event.channel_id;
      const considerPinnedChannels = shouldConsiderPinnedChannels(sort);
      const considerArchivedChannels = shouldConsiderArchivedChannels(filters);
      const pinnedAtSort = extractSortValue({ atIndex: 0, sort, targetKey: "pinned_at" });
      if (!channels || !considerPinnedChannels && !considerArchivedChannels || this.options.lockChannelOrder) {
        return;
      }
      const targetChannel = this.client.channel(channelType, channelId);
      const targetChannelIndex = channels.indexOf(targetChannel);
      const targetChannelExistsWithinList = targetChannelIndex >= 0;
      const isTargetChannelPinned = isChannelPinned(targetChannel);
      const isTargetChannelArchived = isChannelArchived(targetChannel);
      const newChannels = [...channels];
      if (targetChannelExistsWithinList) {
        newChannels.splice(targetChannelIndex, 1);
      }
      if (
        // When archived filter true, and channel is unarchived
        considerArchivedChannels && !isTargetChannelArchived && filters?.archived || // When archived filter false, and channel is archived
        considerArchivedChannels && isTargetChannelArchived && !filters?.archived
      ) {
        this.setChannels(newChannels);
        return;
      }
      let lastPinnedChannelIndex = null;
      if (pinnedAtSort === 1 || pinnedAtSort === -1 && !isTargetChannelPinned) {
        lastPinnedChannelIndex = findLastPinnedChannelIndex({ channels: newChannels });
      }
      const newTargetChannelIndex = typeof lastPinnedChannelIndex === "number" ? lastPinnedChannelIndex + 1 : 0;
      if (channels[newTargetChannelIndex] === targetChannel) {
        return;
      }
      newChannels.splice(newTargetChannelIndex, 0, targetChannel);
      this.setChannels(newChannels);
    };
    this.subscriptionOrOverride = (event) => {
      const handlerName = channelManagerEventToHandlerMapping[event.type];
      const defaultEventHandler = this.eventHandlers.get(handlerName);
      const eventHandlerOverride = this.eventHandlerOverrides.get(handlerName);
      if (eventHandlerOverride && typeof eventHandlerOverride === "function") {
        eventHandlerOverride(this.setChannels, event);
        return;
      }
      if (defaultEventHandler && typeof defaultEventHandler === "function") {
        defaultEventHandler(event);
      }
    };
    this.registerSubscriptions = () => {
      if (this.hasSubscriptions) {
        return;
      }
      for (const eventType of Object.keys(channelManagerEventToHandlerMapping)) {
        this.addUnsubscribeFunction(
          this.client.on(eventType, this.subscriptionOrOverride).unsubscribe
        );
      }
    };
    this.id = `channel-manager-${generateUUIDv4()}`;
    this.client = client;
    this.state = new StateStore({
      channels: [],
      pagination: {
        isLoading: false,
        isLoadingNext: false,
        hasNext: false,
        filters: {},
        sort: {},
        options: DEFAULT_CHANNEL_MANAGER_PAGINATION_OPTIONS
      },
      initialized: false,
      error: void 0
    });
    this.setEventHandlerOverrides(eventHandlerOverrides);
    this.setOptions(options);
    this.queryChannelsRequest = queryChannelsOverride ?? ((...params) => this.client.queryChannels(...params));
    this.eventHandlers = new Map(
      Object.entries({
        channelDeletedHandler: this.channelDeletedHandler,
        channelHiddenHandler: this.channelHiddenHandler,
        channelVisibleHandler: this.channelVisibleHandler,
        memberUpdatedHandler: this.memberUpdatedHandler,
        newMessageHandler: this.newMessageHandler,
        notificationAddedToChannelHandler: this.notificationAddedToChannelHandler,
        notificationNewMessageHandler: this.notificationNewMessageHandler,
        notificationRemovedFromChannelHandler: this.notificationRemovedFromChannelHandler
      })
    );
  }
};

// src/notifications/configuration.ts
var DURATION_MS = 3e3;
var DEFAULT_NOTIFICATION_MANAGER_CONFIG = {
  durations: {
    error: DURATION_MS,
    info: DURATION_MS,
    success: DURATION_MS,
    warning: DURATION_MS
  }
};

// src/notifications/NotificationManager.ts
var NotificationManager = class {
  constructor(config = {}) {
    this.timeouts = /* @__PURE__ */ new Map();
    this.store = new StateStore({ notifications: [] });
    this.config = mergeWith(DEFAULT_NOTIFICATION_MANAGER_CONFIG, config);
  }
  get notifications() {
    return this.store.getLatestValue().notifications;
  }
  get warning() {
    return this.notifications.filter((n) => n.severity === "warning");
  }
  get error() {
    return this.notifications.filter((n) => n.severity === "error");
  }
  get info() {
    return this.notifications.filter((n) => n.severity === "info");
  }
  get success() {
    return this.notifications.filter((n) => n.severity === "success");
  }
  add({ message, origin, options = {} }) {
    const id = generateUUIDv4();
    const now = Date.now();
    const severity = options.severity;
    const duration = options.duration ?? (severity ? this.config.durations[severity] : void 0);
    const notification = {
      id,
      message,
      origin,
      type: options?.type,
      severity,
      createdAt: now,
      duration,
      actions: options.actions,
      metadata: options.metadata,
      tags: options.tags,
      originalError: options.originalError
    };
    const notifications = [...this.store.getLatestValue().notifications, notification];
    this.store.partialNext({
      notifications: this.config.sortComparator ? [...notifications].sort(this.config.sortComparator) : notifications
    });
    return id;
  }
  addError({ message, origin, options }) {
    return this.add({ message, origin, options: { ...options, severity: "error" } });
  }
  addWarning({ message, origin, options }) {
    return this.add({ message, origin, options: { ...options, severity: "warning" } });
  }
  addInfo({ message, origin, options }) {
    return this.add({ message, origin, options: { ...options, severity: "info" } });
  }
  addSuccess({ message, origin, options }) {
    return this.add({ message, origin, options: { ...options, severity: "success" } });
  }
  clearTimeout(id) {
    const timeout = this.timeouts.get(id);
    if (!timeout) return;
    clearTimeout(timeout);
    this.timeouts.delete(id);
  }
  startTimeout(id, durationOverride) {
    const notification = this.store.getLatestValue().notifications.find((n) => n.id === id);
    const duration = durationOverride ?? notification?.duration;
    if (!notification || !duration) return;
    this.clearTimeout(id);
    const timeout = setTimeout(() => {
      this.remove(id);
    }, duration);
    this.timeouts.set(id, timeout);
  }
  remove(id) {
    this.clearTimeout(id);
    this.store.partialNext({
      notifications: this.store.getLatestValue().notifications.filter((n) => n.id !== id)
    });
  }
  clear() {
    this.timeouts.forEach((timeout) => clearTimeout(timeout));
    this.timeouts.clear();
    this.store.partialNext({ notifications: [] });
  }
};

// src/reminders/ReminderTimer.ts
var oneMinute = 60 * 1e3;
var oneHour = 60 * oneMinute;
var oneDay = 24 * oneHour;
var oneWeek = 7 * oneDay;
var GROUP_BOUNDS = {
  minute: { lower: oneMinute, upper: oneHour },
  hour: { lower: oneHour, upper: oneDay },
  day: { lower: oneDay, upper: oneWeek }
};
var DEFAULT_STOP_REFRESH_BOUNDARY_MS = 2 * oneWeek;
var ReminderTimer = class {
  constructor({
    reminder,
    config
  }) {
    this.timeout = null;
    this.stopRefreshBoundaryMs = DEFAULT_STOP_REFRESH_BOUNDARY_MS;
    this.getRefreshIntervalLength = () => {
      if (!this.reminder.remindAt) return null;
      const distanceFromDeadlineMs = Math.abs(timeLeftMs(this.reminder.remindAt.getTime()));
      let refreshInterval;
      if (distanceFromDeadlineMs === 0) {
        refreshInterval = oneMinute;
      } else if (distanceFromDeadlineMs < GROUP_BOUNDS.minute.lower) {
        refreshInterval = distanceFromDeadlineMs;
      } else if (distanceFromDeadlineMs <= GROUP_BOUNDS.minute.upper) {
        refreshInterval = oneMinute;
      } else if (distanceFromDeadlineMs <= GROUP_BOUNDS.hour.upper) {
        refreshInterval = oneHour;
      } else {
        refreshInterval = oneDay;
      }
      return refreshInterval;
    };
    this.init = () => {
      if (!this.reminder.remindAt) return null;
      const timeoutLength = this.getRefreshIntervalLength();
      if (timeoutLength === null) return null;
      const boundaryTimestamp = this.reminder.remindAt?.getTime() + this.stopRefreshBoundaryMs;
      const timeLeftToBoundary = boundaryTimestamp - Date.now();
      if (timeLeftToBoundary <= 0) {
        this.timeout = null;
        return;
      }
      if (this.timeout) clearTimeout(this.timeout);
      this.timeout = setTimeout(() => {
        this.reminder.refreshTimeLeft();
        this.init();
      }, timeoutLength);
    };
    this.clear = () => {
      if (this.timeout) {
        clearInterval(this.timeout);
        this.timeout = null;
      }
    };
    this.reminder = reminder;
    if (typeof config?.stopRefreshBoundaryMs === "number") {
      this.stopRefreshBoundaryMs = config.stopRefreshBoundaryMs;
    }
  }
};

// src/reminders/Reminder.ts
var timeLeftMs = (remindAt) => remindAt - (/* @__PURE__ */ new Date()).getTime();
var _Reminder = class _Reminder {
  constructor({ data, config }) {
    this.setState = (data) => {
      this.state.next((current) => {
        const newState = { ...current, ..._Reminder.toStateValue(data) };
        if (newState.remind_at) {
          newState.timeLeftMs = timeLeftMs(newState.remind_at.getTime());
        }
        return newState;
      });
      if (data.remind_at) {
        this.initTimer();
      } else if (!data.remind_at) {
        this.clearTimer();
      }
    };
    this.refreshTimeLeft = () => {
      if (!this.remindAt) return;
      this.state.partialNext({ timeLeftMs: timeLeftMs(this.remindAt.getTime()) });
    };
    this.initTimer = () => {
      this.timer.init();
    };
    this.clearTimer = () => {
      this.timer.clear();
    };
    this.state = new StateStore(_Reminder.toStateValue(data));
    this.timer = new ReminderTimer({ reminder: this, config });
    this.initTimer();
  }
  get id() {
    return this.state.getLatestValue().message_id;
  }
  get remindAt() {
    return this.state.getLatestValue().remind_at;
  }
  get timeLeftMs() {
    return this.state.getLatestValue().timeLeftMs;
  }
};
_Reminder.toStateValue = (data) => ({
  ...data,
  created_at: new Date(data.created_at),
  message: data.message || null,
  remind_at: data.remind_at ? new Date(data.remind_at) : null,
  timeLeftMs: data.remind_at ? timeLeftMs(new Date(data.remind_at).getTime()) : null,
  updated_at: new Date(data.updated_at),
  user: data.user || null
});
var Reminder = _Reminder;

// src/reminders/ReminderManager.ts
var oneMinute2 = 60 * 1e3;
var oneHour2 = 60 * oneMinute2;
var oneDay2 = 24 * oneHour2;
var DEFAULT_REMINDER_MANAGER_CONFIG = {
  scheduledOffsetsMs: [
    2 * oneMinute2,
    30 * oneMinute2,
    oneHour2,
    2 * oneHour2,
    8 * oneHour2,
    oneDay2
  ],
  stopTimerRefreshBoundaryMs: DEFAULT_STOP_REFRESH_BOUNDARY_MS
};
var isReminderExistsError = (error) => error.message.match("already has reminder created for this message_id");
var isReminderDoesNotExistError = (error) => error.message.match("reminder does not exist");
var _ReminderManager = class _ReminderManager extends WithSubscriptions {
  constructor({ client, config }) {
    super();
    this.upsertToState = ({
      data,
      overwrite = true
    }) => {
      if (!this.client._cacheEnabled()) {
        return;
      }
      const cachedReminder = this.getFromState(data.message_id);
      if (!cachedReminder) {
        const reminder = new Reminder({
          data,
          config: { stopRefreshBoundaryMs: this.stopTimerRefreshBoundaryMs }
        });
        this.state.partialNext({
          reminders: new Map(this.reminders.set(data.message_id, reminder))
        });
      } else if (overwrite) {
        cachedReminder.setState(data);
      }
      return cachedReminder;
    };
    this.removeFromState = (messageId) => {
      const cachedReminder = this.getFromState(messageId);
      if (!cachedReminder) return;
      cachedReminder.clearTimer();
      const reminders = this.reminders;
      reminders.delete(messageId);
      this.state.partialNext({ reminders: new Map(reminders) });
    };
    this.hydrateState = (messages) => {
      messages.forEach(({ reminder }) => {
        if (reminder) {
          this.upsertToState({ data: reminder });
        }
      });
    };
    // State API END //
    // Timers API START //
    this.initTimers = () => {
      this.reminders.forEach((reminder) => reminder.initTimer());
    };
    this.clearTimers = () => {
      this.reminders.forEach((reminder) => reminder.clearTimer());
    };
    this.registerSubscriptions = () => {
      if (this.hasSubscriptions) return;
      this.addUnsubscribeFunction(this.subscribeReminderCreated());
      this.addUnsubscribeFunction(this.subscribeReminderUpdated());
      this.addUnsubscribeFunction(this.subscribeReminderDeleted());
      this.addUnsubscribeFunction(this.subscribeNotificationReminderDue());
      this.addUnsubscribeFunction(this.subscribeMessageDeleted());
      this.addUnsubscribeFunction(this.subscribeMessageUndeleted());
      this.addUnsubscribeFunction(this.subscribePaginatorStateUpdated());
      this.addUnsubscribeFunction(this.subscribeConfigStateUpdated());
    };
    this.subscribeReminderCreated = () => this.client.on("reminder.created", (event) => {
      if (!_ReminderManager.isReminderWsEventPayload(event)) return;
      const { reminder } = event;
      this.upsertToState({ data: reminder });
    }).unsubscribe;
    this.subscribeReminderUpdated = () => this.client.on("reminder.updated", (event) => {
      if (!_ReminderManager.isReminderWsEventPayload(event)) return;
      const { reminder } = event;
      this.upsertToState({ data: reminder });
    }).unsubscribe;
    this.subscribeReminderDeleted = () => this.client.on("reminder.deleted", (event) => {
      if (!_ReminderManager.isReminderWsEventPayload(event)) return;
      this.removeFromState(event.message_id);
    }).unsubscribe;
    this.subscribeMessageDeleted = () => this.client.on("message.deleted", (event) => {
      if (!event.message?.id) return;
      this.removeFromState(event.message.id);
    }).unsubscribe;
    this.subscribeMessageUndeleted = () => this.client.on("message.undeleted", (event) => {
      if (!event.message?.reminder) return;
      this.upsertToState({ data: event.message.reminder });
    }).unsubscribe;
    this.subscribeNotificationReminderDue = () => this.client.on("notification.reminder_due", () => null).unsubscribe;
    // todo: what should be performed on this event?
    this.subscribePaginatorStateUpdated = () => this.paginator.state.subscribeWithSelector(
      ({ items }) => [items],
      ([items]) => {
        if (!items) return;
        for (const reminder of items) {
          this.upsertToState({ data: reminder });
        }
      }
    );
    this.subscribeConfigStateUpdated = () => this.configState.subscribeWithSelector(
      ({ stopTimerRefreshBoundaryMs }) => ({ stopTimerRefreshBoundaryMs }),
      ({ stopTimerRefreshBoundaryMs }, previousValue) => {
        if (typeof stopTimerRefreshBoundaryMs === "number" && stopTimerRefreshBoundaryMs !== previousValue?.stopTimerRefreshBoundaryMs) {
          this.reminders.forEach((reminder) => {
            if (reminder.timer) {
              reminder.timer.stopRefreshBoundaryMs = stopTimerRefreshBoundaryMs;
            }
          });
        }
      }
    );
    // WS event handling END //
    // API calls START //
    this.upsertReminder = async (options) => {
      const { messageId } = options;
      if (this.getFromState(messageId)) {
        try {
          return await this.updateReminder(options);
        } catch (error) {
          if (isReminderDoesNotExistError(error)) {
            return await this.createReminder(options);
          }
          throw error;
        }
      } else {
        try {
          return await this.createReminder(options);
        } catch (error) {
          if (isReminderExistsError(error)) {
            return await this.updateReminder(options);
          }
          throw error;
        }
      }
    };
    this.createReminder = async (options) => {
      const { reminder } = await this.client.createReminder(options);
      return this.upsertToState({ data: reminder, overwrite: false });
    };
    this.updateReminder = async (options) => {
      const { reminder } = await this.client.updateReminder(options);
      return this.upsertToState({ data: reminder });
    };
    this.deleteReminder = async (messageId) => {
      await this.client.deleteReminder(messageId);
      this.removeFromState(messageId);
    };
    this.queryNextReminders = async () => {
      await this.paginator.next();
    };
    this.queryPreviousReminders = async () => {
      await this.paginator.prev();
    };
    this.client = client;
    this.configState = new StateStore({
      scheduledOffsetsMs: config?.scheduledOffsetsMs ?? DEFAULT_REMINDER_MANAGER_CONFIG.scheduledOffsetsMs,
      stopTimerRefreshBoundaryMs: config?.stopTimerRefreshBoundaryMs ?? DEFAULT_REMINDER_MANAGER_CONFIG.stopTimerRefreshBoundaryMs
    });
    this.state = new StateStore({ reminders: /* @__PURE__ */ new Map() });
    this.paginator = new ReminderPaginator(client);
  }
  // Config API START //
  updateConfig(config) {
    if (typeof config.stopTimerRefreshBoundaryMs === "number" && config.stopTimerRefreshBoundaryMs !== this.stopTimerRefreshBoundaryMs) {
      this.reminders.forEach((reminder) => {
        reminder.timer.stopRefreshBoundaryMs = config?.stopTimerRefreshBoundaryMs;
      });
    }
    this.configState.partialNext(config);
  }
  get stopTimerRefreshBoundaryMs() {
    return this.configState.getLatestValue().stopTimerRefreshBoundaryMs;
  }
  get scheduledOffsetsMs() {
    return this.configState.getLatestValue().scheduledOffsetsMs;
  }
  // Config API END //
  // State API START //
  get reminders() {
    return this.state.getLatestValue().reminders;
  }
  getFromState(messageId) {
    return this.reminders.get(messageId);
  }
  // API calls END //
};
// Timers API END //
// WS event handling START //
_ReminderManager.isReminderWsEventPayload = (event) => !!event.reminder && (event.type.startsWith("reminder.") || event.type === "notification.reminder_due");
var ReminderManager = _ReminderManager;

// src/offline-support/util.ts
var isLocalUrl = (value) => !!value && !value.startsWith("http");
var isAttachmentReplayable = (attachment) => {
  if (!attachment || typeof attachment !== "object") {
    return true;
  }
  return !isLocalUrl(attachment.asset_url) && !isLocalUrl(attachment.image_url);
};
var isMessageUpdateReplayable = (message) => !message.attachments?.some((attachment) => !isAttachmentReplayable(attachment));
var getPendingTaskChannelData = (cid) => {
  if (!cid) {
    return {};
  }
  const separatorIndex = cid.indexOf(":");
  if (separatorIndex <= 0 || separatorIndex === cid.length - 1) {
    return {};
  }
  return {
    channelId: cid.slice(separatorIndex + 1),
    channelType: cid.slice(0, separatorIndex)
  };
};

// src/utils/FixedSizeQueueCache.ts
var FixedSizeQueueCache = class {
  constructor(size, options) {
    if (!size) throw new Error("Size must be greater than 0");
    this.keys = [];
    this.size = size;
    this.map = /* @__PURE__ */ new Map();
    this.dispose = options?.dispose ?? null;
  }
  /**
   * Adds a new or moves the existing reference to the front of the queue
   * @param key
   * @param value
   */
  add(key, value) {
    const index = this.keys.indexOf(key);
    if (index > -1) {
      this.keys.splice(this.keys.indexOf(key), 1);
    } else if (this.keys.length >= this.size) {
      const itemKey = this.keys.shift();
      if (itemKey) {
        const item = this.peek(itemKey);
        if (item) {
          this.dispose?.(itemKey, item);
        }
        this.map.delete(itemKey);
      }
    }
    this.keys.push(key);
    this.map.set(key, value);
  }
  /**
   * Retrieves the value by key.
   * @param key
   */
  peek(key) {
    const value = this.map.get(key);
    return value;
  }
  /**
   * Retrieves the value and moves it to the front of the queue.
   * @param key
   */
  get(key) {
    const foundItem = this.peek(key);
    if (foundItem && this.keys.indexOf(key) !== this.size - 1) {
      this.keys.splice(this.keys.indexOf(key), 1);
      this.keys.push(key);
    }
    return foundItem;
  }
  /**
   * Clears queue entirely and disposes of each item individually.
   */
  clear() {
    if (this.dispose) {
      this.map.forEach((entry, key) => this.dispose?.(key, entry));
    }
    this.map.clear();
    this.keys = [];
  }
};

// src/client.ts
function isString2(x) {
  return typeof x === "string" || x instanceof String;
}
var StreamChat = class _StreamChat {
  constructor(key, secretOrOptions, options) {
    /**
     * If true, we will not clean up threads when channel state is in initializing state.
     * The main use case for SDKs who do independent state recovery for channels.
     */
    this.preventThreadCleanup = false;
    this.nextRequestAbortController = null;
    /**
     * @private
     */
    this._messageComposerSetupState = new StateStore({
      setupFunction: null
    });
    this._getConnectionID = () => this.wsConnection?.connectionID || this.wsFallback?.connectionID;
    this._hasConnectionID = () => Boolean(this._getConnectionID());
    this.setMessageComposerSetupFunction = (setupFunction) => {
      this._messageComposerSetupState.partialNext({ setupFunction });
    };
    /**
     * connectUser - Set the current user and open a WebSocket connection
     *
     * @param {OwnUserResponse | UserResponse} user Data about this user. IE {name: "john"}
     * @param {TokenOrProvider} userTokenOrProvider Token or provider
     *
     * @return {ConnectAPIResponse} Returns a promise that resolves when the connection is setup
     */
    this.connectUser = async (user, userTokenOrProvider) => {
      if (!user.id) {
        throw new Error('The "id" field on the user is missing');
      }
      if (this.userID === user.id && this.setUserPromise) {
        console.warn(
          "Consecutive calls to connectUser is detected, ideally you should only call this function once in your app."
        );
        return this.setUserPromise;
      }
      if (this.userID) {
        throw new Error(
          "Use client.disconnect() before trying to connect as a different user. connectUser was called twice."
        );
      }
      if ((this._isUsingServerAuth() || this.node) && !this.options.allowServerSideConnect) {
        console.warn(
          'Please do not use connectUser server side. connectUser impacts MAU and concurrent connection usage and thus your bill. If you have a valid use-case, add "allowServerSideConnect: true" to the client options to disable this warning.'
        );
      }
      this.userID = user.id;
      this.anonymous = false;
      const setTokenPromise = this._setToken(user, userTokenOrProvider);
      this._setUser(user);
      const wsPromise = this.openConnection();
      this.setUserPromise = Promise.all([setTokenPromise, wsPromise]).then(
        (result) => result[1]
        // We only return connection promise;
      );
      try {
        return await this.setUserPromise;
      } catch (err) {
        if (this.persistUserOnConnectionFailure) {
          this.closeConnection();
        } else {
          this.disconnectUser();
        }
        throw err;
      }
    };
    /**
     * @deprecated Please use connectUser() function instead. Its naming is more consistent with its functionality.
     *
     * setUser - Set the current user and open a WebSocket connection
     *
     * @param {OwnUserResponse | UserResponse} user Data about this user. IE {name: "john"}
     * @param {TokenOrProvider} userTokenOrProvider Token or provider
     *
     * @return {ConnectAPIResponse} Returns a promise that resolves when the connection is setup
     */
    this.setUser = this.connectUser;
    this._setToken = (user, userTokenOrProvider) => this.tokenManager.setTokenOrProvider(userTokenOrProvider, user);
    /**
     * Disconnects the websocket connection, without removing the user set on client.
     * client.closeConnection will not trigger default auto-retry mechanism for reconnection. You need
     * to call client.openConnection to reconnect to websocket.
     *
     * This is mainly useful on mobile side. You can only receive push notifications
     * if you don't have active websocket connection.
     * So when your app goes to background, you can call `client.closeConnection`.
     * And when app comes back to foreground, call `client.openConnection`.
     *
     * @param timeout Max number of ms, to wait for close event of websocket, before forcefully assuming succesful disconnection.
     *                https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent
     */
    this.closeConnection = async (timeout) => {
      if (this.cleaningIntervalRef != null) {
        clearInterval(this.cleaningIntervalRef);
        this.cleaningIntervalRef = void 0;
      }
      await Promise.all([
        this.wsConnection?.disconnect(timeout),
        this.wsFallback?.disconnect(timeout)
      ]);
      this.offlineDb?.executeQuerySafely(
        async (db) => {
          if (this.userID) {
            await db.upsertUserSyncStatus({
              userId: this.userID,
              lastSyncedAt: (/* @__PURE__ */ new Date()).toString()
            });
          }
        },
        { method: "upsertUserSyncStatus" }
      );
      return Promise.resolve();
    };
    /**
     * Creates an instance of ChannelManager.
     *
     * @internal
     *
     * @param eventHandlerOverrides - the overrides for event handlers to be used
     * @param options - the options used for the channel manager
     */
    this.createChannelManager = ({
      eventHandlerOverrides = {},
      options = {},
      queryChannelsOverride
    }) => new ChannelManager({
      client: this,
      eventHandlerOverrides,
      options,
      queryChannelsOverride
    });
    /**
     * Creates a new WebSocket connection with the current user. Returns empty promise, if there is an active connection
     */
    this.openConnection = () => {
      if (!this.userID) {
        throw Error(
          "User is not set on client, use client.connectUser or client.connectAnonymousUser instead"
        );
      }
      if (this.wsConnection?.isConnecting && this.wsPromise) {
        this.logger("info", "client:openConnection() - connection already in progress", {
          tags: ["connection", "client"]
        });
        return this.wsPromise;
      }
      if ((this.wsConnection?.isHealthy || this.wsFallback?.isHealthy()) && this._hasConnectionID()) {
        this.logger(
          "info",
          "client:openConnection() - openConnection called twice, healthy connection already exists",
          {
            tags: ["connection", "client"]
          }
        );
        return;
      }
      this.clientID = `${this.userID}--${randomId()}`;
      this.wsPromise = this.connect();
      this._startCleaning();
      return this.wsPromise;
    };
    /**
     * @deprecated Please use client.openConnction instead.
     * @private
     *
     * Creates a new websocket connection with current user.
     */
    this._setupConnection = this.openConnection;
    this._normalizeDate = (before) => {
      if (before instanceof Date) {
        before = before.toISOString();
      }
      if (before === "") {
        throw new Error(
          "Don't pass blank string for since, use null instead if resetting the token revoke"
        );
      }
      return before;
    };
    /**
     * Disconnects the websocket and removes the user from client.
     *
     * @param timeout Max number of ms, to wait for close event of websocket, before forcefully assuming successful disconnection.
     *                https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent
     */
    this.disconnectUser = (timeout) => {
      this.logger("info", "client:disconnect() - Disconnecting the client", {
        tags: ["connection", "client"]
      });
      delete this.user;
      delete this._user;
      delete this.userID;
      this.anonymous = false;
      const closePromise = this.closeConnection(timeout);
      for (const channel of Object.values(this.activeChannels)) {
        channel._disconnect();
      }
      this.activeChannels = {};
      this.state = new ClientState({ client: this });
      this.threads.resetState();
      this.uploadManager.reset();
      this.messageComposerCache.clear();
      closePromise.finally(() => {
        this.tokenManager.reset();
      }).catch((err) => console.error(err));
      return closePromise;
    };
    /**
     *
     * @deprecated Please use client.disconnectUser instead.
     *
     * Disconnects the websocket and removes the user from client.
     */
    this.disconnect = this.disconnectUser;
    /**
     * connectAnonymousUser - Set an anonymous user and open a WebSocket connection
     */
    this.connectAnonymousUser = () => {
      if ((this._isUsingServerAuth() || this.node) && !this.options.allowServerSideConnect) {
        console.warn(
          'Please do not use connectUser server side. connectUser impacts MAU and concurrent connection usage and thus your bill. If you have a valid use-case, add "allowServerSideConnect: true" to the client options to disable this warning.'
        );
      }
      this.anonymous = true;
      this.userID = randomId();
      const anonymousUser = {
        id: this.userID,
        anon: true
      };
      this._setToken(anonymousUser, "");
      this._setUser(anonymousUser);
      return this._setupConnection();
    };
    /**
     * @deprecated Please use connectAnonymousUser. Its naming is more consistent with its functionality.
     */
    this.setAnonymousUser = this.connectAnonymousUser;
    this.doAxiosRequest = async (type, url, data, options = {}) => {
      await this.tokenManager.tokenReady();
      const requestConfig = this._enrichAxiosOptions(options);
      try {
        let response;
        this._logApiRequest(type, url, data, requestConfig);
        switch (type) {
          case "get":
            response = await this.axiosInstance.get(url, requestConfig);
            break;
          case "delete":
            response = await this.axiosInstance.delete(url, requestConfig);
            break;
          case "post":
            response = await this.axiosInstance.post(url, data, requestConfig);
            break;
          case "postForm":
            response = await this.axiosInstance.postForm(url, data, requestConfig);
            break;
          case "put":
            response = await this.axiosInstance.put(url, data, requestConfig);
            break;
          case "patch":
            response = await this.axiosInstance.patch(url, data, requestConfig);
            break;
          case "options":
            response = await this.axiosInstance.options(url, requestConfig);
            break;
          default:
            throw new Error("Invalid request type");
        }
        this._logApiResponse(type, url, response);
        this.consecutiveFailures = 0;
        return this.handleResponse(response);
      } catch (e) {
        e.client_request_id = requestConfig.headers?.["x-client-request-id"];
        if (import_axios4.default.isCancel(e)) throw e;
        this._logApiError(type, url, e);
        this.consecutiveFailures += 1;
        if (e.response) {
          if (e.response.data.code === chatCodes.TOKEN_EXPIRED && !this.tokenManager.isStatic()) {
            if (this.consecutiveFailures > 1) {
              await sleep(retryInterval(this.consecutiveFailures));
            }
            this.tokenManager.loadToken();
            return await this.doAxiosRequest(type, url, data, options);
          }
          return this.handleResponse(e.response);
        } else {
          throw e;
        }
      }
    };
    this.dispatchEvent = (event) => {
      if (!event.received_at) event.received_at = /* @__PURE__ */ new Date();
      const postListenerCallbacks = this._handleClientEvent(event);
      const cid = event.cid;
      const channel = cid ? this.activeChannels[cid] : void 0;
      if (channel) {
        channel._handleChannelEvent(event);
      }
      this._callClientListeners(event);
      if (channel) {
        channel._callChannelListeners(event);
      }
      postListenerCallbacks.forEach((c) => c());
      this.offlineDb?.executeQuerySafely((db) => db.handleEvent({ event }), {
        method: `handleEvent;${event.type}`
      });
    };
    /**
     * Updates the members, watchers and read references of the currently active channels that contain this user
     *
     * @param {UserResponse} user
     */
    this._updateMemberWatcherReferences = (user) => {
      const refMap = this.state.userChannelReferences[user.id] || {};
      for (const channelID in refMap) {
        const channel = this.activeChannels[channelID];
        if (channel?.state) {
          if (channel.state.members[user.id]) {
            channel.state.members[user.id].user = user;
          }
          if (channel.state.watchers[user.id]) {
            channel.state.watchers[user.id] = user;
          }
          if (channel.state.read[user.id]) {
            channel.state.read[user.id].user = user;
          }
        }
      }
    };
    /**
     * @deprecated Please _updateMemberWatcherReferences instead.
     * @private
     */
    this._updateUserReferences = this._updateMemberWatcherReferences;
    /**
     * @private
     *
     * Updates the messages from the currently active channels that contain this user,
     * with updated user object.
     *
     * @param {UserResponse} user
     */
    this._updateUserMessageReferences = (user) => {
      const refMap = this.state.userChannelReferences[user.id] || {};
      for (const channelID in refMap) {
        const channel = this.activeChannels[channelID];
        if (!channel) continue;
        const state = channel.state;
        state?.updateUserMessages(user);
      }
    };
    /**
     * @private
     *
     * Deletes the messages from the currently active channels that contain this user
     *
     * If hardDelete is true, all the content of message will be stripped down.
     * Otherwise, only 'message.type' will be set as 'deleted'.
     *
     * @param {UserResponse} user
     * @param {boolean} hardDelete
     */
    this._deleteUserMessageReference = (user, hardDelete = false, deletedAt) => {
      const refMap = this.state.userChannelReferences[user.id] || {};
      for (const channelID in refMap) {
        const channel = this.activeChannels[channelID];
        if (channel) {
          const state = channel.state;
          state?.deleteUserMessages(user, hardDelete, deletedAt);
        }
      }
    };
    /**
     * @private
     *
     * Handle following user related events:
     * - user.presence.changed
     * - user.updated
     * - user.deleted
     *
     * @param {Event} event
     */
    this._handleUserEvent = (event) => {
      if (!event.user) {
        return;
      }
      if (event.type === "user.presence.changed" || event.type === "user.updated") {
        if (event.user.id === this.userID) {
          const user = { ...this.user };
          const _user = { ...this._user };
          for (const key in this.user) {
            if (key in event.user || isOwnUserBaseProperty(key)) {
              continue;
            }
            const deleteKey = key;
            delete user[deleteKey];
            delete _user[deleteKey];
          }
          for (const key in _user) {
            const updateKey = key;
            if (updateKey in event.user) {
              _user[updateKey] = event.user[updateKey];
            }
          }
          this._user = _user;
          this.user = { ...user, ...event.user };
        }
        this.state.updateUser(event.user);
        this._updateMemberWatcherReferences(event.user);
      }
      if (event.type === "user.updated") {
        this._updateUserMessageReferences(event.user);
      }
      if (event.type === "user.deleted" && event.user.deleted_at && (event.mark_messages_deleted || event.hard_delete)) {
        this._deleteUserMessageReference(
          event.user,
          event.hard_delete,
          event.user.deleted_at ? new Date(event.user.deleted_at) : null
        );
      }
    };
    this._callClientListeners = (event) => {
      const client = this;
      const listeners = [];
      if (client.listeners.all) {
        listeners.push(...client.listeners.all);
      }
      if (client.listeners[event.type]) {
        listeners.push(...client.listeners[event.type]);
      }
      for (const listener of listeners) {
        listener(event);
      }
    };
    this.recoverState = async () => {
      this.logger(
        "info",
        `client:recoverState() - Start of recoverState with connectionID ${this._getConnectionID()}`,
        {
          tags: ["connection"]
        }
      );
      const cids = Object.keys(this.activeChannels);
      if (cids.length && this.recoverStateOnReconnect) {
        this.logger(
          "info",
          `client:recoverState() - Start the querying of ${cids.length} channels`,
          {
            tags: ["connection", "client"]
          }
        );
        await this.queryChannels(
          { cid: { $in: cids } },
          { last_message_at: -1 },
          { limit: 30 }
        );
        this.logger("info", "client:recoverState() - Querying channels finished", {
          tags: ["connection", "client"]
        });
        this.dispatchEvent({
          type: "connection.recovered"
        });
      } else {
        this.dispatchEvent({
          type: "connection.recovered"
        });
      }
      this.wsPromise = Promise.resolve();
      this.setUserPromise = Promise.resolve();
    };
    /**
     * It's a helper method for `client.channel()` method, used to create unique conversation or
     * channel based on member list instead of id.
     *
     * If the channel already exists in `activeChannels` list, then we simply return it, since that
     * means the same channel was already requested or created.
     *
     * Otherwise we create a new instance of Channel class and return it.
     *
     * @private
     *
     * @param {string} channelType The channel type
     * @param {object} [custom]    Custom data to attach to the channel
     *
     * @return {channel} The channel object, initialize it using channel.watch()
     */
    this.getChannelByMembers = (channelType, custom) => {
      const memberIds = (custom.members ?? []).map(
        (member) => typeof member === "string" ? member : member.user_id ?? ""
      );
      const membersStr = memberIds.sort().join(",");
      const tempCid = generateChannelTempCid(channelType, memberIds);
      if (!tempCid) {
        throw Error("Please specify atleast one member when creating unique conversation");
      }
      for (const key in this.activeChannels) {
        const channel2 = this.activeChannels[key];
        if (channel2.disconnected) {
          continue;
        }
        if (key === tempCid) {
          return channel2;
        }
        if (key.indexOf(`${channelType}:!members-`) === 0) {
          const membersStrInExistingChannel = Object.keys(channel2.state.members).sort().join(",");
          if (membersStrInExistingChannel === membersStr) {
            return channel2;
          }
        }
      }
      const channel = new Channel(this, channelType, void 0, custom);
      if (this._cacheEnabled()) {
        this.activeChannels[tempCid] = channel;
      }
      return channel;
    };
    /**
     * Its a helper method for `client.channel()` method, used to channel given the id of channel.
     *
     * If the channel already exists in `activeChannels` list, then we simply return it, since that
     * means the same channel was already requested or created.
     *
     * Otherwise we create a new instance of Channel class and return it.
     *
     * @private
     *
     * @param {string} channelType The channel type
     * @param {string} [channelID] The channel ID
     * @param {object} [custom]    Custom data to attach to the channel
     *
     * @return {channel} The channel object, initialize it using channel.watch()
     */
    this.getChannelById = (channelType, channelID, custom) => {
      if (typeof channelID === "string" && ~channelID.indexOf(":")) {
        throw Error(`Invalid channel id ${channelID}, can't contain the : character`);
      }
      const cid = `${channelType}:${channelID}`;
      if (cid in this.activeChannels && this.activeChannels[cid] && !this.activeChannels[cid].disconnected) {
        const channel2 = this.activeChannels[cid];
        if (Object.keys(custom).length > 0) {
          channel2.data = { ...channel2.data, ...custom };
          channel2._data = { ...channel2._data, ...custom };
        }
        return channel2;
      }
      const channel = new Channel(this, channelType, channelID, custom);
      if (this._cacheEnabled()) {
        this.activeChannels[channel.cid] = channel;
      }
      return channel;
    };
    /**
     * @deprecated Please use upsertUsers() function instead.
     *
     * updateUsers - Batch update the list of users
     *
     * @param {UserResponse[]} users list of users
     * @return {Promise<{ users: { [key: string]: UserResponse } }>}
     */
    this.updateUsers = this.upsertUsers;
    /**
     * @deprecated Please use upsertUser() function instead.
     *
     * updateUser - Update or Create the given user object
     *
     * @param {UserResponse} userObject user object, the only required field is the user id. IE {id: "myuser"} is valid
     * @return {Promise<{ users: { [key: string]: UserResponse } }>}
     */
    this.updateUser = this.upsertUser;
    // alias for backwards compatibility
    this._unblockMessage = this.unblockMessage;
    /**
     * @deprecated use markChannelsRead instead
     *
     * markAllRead - marks all channels for this user as read
     * @param {MarkAllReadOptions} [data]
     *
     * @return {Promise<APIResponse>}
     */
    this.markAllRead = this.markChannelsRead;
    this.getUserAgent = () => {
      if (this.userAgent) {
        return this.userAgent;
      }
      if (!this.cachedUserAgent) {
        const version = "9.52.0";
        const { name: sdkName, version: sdkVersion } = this.sdkIdentifier ?? {};
        const { name: appName, version: appVersion } = this.appIdentifier ?? {};
        const { os, model: deviceModel } = this.deviceIdentifier ?? {};
        const head = sdkName ? `stream-chat-${sdkName}-v${sdkVersion}-llc-v${version}` : `stream-chat-js-v${version}-${this.node ? "node" : "browser"}`;
        this.cachedUserAgent = [
          head,
          // reports the host app, the device OS, the device model, and the picked
          // exports bundle, each only when a value is present
          ...Object.entries({
            app: appName,
            app_version: appVersion,
            os,
            device_model: deviceModel,
            client_bundle: "node-cjs"
          }).filter(([, value]) => value && value.length > 0).map(([key, value]) => `${key}=${value}`)
        ].join("|");
      }
      return this.cachedUserAgent;
    };
    /**
     * _isUsingServerAuth - Returns true if we're using server side auth
     */
    this._isUsingServerAuth = () => !!this.secret;
    this._cacheEnabled = () => !this._isUsingServerAuth() || !this.options.disableCache;
    /**
     * encode ws url payload
     * @private
     * @returns json string
     */
    this._buildWSPayload = (client_request_id) => JSON.stringify({
      user_id: this.userID,
      user_details: this._user,
      device: this.options.device,
      client_request_id
    });
    this.key = key;
    this.listeners = {};
    this.state = new ClientState({ client: this });
    this.mutedChannels = [];
    this.mutedUsers = [];
    this.blockedUsers = new StateStore({ userIds: [] });
    this.moderation = new Moderation(this);
    this.notifications = options?.notifications ?? new NotificationManager();
    this.uploadManager = new UploadManager(this);
    if (secretOrOptions && isString2(secretOrOptions)) {
      this.secret = secretOrOptions;
    }
    const inputOptions = options ? options : secretOrOptions && !isString2(secretOrOptions) ? secretOrOptions : {};
    this.browser = typeof inputOptions.browser !== "undefined" ? inputOptions.browser : typeof window !== "undefined";
    this.node = !this.browser;
    this.options = {
      timeout: 3e3,
      withCredentials: false,
      // making sure cookies are not sent
      warmUp: false,
      recoverStateOnReconnect: true,
      disableCache: false,
      isLocalUnreadCountEnabled: false,
      wsUrlParams: new URLSearchParams({}),
      ...inputOptions
    };
    if (this.node && !this.options.httpsAgent) {
      this.options.httpsAgent = new import_https.default.Agent({
        keepAlive: true,
        keepAliveMsecs: 3e3
      });
    }
    this.axiosInstance = import_axios4.default.create(this.options);
    this.setBaseURL(this.options.baseURL || "https://chat.stream-io-api.com");
    if (typeof process !== "undefined" && "env" in process && process.env.STREAM_LOCAL_TEST_RUN) {
      this.setBaseURL("http://localhost:3030");
    }
    if (typeof process !== "undefined" && "env" in process && process.env.STREAM_LOCAL_TEST_HOST) {
      this.setBaseURL("http://" + process.env.STREAM_LOCAL_TEST_HOST);
    }
    this.wsConnection = null;
    this.wsPromise = null;
    this.setUserPromise = null;
    this.activeChannels = {};
    this.configs = {};
    this.anonymous = false;
    this.persistUserOnConnectionFailure = this.options?.persistUserOnConnectionFailure;
    this.tokenManager = new TokenManager(this.secret);
    this.consecutiveFailures = 0;
    this.insightMetrics = new InsightMetrics();
    this.defaultWSTimeoutWithFallback = 6 * 1e3;
    this.defaultWSTimeout = 15 * 1e3;
    this.axiosInstance.defaults.paramsSerializer = axiosParamsSerializer;
    this.logger = isFunction(inputOptions.logger) ? inputOptions.logger : () => null;
    this.recoverStateOnReconnect = this.options.recoverStateOnReconnect;
    this.threads = new ThreadManager({ client: this });
    this.polls = new PollManager({ client: this });
    this.reminders = new ReminderManager({ client: this });
    this.messageDeliveryReporter = new MessageDeliveryReporter({ client: this });
    this.messageComposerCache = new FixedSizeQueueCache(64);
  }
  static getInstance(key, secretOrOptions, options) {
    if (!_StreamChat._instance) {
      if (typeof secretOrOptions === "string") {
        _StreamChat._instance = new _StreamChat(key, secretOrOptions, options);
      } else {
        _StreamChat._instance = new _StreamChat(key, secretOrOptions);
      }
    }
    return _StreamChat._instance;
  }
  setOfflineDBApi(offlineDBInstance) {
    if (this.offlineDb) {
      return;
    }
    this.offlineDb = offlineDBInstance;
  }
  devToken(userID) {
    return DevToken(userID);
  }
  getAuthType() {
    return this.anonymous ? "anonymous" : "jwt";
  }
  setBaseURL(baseURL) {
    this.baseURL = baseURL;
    this.wsBaseURL = this.baseURL.replace("http", "ws").replace(":3030", ":8800");
  }
  _setUser(user) {
    this.user = user;
    this.userID = user.id;
    this._user = { ...user };
  }
  /**
   * updateAppSettings - updates application settings
   *
   * @param {AppSettings} options App settings.
   * IE: {
      'apn_config': {
        'auth_type': 'token',
        'auth_key": fs.readFileSync(
          './apn-push-auth-key.p8',
          'utf-8',
        ),
        'key_id': 'keyid',
        'team_id': 'teamid',
        'notification_template": 'notification handlebars template',
        'bundle_id': 'com.apple.your.app',
        'development': true
      },
      'firebase_config': {
        'server_key': 'server key from fcm',
        'notification_template': 'notification handlebars template',
        'data_template': 'data handlebars template',
        'apn_template': 'apn notification handlebars template under v2'
      },
      'webhook_url': 'https://acme.com/my/awesome/webhook/',
      'event_hooks': [
        {
          'hook_type': 'webhook',
          'enabled': true,
          'event_types': ['message.new'],
          'webhook_url': 'https://acme.com/my/awesome/webhook/'
        },
        {
          'hook_type': 'sqs',
          'enabled': true,
          'event_types': ['message.new'],
          'sqs_url': 'https://sqs.us-east-1.amazonaws.com/1234567890/my-queue',
          'sqs_auth_type': 'key',
          'sqs_key': 'my-access-key',
          'sqs_secret': 'my-secret-key'
        }
      ]
    }
   */
  async updateAppSettings(options) {
    const apn_config = options.apn_config;
    if (apn_config?.p12_cert) {
      options = {
        ...options,
        apn_config: {
          ...apn_config,
          p12_cert: Buffer.from(apn_config.p12_cert).toString("base64")
        }
      };
    }
    return await this.patch(this.baseURL + "/app", options);
  }
  /**
   * Revokes all tokens on application level issued before given time
   */
  async revokeTokens(before) {
    return await this.updateAppSettings({
      revoke_tokens_issued_before: this._normalizeDate(before)
    });
  }
  /**
   * Revokes token for a user issued before given time
   */
  async revokeUserToken(userID, before) {
    return await this.revokeUsersToken([userID], before);
  }
  /**
   * Revokes tokens for a list of users issued before given time
   */
  async revokeUsersToken(userIDs, before) {
    if (before === void 0) {
      before = (/* @__PURE__ */ new Date()).toISOString();
    } else {
      before = this._normalizeDate(before);
    }
    const users = [];
    for (const userID of userIDs) {
      users.push({
        id: userID,
        set: {
          revoke_tokens_issued_before: before
        }
      });
    }
    return await this.partialUpdateUsers(users);
  }
  /**
   * getAppSettings - retrieves application settings
   */
  async getAppSettings() {
    this.appSettingsPromise = this.get(this.baseURL + "/app");
    return await this.appSettingsPromise;
  }
  /**
   * testPushSettings - Tests the push settings for a user with a random chat message and the configured push templates
   *
   * @param {string} userID User ID. If user has no devices, it will error
   * @param {TestPushDataInput} [data] Overrides for push templates/message used
   *  IE: {
   messageID: 'id-of-message', // will error if message does not exist
   apnTemplate: '{}', // if app doesn't have apn configured it will error
   firebaseTemplate: '{}', // if app doesn't have firebase configured it will error
   firebaseDataTemplate: '{}', // if app doesn't have firebase configured it will error
   skipDevices: true, // skip config/device checks and sending to real devices
   pushProviderName: 'staging' // one of your configured push providers
   pushProviderType: 'apn' // one of supported provider types
   }
   */
  async testPushSettings(userID, data = {}) {
    return await this.post(this.baseURL + "/check_push", {
      user_id: userID,
      ...data.messageID ? { message_id: data.messageID } : {},
      ...data.apnTemplate ? { apn_template: data.apnTemplate } : {},
      ...data.firebaseTemplate ? { firebase_template: data.firebaseTemplate } : {},
      ...data.firebaseDataTemplate ? { firebase_data_template: data.firebaseDataTemplate } : {},
      ...data.skipDevices ? { skip_devices: true } : {},
      ...data.pushProviderName ? { push_provider_name: data.pushProviderName } : {},
      ...data.pushProviderType ? { push_provider_type: data.pushProviderType } : {}
    });
  }
  /**
   * testSQSSettings - Tests that the given or configured SQS configuration is valid
   *
   * @param {TestSQSDataInput} [data] Overrides SQS settings for testing if needed
   *  IE: {
   sqs_key: 'auth_key',
   sqs_secret: 'auth_secret',
   sqs_url: 'url_to_queue',
   }
   */
  async testSQSSettings(data = {}) {
    return await this.post(this.baseURL + "/check_sqs", data);
  }
  /**
   * testSNSSettings - Tests that the given or configured SNS configuration is valid
   *
   * @param {TestSNSDataInput} [data] Overrides SNS settings for testing if needed
   *  IE: {
   sns_key: 'auth_key',
   sns_secret: 'auth_secret',
   sns_topic_arn: 'topic_to_publish_to',
   }
   */
  async testSNSSettings(data = {}) {
    return await this.post(this.baseURL + "/check_sns", data);
  }
  /**
   * setGuestUser - Setup a temporary guest user
   *
   * @param {UserResponse} user Data about this user. IE {name: "john"}
   *
   * @return {ConnectAPIResponse} Returns a promise that resolves when the connection is setup
   */
  async setGuestUser(user) {
    let response;
    this.anonymous = true;
    try {
      response = await this.post(this.baseURL + "/guest", { user });
    } catch (e) {
      this.anonymous = false;
      throw e;
    }
    this.anonymous = false;
    const { created_at, updated_at, last_active, online, ...guestUser } = response.user;
    return await this.connectUser(guestUser, response.access_token);
  }
  /**
   * createToken - Creates a token to authenticate this user. This function is used server side.
   * The resulting token should be passed to the client side when the users registers or logs in.
   *
   * @param {string} userID The User ID
   * @param {number} [exp] The expiration time for the token expressed in the number of seconds since the epoch
   *
   * @return {string} Returns a token
   */
  createToken(userID, exp, iat) {
    if (this.secret == null) {
      throw Error(`tokens can only be created server-side using the API Secret`);
    }
    const extra = {};
    if (exp) {
      extra.exp = exp;
    }
    if (iat) {
      extra.iat = iat;
    }
    return JWTUserToken(this.secret, userID, extra, {});
  }
  on(callbackOrString, callbackOrNothing) {
    const key = callbackOrNothing ? callbackOrString : "all";
    const callback = callbackOrNothing ? callbackOrNothing : callbackOrString;
    if (!(key in this.listeners)) {
      this.listeners[key] = [];
    }
    this.logger("info", `Attaching listener for ${key} event`, {
      tags: ["event", "client"]
    });
    this.listeners[key].push(callback);
    return {
      unsubscribe: () => {
        this.logger("info", `Removing listener for ${key} event`, {
          tags: ["event", "client"]
        });
        this.listeners[key] = this.listeners[key].filter((el) => el !== callback);
      }
    };
  }
  off(callbackOrString, callbackOrNothing) {
    const key = callbackOrNothing ? callbackOrString : "all";
    const callback = callbackOrNothing ? callbackOrNothing : callbackOrString;
    if (!(key in this.listeners)) {
      this.listeners[key] = [];
    }
    this.logger("info", `Removing listener for ${key} event`, {
      tags: ["event", "client"]
    });
    this.listeners[key] = this.listeners[key].filter((value) => value !== callback);
  }
  _logApiRequest(type, url, data, config) {
    this.logger("info", `client: ${type} - Request - ${url}`, {
      tags: ["api", "api_request", "client"],
      url,
      payload: data,
      config
    });
  }
  _logApiResponse(type, url, response) {
    this.logger(
      "info",
      `client:${type} - Response - url: ${url} > status ${response.status}`,
      {
        tags: ["api", "api_response", "client"],
        url,
        response
      }
    );
  }
  _logApiError(type, url, error) {
    this.logger("error", `client:${type} - Error - url: ${url}`, {
      tags: ["api", "api_response", "client"],
      url,
      error
    });
  }
  get(url, params, config) {
    return this.doAxiosRequest("get", url, null, { params, config });
  }
  put(url, data) {
    return this.doAxiosRequest("put", url, data);
  }
  post(url, data, config) {
    return this.doAxiosRequest("post", url, data, { config });
  }
  patch(url, data) {
    return this.doAxiosRequest("patch", url, data);
  }
  delete(url, params) {
    return this.doAxiosRequest("delete", url, null, { params });
  }
  sendFile(url, uri, name, contentType, user, axiosRequestConfig) {
    const data = addFileToFormData(uri, name, contentType || "multipart/form-data");
    if (user != null) data.append("user", JSON.stringify(user));
    return this.doAxiosRequest("postForm", url, data, {
      headers: data.getHeaders ? data.getHeaders() : {},
      // node vs browser
      config: {
        timeout: 0,
        maxContentLength: Infinity,
        maxBodyLength: Infinity,
        ...axiosRequestConfig
      }
    });
  }
  errorFromResponse(response) {
    const message = typeof response.data.code !== "undefined" ? `StreamChat error code ${response.data.code}: ${response.data.message}` : `StreamChat error HTTP code: ${response.status}`;
    return new ErrorFromResponse(message, {
      code: response.data.code ?? null,
      response,
      status: response.status
    });
  }
  handleResponse(response) {
    const data = response.data;
    if (isErrorResponse(response)) {
      throw this.errorFromResponse(response);
    }
    return data;
  }
  _handleClientEvent(event) {
    const client = this;
    const postListenerCallbacks = [];
    this.logger(
      "info",
      `client:_handleClientEvent - Received event of type { ${event.type} }`,
      {
        tags: ["event", "client"],
        event
      }
    );
    if (event.type === "user.presence.changed" || event.type === "user.updated" || event.type === "user.deleted") {
      this._handleUserEvent(event);
    }
    if (event.type === "user.messages.deleted" && !event.cid && event.user) {
      this._deleteUserMessageReference(
        event.user,
        event.hard_delete,
        event.created_at ? new Date(event.created_at) : null
      );
    }
    if (event.type === "health.check" && event.me) {
      client.user = event.me;
      client.state.updateUser(event.me);
      client.mutedChannels = event.me.channel_mutes;
      client.mutedUsers = event.me.mutes;
      client.blockedUsers.partialNext({ userIds: event.me.blocked_user_ids ?? [] });
    }
    if (event.channel && event.type === "notification.message_new") {
      const { channel } = event;
      this._addChannelConfig(channel);
    }
    if (event.type === "notification.channel_mutes_updated" && event.me?.channel_mutes) {
      this.mutedChannels = event.me.channel_mutes;
    }
    if (event.type === "notification.mutes_updated" && event.me?.mutes) {
      this.mutedUsers = event.me.mutes;
    }
    if (event.type === "notification.mark_read" && event.unread_channels === 0) {
      const activeChannelKeys = Object.keys(this.activeChannels);
      activeChannelKeys.forEach(
        (activeChannelKey) => this.activeChannels[activeChannelKey].state.unreadCount = 0
      );
    }
    if ((event.type === "channel.deleted" || event.type === "notification.channel_deleted") && event.cid) {
      const { cid } = event;
      client.state.deleteAllChannelReference(cid);
      this.activeChannels[event.cid]?._disconnect();
      postListenerCallbacks.push(() => {
        if (!cid) return;
        delete this.activeChannels[cid];
      });
    }
    if (event.type === "notification.removed_from_channel" && event.cid) {
      const { cid } = event;
      this.activeChannels[cid]?._disconnect();
      postListenerCallbacks.push(() => {
        delete this.activeChannels[cid];
      });
    }
    return postListenerCallbacks;
  }
  _muteStatus(cid) {
    let muteStatus;
    for (let i = 0; i < this.mutedChannels.length; i++) {
      const mute = this.mutedChannels[i];
      if (mute.channel?.cid === cid) {
        muteStatus = {
          muted: mute.expires ? new Date(mute.expires).getTime() > (/* @__PURE__ */ new Date()).getTime() : true,
          createdAt: mute.created_at ? new Date(mute.created_at) : /* @__PURE__ */ new Date(),
          expiresAt: mute.expires ? new Date(mute.expires) : null
        };
        break;
      }
    }
    if (muteStatus) {
      return muteStatus;
    }
    return {
      muted: false,
      createdAt: null,
      expiresAt: null
    };
  }
  /**
   * @private
   */
  async connect() {
    if (!this.userID || !this._user) {
      throw Error(
        "Call connectUser or connectAnonymousUser before starting the connection"
      );
    }
    if (!this.wsBaseURL) {
      throw Error("Websocket base url not set");
    }
    if (!this.clientID) {
      throw Error("clientID is not set");
    }
    if (!this.wsConnection && (this.options.warmUp || this.options.enableInsights)) {
      this._sayHi();
    }
    if (this.options.wsConnection && this.node) {
      this.options.wsConnection.setClient(this);
      this.wsConnection = this.options.wsConnection;
    } else {
      this.wsConnection = new StableWSConnection({
        client: this
      });
    }
    try {
      if (this.wsFallback) {
        return await this.wsFallback.connect();
      }
      return await this.wsConnection.connect(
        this.options.enableWSFallback ? this.defaultWSTimeoutWithFallback : this.defaultWSTimeout
      );
    } catch (error) {
      if (this.options.enableWSFallback && isWSFailure(error) && isOnline()) {
        this.logger("info", "client:connect() - WS failed, fallback to longpoll", {
          tags: ["connection", "client"]
        });
        this.dispatchEvent({ type: "transport.changed", mode: "longpoll" });
        this.wsConnection._destroyCurrentWSConnection();
        this.wsConnection.disconnect().then();
        this.wsFallback = new WSConnectionFallback({
          client: this
        });
        return await this.wsFallback.connect();
      }
      throw error;
    }
  }
  /**
   * Check the connectivity with server for warmup purpose.
   *
   * @private
   */
  _sayHi() {
    const client_request_id = randomId();
    const opts = { headers: { "x-client-request-id": client_request_id } };
    this.doAxiosRequest("get", this.baseURL + "/hi", null, opts).catch((e) => {
      if (this.options.enableInsights) {
        postInsights("http_hi_failed", {
          api_key: this.key,
          err: e,
          client_request_id
        });
      }
    });
  }
  /**
   * queryUsers - Query users and watch user presence
   *
   * @param {UserFilters} filterConditions MongoDB style filter conditions
   * @param {UserSort} sort Sort options, for instance [{last_active: -1}].
   * When using multiple fields, make sure you use array of objects to guarantee field order, for instance [{last_active: -1}, {created_at: 1}]
   * @param {UserOptions} options Option object, {presence: true}
   * @param {RequestOptions} [requestOptions] Carries an abort signal. Not sent in the request.
   *
   * @return {Promise<{ users: Array<UserResponse> }>} User Query Response
   */
  async queryUsers(filterConditions, sort = [], options = {}, requestOptions = {}) {
    const defaultOptions = {
      presence: false
    };
    await this.wsPromise;
    if (!this._hasConnectionID()) {
      defaultOptions.presence = false;
    }
    const data = await this.get(
      this.baseURL + "/users",
      {
        payload: {
          filter_conditions: filterConditions,
          sort: normalizeQuerySort(sort),
          ...defaultOptions,
          ...options
        }
      },
      requestOptions
    );
    this.state.updateUsers(data.users);
    return data;
  }
  /**
   * queryUserGroups - List user groups with cursor-based pagination.
   *
   * @param {QueryUserGroupsOptions} options The query options
   *
   * @return {Promise<QueryUserGroupsResponse>} User Group Query Response
   */
  async queryUserGroups(options = {}) {
    return await this.get(this.baseURL + "/usergroups", options);
  }
  /**
   * createUserGroup - Create a user group
   *
   * @param {CreateUserGroupOptions} options The create options
   *
   * @return {Promise<CreateUserGroupResponse>} User Group Create Response
   */
  async createUserGroup(options) {
    return await this.post(
      this.baseURL + "/usergroups",
      options
    );
  }
  /**
   * getUserGroup - Get a user group by ID
   *
   * @param {string} id The user group ID
   * @param {GetUserGroupOptions} options Optional query options
   *
   * @return {Promise<GetUserGroupResponse>} User Group Get Response
   */
  async getUserGroup(id, options = {}) {
    return await this.get(
      `${this.baseURL}/usergroups/${encodeURIComponent(id)}`,
      options
    );
  }
  /**
   * searchUserGroups - Search user groups by prefix for autocomplete
   *
   * @param {SearchUserGroupsOptions} options The search options
   * @param {RequestOptions} [requestOptions] Carries an abort signal. Not sent in the request.
   * @return {Promise<SearchUserGroupsResponse>} User Group Search Response
   */
  async searchUserGroups(options, requestOptions = {}) {
    return await this.get(
      this.baseURL + "/usergroups/search",
      options,
      requestOptions
    );
  }
  /**
   * updateUserGroup - Update a user group by ID
   *
   * @param {string} id The user group ID
   * @param {UpdateUserGroupOptions} options The update options
   *
   * @return {Promise<UpdateUserGroupResponse>} User Group Update Response
   */
  async updateUserGroup(id, options) {
    return await this.put(
      `${this.baseURL}/usergroups/${encodeURIComponent(id)}`,
      options
    );
  }
  /**
   * deleteUserGroup - Delete a user group by ID
   *
   * @param {string} id The user group ID
   * @param {DeleteUserGroupOptions} options Optional query options
   *
   * @return {Promise<APIResponse>} User Group Delete Response
   */
  async deleteUserGroup(id, options = {}) {
    return await this.delete(
      `${this.baseURL}/usergroups/${encodeURIComponent(id)}`,
      options
    );
  }
  /**
   * addUserGroupMembers - Add members to a user group
   *
   * @param {string} id The user group ID
   * @param {AddUserGroupMembersOptions} options The add-members options
   *
   * @return {Promise<AddUserGroupMembersResponse>} User Group Add Members Response
   */
  async addUserGroupMembers(id, options) {
    return await this.post(
      `${this.baseURL}/usergroups/${encodeURIComponent(id)}/members`,
      options
    );
  }
  /**
   * removeUserGroupMembers - Remove members from a user group
   *
   * @param {string} id The user group ID
   * @param {RemoveUserGroupMembersOptions} options The remove-members options
   *
   * @return {Promise<RemoveUserGroupMembersResponse>} User Group Remove Members Response
   */
  async removeUserGroupMembers(id, options) {
    return await this.post(
      `${this.baseURL}/usergroups/${encodeURIComponent(id)}/members/delete`,
      options
    );
  }
  /**
   * queryBannedUsers - Query user bans
   *
   * @param {BannedUsersFilters} filterConditions MongoDB style filter conditions
   * @param {BannedUsersSort} sort Sort options [{created_at: 1}].
   * @param {BannedUsersPaginationOptions} options Option object, {limit: 10, offset:0, exclude_expired_bans: true}
   *
   * @return {Promise<BannedUsersResponse>} Ban Query Response
   */
  async queryBannedUsers(filterConditions = {}, sort = [], options = {}) {
    return await this.get(this.baseURL + "/query_banned_users", {
      payload: {
        filter_conditions: filterConditions,
        sort: normalizeQuerySort(sort),
        ...options
      }
    });
  }
  /**
   * queryFutureChannelBans - Query future channel bans created by a user
   *
   * @param {QueryFutureChannelBansOptions} options Option object with user_id, exclude_expired_bans, limit, offset
   * @returns {Promise<FutureChannelBansResponse>} Future Channel Bans Response
   */
  async queryFutureChannelBans(options = {}) {
    return await this.get(
      this.baseURL + "/query_future_channel_bans",
      {
        payload: options
      }
    );
  }
  /**
   * queryMessageFlags - Query message flags
   *
   * @param {MessageFlagsFilters} filterConditions MongoDB style filter conditions
   * @param {MessageFlagsPaginationOptions} options Option object, {limit: 10, offset:0}
   *
   * @return {Promise<MessageFlagsResponse>} Message Flags Response
   */
  async queryMessageFlags(filterConditions = {}, options = {}) {
    return await this.get(
      this.baseURL + "/moderation/flags/message",
      {
        payload: { filter_conditions: filterConditions, ...options }
      }
    );
  }
  /**
   * queryChannelsRequestWithResponse - Queries channels and returns the full API response
   * including top-level metadata such as `predefined_filter`.
   *
   * This exists as a compatibility bridge, as changing `queryChannelsRequest()` to return
   * `QueryChannelsAPIResponse` would be a breaking change because it currently returns
   * only the channel list. In the next major release, the request/response APIs should
   * be consolidated so callers can access the full response through the primary API.
   *
   * @param {ChannelFilters} filterConditions object MongoDB style filters. Can be empty object when using predefined_filter in options.
   * @param {ChannelSort} [sort] Sort options, for instance {created_at: -1}.
   * When using multiple fields, make sure you use array of objects to guarantee field order, for instance [{last_updated: -1}, {created_at: 1}]
   * @param {ChannelOptions} [options] Options object. Can include predefined_filter, filter_values, and sort_values for using predefined filters.
   * @param {RequestOptions} [requestOptions] Carries an abort signal. Not sent in the request.
   *
   * @return {Promise<QueryChannelsAPIResponse>} full search channels response
   */
  async queryChannelsRequestWithResponse(filterConditions, sort = [], options = {}, requestOptions = {}) {
    const defaultOptions = {
      state: true,
      watch: true,
      presence: false
    };
    await this.wsPromise;
    if (!this._hasConnectionID()) {
      defaultOptions.watch = false;
    }
    const { predefined_filter, filter_values, sort_values, ...restOptions } = options;
    const normalizedSort = normalizeQuerySort(sort);
    const payload = predefined_filter ? {
      predefined_filter,
      filter_values,
      sort_values,
      sort: normalizedSort,
      ...defaultOptions,
      ...restOptions
    } : {
      filter_conditions: filterConditions,
      sort: normalizedSort,
      ...defaultOptions,
      ...restOptions
    };
    return await this.post(
      this.baseURL + "/channels",
      payload,
      requestOptions
    );
  }
  /**
   * queryChannelsRequest - Queries channels and returns the raw channel response list.
   *
   * This preserves the historical return shape for backwards compatibility. Use
   * `queryChannelsRequestWithResponse()` when response level metadata such as
   * `predefined_filter` is needed. In the next major release these APIs should be
   * consolidated into a single full-response API.
   *
   * @param {ChannelFilters} filterConditions object MongoDB style filters. Can be empty object when using predefined_filter in options.
   * @param {ChannelSort} [sort] Sort options, for instance {created_at: -1}.
   * When using multiple fields, make sure you use array of objects to guarantee field order, for instance [{last_updated: -1}, {created_at: 1}]
   * @param {ChannelOptions} [options] Options object. Can include predefined_filter, filter_values, and sort_values for using predefined filters.
   *
   * @return {Promise<Array<ChannelAPIResponse>>} search channels response
   */
  async queryChannelsRequest(filterConditions, sort = [], options = {}) {
    const data = await this.queryChannelsRequestWithResponse(
      filterConditions,
      sort,
      options
    );
    return data.channels;
  }
  async queryChannels(filterConditions, sort = [], options = {}, stateOptions = {}) {
    const queryChannelsResponse = await this.queryChannelsRequestWithResponse(
      filterConditions,
      sort,
      options,
      // stateOptions is never serialized, so it carries the abort signal
      { signal: stateOptions.signal }
    );
    const channels = queryChannelsResponse.channels;
    this.dispatchEvent({
      type: "channels.queried",
      queriedChannels: {
        channels,
        isLatestMessageSet: true
      }
    });
    if (channels?.length && this.offlineDb?.upsertChannels) {
      await this.offlineDb.upsertChannels({
        channels,
        isLatestMessagesSet: true
      });
    }
    const hydratedChannels = this.hydrateActiveChannels(channels, stateOptions, options);
    if (stateOptions.withResponse) {
      return {
        ...queryChannelsResponse,
        channels: hydratedChannels
      };
    }
    return hydratedChannels;
  }
  /**
   * queryReactions - Query reactions
   *
   * @param {ReactionFilters} filter object MongoDB style filters
   * @param {ReactionSort} [sort] Sort options, for instance {created_at: -1}.
   * @param {QueryReactionsOptions} [options] Pagination object
   *
   * @return {Promise<{ QueryReactionsAPIResponse } search channels response
   */
  async queryReactions(messageID, filter, sort = [], options = {}) {
    const payload = {
      filter,
      sort: normalizeQuerySort(sort),
      ...options
    };
    if (this.offlineDb?.getReactions && !options.next) {
      try {
        const reactionsFromDb = await this.offlineDb.getReactions({
          messageId: messageID,
          filters: filter,
          sort,
          limit: options.limit
        });
        if (reactionsFromDb) {
          this.dispatchEvent({
            type: "offline_reactions.queried",
            offlineReactions: reactionsFromDb
          });
        }
      } catch (e) {
        this.logger("warn", "An error has occurred while querying offline reactions", {
          error: e
        });
      }
    }
    await this.wsPromise;
    return await this.post(
      this.baseURL + "/messages/" + encodeURIComponent(messageID) + "/reactions",
      payload
    );
  }
  hydrateActiveChannels(channelsFromApi = [], stateOptions = {}, queryChannelsOptions) {
    const { skipInitialization, offlineMode = false } = stateOptions;
    const channels = [];
    for (const channelState of channelsFromApi) {
      this._addChannelConfig(channelState.channel);
      const c = this.channel(channelState.channel.type, channelState.channel.id);
      c.data = channelState.channel;
      c.offlineMode = offlineMode;
      c.initialized = !offlineMode;
      c.push_preferences = channelState.push_preferences;
      let updatedMessagesSet;
      let filteredMessageIds = [];
      if (skipInitialization === void 0) {
        const { messageSet, filteredMessageIds: _filteredMessageIds } = c._initializeState(channelState, "latest");
        filteredMessageIds = _filteredMessageIds;
        updatedMessagesSet = messageSet;
      } else if (!skipInitialization.includes(channelState.channel.id)) {
        c.state.clearMessages();
        const { messageSet, filteredMessageIds: _filteredMessageIds } = c._initializeState(channelState, "latest");
        filteredMessageIds = _filteredMessageIds;
        updatedMessagesSet = messageSet;
      }
      if (updatedMessagesSet) {
        updatedMessagesSet.pagination = {
          ...updatedMessagesSet.pagination,
          ...messageSetPagination({
            parentSet: updatedMessagesSet,
            requestedPageSize: queryChannelsOptions?.message_limit || DEFAULT_QUERY_CHANNELS_MESSAGE_LIST_PAGE_SIZE,
            returnedPage: channelState.messages,
            filteredReturnedPage: channelState.messages.filter(
              (m) => !filteredMessageIds.includes(m.id)
            ),
            logger: this.logger
          })
        };
        this.polls.hydratePollCache(channelState.messages, true);
        this.reminders.hydrateState(channelState.messages);
      }
      c.messageComposer.initStateFromChannelResponse(channelState);
      c.cooldownTimer.refresh();
      channels.push(c);
    }
    this.syncDeliveredCandidates(channels);
    return channels;
  }
  /**
   * search - Query messages
   *
   * @param {ChannelFilters} filterConditions MongoDB style filter conditions
   * @param {MessageFilters | string} query search query or object MongoDB style filters
   * @param {SearchOptions} [options] Option object, {user_id: 'tommaso'}
   * @param {RequestOptions} [requestOptions] Carries an abort signal. Not sent in the request.
   *
   * @return {Promise<SearchAPIResponse>} search messages response
   */
  async search(filterConditions, query, options = {}, requestOptions = {}) {
    if (options.offset && options.next) {
      throw Error(`Cannot specify offset with next`);
    }
    const payload = {
      filter_conditions: filterConditions,
      ...options,
      sort: options.sort ? normalizeQuerySort(options.sort) : void 0
    };
    if (typeof query === "string") {
      payload.query = query;
    } else if (typeof query === "object") {
      payload.message_filter_conditions = query;
    } else {
      throw Error(`Invalid type ${typeof query} for query parameter`);
    }
    await this.wsPromise;
    return await this.get(
      this.baseURL + "/search",
      { payload },
      requestOptions
    );
  }
  /**
   * setLocalDevice - Set the device info for the current client(device) that will be sent via WS connection automatically
   *
   * @param {BaseDeviceFields} device the device object
   * @param {string} device.id device id
   * @param {string} device.push_provider the push provider
   *
   */
  setLocalDevice(device) {
    if (this.wsConnection?.isConnecting && this.wsPromise || (this.wsConnection?.isHealthy || this.wsFallback?.isHealthy()) && this._hasConnectionID()) {
      throw new Error("you can only set device before opening a websocket connection");
    }
    this.options.device = device;
  }
  /**
   * addDevice - Adds a push device for a user.
   *
   * @param {string} id the device id
   * @param {PushProvider} push_provider the push provider
   * @param {string} [userID] the user id (defaults to current user)
   * @param {string} [push_provider_name] user provided push provider name for multi bundle support
   *
   */
  async addDevice(id, push_provider, userID, push_provider_name) {
    return await this.post(this.baseURL + "/devices", {
      id,
      push_provider,
      ...userID != null ? { user_id: userID } : {},
      ...push_provider_name != null ? { push_provider_name } : {}
    });
  }
  /**
   * getDevices - Returns the devices associated with a current user
   *
   * @param {string} [userID] User ID. Only works on serverside
   *
   * @return {Device[]} Array of devices
   */
  async getDevices(userID) {
    return await this.get(
      this.baseURL + "/devices",
      userID ? { user_id: userID } : {}
    );
  }
  /**
   * getUnreadCount - Returns unread counts for a single user
   *
   * @param {string} [userID] User ID.
   *
   * @return {<GetUnreadCountAPIResponse>}
   */
  async getUnreadCount(userID) {
    return await this.get(
      this.baseURL + "/unread",
      userID ? { user_id: userID } : {}
    );
  }
  /**
   * getUnreadCountBatch - Returns unread counts for multiple users at once. Only works server side.
   *
   * @param {string[]} [userIDs] List of user IDs to fetch unread counts for.
   *
   * @return {<GetUnreadCountBatchAPIResponse>}
   */
  async getUnreadCountBatch(userIDs) {
    return await this.post(
      this.baseURL + "/unread_batch",
      { user_ids: userIDs }
    );
  }
  /**
   * setPushPreferences - Applies the list of push preferences.
   *
   * @param {PushPreference[]} A list of push preferences.
   *
   * @return {<UpsertPushPreferencesResponse>}
   */
  async setPushPreferences(preferences) {
    return await this.post(
      this.baseURL + "/push_preferences",
      { preferences }
    );
  }
  /**
   * removeDevice - Removes the device with the given id. Clientside users can only delete their own devices
   *
   * @param {string} id The device id
   * @param {string} [userID] The user id. Only specify this for serverside requests
   *
   */
  async removeDevice(id, userID) {
    return await this.delete(this.baseURL + "/devices", {
      id,
      ...userID ? { user_id: userID } : {}
    });
  }
  /**
   * getRateLimits - Returns the rate limits quota and usage for the current app, possibly filter for a specific platform and/or endpoints.
   * Only available server-side.
   *
   * @param {object} [params] The params for the call. If none of the params are set, all limits for all platforms are returned.
   * @returns {Promise<GetRateLimitsResponse>}
   */
  getRateLimits(params) {
    const { serverSide, web, android, ios, endpoints } = params || {};
    return this.get(this.baseURL + "/rate_limits", {
      server_side: serverSide,
      web,
      android,
      ios,
      endpoints: endpoints ? endpoints.join(",") : void 0
    });
  }
  /**
   * getHookEvents - Get available events for hooks (webhook, SQS, and SNS)
   *
   * @param {Product[]} [products] Optional array of products to filter events by (e.g., [Product.Chat, Product.Video])
   * @returns {Promise<GetHookEventsResponse>} Response containing available hook events
   */
  async getHookEvents(products) {
    const params = products && products.length > 0 ? { product: products.join(",") } : {};
    return await this.get(this.baseURL + "/hook/events", params);
  }
  _addChannelConfig({ cid, config }) {
    if (this._cacheEnabled()) {
      this.configs[cid] = config;
    }
  }
  channel(channelType, channelIDOrCustom, custom = {}) {
    if (!this.userID && !this._isUsingServerAuth()) {
      throw Error("Call connectUser or connectAnonymousUser before creating a channel");
    }
    if (~channelType.indexOf(":")) {
      throw new Error(
        `Invalid channel group ${channelType}, can't contain the : character`
      );
    }
    if (channelIDOrCustom && typeof channelIDOrCustom === "object") {
      return this.getChannelByMembers(channelType, channelIDOrCustom);
    }
    if (!channelIDOrCustom && typeof custom === "object" && custom.members?.length) {
      return this.getChannelByMembers(channelType, custom);
    }
    if (!channelIDOrCustom) {
      return new Channel(this, channelType, void 0, custom);
    }
    return this.getChannelById(channelType, channelIDOrCustom, custom);
  }
  /**
   * partialUpdateUser - Update the given user object
   *
   * @param {PartialUserUpdate} partialUserObject which should contain id and any of "set" or "unset" params;
   * example: {id: "user1", set:{field: value}, unset:["field2"]}
   *
   * @return {Promise<{ users: { [key: string]: UserResponse } }>} list of updated users
   */
  async partialUpdateUser(partialUserObject) {
    return await this.partialUpdateUsers([partialUserObject]);
  }
  /**
   * upsertUsers - Batch upsert the list of users
   *
   * @param {UserResponse[]} users list of users
   *
   * @return {Promise<{ users: { [key: string]: UserResponse } }>}
   */
  async upsertUsers(users) {
    const userMap = {};
    for (const userObject of users) {
      if (!userObject.id) {
        throw Error("User ID is required when updating a user");
      }
      userMap[userObject.id] = userObject;
    }
    return await this.post(this.baseURL + "/users", {
      users: userMap
    });
  }
  /**
   * upsertUser - Update or Create the given user object
   *
   * @param {UserResponse} userObject user object, the only required field is the user id. IE {id: "myuser"} is valid
   *
   * @return {Promise<{ users: { [key: string]: UserResponse } }>}
   */
  upsertUser(userObject) {
    return this.upsertUsers([userObject]);
  }
  /**
   * partialUpdateUsers - Batch partial update of users
   *
   * @param {PartialUserUpdate[]} users list of partial update requests
   *
   * @return {Promise<{ users: { [key: string]: UserResponse } }>}
   */
  async partialUpdateUsers(users) {
    for (const userObject of users) {
      if (!userObject.id) {
        throw Error("User ID is required when updating a user");
      }
    }
    return await this.patch(this.baseURL + "/users", { users });
  }
  async deleteUser(userID, params) {
    return await this.delete(this.baseURL + `/users/${encodeURIComponent(userID)}`, params);
  }
  /**
   * restoreUsers - Restore soft deleted users
   *
   * @param {string[]} user_ids which users to restore
   *
   * @return {APIResponse} An API response
   */
  async restoreUsers(user_ids) {
    return await this.post(this.baseURL + `/users/restore`, {
      user_ids
    });
  }
  /**
   * reactivateUser - Reactivate one user
   *
   * @param {string} userID which user to reactivate
   * @param {ReactivateUserOptions} [options]
   *
   * @return {UserResponse} Reactivated user
   */
  async reactivateUser(userID, options) {
    return await this.post(
      this.baseURL + `/users/${encodeURIComponent(userID)}/reactivate`,
      { ...options }
    );
  }
  /**
   * reactivateUsers - Reactivate many users asynchronously
   *
   * @param {string[]} user_ids which users to reactivate
   * @param {ReactivateUsersOptions} [options]
   *
   * @return {TaskResponse} A task ID
   */
  async reactivateUsers(user_ids, options) {
    return await this.post(
      this.baseURL + `/users/reactivate`,
      { user_ids, ...options }
    );
  }
  /**
   * deactivateUser - Deactivate one user
   *
   * @param {string} userID which user to deactivate
   * @param {DeactivateUsersOptions} [options]
   *
   * @return {UserResponse} Deactivated user
   */
  async deactivateUser(userID, options) {
    return await this.post(
      this.baseURL + `/users/${encodeURIComponent(userID)}/deactivate`,
      { ...options }
    );
  }
  /**
   * deactivateUsers - Deactivate many users asynchronously
   *
   * @param {string[]} user_ids which users to deactivate
   * @param {DeactivateUsersOptions} [options]
   *
   * @return {TaskResponse} A task ID
   */
  async deactivateUsers(user_ids, options) {
    return await this.post(
      this.baseURL + `/users/deactivate`,
      { user_ids, ...options }
    );
  }
  async exportUser(userID, options) {
    return await this.get(this.baseURL + `/users/${encodeURIComponent(userID)}/export`, { ...options });
  }
  /** banUser - bans a user from all channels
   *
   * @param {string} targetUserID
   * @param {BanUserOptions} [options]
   * @returns {Promise<APIResponse>}
   */
  async banUser(targetUserID, options) {
    return await this.post(this.baseURL + "/moderation/ban", {
      target_user_id: targetUserID,
      ...options
    });
  }
  /** unbanUser - revoke global ban for a user
   *
   * @param {string} targetUserID
   * @param {UnBanUserOptions} [options]
   * @returns {Promise<APIResponse>}
   */
  async unbanUser(targetUserID, options) {
    return await this.delete(this.baseURL + "/moderation/ban", {
      target_user_id: targetUserID,
      ...options
    });
  }
  /** shadowBan - shadow bans a user from all channels
   *
   * @param {string} targetUserID
   * @param {BanUserOptions} [options]
   * @returns {Promise<APIResponse>}
   */
  async shadowBan(targetUserID, options) {
    return await this.banUser(targetUserID, {
      shadow: true,
      ...options
    });
  }
  /** removeShadowBan - revoke global shadow ban for a user
   *
   * @param {string} targetUserID
   * @param {UnBanUserOptions} [options]
   * @returns {Promise<APIResponse>}
   */
  async removeShadowBan(targetUserID, options) {
    return await this.unbanUser(targetUserID, {
      shadow: true,
      ...options
    });
  }
  async blockUser(blockedUserID, user_id) {
    const result = await this.post(this.baseURL + "/users/block", {
      blocked_user_id: blockedUserID,
      ...user_id ? { user_id } : {}
    });
    if (this._cacheEnabled()) {
      this.blockedUsers.next(({ userIds }) => ({
        userIds: userIds.concat(blockedUserID)
      }));
    }
    return result;
  }
  async getBlockedUsers(user_id) {
    const result = await this.get(
      this.baseURL + "/users/block",
      {
        ...user_id ? { user_id } : {}
      }
    );
    if (this._cacheEnabled()) {
      this.blockedUsers.partialNext({
        userIds: result.blocks.map(({ blocked_user_id }) => blocked_user_id)
      });
    }
    return result;
  }
  async unBlockUser(blockedUserID, userID) {
    const result = await this.post(this.baseURL + "/users/unblock", {
      blocked_user_id: blockedUserID,
      ...userID ? { user_id: userID } : {}
    });
    if (this._cacheEnabled()) {
      this.blockedUsers.next(({ userIds }) => ({
        userIds: userIds.filter((id) => id !== blockedUserID)
      }));
    }
    return result;
  }
  /** getSharedLocations
   *
   * @returns {Promise<ActiveLiveLocationsAPIResponse>} The server response
   *
   */
  async getSharedLocations() {
    return await this.get(
      this.baseURL + `/users/live_locations`
    );
  }
  /** muteUser - mutes a user
   *
   * @param {string} targetID
   * @param {string} [userID] Only used with serverside auth
   * @param {MuteUserOptions} [options]
   * @returns {Promise<MuteUserResponse>}
   */
  async muteUser(targetID, userID, options = {}) {
    return await this.post(this.baseURL + "/moderation/mute", {
      target_id: targetID,
      ...userID ? { user_id: userID } : {},
      ...options
    });
  }
  /** unmuteUser - unmutes a user
   *
   * @param {string} targetID
   * @param {string} [currentUserID] Only used with serverside auth
   * @returns {Promise<APIResponse>}
   */
  async unmuteUser(targetID, currentUserID) {
    return await this.post(this.baseURL + "/moderation/unmute", {
      target_id: targetID,
      ...currentUserID ? { user_id: currentUserID } : {}
    });
  }
  /** userMuteStatus - check if a user is muted or not, can be used after connectUser() is called
   *
   * @param {string} targetID
   * @returns {boolean}
   */
  userMuteStatus(targetID) {
    if (!this.user || !this.wsPromise) {
      throw new Error("Make sure to await connectUser() first.");
    }
    for (let i = 0; i < this.mutedUsers.length; i += 1) {
      if (this.mutedUsers[i].target.id === targetID) return true;
    }
    return false;
  }
  /**
   * flagMessage - flag a message
   * @param {string} targetMessageID
   * @param {string} [options.user_id] currentUserID, only used with serverside auth
   * @returns {Promise<APIResponse>}
   */
  async flagMessage(targetMessageID, options = {}) {
    return await this.post(this.baseURL + "/moderation/flag", {
      target_message_id: targetMessageID,
      ...options
    });
  }
  /**
   * flagUser - flag a user
   * @param {string} targetID
   * @param {string} [options.user_id] currentUserID, only used with serverside auth
   * @returns {Promise<APIResponse>}
   */
  async flagUser(targetID, options = {}) {
    return await this.post(this.baseURL + "/moderation/flag", {
      target_user_id: targetID,
      ...options
    });
  }
  /**
   * unflagMessage - unflag a message
   *
   * @deprecated The `/moderation/unflag` endpoint is deprecated and is a no-op on
   * the backend - calling this resolves successfully but does not remove the flag.
   * Use `client.moderation.submitAction()` against the review queue item instead.
   *
   * @param {string} targetMessageID
   * @param {string} [options.user_id] currentUserID, only used with serverside auth
   * @returns {Promise<APIResponse>}
   */
  async unflagMessage(targetMessageID, options = {}) {
    return await this.post(this.baseURL + "/moderation/unflag", {
      target_message_id: targetMessageID,
      ...options
    });
  }
  /**
   * unflagUser - unflag a user
   *
   * @deprecated The `/moderation/unflag` endpoint is deprecated and is a no-op on
   * the backend - calling this resolves successfully but does not remove the flag.
   * Use `client.moderation.submitAction()` against the review queue item instead.
   *
   * @param {string} targetID
   * @param {string} [options.user_id] currentUserID, only used with serverside auth
   * @returns {Promise<APIResponse>}
   */
  async unflagUser(targetID, options = {}) {
    return await this.post(this.baseURL + "/moderation/unflag", {
      target_user_id: targetID,
      ...options
    });
  }
  /**
   * _queryFlags - Query flags.
   *
   * Note: Do not use this.
   * It is present for internal usage only.
   * This function can, and will, break and/or be removed at any point in time.
   *
   * @private
   * @param {FlagsFilters} filterConditions MongoDB style filter conditions
   * @param {FlagsPaginationOptions} options Option object, {limit: 10, offset:0}
   *
   * @return {Promise<FlagsResponse>} Flags Response
   */
  async _queryFlags(filterConditions = {}, options = {}) {
    return await this.post(this.baseURL + "/moderation/flags", {
      filter_conditions: filterConditions,
      ...options
    });
  }
  /**
   * _queryFlagReports - Query flag reports.
   *
   * Note: Do not use this.
   * It is present for internal usage only.
   * This function can, and will, break and/or be removed at any point in time.
   *
   * @private
   * @param {FlagReportsFilters} filterConditions MongoDB style filter conditions
   * @param {FlagReportsPaginationOptions} options Option object, {limit: 10, offset:0}
   *
   * @return {Promise<FlagReportsResponse>} Flag Reports Response
   */
  async _queryFlagReports(filterConditions = {}, options = {}) {
    return await this.post(this.baseURL + "/moderation/reports", {
      filter_conditions: filterConditions,
      ...options
    });
  }
  /**
   * _reviewFlagReport - review flag report
   *
   * Note: Do not use this.
   * It is present for internal usage only.
   * This function can, and will, break and/or be removed at any point in time.
   *
   * @private
   * @param {string} [id] flag report to review
   * @param {string} [reviewResult] flag report review result
   * @param {string} [options.user_id] currentUserID, only used with serverside auth
   * @param {string} [options.review_details] custom information about review result
   * @returns {Promise<ReviewFlagReportResponse>>}
   */
  async _reviewFlagReport(id, reviewResult, options = {}) {
    return await this.patch(
      this.baseURL + `/moderation/reports/${encodeURIComponent(id)}`,
      {
        review_result: reviewResult,
        ...options
      }
    );
  }
  /**
   * unblockMessage - unblocks message blocked by automod
   *
   *
   * @param {string} targetMessageID
   * @param {string} [options.user_id] currentUserID, only used with serverside auth
   * @returns {Promise<APIResponse>}
   */
  async unblockMessage(targetMessageID, options = {}) {
    return await this.post(this.baseURL + "/moderation/unblock_message", {
      target_message_id: targetMessageID,
      ...options
    });
  }
  /**
   * markChannelsRead - marks channels read -
   * it accepts a map of cid:messageid pairs, if messageid is empty, the whole channel will be marked as read
   *
   * @param {MarkChannelsReadOptions } [data]
   *
   * @return {Promise<APIResponse>}
   */
  async markChannelsRead(data = {}) {
    await this.post(this.baseURL + "/channels/read", { ...data });
  }
  createCommand(data) {
    return this.post(this.baseURL + "/commands", data);
  }
  getCommand(name) {
    return this.get(
      this.baseURL + `/commands/${encodeURIComponent(name)}`
    );
  }
  updateCommand(name, data) {
    return this.put(
      this.baseURL + `/commands/${encodeURIComponent(name)}`,
      data
    );
  }
  deleteCommand(name) {
    return this.delete(
      this.baseURL + `/commands/${encodeURIComponent(name)}`
    );
  }
  listCommands() {
    return this.get(this.baseURL + `/commands`);
  }
  createChannelType(data) {
    const channelData = Object.assign({}, { commands: ["all"] }, data);
    return this.post(this.baseURL + "/channeltypes", channelData);
  }
  getChannelType(channelType) {
    return this.get(
      this.baseURL + `/channeltypes/${encodeURIComponent(channelType)}`
    );
  }
  updateChannelType(channelType, data) {
    return this.put(
      this.baseURL + `/channeltypes/${encodeURIComponent(channelType)}`,
      data
    );
  }
  deleteChannelType(channelType) {
    return this.delete(
      this.baseURL + `/channeltypes/${encodeURIComponent(channelType)}`
    );
  }
  listChannelTypes() {
    return this.get(this.baseURL + `/channeltypes`);
  }
  /**
   * translateMessage - adds the translation to the message
   *
   * @param {string} messageId
   * @param {string} language
   *
   * @return {MessageResponse} Response that includes the message
   */
  async translateMessage(messageId, language) {
    return await this.post(
      this.baseURL + `/messages/${encodeURIComponent(messageId)}/translate`,
      { language }
    );
  }
  /**
   * translate - translates the given text to provided language
   *
   * @param {string} text
   * @param {string} destination_language
   * @param {string} source_language
   *
   * @return {TranslateResponse} Response that includes the message
   */
  async translate(text, destination_language, source_language) {
    return await this.post(this.baseURL + `/translate`, {
      text,
      source_language,
      destination_language
    });
  }
  /**
   * _normalizeExpiration - transforms expiration value into ISO string
   * @param {undefined|null|number|string|Date} timeoutOrExpirationDate expiration date or timeout. Use number type to set timeout in seconds, string or Date to set exact expiration date
   */
  _normalizeExpiration(timeoutOrExpirationDate) {
    let pinExpires = null;
    if (typeof timeoutOrExpirationDate === "number") {
      const now = /* @__PURE__ */ new Date();
      now.setSeconds(now.getSeconds() + timeoutOrExpirationDate);
      pinExpires = now.toISOString();
    } else if (isString2(timeoutOrExpirationDate)) {
      pinExpires = timeoutOrExpirationDate;
    } else if (timeoutOrExpirationDate instanceof Date) {
      pinExpires = timeoutOrExpirationDate.toISOString();
    }
    return pinExpires;
  }
  /**
   * _messageId - extracts string message id from either message object or message id
   * @param {string | { id: string }} messageOrMessageId message object or message id
   * @param {string} errorText error message to report in case of message id absence
   */
  _validateAndGetMessageId(messageOrMessageId, errorText) {
    let messageId;
    if (typeof messageOrMessageId === "string") {
      messageId = messageOrMessageId;
    } else {
      if (!messageOrMessageId.id) {
        throw Error(errorText);
      }
      messageId = messageOrMessageId.id;
    }
    return messageId;
  }
  /**
   * pinMessage - pins the message
   * @param {string | { id: string }} messageOrMessageId message object or message id
   * @param {undefined|null|number|string|Date} timeoutOrExpirationDate expiration date or timeout. Use number type to set timeout in seconds, string or Date to set exact expiration date
   * @param {undefined|string | { id: string }} [pinnedBy] who will appear as a user who pinned a message. Only for server-side use. Provide `undefined` when pinning message client-side
   * @param {undefined|number|string|Date} pinnedAt date when message should be pinned. It affects the order of pinned messages. Use negative number to set relative time in the past, string or Date to set exact date of pin
   */
  pinMessage(messageOrMessageId, timeoutOrExpirationDate, pinnedBy, pinnedAt) {
    const messageId = this._validateAndGetMessageId(
      messageOrMessageId,
      "Please specify the message id when calling unpinMessage"
    );
    return this.partialUpdateMessage(
      messageId,
      {
        set: {
          pinned: true,
          pin_expires: this._normalizeExpiration(timeoutOrExpirationDate),
          pinned_at: this._normalizeExpiration(pinnedAt)
        }
      },
      pinnedBy
    );
  }
  /**
   * unpinMessage - unpins the message that was previously pinned
   * @param {string | { id: string }} messageOrMessageId message object or message id
   * @param {string | { id: string }} [userId]
   */
  unpinMessage(messageOrMessageId, userId) {
    const messageId = this._validateAndGetMessageId(
      messageOrMessageId,
      "Please specify the message id when calling unpinMessage"
    );
    return this.partialUpdateMessage(
      messageId,
      {
        set: { pinned: false }
      },
      userId
    );
  }
  /**
   * updateMessage - Update the given message
   *
   * @param {Omit<MessageResponse, 'mentioned_users'> & { mentioned_users?: string[] }} message object, id needs to be specified
   * @param {string | { id: string }} [partialUserOrUserId]
   * @param {boolean} [options.skip_enrich_url] Do not try to enrich the URLs within message
   *
   * @return {{ message: LocalMessage | MessageResponse }} Response that includes the message
   */
  async updateMessage(message, partialUserOrUserId, options) {
    if (!message.id) {
      throw Error("Please specify the message.id when calling updateMessage");
    }
    const messageId = message.id;
    try {
      if (this.offlineDb) {
        return await this.offlineDb.queueTask({
          task: {
            ...getPendingTaskChannelData(message.cid),
            messageId,
            payload: [message, partialUserOrUserId, options],
            type: "update-message"
          }
        });
      }
    } catch (error) {
      this.logger("error", `offlineDb:updateMessage`, {
        tags: ["channel", "offlineDb"],
        error
      });
    }
    return await this._updateMessage(message, partialUserOrUserId, options);
  }
  async _updateMessage(message, partialUserOrUserId, options) {
    if (!message.id) {
      throw Error("Please specify the message.id when calling updateMessage");
    }
    const payload = sanitizeOutgoingAttachments({
      client: this,
      message: toUpdatedMessagePayload(message)
    });
    if (typeof partialUserOrUserId === "string") {
      payload.user_id = partialUserOrUserId;
    } else if (typeof partialUserOrUserId?.id === "string") {
      payload.user_id = partialUserOrUserId.id;
    }
    return await this.post(
      this.baseURL + `/messages/${encodeURIComponent(message.id)}`,
      {
        message: payload,
        ...options
      }
    );
  }
  /**
   * partialUpdateMessage - Update the given message id while retaining additional properties
   *
   * @param {string} id the message id
   *
   * @param {PartialUpdateMessage}  partialMessageObject which should contain id and any of "set" or "unset" params;
   *         example: {id: "user1", set:{text: "hi"}, unset:["color"]}
   * @param {string | { id: string }} [userId]
   *
   * @param {boolean} [options.skip_enrich_url] Do not try to enrich the URLs within message
   *
   * @return {{ message: MessageResponse }} Response that includes the updated message
   */
  async partialUpdateMessage(id, partialMessageObject, partialUserOrUserId, options) {
    if (!id) {
      throw Error("Please specify the message.id when calling partialUpdateMessage");
    }
    let user = void 0;
    if (typeof partialUserOrUserId === "string") {
      user = { id: partialUserOrUserId };
    } else if (typeof partialUserOrUserId?.id === "string") {
      user = { id: partialUserOrUserId.id };
    }
    return await this.put(
      this.baseURL + `/messages/${encodeURIComponent(id)}`,
      {
        ...partialMessageObject,
        ...options,
        user
      }
    );
  }
  /**
   * Updates message fields without storing them in the database, only sends update event.
   *
   * Available only on the server-side.
   *
   * @param messageId the message id to update.
   * @param partialMessageObject the message payload.
   * @param partialUserOrUserId the user id linked to this action.
   * @param options additional options.
   */
  async ephemeralUpdateMessage(messageId, partialMessageObject, partialUserOrUserId, options) {
    if (!messageId) throw Error("messageId is required");
    let user = void 0;
    if (typeof partialUserOrUserId === "string") {
      user = { id: partialUserOrUserId };
    } else if (typeof partialUserOrUserId?.id === "string") {
      user = { id: partialUserOrUserId.id };
    }
    return await this.patch(
      `${this.baseURL}/messages/${encodeURIComponent(messageId)}/ephemeral`,
      {
        ...partialMessageObject,
        ...options,
        user
      }
    );
  }
  /**
   * deleteMessage - Delete a message
   *
   * @param {string} messageID The id of the message to delete
   * @param {boolean | DeleteMessageOptions | undefined} [optionsOrHardDelete]
   * @return {Promise<APIResponse & { message: MessageResponse }>} The API response
   */
  // fixme: remove the signature with optionsOrHardDelete boolean with the next major release
  async deleteMessage(messageID, optionsOrHardDelete) {
    let options = {};
    if (typeof optionsOrHardDelete === "boolean") {
      options = optionsOrHardDelete ? { hardDelete: true } : {};
    } else if (optionsOrHardDelete?.deleteForMe) {
      options = { deleteForMe: true };
    } else if (optionsOrHardDelete?.hardDelete) {
      options = { hardDelete: true };
    }
    try {
      if (this.offlineDb) {
        if (options.hardDelete) {
          await this.offlineDb.hardDeleteMessage({ id: messageID });
        } else {
          await this.offlineDb.softDeleteMessage({
            id: messageID,
            deleteForMe: options.deleteForMe
          });
        }
        return await this.offlineDb.queueTask(
          {
            task: {
              messageId: messageID,
              payload: [messageID, options],
              type: "delete-message"
            }
          }
        );
      }
    } catch (error) {
      this.logger("error", `offlineDb:deleteMessage`, {
        tags: ["channel", "offlineDb"],
        error
      });
    }
    return this._deleteMessage(messageID, options);
  }
  // fixme: remove the signature with optionsOrHardDelete boolean with the next major release
  async _deleteMessage(messageID, optionsOrHardDelete) {
    const { deleteForMe, hardDelete } = typeof optionsOrHardDelete === "boolean" ? { hardDelete: optionsOrHardDelete } : optionsOrHardDelete ?? {};
    let params = {};
    if (hardDelete) {
      params = { hard: true };
    }
    if (deleteForMe) {
      params = { ...params, delete_for_me: true };
    }
    const result = await this.delete(
      this.baseURL + `/messages/${encodeURIComponent(messageID)}`,
      params
    );
    if (deleteForMe) {
      result.message.deleted_for_me = true;
      result.message.type = "deleted";
    }
    return result;
  }
  /**
   * undeleteMessage - Undelete a message
   *
   * undeletes a message that was previous soft deleted. Hard deleted messages
   * cannot be undeleted. This is only allowed to be called from server-side
   * clients.
   *
   * @param {string} messageID The id of the message to undelete
   * @param {string} userID The id of the user who undeleted the message
   *
   * @return {{ message: MessageResponse }} Response that includes the message
   */
  async undeleteMessage(messageID, userID) {
    return await this.post(
      this.baseURL + `/messages/${encodeURIComponent(messageID)}/undelete`,
      { undeleted_by: userID }
    );
  }
  async getMessage(messageID, options) {
    return await this.get(
      this.baseURL + `/messages/${encodeURIComponent(messageID)}`,
      {
        ...options
      }
    );
  }
  /**
   * queryThreads - returns the list of threads of current user.
   *
   * @param {QueryThreadsOptions} options Options object for pagination and limiting the participants and replies.
   * @param {number}  options.limit Limits the number of threads to be returned.
   * @param {boolean} options.watch Subscribes the user to the channels of the threads.
   * @param {number}  options.participant_limit Limits the number of participants returned per threads.
   * @param {number}  options.reply_limit Limits the number of replies returned per threads.
   * @param {ThreadFilters} options.filter MongoDB style filters for threads
   * @param {ThreadSort} options.sort MongoDB style sort for threads
   *
   * @returns {{ threads: Thread[], next: string }} Returns the list of threads and the next cursor.
   */
  async queryThreads(options = {}) {
    const optionsWithDefaults = {
      limit: 10,
      participant_limit: 10,
      reply_limit: 3,
      watch: true,
      ...options
    };
    const requestBody = {
      ...optionsWithDefaults
    };
    if (optionsWithDefaults.filter && Object.keys(optionsWithDefaults.filter).length > 0) {
      requestBody.filter = optionsWithDefaults.filter;
    }
    if (optionsWithDefaults.sort && (Array.isArray(optionsWithDefaults.sort) ? optionsWithDefaults.sort.length > 0 : Object.keys(optionsWithDefaults.sort).length > 0)) {
      requestBody.sort = normalizeQuerySort(optionsWithDefaults.sort);
    }
    const response = await this.post(
      `${this.baseURL}/threads`,
      requestBody
    );
    const parentMessages = response.threads.map((thread) => thread.parent_message);
    this.polls.hydratePollCache(parentMessages);
    return {
      threads: response.threads.map(
        (thread) => new Thread({ client: this, threadData: thread })
      ),
      next: response.next
    };
  }
  /**
   * getThread - returns the thread of a message by its id.
   *
   * @param {string}            messageId The message id
   * @param {GetThreadOptions}  options Options object for pagination and limiting the participants and replies.
   * @param {boolean}           options.watch Subscribes the user to the channel of the thread.
   * @param {number}            options.participant_limit Limits the number of participants returned per threads.
   * @param {number}            options.reply_limit Limits the number of replies returned per threads.
   *
   * @returns {Thread} Returns the thread.
   */
  async getThread(messageId, options = {}) {
    if (!messageId) {
      throw new Error("Please specify the messageId when calling getThread");
    }
    const optionsWithDefaults = {
      participant_limit: 100,
      reply_limit: 3,
      watch: true,
      ...options
    };
    const response = await this.get(
      `${this.baseURL}/threads/${encodeURIComponent(messageId)}`,
      optionsWithDefaults
    );
    return new Thread({ client: this, threadData: response.thread });
  }
  /**
   * partialUpdateThread - updates the given thread
   *
   * @param {string}              messageId The id of the thread message which needs to be updated.
   * @param {PartialThreadUpdate} partialThreadObject should contain "set" or "unset" params for any of the thread's non-reserved fields.
   *
   * @returns {GetThreadAPIResponse} Returns the updated thread.
   */
  async partialUpdateThread(messageId, partialThreadObject) {
    if (!messageId) {
      throw Error("Please specify the message id when calling partialUpdateThread");
    }
    const reservedThreadFields = [
      "created_at",
      "id",
      "last_message_at",
      "type",
      "updated_at",
      "user",
      "reply_count",
      "participants",
      "channel",
      "custom"
    ];
    for (const key in { ...partialThreadObject.set, ...partialThreadObject.unset }) {
      if (reservedThreadFields.includes(key)) {
        throw Error(
          `You cannot set ${key} field on Thread object. ${key} is reserved for server-side use. Please omit ${key} from your set object.`
        );
      }
    }
    return await this.patch(
      `${this.baseURL}/threads/${encodeURIComponent(messageId)}`,
      partialThreadObject
    );
  }
  /**
   * @deprecated use sdkIdentifier instead
   * @param userAgent
   */
  setUserAgent(userAgent) {
    this.userAgent = userAgent;
  }
  _enrichAxiosOptions(options = {
    params: {},
    headers: {},
    config: {}
  }) {
    const token = this._getToken();
    const authorization = token ? { Authorization: token } : void 0;
    let signal = null;
    if (this.nextRequestAbortController !== null) {
      signal = this.nextRequestAbortController.signal;
      this.nextRequestAbortController = null;
    }
    if (!options.headers?.["x-client-request-id"]) {
      options.headers = {
        ...options.headers,
        "x-client-request-id": randomId()
      };
    }
    const {
      params: axiosRequestConfigParams,
      headers: axiosRequestConfigHeaders,
      ...axiosRequestConfigRest
    } = this.options.axiosRequestConfig || {};
    const resolvedSignal = options.config?.signal ?? axiosRequestConfigRest.signal ?? signal;
    return {
      params: {
        user_id: this.userID,
        connection_id: this._getConnectionID(),
        api_key: this.key,
        ...options.params,
        ...axiosRequestConfigParams || {}
      },
      headers: {
        ...authorization,
        "stream-auth-type": this.getAuthType(),
        "X-Stream-Client": this.getUserAgent(),
        ...options.headers,
        ...axiosRequestConfigHeaders || {}
      },
      ...options.config,
      ...axiosRequestConfigRest,
      ...resolvedSignal ? { signal: resolvedSignal } : {}
    };
  }
  _getToken() {
    if (!this.tokenManager || this.anonymous) return null;
    return this.tokenManager.getToken();
  }
  _startCleaning() {
    const that = this;
    if (this.cleaningIntervalRef != null) {
      return;
    }
    this.cleaningIntervalRef = setInterval(() => {
      for (const channel of Object.values(that.activeChannels)) {
        channel.clean();
      }
    }, 500);
  }
  /**
   * checks signature of a request
   * @param {string | Buffer} rawBody
   * @param {string} signature from HTTP header
   * @returns {boolean}
   */
  verifyWebhook(requestBody, xSignature) {
    return !!this.secret && verifySignature(requestBody, xSignature, this.secret);
  }
  /**
   * Verify and parse an HTTP webhook event.
   *
   * Decompresses `rawBody` when gzipped (detected from the body bytes),
   * verifies the `X-Signature` header against the app's API secret, and
   * returns the parsed `Event`. Works whether or not Stream is currently
   * compressing payloads for this app, and stays correct behind
   * middleware that auto-decompresses the request.
   *
   * @param rawBody Raw HTTP request body bytes Stream signed
   * @param signature Value of the `X-Signature` header
   * @throws {InvalidWebhookError} When the signature does not match or
   *   the gzip envelope is malformed.
   */
  verifyAndParseWebhook(rawBody, signature) {
    if (!this.secret) {
      throw new InvalidWebhookError(
        "cannot verify webhook signature without an API secret on the client"
      );
    }
    return verifyAndParseWebhook(rawBody, signature, this.secret);
  }
  /**
   * Parse an SQS firehose event: decodes the message `Body` (base64 +
   * optional gzip) and returns the parsed `Event`. No HMAC verification
   * (Stream does not sign SQS bodies).
   *
   * @param messageBody SQS message `Body` string
   * @throws {InvalidWebhookError} When the base64 / gzip envelope is malformed.
   */
  parseSqs(messageBody) {
    return parseSqs(messageBody);
  }
  /**
   * Parse an SNS-delivered event (unwraps envelope JSON when needed, then
   * same decode path as SQS). No HMAC verification.
   *
   * @param notificationBody Raw SNS POST body or pre-extracted `Message` string
   * @throws {InvalidWebhookError} When the envelope cannot be decoded.
   */
  parseSns(notificationBody) {
    return parseSns(notificationBody);
  }
  /** getPermission - gets the definition for a permission
   *
   * @param {string} name
   * @returns {Promise<PermissionAPIResponse>}
   */
  getPermission(name) {
    return this.get(
      `${this.baseURL}/permissions/${encodeURIComponent(name)}`
    );
  }
  /** createPermission - creates a custom permission
   *
   * @param {CustomPermissionOptions} permissionData the permission data
   * @returns {Promise<APIResponse>}
   */
  createPermission(permissionData) {
    return this.post(`${this.baseURL}/permissions`, {
      ...permissionData
    });
  }
  /** updatePermission - updates an existing custom permission
   *
   * @param {string} id
   * @param {Omit<CustomPermissionOptions, 'id'>} permissionData the permission data
   * @returns {Promise<APIResponse>}
   */
  updatePermission(id, permissionData) {
    return this.put(
      `${this.baseURL}/permissions/${encodeURIComponent(id)}`,
      {
        ...permissionData
      }
    );
  }
  /** deletePermission - deletes a custom permission
   *
   * @param {string} name
   * @returns {Promise<APIResponse>}
   */
  deletePermission(name) {
    return this.delete(
      `${this.baseURL}/permissions/${encodeURIComponent(name)}`
    );
  }
  /** listPermissions - returns the list of all permissions for this application
   *
   * @returns {Promise<APIResponse>}
   */
  listPermissions() {
    return this.get(`${this.baseURL}/permissions`);
  }
  /** createRole - creates a custom role
   *
   * @param {string} name the new role name
   * @returns {Promise<APIResponse>}
   */
  createRole(name) {
    return this.post(`${this.baseURL}/roles`, { name });
  }
  /** listRoles - returns the list of all roles for this application
   *
   * @returns {Promise<APIResponse>}
   */
  listRoles() {
    return this.get(`${this.baseURL}/roles`);
  }
  /** listRoles - returns the list of all roles for this application
   *
   * @returns {Promise<APIResponse>}
   */
  searchRoles(options, requestOptions = {}) {
    return this.get(
      `${this.baseURL}/roles/search`,
      options,
      requestOptions
    );
  }
  /** deleteRole - deletes a custom role
   *
   * @param {string} name the role name
   * @returns {Promise<APIResponse>}
   */
  deleteRole(name) {
    return this.delete(`${this.baseURL}/roles/${encodeURIComponent(name)}`);
  }
  /** sync - returns all events that happened for a list of channels since last sync
   * @param {string[]} channel_cids list of channel CIDs
   * @param {string} last_sync_at last time the user was online and in sync. RFC3339 ie. "2020-05-06T15:05:01.207Z"
   * @param {SyncOptions} options See JSDoc in the type fields for more info
   *
   * @returns {Promise<SyncResponse>}
   */
  sync(channel_cids, last_sync_at, options = {}) {
    return this.post(`${this.baseURL}/sync`, {
      channel_cids,
      last_sync_at,
      ...options
    });
  }
  /**
   * sendUserCustomEvent - Send a custom event to a user
   *
   * @param {string} targetUserID target user id
   * @param {UserCustomEvent} event for example {type: 'friendship-request'}
   *
   * @return {Promise<APIResponse>} The Server Response
   */
  async sendUserCustomEvent(targetUserID, event) {
    return await this.post(
      `${this.baseURL}/users/${encodeURIComponent(targetUserID)}/event`,
      {
        event
      }
    );
  }
  /**
   * Creates a new block list
   *
   * @param {BlockList} blockList - The block list to create
   * @param {string} blockList.name - The name of the block list
   * @param {string[]} blockList.words - List of words to block
   * @param {string} [blockList.team] - Team ID the block list belongs to
   *
   * @returns {Promise<APIResponse>} The server response
   */
  createBlockList(blockList) {
    return this.post(`${this.baseURL}/blocklists`, blockList);
  }
  /**
   * Lists all block lists
   *
   * @param {Object} [data] - Query parameters
   * @param {string} [data.team] - Team ID to filter block lists by
   *
   * @returns {Promise<APIResponse & {blocklists: BlockListResponse[]}>} Response containing array of block lists
   */
  listBlockLists(data) {
    return this.get(
      `${this.baseURL}/blocklists`,
      data
    );
  }
  /**
   * Gets a specific block list
   *
   * @param {string} name - The name of the block list to retrieve
   * @param {Object} [data] - Query parameters
   * @param {string} [data.team] - Team ID that blocklist belongs to
   *
   * @returns {Promise<APIResponse & {blocklist: BlockListResponse}>} Response containing the block list
   */
  getBlockList(name, data) {
    return this.get(
      `${this.baseURL}/blocklists/${encodeURIComponent(name)}`,
      data
    );
  }
  /**
   * Updates an existing block list
   *
   * @param {string} name - The name of the block list to update
   * @param {Object} data - The update data
   * @param {string[]} data.words - New list of words to block
   * @param {string} [data.team] - Team ID that blocklist belongs to
   *
   * @returns {Promise<APIResponse>} The server response
   */
  updateBlockList(name, data) {
    return this.put(
      `${this.baseURL}/blocklists/${encodeURIComponent(name)}`,
      data
    );
  }
  /**
   * Deletes a block list
   *
   * @param {string} name - The name of the block list to delete
   * @param {Object} [data] - Query parameters
   * @param {string} [data.team] - Team ID that blocklist belongs to
   *
   * @returns {Promise<APIResponse>} The server response
   */
  deleteBlockList(name, data) {
    return this.delete(
      `${this.baseURL}/blocklists/${encodeURIComponent(name)}`,
      data
    );
  }
  exportChannels(request, options = {}) {
    const payload = { channels: request, ...options };
    return this.post(
      `${this.baseURL}/export_channels`,
      payload
    );
  }
  exportUsers(request) {
    return this.post(
      `${this.baseURL}/export/users`,
      request
    );
  }
  exportChannel(request, options) {
    return this.exportChannels([request], options);
  }
  getExportChannelStatus(id) {
    return this.get(
      `${this.baseURL}/export_channels/${encodeURIComponent(id)}`
    );
  }
  campaign(idOrData, data) {
    if (idOrData && typeof idOrData === "object") {
      return new Campaign(this, null, idOrData);
    }
    return new Campaign(this, idOrData, data);
  }
  /**
   * channelBatchUpdater - Returns a ChannelBatchUpdater instance for batch channel operations
   *
   * @return {ChannelBatchUpdater} A ChannelBatchUpdater instance
   */
  channelBatchUpdater() {
    return new ChannelBatchUpdater(this);
  }
  segment(type, idOrData, data) {
    if (typeof idOrData === "string") {
      return new Segment(this, type, idOrData, data);
    }
    return new Segment(this, type, null, idOrData);
  }
  validateServerSideAuth() {
    if (!this.secret) {
      throw new Error(
        "This feature can be used server-side only. Please initialize the client with a secret to use this feature."
      );
    }
  }
  /**
   * createSegment - Creates a segment
   *
   * @private
   * @param {SegmentType} type Segment type
   * @param {string} id Segment ID
   * @param {string} name Segment name
   * @param {SegmentData} params Segment data
   *
   * @return {{segment: SegmentResponse} & APIResponse} The created Segment
   */
  createSegment(type, id, data) {
    this.validateServerSideAuth();
    const body = {
      id,
      type,
      ...data
    };
    return this.post(this.baseURL + `/segments`, body);
  }
  /**
   * createUserSegment - Creates a user segment
   *
   * @param {string} id Segment ID
   * @param {string} name Segment name
   * @param {SegmentData} data Segment data
   *
   * @return {Segment} The created Segment
   */
  createUserSegment(id, data) {
    this.validateServerSideAuth();
    return this.createSegment("user", id, data);
  }
  /**
   * createChannelSegment - Creates a channel segment
   *
   * @param {string} id Segment ID
   * @param {string} name Segment name
   * @param {SegmentData} data Segment data
   *
   * @return {Segment} The created Segment
   */
  createChannelSegment(id, data) {
    this.validateServerSideAuth();
    return this.createSegment("channel", id, data);
  }
  getSegment(id) {
    this.validateServerSideAuth();
    return this.get(
      this.baseURL + `/segments/${encodeURIComponent(id)}`
    );
  }
  /**
   * updateSegment - Update a segment
   *
   * @param {string} id Segment ID
   * @param {Partial<UpdateSegmentData>} data Data to update
   *
   * @return {Segment} Updated Segment
   */
  updateSegment(id, data) {
    this.validateServerSideAuth();
    return this.put(
      this.baseURL + `/segments/${encodeURIComponent(id)}`,
      data
    );
  }
  /**
   * addSegmentTargets - Add targets to a segment
   *
   * @param {string} id Segment ID
   * @param {string[]} targets Targets to add to the segment
   *
   * @return {APIResponse} API response
   */
  addSegmentTargets(id, targets) {
    this.validateServerSideAuth();
    const body = { target_ids: targets };
    return this.post(
      this.baseURL + `/segments/${encodeURIComponent(id)}/addtargets`,
      body
    );
  }
  querySegmentTargets(id, filter = {}, sort = [], options = {}) {
    this.validateServerSideAuth();
    return this.post(
      this.baseURL + `/segments/${encodeURIComponent(id)}/targets/query`,
      {
        filter: filter || {},
        sort: sort || [],
        ...options
      }
    );
  }
  /**
   * removeSegmentTargets - Remove targets from a segment
   *
   * @param {string} id Segment ID
   * @param {string[]} targets Targets to add to the segment
   *
   * @return {APIResponse} API response
   */
  removeSegmentTargets(id, targets) {
    this.validateServerSideAuth();
    const body = { target_ids: targets };
    return this.post(
      this.baseURL + `/segments/${encodeURIComponent(id)}/deletetargets`,
      body
    );
  }
  /**
   * querySegments - Query Segments
   *
   * @param {filter} filter MongoDB style filter conditions
   * @param {QuerySegmentsOptions} options Options for sorting/paginating the results
   *
   * @return {Segment[]} Segments
   */
  querySegments(filter, sort, options = {}) {
    this.validateServerSideAuth();
    return this.post(this.baseURL + `/segments/query`, {
      filter,
      sort,
      ...options
    });
  }
  /**
   * deleteSegment - Delete a Campaign Segment
   *
   * @param {string} id Segment ID
   *
   * @return {Promise<APIResponse>} The Server Response
   */
  deleteSegment(id) {
    this.validateServerSideAuth();
    return this.delete(this.baseURL + `/segments/${encodeURIComponent(id)}`);
  }
  /**
   * segmentTargetExists - Check if a target exists in a segment
   *
   * @param {string} segmentId Segment ID
   * @param {string} targetId Target ID
   *
   * @return {Promise<APIResponse>} The Server Response
   */
  segmentTargetExists(segmentId, targetId) {
    this.validateServerSideAuth();
    return this.get(
      this.baseURL + `/segments/${encodeURIComponent(segmentId)}/target/${encodeURIComponent(targetId)}`
    );
  }
  /**
   * createCampaign - Creates a Campaign
   *
   * @param {CampaignData} params Campaign data
   *
   * @return {Campaign} The Created Campaign
   */
  createCampaign(params) {
    this.validateServerSideAuth();
    return this.post(this.baseURL + `/campaigns`, { ...params });
  }
  getCampaign(id, options) {
    this.validateServerSideAuth();
    return this.get(this.baseURL + `/campaigns/${encodeURIComponent(id)}`, { ...options?.users });
  }
  startCampaign(id, options) {
    this.validateServerSideAuth();
    return this.post(this.baseURL + `/campaigns/${encodeURIComponent(id)}/start`, {
      scheduled_for: options?.scheduledFor,
      stop_at: options?.stopAt
    });
  }
  /**
   * queryCampaigns - Query Campaigns
   *
   *
   * @return {Campaign[]} Campaigns
   */
  async queryCampaigns(filter, sort, options) {
    this.validateServerSideAuth();
    return await this.post(this.baseURL + `/campaigns/query`, {
      filter,
      sort,
      ...options || {}
    });
  }
  /**
   * updateCampaign - Update a Campaign
   *
   * @param {string} id Campaign ID
   * @param {Partial<CampaignData>} params Campaign data
   *
   * @return {Campaign} Updated Campaign
   */
  updateCampaign(id, params) {
    this.validateServerSideAuth();
    return this.put(this.baseURL + `/campaigns/${encodeURIComponent(id)}`, params);
  }
  /**
   * deleteCampaign - Delete a Campaign
   *
   * @param {string} id Campaign ID
   *
   * @return {Promise<APIResponse>} The Server Response
   */
  deleteCampaign(id) {
    this.validateServerSideAuth();
    return this.delete(
      this.baseURL + `/campaigns/${encodeURIComponent(id)}`
    );
  }
  /**
   * stopCampaign - Stop a Campaign
   *
   * @param {string} id Campaign ID
   *
   * @return {Campaign} Stopped Campaign
   */
  stopCampaign(id) {
    this.validateServerSideAuth();
    return this.post(
      this.baseURL + `/campaigns/${encodeURIComponent(id)}/stop`
    );
  }
  /**
   * enrichURL - Get OpenGraph data of the given link
   *
   * @param {string} url link
   * @return {OGAttachment} OG Attachment
   */
  enrichURL(url) {
    return this.get(this.baseURL + `/og`, { url });
  }
  /**
   * getTask - Gets status of a long running task
   *
   * @param {string} id Task ID
   *
   * @return {TaskStatus} The task status
   */
  getTask(id) {
    return this.get(
      `${this.baseURL}/tasks/${encodeURIComponent(id)}`
    );
  }
  /**
   * deleteChannels - Deletes a list of channel
   *
   * @param {string[]} cids Channel CIDs
   * @param {boolean} [options.hard_delete] Defines if the channel is hard deleted or not
   *
   * @return {DeleteChannelsResponse} Result of the soft deletion, if server-side, it holds the task ID as well
   */
  async deleteChannels(cids, options = {}) {
    return await this.post(
      this.baseURL + `/channels/delete`,
      {
        cids,
        ...options
      }
    );
  }
  /**
   * deleteUsers - Batch Delete Users
   *
   * @param {string[]} user_ids which users to delete
   * @param {DeleteUserOptions} options Configuration how to delete users
   *
   * @return {TaskResponse} A task ID
   */
  async deleteUsers(user_ids, options = {}) {
    if (typeof options.user !== "undefined" && !["soft", "hard", "pruning"].includes(options.user)) {
      throw new Error(
        "Invalid delete user options. user must be one of [soft hard pruning]"
      );
    }
    if (typeof options.conversations !== "undefined" && !["soft", "hard"].includes(options.conversations)) {
      throw new Error(
        "Invalid delete user options. conversations must be one of [soft hard]"
      );
    }
    if (typeof options.messages !== "undefined" && !["soft", "hard", "pruning"].includes(options.messages)) {
      throw new Error(
        "Invalid delete user options. messages must be one of [soft hard pruning]"
      );
    }
    return await this.post(this.baseURL + `/users/delete`, {
      user_ids,
      ...options
    });
  }
  /**
   * _createImportURL - Create an Import upload url.
   *
   * Note: Do not use this.
   * It is present for internal usage only.
   * This function can, and will, break and/or be removed at any point in time.
   *
   * @private
   * @param {string} filename filename of uploaded data
   * @return {APIResponse & CreateImportResponse} An ImportTask
   */
  async _createImportURL(filename) {
    return await this.post(
      this.baseURL + `/import_urls`,
      {
        filename
      }
    );
  }
  /**
   * _createImport - Create an Import Task.
   *
   * Note: Do not use this.
   * It is present for internal usage only.
   * This function can, and will, break and/or be removed at any point in time.
   *
   * @private
   * @param {string} path path of uploaded data
   * @param {CreateImportOptions} options import options
   * @return {APIResponse & CreateImportResponse} An ImportTask
   */
  async _createImport(path, options = { mode: "upsert" }) {
    return await this.post(
      this.baseURL + `/imports`,
      {
        path,
        ...options
      }
    );
  }
  /**
   * _getImport - Get an Import Task.
   *
   * Note: Do not use this.
   * It is present for internal usage only.
   * This function can, and will, break and/or be removed at any point in time.
   *
   * @private
   * @param {string} id id of Import Task
   *
   * @return {APIResponse & GetImportResponse} An ImportTask
   */
  async _getImport(id) {
    return await this.get(
      this.baseURL + `/imports/${encodeURIComponent(id)}`
    );
  }
  /**
   * _listImports - Lists Import Tasks.
   *
   * Note: Do not use this.
   * It is present for internal usage only.
   * This function can, and will, break and/or be removed at any point in time.
   *
   * @private
   * @param {ListImportsPaginationOptions} options pagination options
   *
   * @return {APIResponse & ListImportsResponse} An ImportTask
   */
  async _listImports(options) {
    return await this.get(
      this.baseURL + `/imports`,
      options
    );
  }
  /**
   * upsertPushProvider - Create or Update a push provider
   *
   * Note: Works only for v2 push version is enabled on app settings.
   *
   * @param {PushProviderConfig} configuration of the provider you want to create or update
   *
   * @return {APIResponse & PushProviderUpsertResponse} A push provider
   */
  async upsertPushProvider(pushProvider) {
    return await this.post(
      this.baseURL + `/push_providers`,
      {
        push_provider: pushProvider
      }
    );
  }
  /**
   * deletePushProvider - Delete a push provider
   *
   * Note: Works only for v2 push version is enabled on app settings.
   *
   * @param {PushProviderID} type and foreign id of the push provider to be deleted
   *
   * @return {APIResponse} An API response
   */
  async deletePushProvider({ type, name }) {
    return await this.delete(
      this.baseURL + `/push_providers/${encodeURIComponent(type)}/${encodeURIComponent(name)}`
    );
  }
  /**
   * listPushProviders - Get all push providers in the app
   *
   * Note: Works only for v2 push version is enabled on app settings.
   *
   * @return {APIResponse & PushProviderListResponse} A push provider
   */
  async listPushProviders() {
    return await this.get(
      this.baseURL + `/push_providers`
    );
  }
  /**
   * creates an abort controller that will be used by the next HTTP Request.
   */
  createAbortControllerForNextRequest() {
    return this.nextRequestAbortController = new AbortController();
  }
  /**
   * commits a pending message, making it visible in the channel and for other users
   * @param id the message id
   *
   * @return {APIResponse & MessageResponse} The message
   */
  async commitMessage(id) {
    return await this.post(
      this.baseURL + `/messages/${encodeURIComponent(id)}/commit`
    );
  }
  /**
   * Creates a poll
   * @param poll PollData The poll that will be created
   * @param userId string The user id (only serverside)
   * @returns {APIResponse & CreatePollAPIResponse} The poll
   */
  async createPoll(poll, userId) {
    return await this.post(this.baseURL + `/polls`, {
      ...poll,
      ...userId ? { user_id: userId } : {}
    });
  }
  /**
   * Retrieves a poll
   * @param id string The poll id
   *  @param userId string The user id (only serverside)
   * @returns {APIResponse & GetPollAPIResponse} The poll
   */
  async getPoll(id, userId) {
    return await this.get(
      this.baseURL + `/polls/${encodeURIComponent(id)}`,
      userId ? { user_id: userId } : {}
    );
  }
  /**
   * Updates a poll
   * @param poll PollData The poll that will be updated
   * @param userId string The user id (only serverside)
   * @returns {APIResponse & PollResponse} The poll
   */
  async updatePoll(poll, userId) {
    return await this.put(this.baseURL + `/polls`, {
      ...poll,
      ...userId ? { user_id: userId } : {}
    });
  }
  /**
   * Partially updates a poll
   * @param id string The poll id
   * @param {PartialPollUpdate} partialPollObject which should contain id and any of "set" or "unset" params;
   * @param userId string The user id (only serverside)
   * example: {id: "44f26af5-f2be-4fa7-9dac-71cf893781de", set:{field: value}, unset:["field2"]}
   * @returns {APIResponse & UpdatePollAPIResponse} The poll
   */
  async partialUpdatePoll(id, partialPollObject, userId) {
    return await this.patch(
      this.baseURL + `/polls/${encodeURIComponent(id)}`,
      {
        ...partialPollObject,
        ...userId ? { user_id: userId } : {}
      }
    );
  }
  /**
   * Delete a poll
   * @param id string The poll id
   * @param userId string The user id (only serverside)
   * @returns
   */
  async deletePoll(id, userId) {
    return await this.delete(
      this.baseURL + `/polls/${encodeURIComponent(id)}`,
      {
        ...userId ? { user_id: userId } : {}
      }
    );
  }
  /**
   * Close a poll
   * @param id string The poll id
   * @param userId string The user id (only serverside)
   * @returns {APIResponse & UpdatePollAPIResponse} The poll
   */
  closePoll(id, userId) {
    return this.partialUpdatePoll(
      id,
      {
        set: {
          is_closed: true
        }
      },
      userId
    );
  }
  /**
   * Creates a poll option
   * @param pollId string The poll id
   * @param option PollOptionData The poll option that will be created
   * @param userId string The user id (only serverside)
   * @returns {APIResponse & PollOptionResponse} The poll option
   */
  async createPollOption(pollId, option, userId) {
    return await this.post(
      this.baseURL + `/polls/${encodeURIComponent(pollId)}/options`,
      {
        ...option,
        ...userId ? { user_id: userId } : {}
      }
    );
  }
  /**
   * Retrieves a poll option
   * @param pollId string The poll id
   * @param optionId string The poll option id
   * @param userId string The user id (only serverside)
   * @returns {APIResponse & PollOptionResponse} The poll option
   */
  async getPollOption(pollId, optionId, userId) {
    return await this.get(
      this.baseURL + `/polls/${encodeURIComponent(pollId)}/options/${encodeURIComponent(optionId)}`,
      userId ? { user_id: userId } : {}
    );
  }
  /**
   * Updates a poll option
   * @param pollId string The poll id
   * @param option PollOptionData The poll option that will be updated
   * @param userId string The user id (only serverside)
   * @returns
   */
  async updatePollOption(pollId, option, userId) {
    return await this.put(
      this.baseURL + `/polls/${encodeURIComponent(pollId)}/options`,
      {
        ...option,
        ...userId ? { user_id: userId } : {}
      }
    );
  }
  /**
   * Delete a poll option
   * @param pollId string The poll id
   * @param optionId string The poll option id
   * @param userId string The user id (only serverside)
   * @returns {APIResponse} The poll option
   */
  async deletePollOption(pollId, optionId, userId) {
    return await this.delete(
      this.baseURL + `/polls/${encodeURIComponent(pollId)}/options/${encodeURIComponent(optionId)}`,
      userId ? { user_id: userId } : {}
    );
  }
  /**
   * Cast vote on a poll
   * @param messageId string The message id
   * @param pollId string The poll id
   * @param vote PollVoteData The vote that will be casted
   * @param userId string The user id (only serverside)
   * @returns {APIResponse & CastVoteAPIResponse} The poll vote
   */
  async castPollVote(messageId, pollId, vote, userId) {
    return await this.post(
      this.baseURL + `/messages/${encodeURIComponent(messageId)}/polls/${encodeURIComponent(pollId)}/vote`,
      {
        vote,
        ...userId ? { user_id: userId } : {}
      }
    );
  }
  /**
   * Add a poll answer
   * @param messageId string The message id
   * @param pollId string The poll id
   * @param answerText string The answer text
   * @param userId string The user id (only serverside)
   */
  addPollAnswer(messageId, pollId, answerText, userId) {
    return this.castPollVote(
      messageId,
      pollId,
      {
        answer_text: answerText
      },
      userId
    );
  }
  async removePollVote(messageId, pollId, voteId, userId) {
    return await this.delete(
      this.baseURL + `/messages/${encodeURIComponent(messageId)}/polls/${encodeURIComponent(pollId)}/vote/${encodeURIComponent(
        voteId
      )}`,
      {
        ...userId ? { user_id: userId } : {}
      }
    );
  }
  /**
   * Queries polls
   * @param filter
   * @param sort
   * @param options Option object, {limit: 10, offset:0}
   * @param userId string The user id (only serverside)
   * @returns {APIResponse & QueryPollsResponse} The polls
   */
  async queryPolls(filter = {}, sort = [], options = {}, userId) {
    const q = userId ? `?user_id=${userId}` : "";
    return await this.post(
      this.baseURL + `/polls/query${q}`,
      {
        filter,
        sort: normalizeQuerySort(sort),
        ...options
      }
    );
  }
  /**
   * Queries poll votes
   * @param pollId
   * @param filter
   * @param sort
   * @param options Option object, {limit: 10, offset:0}
   * @param userId string The user id (only serverside)
   * @returns {APIResponse & PollVotesAPIResponse} The poll votes
   */
  async queryPollVotes(pollId, filter = {}, sort = [], options = {}, userId) {
    const q = userId ? `?user_id=${userId}` : "";
    return await this.post(
      this.baseURL + `/polls/${encodeURIComponent(pollId)}/votes${q}`,
      {
        filter,
        sort: normalizeQuerySort(sort),
        ...options
      }
    );
  }
  /**
   * Queries poll answers
   * @param pollId
   * @param filter
   * @param sort
   * @param options Option object, {limit: 10, offset:0}
   * @param userId string The user id (only serverside)
   * @returns {APIResponse & PollAnswersAPIResponse} The poll votes
   */
  async queryPollAnswers(pollId, filter = {}, sort = [], options = {}, userId) {
    const q = userId ? `?user_id=${userId}` : "";
    return await this.post(
      this.baseURL + `/polls/${encodeURIComponent(pollId)}/votes${q}`,
      {
        filter: { ...filter, is_answer: true },
        sort: normalizeQuerySort(sort),
        ...options
      }
    );
  }
  /**
   * Query message history
   * @param filter
   * @param sort
   * @param options Option object, {limit: 10}
   * @returns {APIResponse & QueryMessageHistoryResponse} The message histories
   */
  async queryMessageHistory(filter = {}, sort = [], options = {}) {
    return await this.post(
      this.baseURL + "/messages/history",
      {
        filter,
        sort: normalizeQuerySort(sort),
        ...options
      }
    );
  }
  /**
   * updateFlags - reviews/unflags flagged message
   *
   * @param {string[]} message_ids list of message IDs
   * @param {string} options Option object in case user ID is set to review all the flagged messages by the user
   * @param {string} reviewed_by user ID who reviewed the flagged message
   * @returns {APIResponse}
   */
  async updateFlags(message_ids, reviewed_by, options = {}) {
    return await this.post(
      this.baseURL + "/automod/v1/moderation/update_flags",
      {
        message_ids,
        reviewed_by,
        ...options
      }
    );
  }
  /**
   * queryDrafts - Queries drafts for the current user
   *
   * @param {object} [options] Query options
   * @param {object} [options.filter] Filters for the query
   * @param {number} [options.sort] Sort parameters
   * @param {number} [options.limit] Limit the number of results
   * @param {string} [options.next] Pagination parameter
   * @param {string} [options.prev] Pagination parameter
   * @param {string} [options.user_id] Has to be provided when called server-side
   *
   * @return {Promise<APIResponse & { drafts: DraftResponse[]; next?: string }>} Response containing the drafts
   */
  async queryDrafts(options = {}) {
    const payload = {
      ...options,
      sort: options.sort ? normalizeQuerySort(options.sort) : void 0
    };
    return await this.post(this.baseURL + "/drafts/query", payload);
  }
  /**
   * createReminder - Creates a reminder for a message
   *
   * @param {CreateReminderOptions} options The options for creating the reminder
   * @returns {Promise<ReminderAPIResponse>}
   */
  async createReminder({ messageId, ...options }) {
    return await this.post(
      `${this.baseURL}/messages/${messageId}/reminders`,
      options
    );
  }
  /**
   * updateReminder - Updates an existing reminder for a message
   *
   * @param {UpdateReminderOptions} options The options for updating the reminder
   * @returns {Promise<ReminderAPIResponse>}
   */
  async updateReminder({ messageId, ...options }) {
    return await this.patch(
      `${this.baseURL}/messages/${messageId}/reminders`,
      options
    );
  }
  /**
   * deleteReminder - Deletes a reminder for a message
   *
   * @param {string} messageId The ID of the message whose reminder to delete
   * @param {string} [userId] Optional user ID, required for server-side operations
   * @returns {Promise<APIResponse>}
   */
  async deleteReminder(messageId, userId) {
    return await this.delete(
      `${this.baseURL}/messages/${messageId}/reminders`,
      userId ? { user_id: userId } : {}
    );
  }
  /**
   * queryReminders - Queries reminders based on given filters
   *
   * @param {QueryRemindersOptions} options The options for querying reminders
   * @returns {Promise<QueryRemindersResponse>}
   */
  async queryReminders({ filter, sort, ...rest } = {}) {
    return await this.post(`${this.baseURL}/reminders/query`, {
      filter,
      sort: sort && normalizeQuerySort(sort),
      ...rest
    });
  }
  /**
   * queryTeamUsageStats - Queries team-level usage statistics from the warehouse database
   *
   * Returns all 16 metrics grouped by team with cursor-based pagination.
   *
   * Date Range Options (mutually exclusive):
   * - Use 'month' parameter (YYYY-MM format) for monthly aggregated values
   * - Use 'start_date'/'end_date' parameters (YYYY-MM-DD format) for daily breakdown
   * - If neither provided, defaults to current month (monthly mode)
   *
   * This endpoint is server-side only.
   *
   * @param {QueryTeamUsageStatsOptions} options The options for querying team usage stats
   * @returns {Promise<QueryTeamUsageStatsResponse>}
   */
  async queryTeamUsageStats(options = {}) {
    return await this.post(
      `${this.baseURL}/stats/team_usage`,
      options
    );
  }
  /**
   * updateLocation - Updates a location
   *
   * @param location SharedLocationRequest the location data to update
   *
   * @returns {Promise<SharedLocationResponse>} The server response
   */
  async updateLocation(location) {
    return await this.put(
      this.baseURL + `/users/live_locations`,
      location
    );
  }
  /**
   * uploadFile - Uploads a file to the configured storage (defaults to Stream CDN)
   *
   * @param {string|NodeJS.ReadableStream|Buffer|File} uri The file to upload
   * @param {string} [name] The name of the file
   * @param {string} [contentType] The content type of the file
   * @param {UserResponse} [user] Optional user information
   * @param {AxiosRequestConfig} [axiosRequestConfig] Optional axios config (e.g. onUploadProgress for progress tracking)
   *
   * @return {Promise<SendFileAPIResponse>} Response containing the file URL
   */
  uploadFile(uri, name, contentType, user, axiosRequestConfig) {
    return this.sendFile(
      `${this.baseURL}/uploads/file`,
      uri,
      name,
      contentType,
      user,
      axiosRequestConfig
    );
  }
  /**
   * uploadImage - Uploads an image to the configured storage (defaults to Stream CDN)
   *
   * @param {string|NodeJS.ReadableStream|File} uri The image to upload
   * @param {string} [name] The name of the image
   * @param {string} [contentType] The content type of the image
   * @param {UserResponse} [user] Optional user information
   * @param {AxiosRequestConfig} [axiosRequestConfig] Optional axios config (e.g. onUploadProgress for progress tracking)
   *
   * @return {Promise<SendFileAPIResponse>} Response containing the image URL
   */
  uploadImage(uri, name, contentType, user, axiosRequestConfig) {
    return this.sendFile(
      `${this.baseURL}/uploads/image`,
      uri,
      name,
      contentType,
      user,
      axiosRequestConfig
    );
  }
  /**
   * deleteFile - Deletes a file from the configured storage
   *
   * @param {string} url The URL of the file to delete
   *
   * @return {Promise<APIResponse>} The server response
   */
  deleteFile(url) {
    return this.delete(`${this.baseURL}/uploads/file`, { url });
  }
  /**
   * deleteImage - Deletes an image from the configured storage
   *
   * @param {string} url The URL of the image to delete
   *
   * @return {Promise<APIResponse>} The server response
   */
  deleteImage(url) {
    return this.delete(`${this.baseURL}/uploads/image`, { url });
  }
  /**
   * Mark the channels delivered for the given messages and the user
   *
   * @param {MarkDeliveredOptions} data
   * @return {Promise<EventAPIResponse | void>} Description
   */
  async markChannelsDelivered(data) {
    if (!data?.latest_delivered_messages?.length) return;
    return await this.post(this.baseURL + "/channels/delivered", data);
  }
  syncDeliveredCandidates(collections) {
    this.messageDeliveryReporter.syncDeliveredCandidates(collections);
  }
  /**
   *  Update Channels Batch
   *
   *  @param {UpdateChannelsBatchOptions} payload for updating channels in batch
   *  @return {Promise<APIResponse & UpdateChannelsBatchResponse>} The server response
   */
  async updateChannelsBatch(payload) {
    return await this.put(
      this.baseURL + `/channels/batch`,
      payload
    );
  }
  /**
   * createPredefinedFilter - Creates a new predefined filter (server-side only)
   *
   * @param {CreatePredefinedFilterOptions} options Predefined filter options
   *
   * @return {Promise<PredefinedFilterResponse>} The created predefined filter
   */
  async createPredefinedFilter(options) {
    this.validateServerSideAuth();
    return await this.post(
      `${this.baseURL}/predefined_filters`,
      options
    );
  }
  /**
   * getPredefinedFilter - Gets a predefined filter by name (server-side only)
   *
   * @param {string} name Predefined filter name
   *
   * @return {Promise<PredefinedFilterResponse>} The predefined filter
   */
  async getPredefinedFilter(name) {
    this.validateServerSideAuth();
    return await this.get(
      `${this.baseURL}/predefined_filters/${encodeURIComponent(name)}`
    );
  }
  /**
   * updatePredefinedFilter - Updates a predefined filter (server-side only)
   *
   * @param {string} name Predefined filter name
   * @param {UpdatePredefinedFilterOptions} options Predefined filter options
   *
   * @return {Promise<PredefinedFilterResponse>} The updated predefined filter
   */
  async updatePredefinedFilter(name, options) {
    this.validateServerSideAuth();
    return await this.put(
      `${this.baseURL}/predefined_filters/${encodeURIComponent(name)}`,
      options
    );
  }
  /**
   * deletePredefinedFilter - Deletes a predefined filter (server-side only)
   *
   * @param {string} name Predefined filter name
   *
   * @return {Promise<APIResponse>} The server response
   */
  async deletePredefinedFilter(name) {
    this.validateServerSideAuth();
    return await this.delete(
      `${this.baseURL}/predefined_filters/${encodeURIComponent(name)}`
    );
  }
  /**
   * listPredefinedFilters - Lists all predefined filters (server-side only)
   *
   * @param {ListPredefinedFiltersOptions} options Query options
   *
   * @return {Promise<ListPredefinedFiltersResponse>} The list of predefined filters
   */
  async listPredefinedFilters(options = {}) {
    this.validateServerSideAuth();
    const { sort, ...paginationOptions } = options;
    return await this.get(
      `${this.baseURL}/predefined_filters`,
      {
        ...paginationOptions,
        ...sort ? { sort: JSON.stringify(sort) } : {}
      }
    );
  }
  /**
   * setRetentionPolicy - Creates or updates a retention policy for the app.
   * Server-side only.
   *
   * @param {string} policy The policy type ('old-messages' or 'inactive-channels')
   * @param {number} maxAgeHours Max age in hours (24-43800)
   * @returns {Promise<SetRetentionPolicyResponse>}
   */
  async setRetentionPolicy(policy, maxAgeHours) {
    this.validateServerSideAuth();
    return await this.post(
      this.baseURL + "/retention_policy",
      { policy, max_age_hours: maxAgeHours }
    );
  }
  /**
   * deleteRetentionPolicy - Deletes a retention policy for the app.
   * Server-side only.
   *
   * @param {string} policy The policy type ('old-messages' or 'inactive-channels')
   * @returns {Promise<DeleteRetentionPolicyResponse>}
   */
  async deleteRetentionPolicy(policy) {
    this.validateServerSideAuth();
    return await this.post(
      this.baseURL + "/retention_policy/delete",
      { policy }
    );
  }
  /**
   * getRetentionPolicy - Returns all retention policies configured for the app.
   * Server-side only.
   *
   * @returns {Promise<GetRetentionPolicyResponse>}
   */
  async getRetentionPolicy() {
    this.validateServerSideAuth();
    return await this.get(this.baseURL + "/retention_policy");
  }
  /**
   * getRetentionPolicyRuns - Returns filtered and sorted retention cleanup run history.
   * Supports filter_conditions on 'policy' and 'date' fields.
   * Server-side only.
   *
   * @param {GetRetentionPolicyRunsOptions} options Filter, sort, and pagination options
   * @returns {Promise<GetRetentionPolicyRunsResponse>}
   */
  async getRetentionPolicyRuns(options = {}) {
    this.validateServerSideAuth();
    return await this.post(
      this.baseURL + "/retention_policy/runs",
      options
    );
  }
};

// src/events.ts
var EVENT_MAP = {
  "channel.created": true,
  "channel.deleted": true,
  "channel.hidden": true,
  "channel.kicked": true,
  "channel.muted": true,
  "channel.truncated": true,
  "channel.unmuted": true,
  "channel.updated": true,
  "channel.visible": true,
  "draft.deleted": true,
  "draft.updated": true,
  "health.check": true,
  "member.added": true,
  "member.removed": true,
  "member.updated": true,
  "message.deleted": true,
  "message.new": true,
  "message.read": true,
  "message.updated": true,
  "message.undeleted": true,
  "notification.added_to_channel": true,
  "notification.channel_deleted": true,
  "message.delivered": true,
  "notification.channel_mutes_updated": true,
  "notification.channel_truncated": true,
  "notification.invite_accepted": true,
  "notification.invite_rejected": true,
  "notification.invited": true,
  "notification.mark_read": true,
  "notification.mark_unread": true,
  "notification.message_new": true,
  "notification.mutes_updated": true,
  "notification.reminder_due": true,
  "notification.removed_from_channel": true,
  "notification.thread_message_new": true,
  "poll.closed": true,
  "poll.updated": true,
  "poll.vote_casted": true,
  "poll.vote_changed": true,
  "poll.vote_removed": true,
  "reaction.deleted": true,
  "reaction.new": true,
  "reaction.updated": true,
  "reminder.created": true,
  "reminder.deleted": true,
  "reminder.updated": true,
  "thread.updated": true,
  "typing.start": true,
  "typing.stop": true,
  "user.banned": true,
  "user.deleted": true,
  "user.messages.deleted": true,
  "user.presence.changed": true,
  "user.unbanned": true,
  "user.unread_message_reminder": true,
  "user.updated": true,
  "user.watching.start": true,
  "user.watching.stop": true,
  // AI events
  "ai_indicator.update": true,
  "ai_indicator.stop": true,
  "ai_indicator.clear": true,
  // local events
  "message.read_locally": true,
  "channels.queried": true,
  "offline_reactions.queried": true,
  "connection.changed": true,
  "connection.recovered": true,
  "transport.changed": true,
  "capabilities.changed": true,
  "live_location_sharing.started": true,
  "live_location_sharing.stopped": true
};

// src/permissions.ts
var Allow = "Allow";
var Deny = "Deny";
var AnyResource = ["*"];
var AnyRole = ["*"];
var MaxPriority = 999;
var MinPriority = 1;
var Permission = class {
  constructor(name, priority, resources = AnyResource, roles = AnyRole, owner = false, action = Allow) {
    this.name = name;
    this.action = action;
    this.owner = owner;
    this.priority = priority;
    this.resources = resources;
    this.roles = roles;
  }
};
var AllowAll = new Permission(
  "Allow all",
  MaxPriority,
  AnyResource,
  AnyRole,
  false,
  Allow
);
var DenyAll = new Permission(
  "Deny all",
  MinPriority,
  AnyResource,
  AnyRole,
  false,
  Deny
);
var BuiltinRoles = {
  Admin: "admin",
  Anonymous: "anonymous",
  ChannelMember: "channel_member",
  ChannelModerator: "channel_moderator",
  Guest: "guest",
  User: "user"
};
var BuiltinPermissions = {
  AddLinks: "Add Links",
  BanUser: "Ban User",
  CreateChannel: "Create Channel",
  CreateMessage: "Create Message",
  CreateReaction: "Create Reaction",
  DeleteAnyAttachment: "Delete Any Attachment",
  DeleteAnyChannel: "Delete Any Channel",
  DeleteAnyMessage: "Delete Any Message",
  DeleteAnyReaction: "Delete Any Reaction",
  DeleteOwnAttachment: "Delete Own Attachment",
  DeleteOwnChannel: "Delete Own Channel",
  DeleteOwnMessage: "Delete Own Message",
  DeleteOwnReaction: "Delete Own Reaction",
  ReadAnyChannel: "Read Any Channel",
  ReadOwnChannel: "Read Own Channel",
  RunMessageAction: "Run Message Action",
  UpdateAnyChannel: "Update Any Channel",
  UpdateAnyMessage: "Update Any Message",
  UpdateMembersAnyChannel: "Update Members Any Channel",
  UpdateMembersOwnChannel: "Update Members Own Channel",
  UpdateOwnChannel: "Update Own Channel",
  UpdateOwnMessage: "Update Own Message",
  UploadAttachment: "Upload Attachment",
  UseFrozenChannel: "Send messages and reactions to frozen channels"
};

// src/offline-support/types.ts
var OfflineError = class extends Error {
  constructor(message, {
    type
  }) {
    super(message);
    this.name = "OfflineError";
    this.type = type;
  }
  // Vitest helper (serialized errors are too large to read)
  // https://github.com/vitest-dev/vitest/blob/v3.1.3/packages/utils/src/error.ts#L60-L62
  toJSON() {
    return {
      message: `${this.type} - ${this.message}`,
      stack: this.stack,
      name: this.name
    };
  }
};

// src/offline-support/offline_sync_manager.ts
var import_axios5 = require("axios");
var OfflineDBSyncManager = class {
  constructor({
    client,
    offlineDb,
    syncMaxEventCount
  }) {
    this.syncStatus = false;
    this.connectionChangedListener = null;
    this.syncStatusListeners = [];
    this.scheduledSyncStatusCallbacks = /* @__PURE__ */ new Map();
    /**
     * Initializes the sync manager. Should only be called once per session.
     *
     * Cleans up old listeners if re-initialized to avoid memory leaks.
     * Starts syncing immediately if already connected, otherwise waits for reconnection.
     */
    this.init = async () => {
      try {
        if (this.client.user?.id && this.client.wsConnection?.isHealthy) {
          await this.syncAndExecutePendingTasks();
          await this.invokeSyncStatusListeners(true);
        }
        if (this.connectionChangedListener) {
          this.connectionChangedListener.unsubscribe();
        }
        this.connectionChangedListener = this.client.on(
          "connection.changed",
          async (event) => {
            if (event.online) {
              await this.syncAndExecutePendingTasks();
              await this.invokeSyncStatusListeners(true);
            } else {
              await this.invokeSyncStatusListeners(false);
            }
          }
        );
      } catch (error) {
        console.log("Error in DBSyncManager.init: ", error);
      }
    };
    /**
     * Registers a listener that is called whenever the sync status changes.
     *
     * @param listener - A callback invoked with the new sync status (`true` or `false`).
     * @returns An object with an `unsubscribe` function to remove the listener.
     */
    this.onSyncStatusChange = (listener) => {
      this.syncStatusListeners.push(listener);
      return {
        unsubscribe: () => {
          this.syncStatusListeners = this.syncStatusListeners.filter(
            (el) => el !== listener
          );
        }
      };
    };
    /**
     * Schedules a one-time callback to be invoked after the next successful sync.
     *
     * @param tag - A unique key to identify and manage the callback.
     * @param callback - An async function to run after sync.
     */
    this.scheduleSyncStatusChangeCallback = (tag, callback) => {
      this.scheduledSyncStatusCallbacks.set(tag, callback);
    };
    /**
     * Invokes all registered sync status listeners and executes any scheduled sync callbacks.
     *
     * @param status - The new sync status (`true` or `false`).
     */
    this.invokeSyncStatusListeners = async (status) => {
      this.syncStatus = status;
      this.syncStatusListeners.forEach((l) => {
        try {
          l(status);
        } catch (error) {
          console.log("Error in a sync status listener.", error);
        }
      });
      if (status) {
        const promises = Array.from(this.scheduledSyncStatusCallbacks.values()).map(
          async (cb) => {
            try {
              await cb();
            } catch (error) {
              console.log("Error executing a scheduled sync status callback.", error);
            }
          }
        );
        await Promise.all(promises);
        this.scheduledSyncStatusCallbacks.clear();
      }
    };
    /**
     * Performs synchronization with the Stream backend.
     *
     * This includes downloading events since the last sync, updating the local DB,
     * and handling sync failures (e.g., if syncing beyond the allowed retention window).
     */
    this.sync = async () => {
      if (!this.client?.user) {
        return;
      }
      try {
        const cids = await this.offlineDb.getAllChannelCids();
        if (cids.length === 0) {
          return;
        }
        const lastSyncedAt = await this.offlineDb.getLastSyncedAt({
          userId: this.client.user.id
        });
        if (lastSyncedAt) {
          const lastSyncedAtDate = new Date(lastSyncedAt);
          const nowDate = /* @__PURE__ */ new Date();
          const diff = Math.floor(
            (nowDate.getTime() - lastSyncedAtDate.getTime()) / (1e3 * 60 * 60 * 24)
          );
          if (diff > 30) {
            await this.offlineDb.resetDB();
          } else {
            const result = await this.client.sync(cids, lastSyncedAtDate.toISOString());
            const { syncMaxEventCount } = this;
            const exceedsLimit = typeof syncMaxEventCount === "number" && syncMaxEventCount > 0 && result.events.length > syncMaxEventCount;
            if (exceedsLimit) {
              this.client.logger(
                "warn",
                `Skipping sync event replay: received ${result.events.length} events, which exceeds the configured limit of ${syncMaxEventCount}. Visible channels are refreshed on reconnect instead.`,
                { tags: ["sync", "offline"] }
              );
            } else {
              const queryPromises = result.events.map(
                (event) => this.offlineDb.handleEvent({ event, execute: false })
              );
              const queriesArray = await Promise.all(queryPromises);
              const queries = queriesArray.flat();
              if (queries.length) {
                await this.offlineDb.executeSqlBatch(queries);
              }
            }
          }
        }
        await this.offlineDb.upsertUserSyncStatus({
          userId: this.client.user.id,
          lastSyncedAt: (/* @__PURE__ */ new Date()).toString()
        });
      } catch (e) {
        console.log("An error has occurred while syncing the DB.", e);
        if ((0, import_axios5.isAxiosError)(e) && e.code === "ECONNABORTED") {
          return;
        }
        const error = e;
        if (error.response?.data?.code === 23) {
          return;
        }
        await this.offlineDb.resetDB();
      }
    };
    /**
     * Executes any tasks that were queued while offline and then performs a sync.
     *
     * Each step is isolated so a failure in one does not prevent the other, and
     * neither can escape to the callers (init + the connection.changed handler).
     * This guarantees the subsequent invokeSyncStatusListeners(true) always runs,
     * so syncStatus recovers to true and gated channel queries are unblocked.
     * Failed syncs degrade to "possibly stale data until the next query" rather
     * than freezing all future queries. See issue #1816.
     */
    this.syncAndExecutePendingTasks = async () => {
      try {
        await this.offlineDb.executePendingTasks();
      } catch (error) {
        console.log("Error executing pending tasks during sync.", error);
      }
      try {
        await this.sync();
      } catch (error) {
        console.log("Error while syncing the DB.", error);
      }
    };
    this.client = client;
    this.offlineDb = offlineDb;
    this.syncMaxEventCount = syncMaxEventCount;
  }
};

// src/offline-support/offline_support_api.ts
var AbstractOfflineDB = class {
  constructor({
    client,
    syncMaxEventCount
  }) {
    /**
     * Initializes the DB as well as its syncManager for a given userId.
     * It will update the DBs reactive state with initialization values.
     * @param userId - the user ID for which we want to initialize
     */
    this.init = async (userId) => {
      try {
        if (!this.shouldInitialize(userId)) {
          const initialized = await this.initializeDB();
          if (initialized) {
            await this.syncManager.init();
            this.state.partialNext({ initialized: true, userId });
          } else {
            this.state.partialNext({ initialized: false });
          }
        }
      } catch (error) {
        this.state.partialNext({ initialized: false, userId: void 0 });
        console.log("Error Initializing DB:", error);
      }
    };
    /**
     * A utility method used to execute a query in a detached manner. The callback
     * passed uses a reference to the DB itself and will handle errors gracefully
     * and silently. Only really meant to be used for write queries that need to
     * be run in synchronous functions.
     * @param queryCallback - a callback wrapping all query logic that is to be executed
     * @param method - a utility parameter used for proper logging (will make sure the method
     * is logged on failure)
     */
    this.executeQuerySafely = (queryCallback, { method }) => {
      const { initialized } = this.state.getLatestValue();
      if (!initialized) {
        return;
      }
      runDetached(queryCallback(this), { context: `OfflineDB(${method})` });
    };
    /**
     * A utility method used to guard a certain DB query with the possible non-existance
     * of a channel inside of the DB. If the channel we want to guard against does not exist
     * in the DB yet, it will try to:
     *
     * 1. Use the channel from the WS event
     * 2. Use the channel from state
     *
     * and upsert the channels in the DB.
     *
     * If both fail, it will not execute the query as it would result in a foreign key constraint
     * error.
     *
     * @param event - the WS event we are trying to process
     * @param execute - whether to immediately execute the operation.
     * @param forceUpdate - whether to upsert the channel data anyway
     * @param createQueries - a callback function to creation of the queries that we want to execute
     */
    this.queriesWithChannelGuard = async ({
      event,
      execute = true,
      forceUpdate = false
    }, createQueries) => {
      const channelFromEvent = event.channel;
      const cid = event.cid || channelFromEvent?.cid;
      const type = event.type;
      if (!cid) {
        return await createQueries(execute);
      }
      const shouldUpsertChannelData = forceUpdate || !await this.channelExists({ cid });
      if (shouldUpsertChannelData) {
        let channelData = channelFromEvent;
        if (!channelData && event.channel_type && event.channel_id) {
          const channelFromState = this.client.channel(
            event.channel_type,
            event.channel_id
          );
          if (channelFromState.initialized && !channelFromState.disconnected) {
            channelData = channelFromState.data;
          }
        }
        if (channelData) {
          const channelQuery = await this.upsertChannelData({
            channel: channelData,
            execute: false
          });
          if (channelQuery) {
            const createdQueries = await createQueries(false);
            const newQueries = [...channelQuery, ...createdQueries];
            if (execute) {
              await this.executeSqlBatch(newQueries);
            }
            return newQueries;
          } else {
            console.warn(
              `Couldn't create channel queries on ${type} event for an initialized channel that is not in DB, skipping event`,
              { event }
            );
            return [];
          }
        } else {
          console.warn(
            `Received ${type} event for a non initialized channel that is not in DB, skipping event`,
            { event }
          );
          return [];
        }
      }
      return await createQueries(execute);
    };
    /**
     * Handles a message.new event. Will always use a channel guard for the inner queries
     * and it is going to make sure that both messages and reads are upserted. It will not
     * try to fetch the reads from the DB first and it will rely on channel.state to handle
     * the number of unreads.
     * @param event - the WS event we are trying to process
     * @param execute - whether to immediately execute the operation.
     */
    this.handleNewMessage = async ({
      event,
      execute = true
    }) => {
      const client = this.client;
      const { cid, message, user } = event;
      if (!message || message.parent_id && !message.show_in_channel) {
        return [];
      }
      const finalQueries = await this.queriesWithChannelGuard(
        { event, execute },
        async () => {
          let queries = await this.upsertMessages({
            execute: false,
            messages: [message]
          });
          if (cid && client.user && client.user.id !== user?.id) {
            const userId = client.user.id;
            const channel = client.activeChannels[cid];
            const tracksReadLocally = channelTracksReadLocally(channel);
            if (channel && (channelHasReadEvents(channel) || tracksReadLocally)) {
              const ownReads = channel.state.read[userId];
              const unreadCount = channel.countUnread();
              const upsertReadsQueries = await this.upsertReads({
                cid,
                execute: false,
                reads: [
                  {
                    last_read: (ownReads?.last_read ?? /* @__PURE__ */ new Date(0)).toISOString(),
                    last_read_message_id: ownReads?.last_read_message_id,
                    unread_messages: unreadCount,
                    user: client.user
                  }
                ]
              });
              queries = [...queries, ...upsertReadsQueries];
            }
          }
          return queries;
        }
      );
      if (execute) {
        await this.executeSqlBatch(finalQueries);
      }
      return finalQueries;
    };
    /**
     * A handler for message deletion. It provides a channel guard and determines whether
     * it should hard delete or soft delete the message.
     * @param event - the WS event we are trying to process
     * @param execute - whether to immediately execute the operation.
     */
    this.handleDeleteMessage = async ({
      event,
      execute = true
    }) => {
      const { message, deleted_for_me, hard_delete = false } = event;
      if (message) {
        const deleteMethod = hard_delete ? this.hardDeleteMessage : this.softDeleteMessage;
        return await this.queriesWithChannelGuard(
          { event, execute },
          async (executeOverride) => await deleteMethod({
            id: message.id,
            deleteForMe: deleted_for_me,
            execute: executeOverride
          })
        );
      }
      return [];
    };
    /**
     * A utility method used for removing a message that has already failed from the
     * state as well as the DB. We want to drop all pending tasks and finally hard
     * delete the message from the DB.
     * @param messageId - the message id of the message we want to remove
     * @param execute - whether to immediately execute the operation.
     */
    this.handleRemoveMessage = async ({
      messageId,
      execute = true
    }) => {
      const dropPendingTasksQueries = await this.dropPendingTasks({
        messageId,
        execute: false
      });
      const hardDeleteMessageQueries = await this.hardDeleteMessage({
        id: messageId,
        execute: false
      });
      const queries = [...dropPendingTasksQueries, ...hardDeleteMessageQueries];
      if (execute) {
        await this.executeSqlBatch(queries);
      }
      return queries;
    };
    /**
     * A utility method to handle read events. It will calculate the state of the reads if
     * present in the event, or optionally rely on the hard override in unreadMessages.
     * The unreadMessages argument is useful for cases where we know the exact number of unreads
     * (for example reading an entire channel), but `unread_messages` might not necessarily exist
     * in the event (or it exists with a stale value if we know what we want to ultimately update to).
     * @param event - the WS event we are trying to process
     * @param unreadMessages - an override of unread_messages that will be preferred when upserting reads
     * @param execute - whether to immediately execute the operation.
     */
    this.handleRead = async ({
      event,
      unreadMessages,
      execute = true
    }) => {
      const {
        received_at: last_read,
        last_read_message_id,
        unread_messages = 0,
        user,
        cid
      } = event;
      const overriddenUnreadMessages = unreadMessages ?? unread_messages;
      if (user?.id && cid) {
        return await this.queriesWithChannelGuard(
          { event, execute },
          (executeOverride) => this.upsertReads({
            cid,
            execute: executeOverride,
            reads: [
              {
                last_read,
                last_read_message_id,
                unread_messages: overriddenUnreadMessages,
                user
              }
            ]
          })
        );
      }
      return [];
    };
    /**
     * A utility method used to handle member events. It guards the processing
     * of each event with a channel guard and also forces an update of member_count
     * for the respective channel if applicable.
     * @param event - the WS event we are trying to process
     * @param execute - whether to immediately execute the operation.
     */
    this.handleMemberEvent = async ({
      event,
      execute = true
    }) => {
      const { member, cid, type } = event;
      if (member && cid) {
        return await this.queriesWithChannelGuard(
          { event, execute, forceUpdate: true },
          async (executeOverride) => {
            if (type === "member.removed") {
              return await this.deleteMember({ member, cid, execute: executeOverride });
            }
            return await this.upsertMembers({
              cid,
              members: [member],
              execute: executeOverride
            });
          }
        );
      }
      return [];
    };
    /**
     * A utility method used to handle message.updated events. It guards each
     * event handler within a channel guard.
     * @param event - the WS event we are trying to process
     * @param execute - whether to immediately execute the operation.
     */
    this.handleMessageUpdatedEvent = async ({
      event,
      execute = true
    }) => {
      const { message } = event;
      if (message && !message.parent_id) {
        return await this.queriesWithChannelGuard(
          { event, execute },
          async (executeOverride) => await this.updateMessage({ message, execute: executeOverride })
        );
      }
      return [];
    };
    /**
     * An event handler for channel.visible and channel.hidden events. We need a separate
     * handler because event.channel.hidden does not arrive with the baseline event, so a
     * simple upsertion is not enough.
     * It will update the hidden property of a channel to true if handling the `channel.hidden`
     * event and to false if handling `channel.visible`.
     * @param event - the WS event we are trying to process
     * @param execute - whether to immediately execute the operation.
     */
    this.handleChannelVisibilityEvent = async ({
      event,
      execute = true
    }) => {
      const { type, channel } = event;
      if (channel && type) {
        const hidden = type === "channel.hidden";
        return await this.upsertChannelData({
          channel: { ...channel, hidden },
          execute
        });
      }
      return [];
    };
    /**
     * A utility handler used to handle channel.truncated events. It handles both
     * removing all messages and relying on truncated_at as well. It will also upsert
     * reads adequately (and calculate the correct unread messages when truncating).
     * @param event - the WS event we are trying to process
     * @param execute - whether to immediately execute the operation.
     */
    this.handleChannelTruncatedEvent = async ({
      event,
      execute = true
    }) => {
      const { channel } = event;
      const ownUser = this.client.user;
      if (channel && ownUser) {
        const { cid, truncated_at } = channel;
        const truncateQueries = await this.deleteMessagesForChannel({
          cid,
          truncated_at,
          execute: false
        });
        let finalQueries = [...truncateQueries];
        const userId = ownUser.id;
        const activeChannel = this.client.activeChannels[cid];
        const tracksReadLocally = channelTracksReadLocally(activeChannel);
        if (activeChannel && (channelHasReadEvents(activeChannel) || tracksReadLocally)) {
          const ownReads = activeChannel.state.read[userId];
          let unreadCount = 0;
          if (truncated_at) {
            unreadCount = activeChannel.countUnread(new Date(truncated_at));
          }
          const upsertReadQueries = await this.upsertReads({
            cid,
            execute: false,
            reads: [
              {
                last_read: (ownReads?.last_read ?? /* @__PURE__ */ new Date(0)).toString(),
                last_read_message_id: ownReads?.last_read_message_id,
                unread_messages: unreadCount,
                user: ownUser
              }
            ]
          });
          finalQueries = [...finalQueries, ...upsertReadQueries];
        }
        if (execute) {
          await this.executeSqlBatch(finalQueries);
        }
        return finalQueries;
      }
      return [];
    };
    /**
     * A utility handler for all reaction events. It wraps the inner queries
     * within a channel guard and maps them like so:
     * - reaction.new -> insertReaction
     * - reaction.updated -> updateReaction
     * - reaction.deleted -> deleteReaction
     * @param event - the WS event we are trying to process
     * @param execute - whether to immediately execute the operation.
     */
    this.handleReactionEvent = async ({
      event,
      execute = true
    }) => {
      const { type, message, reaction } = event;
      if (!(message && reaction)) {
        return [];
      }
      const getReactionMethod = (type2) => {
        switch (type2) {
          case "reaction.new":
            return this.insertReaction;
          case "reaction.deleted":
            return this.deleteReaction;
          case "reaction.updated":
            return this.updateReaction;
          default:
            throw new Error(
              `You are trying to handle a non-reaction event (${type2}) through the reaction DB api.`
            );
        }
      };
      const reactionMethod = getReactionMethod(type);
      return await this.queriesWithChannelGuard(
        { event, execute },
        (executeOverride) => reactionMethod({ message, reaction, execute: executeOverride })
      );
    };
    /**
     * A utility handler for all draft events:
     * - draft.updated -> updateDraft
     * - draft.deleted -> deleteDraft
     * @param event - the WS event we are trying to process
     * @param execute - whether to immediately execute the operation.
     */
    this.handleDraftEvent = async ({
      event,
      execute = true
    }) => {
      const { cid, draft, type } = event;
      if (!draft) return [];
      if (type === "draft.updated") {
        return await this.upsertDraft({
          draft,
          execute
        });
      }
      if (type === "draft.deleted") {
        if (!cid) return [];
        return await this.deleteDraft({
          cid,
          parent_id: draft.parent_id,
          execute
        });
      }
      return [];
    };
    /**
     * A generic event handler that decides which DB API to invoke based on
     * event.type for all events we are currently handling. It is used to both
     * react on WS events as well as process the sync API events.
     * @param event - the WS event we are trying to process
     * @param execute - whether to immediately execute the operation.
     */
    this.handleEvent = async ({
      event,
      execute = true
    }) => {
      const { type, channel } = event;
      if (type.startsWith("reaction")) {
        return await this.handleReactionEvent({ event, execute });
      }
      if (type === "message.new") {
        return await this.handleNewMessage({ event, execute });
      }
      if (type === "message.deleted") {
        return await this.handleDeleteMessage({ event, execute });
      }
      if (type === "message.updated" || type === "message.undeleted") {
        return this.handleMessageUpdatedEvent({ event, execute });
      }
      if (type === "message.read" || type === "notification.mark_read") {
        if (event.thread) return [];
        return this.handleRead({ event, unreadMessages: 0, execute });
      }
      if (type === "message.read_locally") {
        const localChannel = event.cid ? this.client.activeChannels[event.cid] : void 0;
        if (channelTracksReadLocally(localChannel)) {
          return this.handleRead({ event, unreadMessages: 0, execute });
        }
        return [];
      }
      if (type === "notification.mark_unread") {
        return this.handleRead({ event, execute });
      }
      if (type.startsWith("member.")) {
        return await this.handleMemberEvent({ event, execute });
      }
      if (type === "channel.hidden" || type === "channel.visible") {
        return await this.handleChannelVisibilityEvent({ event, execute });
      }
      if (type === "draft.updated" || type === "draft.deleted") {
        return await this.handleDraftEvent({ event, execute });
      }
      if ((type === "channel.updated" || type === "notification.message_new" || type === "notification.added_to_channel") && channel) {
        return await this.upsertChannelData({ channel, execute });
      }
      if ((type === "channel.deleted" || type === "notification.channel_deleted" || type === "notification.removed_from_channel") && channel) {
        return await this.deleteChannel({ cid: channel.cid, execute });
      }
      if (type === "channel.truncated") {
        return await this.handleChannelTruncatedEvent({ event, execute });
      }
      return [];
    };
    /**
     * A method used to enqueue a pending task if the execution of it fails.
     * It will try to do the following:
     *
     * 1. Execute the task immediately
     * 2. If this fails, checks if the failure was due to something valid for a pending task
     * 3. If it is, it will insert the task in the pending tasks table
     *
     * It will return the response from the execution if it succeeded.
     * @param task - the pending task we want to execute
     */
    this.queueTask = async ({ task }) => {
      const attemptTaskExecution = async () => {
        if (!this.client.wsConnection?.isHealthy) {
          throw new OfflineError(
            "Cannot execute task because the connection has been lost.",
            { type: "connection:lost" }
          );
        }
        return await this.executeTask({ task });
      };
      try {
        return await attemptTaskExecution();
      } catch (e) {
        if (!this.shouldSkipQueueingTask(e)) {
          await this.handleAddPendingTask({ task });
        }
        throw e;
      }
    };
    /**
     * A utility method that determines if a failed task should be added to the
     * queue based on its error.
     * Error code 4 - bad request data
     * Error code 17 - missing own_capabilities to execute the task
     * @param error
     */
    this.shouldSkipQueueingTask = (error) => error?.response?.data?.code === 4 || error?.response?.data?.code === 17;
    this.mergeFailedMessageUpdateIntoPendingSendMessage = ({
      editedMessage,
      pendingMessage
    }) => {
      const normalizedEditedMessageSource = {
        ...editedMessage
      };
      if (editedMessage.status === "failed") {
        delete normalizedEditedMessageSource.message_text_updated_at;
      }
      const normalizedEditedMessage = localMessageToNewMessagePayload(
        normalizedEditedMessageSource
      );
      const pendingMessageStatus = pendingMessage.status;
      return {
        ...pendingMessage,
        ...normalizedEditedMessage,
        ...typeof pendingMessageStatus !== "undefined" ? { status: pendingMessageStatus } : {}
      };
    };
    this.isPendingSendMessageTask = (task) => task.type === "send-message";
    this.handleOfflineFailedUpdateMessagePendingTask = async (task) => {
      const [message] = task.payload;
      if (!message.id) {
        return;
      }
      const pendingTasks = await this.getPendingTasks({ messageId: message.id });
      const pendingSendMessageTask = pendingTasks.find(this.isPendingSendMessageTask);
      if (!pendingSendMessageTask) {
        return;
      }
      const updatedPendingSendMessage = this.mergeFailedMessageUpdateIntoPendingSendMessage(
        {
          editedMessage: message,
          pendingMessage: pendingSendMessageTask.payload[0]
        }
      );
      const updatedPendingTask = {
        ...pendingSendMessageTask,
        payload: [updatedPendingSendMessage, pendingSendMessageTask.payload[1]]
      };
      if (pendingSendMessageTask.id) {
        await this.updatePendingTask({
          id: pendingSendMessageTask.id,
          task: updatedPendingTask
        });
        return;
      }
      await this.addPendingTask({
        ...updatedPendingTask,
        id: void 0
      });
    };
    /**
     * Central ingress for persisting pending tasks. It either stores the task as-is
     * or rewrites an existing pending `send-message` task for offline edits of failed messages.
     */
    this.handleAddPendingTask = async ({ task }) => {
      if (task.type === "update-message" && !isMessageUpdateReplayable(task.payload[0])) {
        return;
      }
      if (task.type === "update-message" && !this.client.wsConnection?.isHealthy && task.payload[0].status === "failed") {
        await this.handleOfflineFailedUpdateMessagePendingTask(task);
        return;
      }
      await this.addPendingTask(task);
    };
    /**
     * Executes a task from the list of supported pending tasks. Currently supported pending tasks
     * are:
     * - Updating a message
     * - Deleting a message
     * - Sending a reaction
     * - Removing a reaction
     * - Sending a message
     * - Creating a draft
     * - Deleting a draft
     * It will throw if we try to execute a pending task that is not supported.
     * @param task - The task we want to execute
     * @param isPendingTask - a control value telling us if it's an actual pending task being executed
     * or delayed execution
     */
    this.executeTask = async ({ task }, isPendingTask = false) => {
      if (task.type === "update-message") {
        return await this.client._updateMessage(...task.payload);
      }
      if (task.type === "delete-message") {
        return await this.client._deleteMessage(...task.payload);
      }
      const { channelType, channelId } = task;
      if (channelType && channelId) {
        const channel = this.client.channel(channelType, channelId);
        if (task.type === "send-reaction") {
          return await channel._sendReaction(...task.payload);
        }
        if (task.type === "delete-reaction") {
          return await channel._deleteReaction(...task.payload);
        }
        if (task.type === "create-draft") {
          return await channel._createDraft(...task.payload);
        }
        if (task.type === "delete-draft") {
          return await channel._deleteDraft(...task.payload);
        }
        if (task.type === "send-message") {
          const newMessageResponse = await channel._sendMessage(...task.payload);
          const newMessage = newMessageResponse?.message;
          if (isPendingTask && newMessage) {
            if (newMessage?.parent_id) {
              this.client.threads.threadsById[newMessage.parent_id]?.upsertReplyLocally({
                message: newMessage,
                timestampChanged: true
              });
            }
            channel.state.addMessageSorted(newMessage, true);
          }
          return newMessageResponse;
        }
      }
      throw new Error(
        `Tried to execute invalid pending task type (${task.type}) while synchronizing the database.`
      );
    };
    /**
     * A utility method used to execute all pending tasks. As each task succeeds execution,
     * it is going to be removed from the DB. If the execution failed due to a valid reason
     * it is going to remove the pending task from the DB even if execution fails, otherwise
     * it will keep it for the next time we try to execute all pending taks.
     */
    this.executePendingTasks = async () => {
      const queue = await this.getPendingTasks();
      for (const task of queue) {
        if (!task.id) {
          continue;
        }
        try {
          await this.executeTask(
            {
              task
            },
            true
          );
        } catch (e) {
          const error = e;
          if (!this.shouldSkipQueueingTask(error)) {
            continue;
          }
        }
        await this.deletePendingTask({
          id: task.id
        });
      }
    };
    this.client = client;
    this.syncManager = new OfflineDBSyncManager({
      client,
      offlineDb: this,
      syncMaxEventCount
    });
    this.state = new StateStore({
      initialized: false,
      userId: this.client.userID
    });
  }
  /**
   * Checks whether the DB should be initialized or if it has been initialized already.
   * @param {string} userId - the user ID for which we want to check initialization
   */
  shouldInitialize(userId) {
    const { userId: userIdFromState, initialized } = this.state.getLatestValue();
    return userId === userIdFromState && initialized;
  }
};

// src/LiveLocationManager.ts
var isExpiredLocation = (location) => {
  const endTimeTimestamp = new Date(location.end_at).getTime();
  return endTimeTimestamp < Date.now();
};
function isValidLiveLocationMessage(message) {
  if (!message || message.type === "deleted" || !message.shared_location?.end_at)
    return false;
  return !isExpiredLocation(message.shared_location);
}
var UPDATE_LIVE_LOCATION_REQUEST_MIN_THROTTLE_TIMEOUT = 3e3;
var _LiveLocationManager = class _LiveLocationManager extends WithSubscriptions {
  constructor({
    client,
    getDeviceId,
    watchLocation
  }) {
    if (!client.userID) {
      throw new Error("Live-location sharing is reserved for client-side use only");
    }
    super();
    this.registerSubscriptions = () => {
      this.incrementRefCount();
      if (this.hasSubscriptions) return;
      this.addUnsubscribeFunction(this.subscribeLiveLocationSharingUpdates());
      this.addUnsubscribeFunction(this.subscribeTargetMessagesChange());
    };
    this.unregisterSubscriptions = () => super.unregisterSubscriptions();
    this.client = client;
    this.state = new StateStore({
      messages: /* @__PURE__ */ new Map(),
      ready: false
    });
    this._deviceId = getDeviceId();
    this.getDeviceId = getDeviceId;
    this.watchLocation = watchLocation;
  }
  async init() {
    await this.assureStateInit();
    this.registerSubscriptions();
  }
  get messages() {
    return this.state.getLatestValue().messages;
  }
  get stateIsReady() {
    return this.state.getLatestValue().ready;
  }
  get deviceId() {
    if (!this._deviceId) {
      this._deviceId = this.getDeviceId();
    }
    return this._deviceId;
  }
  async assureStateInit() {
    if (this.stateIsReady) return;
    const { active_live_locations } = await this.client.getSharedLocations();
    this.state.next({
      messages: new Map(
        active_live_locations.filter((location) => !isExpiredLocation(location)).map((location) => [
          location.message_id,
          {
            ...location,
            stopSharingTimeout: setTimeout(
              () => {
                this.unregisterMessages([location.message_id]);
              },
              new Date(location.end_at).getTime() - Date.now()
            )
          }
        ])
      ),
      ready: true
    });
  }
  subscribeTargetMessagesChange() {
    let unsubscribeWatchLocation = null;
    const unsubscribe = this.state.subscribeWithSelector(
      ({ messages }) => ({ messages }),
      ({ messages }) => {
        if (!messages.size) {
          unsubscribeWatchLocation?.();
          unsubscribeWatchLocation = null;
        } else if (messages.size && !unsubscribeWatchLocation) {
          unsubscribeWatchLocation = this.subscribeWatchLocation();
        }
      }
    );
    return () => {
      unsubscribe();
      unsubscribeWatchLocation?.();
    };
  }
  subscribeWatchLocation() {
    let nextAllowedUpdateCallTimestamp = Date.now();
    const unsubscribe = this.watchLocation(({ latitude, longitude }) => {
      if (Date.now() < nextAllowedUpdateCallTimestamp) return;
      nextAllowedUpdateCallTimestamp = Date.now() + UPDATE_LIVE_LOCATION_REQUEST_MIN_THROTTLE_TIMEOUT;
      withCancellation(_LiveLocationManager.symbol, async () => {
        const promises = [];
        await this.assureStateInit();
        const expiredLocations = [];
        for (const [messageId, location] of this.messages) {
          if (isExpiredLocation(location)) {
            expiredLocations.push(location.message_id);
            continue;
          }
          if (location.latitude === latitude && location.longitude === longitude)
            continue;
          const promise = this.client.updateLocation({
            created_by_device_id: location.created_by_device_id,
            message_id: messageId,
            latitude,
            longitude
          });
          promises.push(promise);
        }
        this.unregisterMessages(expiredLocations);
        if (promises.length > 0) {
          await Promise.allSettled(promises);
        }
      });
    });
    return unsubscribe;
  }
  subscribeLiveLocationSharingUpdates() {
    const subscriptions = [
      ...[
        "live_location_sharing.started",
        "message.updated",
        "message.deleted"
      ].map(
        (eventType) => this.client.on(eventType, (event) => {
          if (!event.message) return;
          if (event.type === "live_location_sharing.started") {
            this.registerMessage(event.message);
          } else if (event.type === "message.updated") {
            const isRegistered = this.messages.has(event.message.id);
            if (isRegistered && !isValidLiveLocationMessage(event.message)) {
              this.unregisterMessages([event.message.id]);
            }
            this.registerMessage(event.message);
          } else {
            this.unregisterMessages([event.message.id]);
          }
        })
      ),
      this.client.on("live_location_sharing.stopped", (event) => {
        if (!event.live_location) return;
        this.unregisterMessages([event.live_location?.message_id]);
      })
    ];
    return () => subscriptions.forEach((subscription) => subscription.unsubscribe());
  }
  registerMessage(message) {
    if (!this.client.userID || message?.user?.id !== this.client.userID || !isValidLiveLocationMessage(message))
      return;
    this.state.next((currentValue) => {
      const messages = new Map(currentValue.messages);
      messages.set(message.id, {
        ...message.shared_location,
        stopSharingTimeout: setTimeout(
          () => {
            this.unregisterMessages([message.id]);
          },
          new Date(message.shared_location.end_at).getTime() - Date.now()
        )
      });
      return {
        ...currentValue,
        messages
      };
    });
  }
  unregisterMessages(messageIds) {
    const messages = this.messages;
    const removedMessages = new Set(messageIds);
    const newMessages = new Map(
      Array.from(messages).filter(([messageId, location]) => {
        if (removedMessages.has(messageId) && location.stopSharingTimeout) {
          clearTimeout(location.stopSharingTimeout);
          location.stopSharingTimeout = null;
        }
        return !removedMessages.has(messageId);
      })
    );
    if (newMessages.size === messages.size) return;
    this.state.partialNext({
      messages: newMessages
    });
  }
};
_LiveLocationManager.symbol = Symbol(_LiveLocationManager.name);
var LiveLocationManager = _LiveLocationManager;
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
  AbstractOfflineDB,
  Allow,
  AllowAll,
  AnyResource,
  AnyRole,
  AttachmentManager,
  AttachmentPostUploadMiddlewareExecutor,
  AttachmentPreUploadMiddlewareExecutor,
  BasePaginator,
  BaseSearchSource,
  BaseSearchSourceSync,
  BuiltinPermissions,
  BuiltinRoles,
  Campaign,
  Channel,
  ChannelBatchUpdater,
  ChannelManager,
  ChannelMemberSearchSource,
  ChannelSearchSource,
  ChannelState,
  CheckSignature,
  ClientState,
  CommandSearchSource,
  CustomDataManager,
  DEFAULT_ATTACHMENT_MANAGER_CONFIG,
  DEFAULT_CHANNEL_MANAGER_OPTIONS,
  DEFAULT_CHANNEL_MANAGER_PAGINATION_OPTIONS,
  DEFAULT_COMMANDS_CONFIG,
  DEFAULT_COMPOSER_CONFIG,
  DEFAULT_LINK_PREVIEW_MANAGER_CONFIG,
  DEFAULT_LOCATION_COMPOSER_CONFIG,
  DEFAULT_PAGINATION_OPTIONS,
  DEFAULT_REMINDER_MANAGER_CONFIG,
  DEFAULT_STOP_REFRESH_BOUNDARY_MS,
  DEFAULT_TEXT_COMPOSER_CONFIG,
  Deny,
  DenyAll,
  DevToken,
  EVENT_MAP,
  ErrorFromResponse,
  FilterBuilder,
  FixedSizeQueueCache,
  InsightMetrics,
  InvalidWebhookError,
  InvalidWebhookErrorMessages,
  JWTServerToken,
  JWTUserToken,
  LinkPreviewStatus,
  LinkPreviewsManager,
  LiveLocationManager,
  LocationComposer,
  MAX_POLL_OPTIONS,
  MENTION_ONLY_COMMANDS,
  MODERATION_ENTITY_TYPES,
  MaxPriority,
  MentionsSearchSource,
  MergedStateStore,
  MessageComposer,
  MessageComposerMiddlewareExecutor,
  MessageDeliveryReporter,
  MessageDraftComposerMiddlewareExecutor,
  MessageReceiptsTracker,
  MessageSearchSource,
  MiddlewareExecutor,
  MinPriority,
  Moderation,
  NotificationManager,
  OfflineDBSyncManager,
  OfflineError,
  Permission,
  Poll,
  PollComposer,
  PollComposerCompositionMiddlewareExecutor,
  PollComposerStateMiddlewareExecutor,
  PollManager,
  Product,
  Reminder,
  ReminderManager,
  ReminderPaginator,
  ReminderTimer,
  SearchController,
  Segment,
  StableWSConnection,
  StateStore,
  StreamChat,
  THREAD_MANAGER_INITIAL_STATE,
  TextComposer,
  TextComposerMiddlewareExecutor,
  Thread,
  ThreadManager,
  TokenManager,
  UPDATE_LIVE_LOCATION_REQUEST_MIN_THROTTLE_TIMEOUT,
  UploadManager,
  UserFromToken,
  UserGroupPaginator,
  UserSearchSource,
  VALID_MAX_VOTES_VALUE_REGEX,
  VotingVisibility,
  accentsMap,
  applyCommandValidatorOverride,
  buildWsFatalInsight,
  buildWsSuccessAfterFailureInsight,
  calculateLevenshtein,
  channelManagerEventToHandlerMapping,
  chatCodes,
  createActiveCommandGuardMiddleware,
  createAttachmentsCompositionMiddleware,
  createBlockedAttachmentUploadNotificationMiddleware,
  createCommandEffectsMiddleware,
  createCommandInjectionMiddleware,
  createCommandStringExtractionMiddleware,
  createCommandsMiddleware,
  createCompositionDataCleanupMiddleware,
  createCompositionValidationMiddleware,
  createCustomDataCompositionMiddleware,
  createDraftAttachmentsCompositionMiddleware,
  createDraftCommandInjectionMiddleware,
  createDraftCompositionValidationMiddleware,
  createDraftCustomDataCompositionMiddleware,
  createDraftLinkPreviewsCompositionMiddleware,
  createDraftMessageComposerStateCompositionMiddleware,
  createDraftTextComposerCompositionMiddleware,
  createFileFromBlobs,
  createLinkPreviewsCompositionMiddleware,
  createMentionsMiddleware,
  createMessageComposerStateCompositionMiddleware,
  createPollComposerStateMiddleware,
  createPostUploadAttachmentEnrichmentMiddleware,
  createSendWithPendingUploadsAttachmentsMiddleware,
  createSharedLocationCompositionMiddleware,
  createTextComposerCompositionMiddleware,
  createTextComposerPreValidationMiddleware,
  createUploadConfigCheckMiddleware,
  createUploadErrorHandlerMiddleware,
  decodeBase64,
  decodeSnsPayload,
  decodeSqsPayload,
  defaultCommandSendabilityValidator,
  defaultPollFieldBlurEventValidators,
  defaultPollFieldChangeEventValidators,
  encodeBase64,
  ensureIsLocalAttachment,
  escapeCommandRegExp,
  extractPollData,
  extractPollEnrichedData,
  formatMessage,
  generateFileName,
  getAllowedMentionTypesFromCapabilities,
  getAttachmentTypeFromMimeType,
  getCommandByName,
  getCompleteCommandInString,
  getExtensionFromMimeType,
  getMentionedUsersInText,
  getRawCommandName,
  getTokenizedSuggestionDisplayName,
  getTriggerCharWithToken,
  gunzipPayload,
  hasPending,
  insertItemWithTrigger,
  isAudioAttachment,
  isBlobButNotFile,
  isFile,
  isFileAttachment,
  isFileList,
  isFileReference,
  isFinishedUpload,
  isGiphyAttachment,
  isImageAttachment,
  isImageFile,
  isLocalAttachment,
  isLocalAudioAttachment,
  isLocalFileAttachment,
  isLocalImageAttachment,
  isLocalUploadAttachment,
  isLocalVideoAttachment,
  isLocalVoiceRecordingAttachment,
  isOwnUser,
  isPatch,
  isPendingUpload,
  isScrapedContent,
  isSharedLocationResponse,
  isTargetedOptionTextUpdate,
  isUploadCancellation,
  isUploadedAttachment,
  isVideoAttachment,
  isVoiceRecordingAttachment,
  isVoteAnswer,
  localMessageToNewMessagePayload,
  logChatPromiseExecution,
  mapPollStateToResponse,
  notifyCommandDisabled,
  notifyCommandNotReady,
  parseEvent,
  parseSns,
  parseSqs,
  pollCompositionStateProcessors,
  pollStateChangeValidators,
  postInsights,
  promoteChannel,
  readFileAsArrayBuffer,
  removeDiacritics,
  replaceWordWithEntity,
  settled,
  stripCommandFromText,
  stripMentionTokens,
  textIsEmpty,
  timeLeftMs,
  verifyAndParseWebhook,
  verifySignature,
  withCancellation,
  withoutConcurrency
});
//# sourceMappingURL=index.node.js.map