// ==UserScript== // @name NipahTV // @namespace https://github.com/Xzensi/NipahTV // @version 1.5.85 // @author Xzensi // @description Better Kick and 7TV emote integration for Kick chat. // @match https://kick.com/* // @match https://dashboard.kick.com/* // @resource KICK_CSS https://raw.githubusercontent.com/Xzensi/NipahTV/master/dist/userscript/kick-d94a0691.min.css // @supportURL https://github.com/Xzensi/NipahTV // @homepageURL https://github.com/Xzensi/NipahTV // @downloadURL https://raw.githubusercontent.com/Xzensi/NipahTV/master/dist/userscript/client.user.js // @grant unsafeWindow // @grant GM_getValue // @grant GM_addStyle // @grant GM_getResourceText // ==/UserScript== var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __commonJS = (cb, mod) => function __require() { return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); // node_modules/pusher-js/dist/web/pusher.js var require_pusher = __commonJS({ "node_modules/pusher-js/dist/web/pusher.js"(exports, module) { "use strict"; (function webpackUniversalModuleDefinition(root, factory) { if (typeof exports === "object" && typeof module === "object") module.exports = factory(); else if (typeof define === "function" && define.amd) define([], factory); else if (typeof exports === "object") exports["Pusher"] = factory(); else root["Pusher"] = factory(); })(window, function() { return ( /******/ function(modules) { var installedModules = {}; function __webpack_require__(moduleId) { if (installedModules[moduleId]) { return installedModules[moduleId].exports; } var module2 = installedModules[moduleId] = { /******/ i: moduleId, /******/ l: false, /******/ exports: {} /******/ }; modules[moduleId].call(module2.exports, module2, module2.exports, __webpack_require__); module2.l = true; return module2.exports; } __webpack_require__.m = modules; __webpack_require__.c = installedModules; __webpack_require__.d = function(exports2, name, getter) { if (!__webpack_require__.o(exports2, name)) { Object.defineProperty(exports2, name, { enumerable: true, get: getter }); } }; __webpack_require__.r = function(exports2) { if (typeof Symbol !== "undefined" && Symbol.toStringTag) { Object.defineProperty(exports2, Symbol.toStringTag, { value: "Module" }); } Object.defineProperty(exports2, "__esModule", { value: true }); }; __webpack_require__.t = function(value, mode) { if (mode & 1) value = __webpack_require__(value); if (mode & 8) return value; if (mode & 4 && typeof value === "object" && value && value.__esModule) return value; var ns = /* @__PURE__ */ Object.create(null); __webpack_require__.r(ns); Object.defineProperty(ns, "default", { enumerable: true, value }); if (mode & 2 && typeof value != "string") for (var key in value) __webpack_require__.d(ns, key, function(key2) { return value[key2]; }.bind(null, key)); return ns; }; __webpack_require__.n = function(module2) { var getter = module2 && module2.__esModule ? ( /******/ function getDefault() { return module2["default"]; } ) : ( /******/ function getModuleExports() { return module2; } ); __webpack_require__.d(getter, "a", getter); return getter; }; __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; __webpack_require__.p = ""; return __webpack_require__(__webpack_require__.s = 2); }([ /* 0 */ /***/ function(module2, exports2, __webpack_require__) { "use strict"; var __extends = this && this.__extends || /* @__PURE__ */ function() { var extendStatics = function(d, b) { extendStatics = Object.setPrototypeOf || { __proto__: [] } instanceof Array && function(d2, b2) { d2.__proto__ = b2; } || function(d2, b2) { for (var p in b2) if (b2.hasOwnProperty(p)) d2[p] = b2[p]; }; return extendStatics(d, b); }; return function(d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; }(); Object.defineProperty(exports2, "__esModule", { value: true }); var INVALID_BYTE = 256; var Coder = ( /** @class */ function() { function Coder2(_paddingCharacter) { if (_paddingCharacter === void 0) { _paddingCharacter = "="; } this._paddingCharacter = _paddingCharacter; } Coder2.prototype.encodedLength = function(length) { if (!this._paddingCharacter) { return (length * 8 + 5) / 6 | 0; } return (length + 2) / 3 * 4 | 0; }; Coder2.prototype.encode = function(data) { var out = ""; var i = 0; for (; i < data.length - 2; i += 3) { var c = data[i] << 16 | data[i + 1] << 8 | data[i + 2]; out += this._encodeByte(c >>> 3 * 6 & 63); out += this._encodeByte(c >>> 2 * 6 & 63); out += this._encodeByte(c >>> 1 * 6 & 63); out += this._encodeByte(c >>> 0 * 6 & 63); } var left = data.length - i; if (left > 0) { var c = data[i] << 16 | (left === 2 ? data[i + 1] << 8 : 0); out += this._encodeByte(c >>> 3 * 6 & 63); out += this._encodeByte(c >>> 2 * 6 & 63); if (left === 2) { out += this._encodeByte(c >>> 1 * 6 & 63); } else { out += this._paddingCharacter || ""; } out += this._paddingCharacter || ""; } return out; }; Coder2.prototype.maxDecodedLength = function(length) { if (!this._paddingCharacter) { return (length * 6 + 7) / 8 | 0; } return length / 4 * 3 | 0; }; Coder2.prototype.decodedLength = function(s) { return this.maxDecodedLength(s.length - this._getPaddingLength(s)); }; Coder2.prototype.decode = function(s) { if (s.length === 0) { return new Uint8Array(0); } var paddingLength = this._getPaddingLength(s); var length = s.length - paddingLength; var out = new Uint8Array(this.maxDecodedLength(length)); var op = 0; var i = 0; var haveBad = 0; var v0 = 0, v1 = 0, v2 = 0, v3 = 0; for (; i < length - 4; i += 4) { v0 = this._decodeChar(s.charCodeAt(i + 0)); v1 = this._decodeChar(s.charCodeAt(i + 1)); v2 = this._decodeChar(s.charCodeAt(i + 2)); v3 = this._decodeChar(s.charCodeAt(i + 3)); out[op++] = v0 << 2 | v1 >>> 4; out[op++] = v1 << 4 | v2 >>> 2; out[op++] = v2 << 6 | v3; haveBad |= v0 & INVALID_BYTE; haveBad |= v1 & INVALID_BYTE; haveBad |= v2 & INVALID_BYTE; haveBad |= v3 & INVALID_BYTE; } if (i < length - 1) { v0 = this._decodeChar(s.charCodeAt(i)); v1 = this._decodeChar(s.charCodeAt(i + 1)); out[op++] = v0 << 2 | v1 >>> 4; haveBad |= v0 & INVALID_BYTE; haveBad |= v1 & INVALID_BYTE; } if (i < length - 2) { v2 = this._decodeChar(s.charCodeAt(i + 2)); out[op++] = v1 << 4 | v2 >>> 2; haveBad |= v2 & INVALID_BYTE; } if (i < length - 3) { v3 = this._decodeChar(s.charCodeAt(i + 3)); out[op++] = v2 << 6 | v3; haveBad |= v3 & INVALID_BYTE; } if (haveBad !== 0) { throw new Error("Base64Coder: incorrect characters for decoding"); } return out; }; Coder2.prototype._encodeByte = function(b) { var result = b; result += 65; result += 25 - b >>> 8 & 0 - 65 - 26 + 97; result += 51 - b >>> 8 & 26 - 97 - 52 + 48; result += 61 - b >>> 8 & 52 - 48 - 62 + 43; result += 62 - b >>> 8 & 62 - 43 - 63 + 47; return String.fromCharCode(result); }; Coder2.prototype._decodeChar = function(c) { var result = INVALID_BYTE; result += (42 - c & c - 44) >>> 8 & -INVALID_BYTE + c - 43 + 62; result += (46 - c & c - 48) >>> 8 & -INVALID_BYTE + c - 47 + 63; result += (47 - c & c - 58) >>> 8 & -INVALID_BYTE + c - 48 + 52; result += (64 - c & c - 91) >>> 8 & -INVALID_BYTE + c - 65 + 0; result += (96 - c & c - 123) >>> 8 & -INVALID_BYTE + c - 97 + 26; return result; }; Coder2.prototype._getPaddingLength = function(s) { var paddingLength = 0; if (this._paddingCharacter) { for (var i = s.length - 1; i >= 0; i--) { if (s[i] !== this._paddingCharacter) { break; } paddingLength++; } if (s.length < 4 || paddingLength > 2) { throw new Error("Base64Coder: incorrect padding"); } } return paddingLength; }; return Coder2; }() ); exports2.Coder = Coder; var stdCoder = new Coder(); function encode(data) { return stdCoder.encode(data); } exports2.encode = encode; function decode(s) { return stdCoder.decode(s); } exports2.decode = decode; var URLSafeCoder = ( /** @class */ function(_super) { __extends(URLSafeCoder2, _super); function URLSafeCoder2() { return _super !== null && _super.apply(this, arguments) || this; } URLSafeCoder2.prototype._encodeByte = function(b) { var result = b; result += 65; result += 25 - b >>> 8 & 0 - 65 - 26 + 97; result += 51 - b >>> 8 & 26 - 97 - 52 + 48; result += 61 - b >>> 8 & 52 - 48 - 62 + 45; result += 62 - b >>> 8 & 62 - 45 - 63 + 95; return String.fromCharCode(result); }; URLSafeCoder2.prototype._decodeChar = function(c) { var result = INVALID_BYTE; result += (44 - c & c - 46) >>> 8 & -INVALID_BYTE + c - 45 + 62; result += (94 - c & c - 96) >>> 8 & -INVALID_BYTE + c - 95 + 63; result += (47 - c & c - 58) >>> 8 & -INVALID_BYTE + c - 48 + 52; result += (64 - c & c - 91) >>> 8 & -INVALID_BYTE + c - 65 + 0; result += (96 - c & c - 123) >>> 8 & -INVALID_BYTE + c - 97 + 26; return result; }; return URLSafeCoder2; }(Coder) ); exports2.URLSafeCoder = URLSafeCoder; var urlSafeCoder = new URLSafeCoder(); function encodeURLSafe(data) { return urlSafeCoder.encode(data); } exports2.encodeURLSafe = encodeURLSafe; function decodeURLSafe(s) { return urlSafeCoder.decode(s); } exports2.decodeURLSafe = decodeURLSafe; exports2.encodedLength = function(length) { return stdCoder.encodedLength(length); }; exports2.maxDecodedLength = function(length) { return stdCoder.maxDecodedLength(length); }; exports2.decodedLength = function(s) { return stdCoder.decodedLength(s); }; }, /* 1 */ /***/ function(module2, exports2, __webpack_require__) { "use strict"; Object.defineProperty(exports2, "__esModule", { value: true }); var INVALID_UTF16 = "utf8: invalid string"; var INVALID_UTF8 = "utf8: invalid source encoding"; function encode(s) { var arr = new Uint8Array(encodedLength(s)); var pos = 0; for (var i = 0; i < s.length; i++) { var c = s.charCodeAt(i); if (c < 128) { arr[pos++] = c; } else if (c < 2048) { arr[pos++] = 192 | c >> 6; arr[pos++] = 128 | c & 63; } else if (c < 55296) { arr[pos++] = 224 | c >> 12; arr[pos++] = 128 | c >> 6 & 63; arr[pos++] = 128 | c & 63; } else { i++; c = (c & 1023) << 10; c |= s.charCodeAt(i) & 1023; c += 65536; arr[pos++] = 240 | c >> 18; arr[pos++] = 128 | c >> 12 & 63; arr[pos++] = 128 | c >> 6 & 63; arr[pos++] = 128 | c & 63; } } return arr; } exports2.encode = encode; function encodedLength(s) { var result = 0; for (var i = 0; i < s.length; i++) { var c = s.charCodeAt(i); if (c < 128) { result += 1; } else if (c < 2048) { result += 2; } else if (c < 55296) { result += 3; } else if (c <= 57343) { if (i >= s.length - 1) { throw new Error(INVALID_UTF16); } i++; result += 4; } else { throw new Error(INVALID_UTF16); } } return result; } exports2.encodedLength = encodedLength; function decode(arr) { var chars = []; for (var i = 0; i < arr.length; i++) { var b = arr[i]; if (b & 128) { var min = void 0; if (b < 224) { if (i >= arr.length) { throw new Error(INVALID_UTF8); } var n1 = arr[++i]; if ((n1 & 192) !== 128) { throw new Error(INVALID_UTF8); } b = (b & 31) << 6 | n1 & 63; min = 128; } else if (b < 240) { if (i >= arr.length - 1) { throw new Error(INVALID_UTF8); } var n1 = arr[++i]; var n2 = arr[++i]; if ((n1 & 192) !== 128 || (n2 & 192) !== 128) { throw new Error(INVALID_UTF8); } b = (b & 15) << 12 | (n1 & 63) << 6 | n2 & 63; min = 2048; } else if (b < 248) { if (i >= arr.length - 2) { throw new Error(INVALID_UTF8); } var n1 = arr[++i]; var n2 = arr[++i]; var n3 = arr[++i]; if ((n1 & 192) !== 128 || (n2 & 192) !== 128 || (n3 & 192) !== 128) { throw new Error(INVALID_UTF8); } b = (b & 15) << 18 | (n1 & 63) << 12 | (n2 & 63) << 6 | n3 & 63; min = 65536; } else { throw new Error(INVALID_UTF8); } if (b < min || b >= 55296 && b <= 57343) { throw new Error(INVALID_UTF8); } if (b >= 65536) { if (b > 1114111) { throw new Error(INVALID_UTF8); } b -= 65536; chars.push(String.fromCharCode(55296 | b >> 10)); b = 56320 | b & 1023; } } chars.push(String.fromCharCode(b)); } return chars.join(""); } exports2.decode = decode; }, /* 2 */ /***/ function(module2, exports2, __webpack_require__) { module2.exports = __webpack_require__(3).default; }, /* 3 */ /***/ function(module2, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); class ScriptReceiverFactory { constructor(prefix2, name) { this.lastId = 0; this.prefix = prefix2; this.name = name; } create(callback) { this.lastId++; var number = this.lastId; var id = this.prefix + number; var name = this.name + "[" + number + "]"; var called = false; var callbackWrapper = function() { if (!called) { callback.apply(null, arguments); called = true; } }; this[number] = callbackWrapper; return { number, id, name, callback: callbackWrapper }; } remove(receiver) { delete this[receiver.number]; } } var ScriptReceivers = new ScriptReceiverFactory("_pusher_script_", "Pusher.ScriptReceivers"); var Defaults = { VERSION: "8.4.0-rc2", PROTOCOL: 7, wsPort: 80, wssPort: 443, wsPath: "", httpHost: "sockjs.pusher.com", httpPort: 80, httpsPort: 443, httpPath: "/pusher", stats_host: "stats.pusher.com", authEndpoint: "/pusher/auth", authTransport: "ajax", activityTimeout: 12e4, pongTimeout: 3e4, unavailableTimeout: 1e4, userAuthentication: { endpoint: "/pusher/user-auth", transport: "ajax" }, channelAuthorization: { endpoint: "/pusher/auth", transport: "ajax" }, cdn_http: "http://js.pusher.com", cdn_https: "https://js.pusher.com", dependency_suffix: "" }; var defaults = Defaults; class dependency_loader_DependencyLoader { constructor(options) { this.options = options; this.receivers = options.receivers || ScriptReceivers; this.loading = {}; } load(name, options, callback) { var self2 = this; if (self2.loading[name] && self2.loading[name].length > 0) { self2.loading[name].push(callback); } else { self2.loading[name] = [callback]; var request = runtime.createScriptRequest(self2.getPath(name, options)); var receiver = self2.receivers.create(function(error40) { self2.receivers.remove(receiver); if (self2.loading[name]) { var callbacks = self2.loading[name]; delete self2.loading[name]; var successCallback = function(wasSuccessful) { if (!wasSuccessful) { request.cleanup(); } }; for (var i = 0; i < callbacks.length; i++) { callbacks[i](error40, successCallback); } } }); request.send(receiver); } } getRoot(options) { var cdn; var protocol = runtime.getDocument().location.protocol; if (options && options.useTLS || protocol === "https:") { cdn = this.options.cdn_https; } else { cdn = this.options.cdn_http; } return cdn.replace(/\/*$/, "") + "/" + this.options.version; } getPath(name, options) { return this.getRoot(options) + "/" + name + this.options.suffix + ".js"; } } var DependenciesReceivers = new ScriptReceiverFactory("_pusher_dependencies", "Pusher.DependenciesReceivers"); var Dependencies = new dependency_loader_DependencyLoader({ cdn_http: defaults.cdn_http, cdn_https: defaults.cdn_https, version: defaults.VERSION, suffix: defaults.dependency_suffix, receivers: DependenciesReceivers }); const urlStore = { baseUrl: "https://pusher.com", urls: { authenticationEndpoint: { path: "/docs/channels/server_api/authenticating_users" }, authorizationEndpoint: { path: "/docs/channels/server_api/authorizing-users/" }, javascriptQuickStart: { path: "/docs/javascript_quick_start" }, triggeringClientEvents: { path: "/docs/client_api_guide/client_events#trigger-events" }, encryptedChannelSupport: { fullUrl: "https://github.com/pusher/pusher-js/tree/cc491015371a4bde5743d1c87a0fbac0feb53195#encrypted-channel-support" } } }; const buildLogSuffix = function(key) { const urlPrefix = "See:"; const urlObj = urlStore.urls[key]; if (!urlObj) return ""; let url; if (urlObj.fullUrl) { url = urlObj.fullUrl; } else if (urlObj.path) { url = urlStore.baseUrl + urlObj.path; } if (!url) return ""; return `${urlPrefix} ${url}`; }; var url_store = { buildLogSuffix }; var AuthRequestType; (function(AuthRequestType2) { AuthRequestType2["UserAuthentication"] = "user-authentication"; AuthRequestType2["ChannelAuthorization"] = "channel-authorization"; })(AuthRequestType || (AuthRequestType = {})); class BadEventName extends Error { constructor(msg) { super(msg); Object.setPrototypeOf(this, new.target.prototype); } } class BadChannelName extends Error { constructor(msg) { super(msg); Object.setPrototypeOf(this, new.target.prototype); } } class RequestTimedOut extends Error { constructor(msg) { super(msg); Object.setPrototypeOf(this, new.target.prototype); } } class TransportPriorityTooLow extends Error { constructor(msg) { super(msg); Object.setPrototypeOf(this, new.target.prototype); } } class TransportClosed extends Error { constructor(msg) { super(msg); Object.setPrototypeOf(this, new.target.prototype); } } class UnsupportedFeature extends Error { constructor(msg) { super(msg); Object.setPrototypeOf(this, new.target.prototype); } } class UnsupportedTransport extends Error { constructor(msg) { super(msg); Object.setPrototypeOf(this, new.target.prototype); } } class UnsupportedStrategy extends Error { constructor(msg) { super(msg); Object.setPrototypeOf(this, new.target.prototype); } } class HTTPAuthError extends Error { constructor(status, msg) { super(msg); this.status = status; Object.setPrototypeOf(this, new.target.prototype); } } const ajax = function(context, query, authOptions, authRequestType, callback) { const xhr = runtime.createXHR(); xhr.open("POST", authOptions.endpoint, true); xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); for (var headerName in authOptions.headers) { xhr.setRequestHeader(headerName, authOptions.headers[headerName]); } if (authOptions.headersProvider != null) { let dynamicHeaders = authOptions.headersProvider(); for (var headerName in dynamicHeaders) { xhr.setRequestHeader(headerName, dynamicHeaders[headerName]); } } xhr.onreadystatechange = function() { if (xhr.readyState === 4) { if (xhr.status === 200) { let data; let parsed = false; try { data = JSON.parse(xhr.responseText); parsed = true; } catch (e) { callback(new HTTPAuthError(200, `JSON returned from ${authRequestType.toString()} endpoint was invalid, yet status code was 200. Data was: ${xhr.responseText}`), null); } if (parsed) { callback(null, data); } } else { let suffix = ""; switch (authRequestType) { case AuthRequestType.UserAuthentication: suffix = url_store.buildLogSuffix("authenticationEndpoint"); break; case AuthRequestType.ChannelAuthorization: suffix = `Clients must be authorized to join private or presence channels. ${url_store.buildLogSuffix("authorizationEndpoint")}`; break; } callback(new HTTPAuthError(xhr.status, `Unable to retrieve auth string from ${authRequestType.toString()} endpoint - received status: ${xhr.status} from ${authOptions.endpoint}. ${suffix}`), null); } } }; xhr.send(query); return xhr; }; var xhr_auth = ajax; function encode(s) { return btoa(utob(s)); } var fromCharCode = String.fromCharCode; var b64chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; var b64tab = {}; for (var base64_i = 0, l = b64chars.length; base64_i < l; base64_i++) { b64tab[b64chars.charAt(base64_i)] = base64_i; } var cb_utob = function(c) { var cc = c.charCodeAt(0); return cc < 128 ? c : cc < 2048 ? fromCharCode(192 | cc >>> 6) + fromCharCode(128 | cc & 63) : fromCharCode(224 | cc >>> 12 & 15) + fromCharCode(128 | cc >>> 6 & 63) + fromCharCode(128 | cc & 63); }; var utob = function(u) { return u.replace(/[^\x00-\x7F]/g, cb_utob); }; var cb_encode = function(ccc) { var padlen = [0, 2, 1][ccc.length % 3]; var ord = ccc.charCodeAt(0) << 16 | (ccc.length > 1 ? ccc.charCodeAt(1) : 0) << 8 | (ccc.length > 2 ? ccc.charCodeAt(2) : 0); var chars = [ b64chars.charAt(ord >>> 18), b64chars.charAt(ord >>> 12 & 63), padlen >= 2 ? "=" : b64chars.charAt(ord >>> 6 & 63), padlen >= 1 ? "=" : b64chars.charAt(ord & 63) ]; return chars.join(""); }; var btoa = window.btoa || function(b) { return b.replace(/[\s\S]{1,3}/g, cb_encode); }; class Timer { constructor(set, clear, delay, callback) { this.clear = clear; this.timer = set(() => { if (this.timer) { this.timer = callback(this.timer); } }, delay); } isRunning() { return this.timer !== null; } ensureAborted() { if (this.timer) { this.clear(this.timer); this.timer = null; } } } var abstract_timer = Timer; function timers_clearTimeout(timer) { window.clearTimeout(timer); } function timers_clearInterval(timer) { window.clearInterval(timer); } class timers_OneOffTimer extends abstract_timer { constructor(delay, callback) { super(setTimeout, timers_clearTimeout, delay, function(timer) { callback(); return null; }); } } class timers_PeriodicTimer extends abstract_timer { constructor(delay, callback) { super(setInterval, timers_clearInterval, delay, function(timer) { callback(); return timer; }); } } var Util = { now() { if (Date.now) { return Date.now(); } else { return (/* @__PURE__ */ new Date()).valueOf(); } }, defer(callback) { return new timers_OneOffTimer(0, callback); }, method(name, ...args) { var boundArguments = Array.prototype.slice.call(arguments, 1); return function(object) { return object[name].apply(object, boundArguments.concat(arguments)); }; } }; var util = Util; function extend(target, ...sources) { for (var i = 0; i < sources.length; i++) { var extensions = sources[i]; for (var property in extensions) { if (extensions[property] && extensions[property].constructor && extensions[property].constructor === Object) { target[property] = extend(target[property] || {}, extensions[property]); } else { target[property] = extensions[property]; } } } return target; } function stringify() { var m = ["Pusher"]; for (var i = 0; i < arguments.length; i++) { if (typeof arguments[i] === "string") { m.push(arguments[i]); } else { m.push(safeJSONStringify(arguments[i])); } } return m.join(" : "); } function arrayIndexOf(array, item) { var nativeIndexOf = Array.prototype.indexOf; if (array === null) { return -1; } if (nativeIndexOf && array.indexOf === nativeIndexOf) { return array.indexOf(item); } for (var i = 0, l2 = array.length; i < l2; i++) { if (array[i] === item) { return i; } } return -1; } function objectApply(object, f) { for (var key in object) { if (Object.prototype.hasOwnProperty.call(object, key)) { f(object[key], key, object); } } } function keys(object) { var keys2 = []; objectApply(object, function(_, key) { keys2.push(key); }); return keys2; } function values(object) { var values2 = []; objectApply(object, function(value) { values2.push(value); }); return values2; } function apply(array, f, context) { for (var i = 0; i < array.length; i++) { f.call(context || window, array[i], i, array); } } function map(array, f) { var result = []; for (var i = 0; i < array.length; i++) { result.push(f(array[i], i, array, result)); } return result; } function mapObject(object, f) { var result = {}; objectApply(object, function(value, key) { result[key] = f(value); }); return result; } function filter(array, test) { test = test || function(value) { return !!value; }; var result = []; for (var i = 0; i < array.length; i++) { if (test(array[i], i, array, result)) { result.push(array[i]); } } return result; } function filterObject(object, test) { var result = {}; objectApply(object, function(value, key) { if (test && test(value, key, object, result) || Boolean(value)) { result[key] = value; } }); return result; } function flatten(object) { var result = []; objectApply(object, function(value, key) { result.push([key, value]); }); return result; } function any(array, test) { for (var i = 0; i < array.length; i++) { if (test(array[i], i, array)) { return true; } } return false; } function collections_all(array, test) { for (var i = 0; i < array.length; i++) { if (!test(array[i], i, array)) { return false; } } return true; } function encodeParamsObject(data) { return mapObject(data, function(value) { if (typeof value === "object") { value = safeJSONStringify(value); } return encodeURIComponent(encode(value.toString())); }); } function buildQueryString(data) { var params = filterObject(data, function(value) { return value !== void 0; }); var query = map(flatten(encodeParamsObject(params)), util.method("join", "=")).join("&"); return query; } function decycleObject(object) { var objects = [], paths = []; return function derez(value, path) { var i, name, nu; switch (typeof value) { case "object": if (!value) { return null; } for (i = 0; i < objects.length; i += 1) { if (objects[i] === value) { return { $ref: paths[i] }; } } objects.push(value); paths.push(path); if (Object.prototype.toString.apply(value) === "[object Array]") { nu = []; for (i = 0; i < value.length; i += 1) { nu[i] = derez(value[i], path + "[" + i + "]"); } } else { nu = {}; for (name in value) { if (Object.prototype.hasOwnProperty.call(value, name)) { nu[name] = derez(value[name], path + "[" + JSON.stringify(name) + "]"); } } } return nu; case "number": case "string": case "boolean": return value; } }(object, "$"); } function safeJSONStringify(source) { try { return JSON.stringify(source); } catch (e) { return JSON.stringify(decycleObject(source)); } } class logger_Logger { constructor() { this.globalLog = (message) => { if (window.console && window.console.log) { window.console.log(message); } }; } debug(...args) { this.log(this.globalLog, args); } warn(...args) { this.log(this.globalLogWarn, args); } error(...args) { this.log(this.globalLogError, args); } globalLogWarn(message) { if (window.console && window.console.warn) { window.console.warn(message); } else { this.globalLog(message); } } globalLogError(message) { if (window.console && window.console.error) { window.console.error(message); } else { this.globalLogWarn(message); } } log(defaultLoggingFunction, ...args) { var message = stringify.apply(this, arguments); if (core_pusher.log) { core_pusher.log(message); } else if (core_pusher.logToConsole) { const log39 = defaultLoggingFunction.bind(this); log39(message); } } } var logger40 = new logger_Logger(); var jsonp = function(context, query, authOptions, authRequestType, callback) { if (authOptions.headers !== void 0 || authOptions.headersProvider != null) { logger40.warn(`To send headers with the ${authRequestType.toString()} request, you must use AJAX, rather than JSONP.`); } var callbackName = context.nextAuthCallbackID.toString(); context.nextAuthCallbackID++; var document2 = context.getDocument(); var script = document2.createElement("script"); context.auth_callbacks[callbackName] = function(data) { callback(null, data); }; var callback_name = "Pusher.auth_callbacks['" + callbackName + "']"; script.src = authOptions.endpoint + "?callback=" + encodeURIComponent(callback_name) + "&" + query; var head = document2.getElementsByTagName("head")[0] || document2.documentElement; head.insertBefore(script, head.firstChild); }; var jsonp_auth = jsonp; class ScriptRequest { constructor(src) { this.src = src; } send(receiver) { var self2 = this; var errorString = "Error loading " + self2.src; self2.script = document.createElement("script"); self2.script.id = receiver.id; self2.script.src = self2.src; self2.script.type = "text/javascript"; self2.script.charset = "UTF-8"; if (self2.script.addEventListener) { self2.script.onerror = function() { receiver.callback(errorString); }; self2.script.onload = function() { receiver.callback(null); }; } else { self2.script.onreadystatechange = function() { if (self2.script.readyState === "loaded" || self2.script.readyState === "complete") { receiver.callback(null); } }; } if (self2.script.async === void 0 && document.attachEvent && /opera/i.test(navigator.userAgent)) { self2.errorScript = document.createElement("script"); self2.errorScript.id = receiver.id + "_error"; self2.errorScript.text = receiver.name + "('" + errorString + "');"; self2.script.async = self2.errorScript.async = false; } else { self2.script.async = true; } var head = document.getElementsByTagName("head")[0]; head.insertBefore(self2.script, head.firstChild); if (self2.errorScript) { head.insertBefore(self2.errorScript, self2.script.nextSibling); } } cleanup() { if (this.script) { this.script.onload = this.script.onerror = null; this.script.onreadystatechange = null; } if (this.script && this.script.parentNode) { this.script.parentNode.removeChild(this.script); } if (this.errorScript && this.errorScript.parentNode) { this.errorScript.parentNode.removeChild(this.errorScript); } this.script = null; this.errorScript = null; } } class jsonp_request_JSONPRequest { constructor(url, data) { this.url = url; this.data = data; } send(receiver) { if (this.request) { return; } var query = buildQueryString(this.data); var url = this.url + "/" + receiver.number + "?" + query; this.request = runtime.createScriptRequest(url); this.request.send(receiver); } cleanup() { if (this.request) { this.request.cleanup(); } } } var getAgent = function(sender, useTLS) { return function(data, callback) { var scheme = "http" + (useTLS ? "s" : "") + "://"; var url = scheme + (sender.host || sender.options.host) + sender.options.path; var request = runtime.createJSONPRequest(url, data); var receiver = runtime.ScriptReceivers.create(function(error40, result) { ScriptReceivers.remove(receiver); request.cleanup(); if (result && result.host) { sender.host = result.host; } if (callback) { callback(error40, result); } }); request.send(receiver); }; }; var jsonp_timeline_jsonp = { name: "jsonp", getAgent }; var jsonp_timeline = jsonp_timeline_jsonp; function getGenericURL(baseScheme, params, path) { var scheme = baseScheme + (params.useTLS ? "s" : ""); var host = params.useTLS ? params.hostTLS : params.hostNonTLS; return scheme + "://" + host + path; } function getGenericPath(key, queryString) { var path = "/app/" + key; var query = "?protocol=" + defaults.PROTOCOL + "&client=js&version=" + defaults.VERSION + (queryString ? "&" + queryString : ""); return path + query; } var ws = { getInitial: function(key, params) { var path = (params.httpPath || "") + getGenericPath(key, "flash=false"); return getGenericURL("ws", params, path); } }; var http = { getInitial: function(key, params) { var path = (params.httpPath || "/pusher") + getGenericPath(key); return getGenericURL("http", params, path); } }; var sockjs = { getInitial: function(key, params) { return getGenericURL("http", params, params.httpPath || "/pusher"); }, getPath: function(key, params) { return getGenericPath(key); } }; class callback_registry_CallbackRegistry { constructor() { this._callbacks = {}; } get(name) { return this._callbacks[prefix(name)]; } add(name, callback, context) { var prefixedEventName = prefix(name); this._callbacks[prefixedEventName] = this._callbacks[prefixedEventName] || []; this._callbacks[prefixedEventName].push({ fn: callback, context }); } remove(name, callback, context) { if (!name && !callback && !context) { this._callbacks = {}; return; } var names = name ? [prefix(name)] : keys(this._callbacks); if (callback || context) { this.removeCallback(names, callback, context); } else { this.removeAllCallbacks(names); } } removeCallback(names, callback, context) { apply(names, function(name) { this._callbacks[name] = filter(this._callbacks[name] || [], function(binding) { return callback && callback !== binding.fn || context && context !== binding.context; }); if (this._callbacks[name].length === 0) { delete this._callbacks[name]; } }, this); } removeAllCallbacks(names) { apply(names, function(name) { delete this._callbacks[name]; }, this); } } function prefix(name) { return "_" + name; } class dispatcher_Dispatcher { constructor(failThrough) { this.callbacks = new callback_registry_CallbackRegistry(); this.global_callbacks = []; this.failThrough = failThrough; } bind(eventName, callback, context) { this.callbacks.add(eventName, callback, context); return this; } bind_global(callback) { this.global_callbacks.push(callback); return this; } unbind(eventName, callback, context) { this.callbacks.remove(eventName, callback, context); return this; } unbind_global(callback) { if (!callback) { this.global_callbacks = []; return this; } this.global_callbacks = filter(this.global_callbacks || [], (c) => c !== callback); return this; } unbind_all() { this.unbind(); this.unbind_global(); return this; } emit(eventName, data, metadata) { for (var i = 0; i < this.global_callbacks.length; i++) { this.global_callbacks[i](eventName, data); } var callbacks = this.callbacks.get(eventName); var args = []; if (metadata) { args.push(data, metadata); } else if (data) { args.push(data); } if (callbacks && callbacks.length > 0) { for (var i = 0; i < callbacks.length; i++) { callbacks[i].fn.apply(callbacks[i].context || window, args); } } else if (this.failThrough) { this.failThrough(eventName, data); } return this; } } class transport_connection_TransportConnection extends dispatcher_Dispatcher { constructor(hooks, name, priority, key, options) { super(); this.initialize = runtime.transportConnectionInitializer; this.hooks = hooks; this.name = name; this.priority = priority; this.key = key; this.options = options; this.state = "new"; this.timeline = options.timeline; this.activityTimeout = options.activityTimeout; this.id = this.timeline.generateUniqueID(); } handlesActivityChecks() { return Boolean(this.hooks.handlesActivityChecks); } supportsPing() { return Boolean(this.hooks.supportsPing); } connect() { if (this.socket || this.state !== "initialized") { return false; } var url = this.hooks.urls.getInitial(this.key, this.options); try { this.socket = this.hooks.getSocket(url, this.options); } catch (e) { util.defer(() => { this.onError(e); this.changeState("closed"); }); return false; } this.bindListeners(); logger40.debug("Connecting", { transport: this.name, url }); this.changeState("connecting"); return true; } close() { if (this.socket) { this.socket.close(); return true; } else { return false; } } send(data) { if (this.state === "open") { util.defer(() => { if (this.socket) { this.socket.send(data); } }); return true; } else { return false; } } ping() { if (this.state === "open" && this.supportsPing()) { this.socket.ping(); } } onOpen() { if (this.hooks.beforeOpen) { this.hooks.beforeOpen(this.socket, this.hooks.urls.getPath(this.key, this.options)); } this.changeState("open"); this.socket.onopen = void 0; } onError(error40) { this.emit("error", { type: "WebSocketError", error: error40 }); this.timeline.error(this.buildTimelineMessage({ error: error40.toString() })); } onClose(closeEvent) { if (closeEvent) { this.changeState("closed", { code: closeEvent.code, reason: closeEvent.reason, wasClean: closeEvent.wasClean }); } else { this.changeState("closed"); } this.unbindListeners(); this.socket = void 0; } onMessage(message) { this.emit("message", message); } onActivity() { this.emit("activity"); } bindListeners() { this.socket.onopen = () => { this.onOpen(); }; this.socket.onerror = (error40) => { this.onError(error40); }; this.socket.onclose = (closeEvent) => { this.onClose(closeEvent); }; this.socket.onmessage = (message) => { this.onMessage(message); }; if (this.supportsPing()) { this.socket.onactivity = () => { this.onActivity(); }; } } unbindListeners() { if (this.socket) { this.socket.onopen = void 0; this.socket.onerror = void 0; this.socket.onclose = void 0; this.socket.onmessage = void 0; if (this.supportsPing()) { this.socket.onactivity = void 0; } } } changeState(state2, params) { this.state = state2; this.timeline.info(this.buildTimelineMessage({ state: state2, params })); this.emit(state2, params); } buildTimelineMessage(message) { return extend({ cid: this.id }, message); } } class transport_Transport { constructor(hooks) { this.hooks = hooks; } isSupported(environment) { return this.hooks.isSupported(environment); } createConnection(name, priority, key, options) { return new transport_connection_TransportConnection(this.hooks, name, priority, key, options); } } var WSTransport = new transport_Transport({ urls: ws, handlesActivityChecks: false, supportsPing: false, isInitialized: function() { return Boolean(runtime.getWebSocketAPI()); }, isSupported: function() { return Boolean(runtime.getWebSocketAPI()); }, getSocket: function(url) { return runtime.createWebSocket(url); } }); var httpConfiguration = { urls: http, handlesActivityChecks: false, supportsPing: true, isInitialized: function() { return true; } }; var streamingConfiguration = extend({ getSocket: function(url) { return runtime.HTTPFactory.createStreamingSocket(url); } }, httpConfiguration); var pollingConfiguration = extend({ getSocket: function(url) { return runtime.HTTPFactory.createPollingSocket(url); } }, httpConfiguration); var xhrConfiguration = { isSupported: function() { return runtime.isXHRSupported(); } }; var XHRStreamingTransport = new transport_Transport(extend({}, streamingConfiguration, xhrConfiguration)); var XHRPollingTransport = new transport_Transport(extend({}, pollingConfiguration, xhrConfiguration)); var Transports = { ws: WSTransport, xhr_streaming: XHRStreamingTransport, xhr_polling: XHRPollingTransport }; var transports = Transports; var SockJSTransport = new transport_Transport({ file: "sockjs", urls: sockjs, handlesActivityChecks: true, supportsPing: false, isSupported: function() { return true; }, isInitialized: function() { return window.SockJS !== void 0; }, getSocket: function(url, options) { return new window.SockJS(url, null, { js_path: Dependencies.getPath("sockjs", { useTLS: options.useTLS }), ignore_null_origin: options.ignoreNullOrigin }); }, beforeOpen: function(socket, path) { socket.send(JSON.stringify({ path })); } }); var xdrConfiguration = { isSupported: function(environment) { var yes = runtime.isXDRSupported(environment.useTLS); return yes; } }; var XDRStreamingTransport = new transport_Transport(extend({}, streamingConfiguration, xdrConfiguration)); var XDRPollingTransport = new transport_Transport(extend({}, pollingConfiguration, xdrConfiguration)); transports.xdr_streaming = XDRStreamingTransport; transports.xdr_polling = XDRPollingTransport; transports.sockjs = SockJSTransport; var transports_transports = transports; class net_info_NetInfo extends dispatcher_Dispatcher { constructor() { super(); var self2 = this; if (window.addEventListener !== void 0) { window.addEventListener("online", function() { self2.emit("online"); }, false); window.addEventListener("offline", function() { self2.emit("offline"); }, false); } } isOnline() { if (window.navigator.onLine === void 0) { return true; } else { return window.navigator.onLine; } } } var net_info_Network = new net_info_NetInfo(); class assistant_to_the_transport_manager_AssistantToTheTransportManager { constructor(manager, transport, options) { this.manager = manager; this.transport = transport; this.minPingDelay = options.minPingDelay; this.maxPingDelay = options.maxPingDelay; this.pingDelay = void 0; } createConnection(name, priority, key, options) { options = extend({}, options, { activityTimeout: this.pingDelay }); var connection = this.transport.createConnection(name, priority, key, options); var openTimestamp = null; var onOpen = function() { connection.unbind("open", onOpen); connection.bind("closed", onClosed); openTimestamp = util.now(); }; var onClosed = (closeEvent) => { connection.unbind("closed", onClosed); if (closeEvent.code === 1002 || closeEvent.code === 1003) { this.manager.reportDeath(); } else if (!closeEvent.wasClean && openTimestamp) { var lifespan = util.now() - openTimestamp; if (lifespan < 2 * this.maxPingDelay) { this.manager.reportDeath(); this.pingDelay = Math.max(lifespan / 2, this.minPingDelay); } } }; connection.bind("open", onOpen); return connection; } isSupported(environment) { return this.manager.isAlive() && this.transport.isSupported(environment); } } const Protocol = { decodeMessage: function(messageEvent) { try { var messageData = JSON.parse(messageEvent.data); var pusherEventData = messageData.data; if (typeof pusherEventData === "string") { try { pusherEventData = JSON.parse(messageData.data); } catch (e) { } } var pusherEvent = { event: messageData.event, channel: messageData.channel, data: pusherEventData }; if (messageData.user_id) { pusherEvent.user_id = messageData.user_id; } return pusherEvent; } catch (e) { throw { type: "MessageParseError", error: e, data: messageEvent.data }; } }, encodeMessage: function(event) { return JSON.stringify(event); }, processHandshake: function(messageEvent) { var message = Protocol.decodeMessage(messageEvent); if (message.event === "pusher:connection_established") { if (!message.data.activity_timeout) { throw "No activity timeout specified in handshake"; } return { action: "connected", id: message.data.socket_id, activityTimeout: message.data.activity_timeout * 1e3 }; } else if (message.event === "pusher:error") { return { action: this.getCloseAction(message.data), error: this.getCloseError(message.data) }; } else { throw "Invalid handshake"; } }, getCloseAction: function(closeEvent) { if (closeEvent.code < 4e3) { if (closeEvent.code >= 1002 && closeEvent.code <= 1004) { return "backoff"; } else { return null; } } else if (closeEvent.code === 4e3) { return "tls_only"; } else if (closeEvent.code < 4100) { return "refused"; } else if (closeEvent.code < 4200) { return "backoff"; } else if (closeEvent.code < 4300) { return "retry"; } else { return "refused"; } }, getCloseError: function(closeEvent) { if (closeEvent.code !== 1e3 && closeEvent.code !== 1001) { return { type: "PusherError", data: { code: closeEvent.code, message: closeEvent.reason || closeEvent.message } }; } else { return null; } } }; var protocol_protocol = Protocol; class connection_Connection extends dispatcher_Dispatcher { constructor(id, transport) { super(); this.id = id; this.transport = transport; this.activityTimeout = transport.activityTimeout; this.bindListeners(); } handlesActivityChecks() { return this.transport.handlesActivityChecks(); } send(data) { return this.transport.send(data); } send_event(name, data, channel) { var event = { event: name, data }; if (channel) { event.channel = channel; } logger40.debug("Event sent", event); return this.send(protocol_protocol.encodeMessage(event)); } ping() { if (this.transport.supportsPing()) { this.transport.ping(); } else { this.send_event("pusher:ping", {}); } } close() { this.transport.close(); } bindListeners() { var listeners = { message: (messageEvent) => { var pusherEvent; try { pusherEvent = protocol_protocol.decodeMessage(messageEvent); } catch (e) { this.emit("error", { type: "MessageParseError", error: e, data: messageEvent.data }); } if (pusherEvent !== void 0) { logger40.debug("Event recd", pusherEvent); switch (pusherEvent.event) { case "pusher:error": this.emit("error", { type: "PusherError", data: pusherEvent.data }); break; case "pusher:ping": this.emit("ping"); break; case "pusher:pong": this.emit("pong"); break; } this.emit("message", pusherEvent); } }, activity: () => { this.emit("activity"); }, error: (error40) => { this.emit("error", error40); }, closed: (closeEvent) => { unbindListeners(); if (closeEvent && closeEvent.code) { this.handleCloseEvent(closeEvent); } this.transport = null; this.emit("closed"); } }; var unbindListeners = () => { objectApply(listeners, (listener, event) => { this.transport.unbind(event, listener); }); }; objectApply(listeners, (listener, event) => { this.transport.bind(event, listener); }); } handleCloseEvent(closeEvent) { var action = protocol_protocol.getCloseAction(closeEvent); var error40 = protocol_protocol.getCloseError(closeEvent); if (error40) { this.emit("error", error40); } if (action) { this.emit(action, { action, error: error40 }); } } } class handshake_Handshake { constructor(transport, callback) { this.transport = transport; this.callback = callback; this.bindListeners(); } close() { this.unbindListeners(); this.transport.close(); } bindListeners() { this.onMessage = (m) => { this.unbindListeners(); var result; try { result = protocol_protocol.processHandshake(m); } catch (e) { this.finish("error", { error: e }); this.transport.close(); return; } if (result.action === "connected") { this.finish("connected", { connection: new connection_Connection(result.id, this.transport), activityTimeout: result.activityTimeout }); } else { this.finish(result.action, { error: result.error }); this.transport.close(); } }; this.onClosed = (closeEvent) => { this.unbindListeners(); var action = protocol_protocol.getCloseAction(closeEvent) || "backoff"; var error40 = protocol_protocol.getCloseError(closeEvent); this.finish(action, { error: error40 }); }; this.transport.bind("message", this.onMessage); this.transport.bind("closed", this.onClosed); } unbindListeners() { this.transport.unbind("message", this.onMessage); this.transport.unbind("closed", this.onClosed); } finish(action, params) { this.callback(extend({ transport: this.transport, action }, params)); } } class timeline_sender_TimelineSender { constructor(timeline, options) { this.timeline = timeline; this.options = options || {}; } send(useTLS, callback) { if (this.timeline.isEmpty()) { return; } this.timeline.send(runtime.TimelineTransport.getAgent(this, useTLS), callback); } } class channel_Channel extends dispatcher_Dispatcher { constructor(name, pusher) { super(function(event, data) { logger40.debug("No callbacks on " + name + " for " + event); }); this.name = name; this.pusher = pusher; this.subscribed = false; this.subscriptionPending = false; this.subscriptionCancelled = false; } authorize(socketId, callback) { return callback(null, { auth: "" }); } trigger(event, data) { if (event.indexOf("client-") !== 0) { throw new BadEventName("Event '" + event + "' does not start with 'client-'"); } if (!this.subscribed) { var suffix = url_store.buildLogSuffix("triggeringClientEvents"); logger40.warn(`Client event triggered before channel 'subscription_succeeded' event . ${suffix}`); } return this.pusher.send_event(event, data, this.name); } disconnect() { this.subscribed = false; this.subscriptionPending = false; } handleEvent(event) { var eventName = event.event; var data = event.data; if (eventName === "pusher_internal:subscription_succeeded") { this.handleSubscriptionSucceededEvent(event); } else if (eventName === "pusher_internal:subscription_count") { this.handleSubscriptionCountEvent(event); } else if (eventName.indexOf("pusher_internal:") !== 0) { var metadata = {}; this.emit(eventName, data, metadata); } } handleSubscriptionSucceededEvent(event) { this.subscriptionPending = false; this.subscribed = true; if (this.subscriptionCancelled) { this.pusher.unsubscribe(this.name); } else { this.emit("pusher:subscription_succeeded", event.data); } } handleSubscriptionCountEvent(event) { if (event.data.subscription_count) { this.subscriptionCount = event.data.subscription_count; } this.emit("pusher:subscription_count", event.data); } subscribe() { if (this.subscribed) { return; } this.subscriptionPending = true; this.subscriptionCancelled = false; this.authorize(this.pusher.connection.socket_id, (error40, data) => { if (error40) { this.subscriptionPending = false; logger40.error(error40.toString()); this.emit("pusher:subscription_error", Object.assign({}, { type: "AuthError", error: error40.message }, error40 instanceof HTTPAuthError ? { status: error40.status } : {})); } else { this.pusher.send_event("pusher:subscribe", { auth: data.auth, channel_data: data.channel_data, channel: this.name }); } }); } unsubscribe() { this.subscribed = false; this.pusher.send_event("pusher:unsubscribe", { channel: this.name }); } cancelSubscription() { this.subscriptionCancelled = true; } reinstateSubscription() { this.subscriptionCancelled = false; } } class private_channel_PrivateChannel extends channel_Channel { authorize(socketId, callback) { return this.pusher.config.channelAuthorizer({ channelName: this.name, socketId }, callback); } } class members_Members { constructor() { this.reset(); } get(id) { if (Object.prototype.hasOwnProperty.call(this.members, id)) { return { id, info: this.members[id] }; } else { return null; } } each(callback) { objectApply(this.members, (member, id) => { callback(this.get(id)); }); } setMyID(id) { this.myID = id; } onSubscription(subscriptionData) { this.members = subscriptionData.presence.hash; this.count = subscriptionData.presence.count; this.me = this.get(this.myID); } addMember(memberData) { if (this.get(memberData.user_id) === null) { this.count++; } this.members[memberData.user_id] = memberData.user_info; return this.get(memberData.user_id); } removeMember(memberData) { var member = this.get(memberData.user_id); if (member) { delete this.members[memberData.user_id]; this.count--; } return member; } reset() { this.members = {}; this.count = 0; this.myID = null; this.me = null; } } var __awaiter = function(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function(resolve) { resolve(value); }); } return new (P || (P = Promise))(function(resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; class presence_channel_PresenceChannel extends private_channel_PrivateChannel { constructor(name, pusher) { super(name, pusher); this.members = new members_Members(); } authorize(socketId, callback) { super.authorize(socketId, (error40, authData) => __awaiter(this, void 0, void 0, function* () { if (!error40) { authData = authData; if (authData.channel_data != null) { var channelData = JSON.parse(authData.channel_data); this.members.setMyID(channelData.user_id); } else { yield this.pusher.user.signinDonePromise; if (this.pusher.user.user_data != null) { this.members.setMyID(this.pusher.user.user_data.id); } else { let suffix = url_store.buildLogSuffix("authorizationEndpoint"); logger40.error(`Invalid auth response for channel '${this.name}', expected 'channel_data' field. ${suffix}, or the user should be signed in.`); callback("Invalid auth response"); return; } } } callback(error40, authData); })); } handleEvent(event) { var eventName = event.event; if (eventName.indexOf("pusher_internal:") === 0) { this.handleInternalEvent(event); } else { var data = event.data; var metadata = {}; if (event.user_id) { metadata.user_id = event.user_id; } this.emit(eventName, data, metadata); } } handleInternalEvent(event) { var eventName = event.event; var data = event.data; switch (eventName) { case "pusher_internal:subscription_succeeded": this.handleSubscriptionSucceededEvent(event); break; case "pusher_internal:subscription_count": this.handleSubscriptionCountEvent(event); break; case "pusher_internal:member_added": var addedMember = this.members.addMember(data); this.emit("pusher:member_added", addedMember); break; case "pusher_internal:member_removed": var removedMember = this.members.removeMember(data); if (removedMember) { this.emit("pusher:member_removed", removedMember); } break; } } handleSubscriptionSucceededEvent(event) { this.subscriptionPending = false; this.subscribed = true; if (this.subscriptionCancelled) { this.pusher.unsubscribe(this.name); } else { this.members.onSubscription(event.data); this.emit("pusher:subscription_succeeded", this.members); } } disconnect() { this.members.reset(); super.disconnect(); } } var utf8 = __webpack_require__(1); var base64 = __webpack_require__(0); class encrypted_channel_EncryptedChannel extends private_channel_PrivateChannel { constructor(name, pusher, nacl) { super(name, pusher); this.key = null; this.nacl = nacl; } authorize(socketId, callback) { super.authorize(socketId, (error40, authData) => { if (error40) { callback(error40, authData); return; } let sharedSecret = authData["shared_secret"]; if (!sharedSecret) { callback(new Error(`No shared_secret key in auth payload for encrypted channel: ${this.name}`), null); return; } this.key = Object(base64["decode"])(sharedSecret); delete authData["shared_secret"]; callback(null, authData); }); } trigger(event, data) { throw new UnsupportedFeature("Client events are not currently supported for encrypted channels"); } handleEvent(event) { var eventName = event.event; var data = event.data; if (eventName.indexOf("pusher_internal:") === 0 || eventName.indexOf("pusher:") === 0) { super.handleEvent(event); return; } this.handleEncryptedEvent(eventName, data); } handleEncryptedEvent(event, data) { if (!this.key) { logger40.debug("Received encrypted event before key has been retrieved from the authEndpoint"); return; } if (!data.ciphertext || !data.nonce) { logger40.error("Unexpected format for encrypted event, expected object with `ciphertext` and `nonce` fields, got: " + data); return; } let cipherText = Object(base64["decode"])(data.ciphertext); if (cipherText.length < this.nacl.secretbox.overheadLength) { logger40.error(`Expected encrypted event ciphertext length to be ${this.nacl.secretbox.overheadLength}, got: ${cipherText.length}`); return; } let nonce = Object(base64["decode"])(data.nonce); if (nonce.length < this.nacl.secretbox.nonceLength) { logger40.error(`Expected encrypted event nonce length to be ${this.nacl.secretbox.nonceLength}, got: ${nonce.length}`); return; } let bytes = this.nacl.secretbox.open(cipherText, nonce, this.key); if (bytes === null) { logger40.debug("Failed to decrypt an event, probably because it was encrypted with a different key. Fetching a new key from the authEndpoint..."); this.authorize(this.pusher.connection.socket_id, (error40, authData) => { if (error40) { logger40.error(`Failed to make a request to the authEndpoint: ${authData}. Unable to fetch new key, so dropping encrypted event`); return; } bytes = this.nacl.secretbox.open(cipherText, nonce, this.key); if (bytes === null) { logger40.error(`Failed to decrypt event with new key. Dropping encrypted event`); return; } this.emit(event, this.getDataToEmit(bytes)); return; }); return; } this.emit(event, this.getDataToEmit(bytes)); } getDataToEmit(bytes) { let raw = Object(utf8["decode"])(bytes); try { return JSON.parse(raw); } catch (_a) { return raw; } } } class connection_manager_ConnectionManager extends dispatcher_Dispatcher { constructor(key, options) { super(); this.state = "initialized"; this.connection = null; this.key = key; this.options = options; this.timeline = this.options.timeline; this.usingTLS = this.options.useTLS; this.errorCallbacks = this.buildErrorCallbacks(); this.connectionCallbacks = this.buildConnectionCallbacks(this.errorCallbacks); this.handshakeCallbacks = this.buildHandshakeCallbacks(this.errorCallbacks); var Network = runtime.getNetwork(); Network.bind("online", () => { this.timeline.info({ netinfo: "online" }); if (this.state === "connecting" || this.state === "unavailable") { this.retryIn(0); } }); Network.bind("offline", () => { this.timeline.info({ netinfo: "offline" }); if (this.connection) { this.sendActivityCheck(); } }); this.updateStrategy(); } switchCluster(key) { this.key = key; this.updateStrategy(); this.retryIn(0); } connect() { if (this.connection || this.runner) { return; } if (!this.strategy.isSupported()) { this.updateState("failed"); return; } this.updateState("connecting"); this.startConnecting(); this.setUnavailableTimer(); } send(data) { if (this.connection) { return this.connection.send(data); } else { return false; } } send_event(name, data, channel) { if (this.connection) { return this.connection.send_event(name, data, channel); } else { return false; } } disconnect() { this.disconnectInternally(); this.updateState("disconnected"); } isUsingTLS() { return this.usingTLS; } startConnecting() { var callback = (error40, handshake) => { if (error40) { this.runner = this.strategy.connect(0, callback); } else { if (handshake.action === "error") { this.emit("error", { type: "HandshakeError", error: handshake.error }); this.timeline.error({ handshakeError: handshake.error }); } else { this.abortConnecting(); this.handshakeCallbacks[handshake.action](handshake); } } }; this.runner = this.strategy.connect(0, callback); } abortConnecting() { if (this.runner) { this.runner.abort(); this.runner = null; } } disconnectInternally() { this.abortConnecting(); this.clearRetryTimer(); this.clearUnavailableTimer(); if (this.connection) { var connection = this.abandonConnection(); connection.close(); } } updateStrategy() { this.strategy = this.options.getStrategy({ key: this.key, timeline: this.timeline, useTLS: this.usingTLS }); } retryIn(delay) { this.timeline.info({ action: "retry", delay }); if (delay > 0) { this.emit("connecting_in", Math.round(delay / 1e3)); } this.retryTimer = new timers_OneOffTimer(delay || 0, () => { this.disconnectInternally(); this.connect(); }); } clearRetryTimer() { if (this.retryTimer) { this.retryTimer.ensureAborted(); this.retryTimer = null; } } setUnavailableTimer() { this.unavailableTimer = new timers_OneOffTimer(this.options.unavailableTimeout, () => { this.updateState("unavailable"); }); } clearUnavailableTimer() { if (this.unavailableTimer) { this.unavailableTimer.ensureAborted(); } } sendActivityCheck() { this.stopActivityCheck(); this.connection.ping(); this.activityTimer = new timers_OneOffTimer(this.options.pongTimeout, () => { this.timeline.error({ pong_timed_out: this.options.pongTimeout }); this.retryIn(0); }); } resetActivityCheck() { this.stopActivityCheck(); if (this.connection && !this.connection.handlesActivityChecks()) { this.activityTimer = new timers_OneOffTimer(this.activityTimeout, () => { this.sendActivityCheck(); }); } } stopActivityCheck() { if (this.activityTimer) { this.activityTimer.ensureAborted(); } } buildConnectionCallbacks(errorCallbacks) { return extend({}, errorCallbacks, { message: (message) => { this.resetActivityCheck(); this.emit("message", message); }, ping: () => { this.send_event("pusher:pong", {}); }, activity: () => { this.resetActivityCheck(); }, error: (error40) => { this.emit("error", error40); }, closed: () => { this.abandonConnection(); if (this.shouldRetry()) { this.retryIn(1e3); } } }); } buildHandshakeCallbacks(errorCallbacks) { return extend({}, errorCallbacks, { connected: (handshake) => { this.activityTimeout = Math.min(this.options.activityTimeout, handshake.activityTimeout, handshake.connection.activityTimeout || Infinity); this.clearUnavailableTimer(); this.setConnection(handshake.connection); this.socket_id = this.connection.id; this.updateState("connected", { socket_id: this.socket_id }); } }); } buildErrorCallbacks() { let withErrorEmitted = (callback) => { return (result) => { if (result.error) { this.emit("error", { type: "WebSocketError", error: result.error }); } callback(result); }; }; return { tls_only: withErrorEmitted(() => { this.usingTLS = true; this.updateStrategy(); this.retryIn(0); }), refused: withErrorEmitted(() => { this.disconnect(); }), backoff: withErrorEmitted(() => { this.retryIn(1e3); }), retry: withErrorEmitted(() => { this.retryIn(0); }) }; } setConnection(connection) { this.connection = connection; for (var event in this.connectionCallbacks) { this.connection.bind(event, this.connectionCallbacks[event]); } this.resetActivityCheck(); } abandonConnection() { if (!this.connection) { return; } this.stopActivityCheck(); for (var event in this.connectionCallbacks) { this.connection.unbind(event, this.connectionCallbacks[event]); } var connection = this.connection; this.connection = null; return connection; } updateState(newState, data) { var previousState = this.state; this.state = newState; if (previousState !== newState) { var newStateDescription = newState; if (newStateDescription === "connected") { newStateDescription += " with new socket ID " + data.socket_id; } logger40.debug("State changed", previousState + " -> " + newStateDescription); this.timeline.info({ state: newState, params: data }); this.emit("state_change", { previous: previousState, current: newState }); this.emit(newState, data); } } shouldRetry() { return this.state === "connecting" || this.state === "connected"; } } class channels_Channels { constructor() { this.channels = {}; } add(name, pusher) { if (!this.channels[name]) { this.channels[name] = createChannel(name, pusher); } return this.channels[name]; } all() { return values(this.channels); } find(name) { return this.channels[name]; } remove(name) { var channel = this.channels[name]; delete this.channels[name]; return channel; } disconnect() { objectApply(this.channels, function(channel) { channel.disconnect(); }); } } function createChannel(name, pusher) { if (name.indexOf("private-encrypted-") === 0) { if (pusher.config.nacl) { return factory.createEncryptedChannel(name, pusher, pusher.config.nacl); } let errMsg = "Tried to subscribe to a private-encrypted- channel but no nacl implementation available"; let suffix = url_store.buildLogSuffix("encryptedChannelSupport"); throw new UnsupportedFeature(`${errMsg}. ${suffix}`); } else if (name.indexOf("private-") === 0) { return factory.createPrivateChannel(name, pusher); } else if (name.indexOf("presence-") === 0) { return factory.createPresenceChannel(name, pusher); } else if (name.indexOf("#") === 0) { throw new BadChannelName('Cannot create a channel with name "' + name + '".'); } else { return factory.createChannel(name, pusher); } } var Factory = { createChannels() { return new channels_Channels(); }, createConnectionManager(key, options) { return new connection_manager_ConnectionManager(key, options); }, createChannel(name, pusher) { return new channel_Channel(name, pusher); }, createPrivateChannel(name, pusher) { return new private_channel_PrivateChannel(name, pusher); }, createPresenceChannel(name, pusher) { return new presence_channel_PresenceChannel(name, pusher); }, createEncryptedChannel(name, pusher, nacl) { return new encrypted_channel_EncryptedChannel(name, pusher, nacl); }, createTimelineSender(timeline, options) { return new timeline_sender_TimelineSender(timeline, options); }, createHandshake(transport, callback) { return new handshake_Handshake(transport, callback); }, createAssistantToTheTransportManager(manager, transport, options) { return new assistant_to_the_transport_manager_AssistantToTheTransportManager(manager, transport, options); } }; var factory = Factory; class transport_manager_TransportManager { constructor(options) { this.options = options || {}; this.livesLeft = this.options.lives || Infinity; } getAssistant(transport) { return factory.createAssistantToTheTransportManager(this, transport, { minPingDelay: this.options.minPingDelay, maxPingDelay: this.options.maxPingDelay }); } isAlive() { return this.livesLeft > 0; } reportDeath() { this.livesLeft -= 1; } } class sequential_strategy_SequentialStrategy { constructor(strategies, options) { this.strategies = strategies; this.loop = Boolean(options.loop); this.failFast = Boolean(options.failFast); this.timeout = options.timeout; this.timeoutLimit = options.timeoutLimit; } isSupported() { return any(this.strategies, util.method("isSupported")); } connect(minPriority, callback) { var strategies = this.strategies; var current = 0; var timeout = this.timeout; var runner = null; var tryNextStrategy = (error40, handshake) => { if (handshake) { callback(null, handshake); } else { current = current + 1; if (this.loop) { current = current % strategies.length; } if (current < strategies.length) { if (timeout) { timeout = timeout * 2; if (this.timeoutLimit) { timeout = Math.min(timeout, this.timeoutLimit); } } runner = this.tryStrategy(strategies[current], minPriority, { timeout, failFast: this.failFast }, tryNextStrategy); } else { callback(true); } } }; runner = this.tryStrategy(strategies[current], minPriority, { timeout, failFast: this.failFast }, tryNextStrategy); return { abort: function() { runner.abort(); }, forceMinPriority: function(p) { minPriority = p; if (runner) { runner.forceMinPriority(p); } } }; } tryStrategy(strategy, minPriority, options, callback) { var timer = null; var runner = null; if (options.timeout > 0) { timer = new timers_OneOffTimer(options.timeout, function() { runner.abort(); callback(true); }); } runner = strategy.connect(minPriority, function(error40, handshake) { if (error40 && timer && timer.isRunning() && !options.failFast) { return; } if (timer) { timer.ensureAborted(); } callback(error40, handshake); }); return { abort: function() { if (timer) { timer.ensureAborted(); } runner.abort(); }, forceMinPriority: function(p) { runner.forceMinPriority(p); } }; } } class best_connected_ever_strategy_BestConnectedEverStrategy { constructor(strategies) { this.strategies = strategies; } isSupported() { return any(this.strategies, util.method("isSupported")); } connect(minPriority, callback) { return connect(this.strategies, minPriority, function(i, runners) { return function(error40, handshake) { runners[i].error = error40; if (error40) { if (allRunnersFailed(runners)) { callback(true); } return; } apply(runners, function(runner) { runner.forceMinPriority(handshake.transport.priority); }); callback(null, handshake); }; }); } } function connect(strategies, minPriority, callbackBuilder) { var runners = map(strategies, function(strategy, i, _, rs) { return strategy.connect(minPriority, callbackBuilder(i, rs)); }); return { abort: function() { apply(runners, abortRunner); }, forceMinPriority: function(p) { apply(runners, function(runner) { runner.forceMinPriority(p); }); } }; } function allRunnersFailed(runners) { return collections_all(runners, function(runner) { return Boolean(runner.error); }); } function abortRunner(runner) { if (!runner.error && !runner.aborted) { runner.abort(); runner.aborted = true; } } class websocket_prioritized_cached_strategy_WebSocketPrioritizedCachedStrategy { constructor(strategy, transports2, options) { this.strategy = strategy; this.transports = transports2; this.ttl = options.ttl || 1800 * 1e3; this.usingTLS = options.useTLS; this.timeline = options.timeline; } isSupported() { return this.strategy.isSupported(); } connect(minPriority, callback) { var usingTLS = this.usingTLS; var info37 = fetchTransportCache(usingTLS); var cacheSkipCount = info37 && info37.cacheSkipCount ? info37.cacheSkipCount : 0; var strategies = [this.strategy]; if (info37 && info37.timestamp + this.ttl >= util.now()) { var transport = this.transports[info37.transport]; if (transport) { if (["ws", "wss"].includes(info37.transport) || cacheSkipCount > 3) { this.timeline.info({ cached: true, transport: info37.transport, latency: info37.latency }); strategies.push(new sequential_strategy_SequentialStrategy([transport], { timeout: info37.latency * 2 + 1e3, failFast: true })); } else { cacheSkipCount++; } } } var startTimestamp = util.now(); var runner = strategies.pop().connect(minPriority, function cb(error40, handshake) { if (error40) { flushTransportCache(usingTLS); if (strategies.length > 0) { startTimestamp = util.now(); runner = strategies.pop().connect(minPriority, cb); } else { callback(error40); } } else { storeTransportCache(usingTLS, handshake.transport.name, util.now() - startTimestamp, cacheSkipCount); callback(null, handshake); } }); return { abort: function() { runner.abort(); }, forceMinPriority: function(p) { minPriority = p; if (runner) { runner.forceMinPriority(p); } } }; } } function getTransportCacheKey(usingTLS) { return "pusherTransport" + (usingTLS ? "TLS" : "NonTLS"); } function fetchTransportCache(usingTLS) { var storage = runtime.getLocalStorage(); if (storage) { try { var serializedCache = storage[getTransportCacheKey(usingTLS)]; if (serializedCache) { return JSON.parse(serializedCache); } } catch (e) { flushTransportCache(usingTLS); } } return null; } function storeTransportCache(usingTLS, transport, latency, cacheSkipCount) { var storage = runtime.getLocalStorage(); if (storage) { try { storage[getTransportCacheKey(usingTLS)] = safeJSONStringify({ timestamp: util.now(), transport, latency, cacheSkipCount }); } catch (e) { } } } function flushTransportCache(usingTLS) { var storage = runtime.getLocalStorage(); if (storage) { try { delete storage[getTransportCacheKey(usingTLS)]; } catch (e) { } } } class delayed_strategy_DelayedStrategy { constructor(strategy, { delay: number }) { this.strategy = strategy; this.options = { delay: number }; } isSupported() { return this.strategy.isSupported(); } connect(minPriority, callback) { var strategy = this.strategy; var runner; var timer = new timers_OneOffTimer(this.options.delay, function() { runner = strategy.connect(minPriority, callback); }); return { abort: function() { timer.ensureAborted(); if (runner) { runner.abort(); } }, forceMinPriority: function(p) { minPriority = p; if (runner) { runner.forceMinPriority(p); } } }; } } class IfStrategy { constructor(test, trueBranch, falseBranch) { this.test = test; this.trueBranch = trueBranch; this.falseBranch = falseBranch; } isSupported() { var branch = this.test() ? this.trueBranch : this.falseBranch; return branch.isSupported(); } connect(minPriority, callback) { var branch = this.test() ? this.trueBranch : this.falseBranch; return branch.connect(minPriority, callback); } } class FirstConnectedStrategy { constructor(strategy) { this.strategy = strategy; } isSupported() { return this.strategy.isSupported(); } connect(minPriority, callback) { var runner = this.strategy.connect(minPriority, function(error40, handshake) { if (handshake) { runner.abort(); } callback(error40, handshake); }); return runner; } } function testSupportsStrategy(strategy) { return function() { return strategy.isSupported(); }; } var getDefaultStrategy = function(config, baseOptions, defineTransport) { var definedTransports = {}; function defineTransportStrategy(name, type, priority, options, manager) { var transport = defineTransport(config, name, type, priority, options, manager); definedTransports[name] = transport; return transport; } var ws_options = Object.assign({}, baseOptions, { hostNonTLS: config.wsHost + ":" + config.wsPort, hostTLS: config.wsHost + ":" + config.wssPort, httpPath: config.wsPath }); var wss_options = Object.assign({}, ws_options, { useTLS: true }); var sockjs_options = Object.assign({}, baseOptions, { hostNonTLS: config.httpHost + ":" + config.httpPort, hostTLS: config.httpHost + ":" + config.httpsPort, httpPath: config.httpPath }); var timeouts = { loop: true, timeout: 15e3, timeoutLimit: 6e4 }; var ws_manager = new transport_manager_TransportManager({ minPingDelay: 1e4, maxPingDelay: config.activityTimeout }); var streaming_manager = new transport_manager_TransportManager({ lives: 2, minPingDelay: 1e4, maxPingDelay: config.activityTimeout }); var ws_transport = defineTransportStrategy("ws", "ws", 3, ws_options, ws_manager); var wss_transport = defineTransportStrategy("wss", "ws", 3, wss_options, ws_manager); var sockjs_transport = defineTransportStrategy("sockjs", "sockjs", 1, sockjs_options); var xhr_streaming_transport = defineTransportStrategy("xhr_streaming", "xhr_streaming", 1, sockjs_options, streaming_manager); var xdr_streaming_transport = defineTransportStrategy("xdr_streaming", "xdr_streaming", 1, sockjs_options, streaming_manager); var xhr_polling_transport = defineTransportStrategy("xhr_polling", "xhr_polling", 1, sockjs_options); var xdr_polling_transport = defineTransportStrategy("xdr_polling", "xdr_polling", 1, sockjs_options); var ws_loop = new sequential_strategy_SequentialStrategy([ws_transport], timeouts); var wss_loop = new sequential_strategy_SequentialStrategy([wss_transport], timeouts); var sockjs_loop = new sequential_strategy_SequentialStrategy([sockjs_transport], timeouts); var streaming_loop = new sequential_strategy_SequentialStrategy([ new IfStrategy(testSupportsStrategy(xhr_streaming_transport), xhr_streaming_transport, xdr_streaming_transport) ], timeouts); var polling_loop = new sequential_strategy_SequentialStrategy([ new IfStrategy(testSupportsStrategy(xhr_polling_transport), xhr_polling_transport, xdr_polling_transport) ], timeouts); var http_loop = new sequential_strategy_SequentialStrategy([ new IfStrategy(testSupportsStrategy(streaming_loop), new best_connected_ever_strategy_BestConnectedEverStrategy([ streaming_loop, new delayed_strategy_DelayedStrategy(polling_loop, { delay: 4e3 }) ]), polling_loop) ], timeouts); var http_fallback_loop = new IfStrategy(testSupportsStrategy(http_loop), http_loop, sockjs_loop); var wsStrategy; if (baseOptions.useTLS) { wsStrategy = new best_connected_ever_strategy_BestConnectedEverStrategy([ ws_loop, new delayed_strategy_DelayedStrategy(http_fallback_loop, { delay: 2e3 }) ]); } else { wsStrategy = new best_connected_ever_strategy_BestConnectedEverStrategy([ ws_loop, new delayed_strategy_DelayedStrategy(wss_loop, { delay: 2e3 }), new delayed_strategy_DelayedStrategy(http_fallback_loop, { delay: 5e3 }) ]); } return new websocket_prioritized_cached_strategy_WebSocketPrioritizedCachedStrategy(new FirstConnectedStrategy(new IfStrategy(testSupportsStrategy(ws_transport), wsStrategy, http_fallback_loop)), definedTransports, { ttl: 18e5, timeline: baseOptions.timeline, useTLS: baseOptions.useTLS }); }; var default_strategy = getDefaultStrategy; var transport_connection_initializer = function() { var self2 = this; self2.timeline.info(self2.buildTimelineMessage({ transport: self2.name + (self2.options.useTLS ? "s" : "") })); if (self2.hooks.isInitialized()) { self2.changeState("initialized"); } else if (self2.hooks.file) { self2.changeState("initializing"); Dependencies.load(self2.hooks.file, { useTLS: self2.options.useTLS }, function(error40, callback) { if (self2.hooks.isInitialized()) { self2.changeState("initialized"); callback(true); } else { if (error40) { self2.onError(error40); } self2.onClose(); callback(false); } }); } else { self2.onClose(); } }; var http_xdomain_request_hooks = { getRequest: function(socket) { var xdr = new window.XDomainRequest(); xdr.ontimeout = function() { socket.emit("error", new RequestTimedOut()); socket.close(); }; xdr.onerror = function(e) { socket.emit("error", e); socket.close(); }; xdr.onprogress = function() { if (xdr.responseText && xdr.responseText.length > 0) { socket.onChunk(200, xdr.responseText); } }; xdr.onload = function() { if (xdr.responseText && xdr.responseText.length > 0) { socket.onChunk(200, xdr.responseText); } socket.emit("finished", 200); socket.close(); }; return xdr; }, abortRequest: function(xdr) { xdr.ontimeout = xdr.onerror = xdr.onprogress = xdr.onload = null; xdr.abort(); } }; var http_xdomain_request = http_xdomain_request_hooks; const MAX_BUFFER_LENGTH = 256 * 1024; class http_request_HTTPRequest extends dispatcher_Dispatcher { constructor(hooks, method, url) { super(); this.hooks = hooks; this.method = method; this.url = url; } start(payload) { this.position = 0; this.xhr = this.hooks.getRequest(this); this.unloader = () => { this.close(); }; runtime.addUnloadListener(this.unloader); this.xhr.open(this.method, this.url, true); if (this.xhr.setRequestHeader) { this.xhr.setRequestHeader("Content-Type", "application/json"); } this.xhr.send(payload); } close() { if (this.unloader) { runtime.removeUnloadListener(this.unloader); this.unloader = null; } if (this.xhr) { this.hooks.abortRequest(this.xhr); this.xhr = null; } } onChunk(status, data) { while (true) { var chunk = this.advanceBuffer(data); if (chunk) { this.emit("chunk", { status, data: chunk }); } else { break; } } if (this.isBufferTooLong(data)) { this.emit("buffer_too_long"); } } advanceBuffer(buffer) { var unreadData = buffer.slice(this.position); var endOfLinePosition = unreadData.indexOf("\n"); if (endOfLinePosition !== -1) { this.position += endOfLinePosition + 1; return unreadData.slice(0, endOfLinePosition); } else { return null; } } isBufferTooLong(buffer) { return this.position === buffer.length && buffer.length > MAX_BUFFER_LENGTH; } } var State; (function(State2) { State2[State2["CONNECTING"] = 0] = "CONNECTING"; State2[State2["OPEN"] = 1] = "OPEN"; State2[State2["CLOSED"] = 3] = "CLOSED"; })(State || (State = {})); var state = State; var autoIncrement = 1; class http_socket_HTTPSocket { constructor(hooks, url) { this.hooks = hooks; this.session = randomNumber(1e3) + "/" + randomString(8); this.location = getLocation(url); this.readyState = state.CONNECTING; this.openStream(); } send(payload) { return this.sendRaw(JSON.stringify([payload])); } ping() { this.hooks.sendHeartbeat(this); } close(code, reason) { this.onClose(code, reason, true); } sendRaw(payload) { if (this.readyState === state.OPEN) { try { runtime.createSocketRequest("POST", getUniqueURL(getSendURL(this.location, this.session))).start(payload); return true; } catch (e) { return false; } } else { return false; } } reconnect() { this.closeStream(); this.openStream(); } onClose(code, reason, wasClean) { this.closeStream(); this.readyState = state.CLOSED; if (this.onclose) { this.onclose({ code, reason, wasClean }); } } onChunk(chunk) { if (chunk.status !== 200) { return; } if (this.readyState === state.OPEN) { this.onActivity(); } var payload; var type = chunk.data.slice(0, 1); switch (type) { case "o": payload = JSON.parse(chunk.data.slice(1) || "{}"); this.onOpen(payload); break; case "a": payload = JSON.parse(chunk.data.slice(1) || "[]"); for (var i = 0; i < payload.length; i++) { this.onEvent(payload[i]); } break; case "m": payload = JSON.parse(chunk.data.slice(1) || "null"); this.onEvent(payload); break; case "h": this.hooks.onHeartbeat(this); break; case "c": payload = JSON.parse(chunk.data.slice(1) || "[]"); this.onClose(payload[0], payload[1], true); break; } } onOpen(options) { if (this.readyState === state.CONNECTING) { if (options && options.hostname) { this.location.base = replaceHost(this.location.base, options.hostname); } this.readyState = state.OPEN; if (this.onopen) { this.onopen(); } } else { this.onClose(1006, "Server lost session", true); } } onEvent(event) { if (this.readyState === state.OPEN && this.onmessage) { this.onmessage({ data: event }); } } onActivity() { if (this.onactivity) { this.onactivity(); } } onError(error40) { if (this.onerror) { this.onerror(error40); } } openStream() { this.stream = runtime.createSocketRequest("POST", getUniqueURL(this.hooks.getReceiveURL(this.location, this.session))); this.stream.bind("chunk", (chunk) => { this.onChunk(chunk); }); this.stream.bind("finished", (status) => { this.hooks.onFinished(this, status); }); this.stream.bind("buffer_too_long", () => { this.reconnect(); }); try { this.stream.start(); } catch (error40) { util.defer(() => { this.onError(error40); this.onClose(1006, "Could not start streaming", false); }); } } closeStream() { if (this.stream) { this.stream.unbind_all(); this.stream.close(); this.stream = null; } } } function getLocation(url) { var parts = /([^\?]*)\/*(\??.*)/.exec(url); return { base: parts[1], queryString: parts[2] }; } function getSendURL(url, session) { return url.base + "/" + session + "/xhr_send"; } function getUniqueURL(url) { var separator = url.indexOf("?") === -1 ? "?" : "&"; return url + separator + "t=" + +/* @__PURE__ */ new Date() + "&n=" + autoIncrement++; } function replaceHost(url, hostname) { var urlParts = /(https?:\/\/)([^\/:]+)((\/|:)?.*)/.exec(url); return urlParts[1] + hostname + urlParts[3]; } function randomNumber(max) { return runtime.randomInt(max); } function randomString(length) { var result = []; for (var i = 0; i < length; i++) { result.push(randomNumber(32).toString(32)); } return result.join(""); } var http_socket = http_socket_HTTPSocket; var http_streaming_socket_hooks = { getReceiveURL: function(url, session) { return url.base + "/" + session + "/xhr_streaming" + url.queryString; }, onHeartbeat: function(socket) { socket.sendRaw("[]"); }, sendHeartbeat: function(socket) { socket.sendRaw("[]"); }, onFinished: function(socket, status) { socket.onClose(1006, "Connection interrupted (" + status + ")", false); } }; var http_streaming_socket = http_streaming_socket_hooks; var http_polling_socket_hooks = { getReceiveURL: function(url, session) { return url.base + "/" + session + "/xhr" + url.queryString; }, onHeartbeat: function() { }, sendHeartbeat: function(socket) { socket.sendRaw("[]"); }, onFinished: function(socket, status) { if (status === 200) { socket.reconnect(); } else { socket.onClose(1006, "Connection interrupted (" + status + ")", false); } } }; var http_polling_socket = http_polling_socket_hooks; var http_xhr_request_hooks = { getRequest: function(socket) { var Constructor = runtime.getXHRAPI(); var xhr = new Constructor(); xhr.onreadystatechange = xhr.onprogress = function() { switch (xhr.readyState) { case 3: if (xhr.responseText && xhr.responseText.length > 0) { socket.onChunk(xhr.status, xhr.responseText); } break; case 4: if (xhr.responseText && xhr.responseText.length > 0) { socket.onChunk(xhr.status, xhr.responseText); } socket.emit("finished", xhr.status); socket.close(); break; } }; return xhr; }, abortRequest: function(xhr) { xhr.onreadystatechange = null; xhr.abort(); } }; var http_xhr_request = http_xhr_request_hooks; var HTTP = { createStreamingSocket(url) { return this.createSocket(http_streaming_socket, url); }, createPollingSocket(url) { return this.createSocket(http_polling_socket, url); }, createSocket(hooks, url) { return new http_socket(hooks, url); }, createXHR(method, url) { return this.createRequest(http_xhr_request, method, url); }, createRequest(hooks, method, url) { return new http_request_HTTPRequest(hooks, method, url); } }; var http_http = HTTP; http_http.createXDR = function(method, url) { return this.createRequest(http_xdomain_request, method, url); }; var web_http_http = http_http; var Runtime = { nextAuthCallbackID: 1, auth_callbacks: {}, ScriptReceivers, DependenciesReceivers, getDefaultStrategy: default_strategy, Transports: transports_transports, transportConnectionInitializer: transport_connection_initializer, HTTPFactory: web_http_http, TimelineTransport: jsonp_timeline, getXHRAPI() { return window.XMLHttpRequest; }, getWebSocketAPI() { return window.WebSocket || window.MozWebSocket; }, setup(PusherClass) { window.Pusher = PusherClass; var initializeOnDocumentBody = () => { this.onDocumentBody(PusherClass.ready); }; if (!window.JSON) { Dependencies.load("json2", {}, initializeOnDocumentBody); } else { initializeOnDocumentBody(); } }, getDocument() { return document; }, getProtocol() { return this.getDocument().location.protocol; }, getAuthorizers() { return { ajax: xhr_auth, jsonp: jsonp_auth }; }, onDocumentBody(callback) { if (document.body) { callback(); } else { setTimeout(() => { this.onDocumentBody(callback); }, 0); } }, createJSONPRequest(url, data) { return new jsonp_request_JSONPRequest(url, data); }, createScriptRequest(src) { return new ScriptRequest(src); }, getLocalStorage() { try { return window.localStorage; } catch (e) { return void 0; } }, createXHR() { if (this.getXHRAPI()) { return this.createXMLHttpRequest(); } else { return this.createMicrosoftXHR(); } }, createXMLHttpRequest() { var Constructor = this.getXHRAPI(); return new Constructor(); }, createMicrosoftXHR() { return new ActiveXObject("Microsoft.XMLHTTP"); }, getNetwork() { return net_info_Network; }, createWebSocket(url) { var Constructor = this.getWebSocketAPI(); return new Constructor(url); }, createSocketRequest(method, url) { if (this.isXHRSupported()) { return this.HTTPFactory.createXHR(method, url); } else if (this.isXDRSupported(url.indexOf("https:") === 0)) { return this.HTTPFactory.createXDR(method, url); } else { throw "Cross-origin HTTP requests are not supported"; } }, isXHRSupported() { var Constructor = this.getXHRAPI(); return Boolean(Constructor) && new Constructor().withCredentials !== void 0; }, isXDRSupported(useTLS) { var protocol = useTLS ? "https:" : "http:"; var documentProtocol = this.getProtocol(); return Boolean(window["XDomainRequest"]) && documentProtocol === protocol; }, addUnloadListener(listener) { if (window.addEventListener !== void 0) { window.addEventListener("unload", listener, false); } else if (window.attachEvent !== void 0) { window.attachEvent("onunload", listener); } }, removeUnloadListener(listener) { if (window.addEventListener !== void 0) { window.removeEventListener("unload", listener, false); } else if (window.detachEvent !== void 0) { window.detachEvent("onunload", listener); } }, randomInt(max) { const random = function() { const crypto2 = window.crypto || window["msCrypto"]; const random2 = crypto2.getRandomValues(new Uint32Array(1))[0]; return random2 / Math.pow(2, 32); }; return Math.floor(random() * max); } }; var runtime = Runtime; var TimelineLevel; (function(TimelineLevel2) { TimelineLevel2[TimelineLevel2["ERROR"] = 3] = "ERROR"; TimelineLevel2[TimelineLevel2["INFO"] = 6] = "INFO"; TimelineLevel2[TimelineLevel2["DEBUG"] = 7] = "DEBUG"; })(TimelineLevel || (TimelineLevel = {})); var timeline_level = TimelineLevel; class timeline_Timeline { constructor(key, session, options) { this.key = key; this.session = session; this.events = []; this.options = options || {}; this.sent = 0; this.uniqueID = 0; } log(level, event) { if (level <= this.options.level) { this.events.push(extend({}, event, { timestamp: util.now() })); if (this.options.limit && this.events.length > this.options.limit) { this.events.shift(); } } } error(event) { this.log(timeline_level.ERROR, event); } info(event) { this.log(timeline_level.INFO, event); } debug(event) { this.log(timeline_level.DEBUG, event); } isEmpty() { return this.events.length === 0; } send(sendfn, callback) { var data = extend({ session: this.session, bundle: this.sent + 1, key: this.key, lib: "js", version: this.options.version, cluster: this.options.cluster, features: this.options.features, timeline: this.events }, this.options.params); this.events = []; sendfn(data, (error40, result) => { if (!error40) { this.sent++; } if (callback) { callback(error40, result); } }); return true; } generateUniqueID() { this.uniqueID++; return this.uniqueID; } } class transport_strategy_TransportStrategy { constructor(name, priority, transport, options) { this.name = name; this.priority = priority; this.transport = transport; this.options = options || {}; } isSupported() { return this.transport.isSupported({ useTLS: this.options.useTLS }); } connect(minPriority, callback) { if (!this.isSupported()) { return failAttempt(new UnsupportedStrategy(), callback); } else if (this.priority < minPriority) { return failAttempt(new TransportPriorityTooLow(), callback); } var connected = false; var transport = this.transport.createConnection(this.name, this.priority, this.options.key, this.options); var handshake = null; var onInitialized = function() { transport.unbind("initialized", onInitialized); transport.connect(); }; var onOpen = function() { handshake = factory.createHandshake(transport, function(result) { connected = true; unbindListeners(); callback(null, result); }); }; var onError = function(error40) { unbindListeners(); callback(error40); }; var onClosed = function() { unbindListeners(); var serializedTransport; serializedTransport = safeJSONStringify(transport); callback(new TransportClosed(serializedTransport)); }; var unbindListeners = function() { transport.unbind("initialized", onInitialized); transport.unbind("open", onOpen); transport.unbind("error", onError); transport.unbind("closed", onClosed); }; transport.bind("initialized", onInitialized); transport.bind("open", onOpen); transport.bind("error", onError); transport.bind("closed", onClosed); transport.initialize(); return { abort: () => { if (connected) { return; } unbindListeners(); if (handshake) { handshake.close(); } else { transport.close(); } }, forceMinPriority: (p) => { if (connected) { return; } if (this.priority < p) { if (handshake) { handshake.close(); } else { transport.close(); } } } }; } } function failAttempt(error40, callback) { util.defer(function() { callback(error40); }); return { abort: function() { }, forceMinPriority: function() { } }; } const { Transports: strategy_builder_Transports } = runtime; var strategy_builder_defineTransport = function(config, name, type, priority, options, manager) { var transportClass = strategy_builder_Transports[type]; if (!transportClass) { throw new UnsupportedTransport(type); } var enabled = (!config.enabledTransports || arrayIndexOf(config.enabledTransports, name) !== -1) && (!config.disabledTransports || arrayIndexOf(config.disabledTransports, name) === -1); var transport; if (enabled) { options = Object.assign({ ignoreNullOrigin: config.ignoreNullOrigin }, options); transport = new transport_strategy_TransportStrategy(name, priority, manager ? manager.getAssistant(transportClass) : transportClass, options); } else { transport = strategy_builder_UnsupportedStrategy; } return transport; }; var strategy_builder_UnsupportedStrategy = { isSupported: function() { return false; }, connect: function(_, callback) { var deferred = util.defer(function() { callback(new UnsupportedStrategy()); }); return { abort: function() { deferred.ensureAborted(); }, forceMinPriority: function() { } }; } }; function validateOptions(options) { if (options == null) { throw "You must pass an options object"; } if (options.cluster == null) { throw "Options object must provide a cluster"; } if ("disableStats" in options) { logger40.warn("The disableStats option is deprecated in favor of enableStats"); } } const composeChannelQuery = (params, authOptions) => { var query = "socket_id=" + encodeURIComponent(params.socketId); for (var key in authOptions.params) { query += "&" + encodeURIComponent(key) + "=" + encodeURIComponent(authOptions.params[key]); } if (authOptions.paramsProvider != null) { let dynamicParams = authOptions.paramsProvider(); for (var key in dynamicParams) { query += "&" + encodeURIComponent(key) + "=" + encodeURIComponent(dynamicParams[key]); } } return query; }; const UserAuthenticator = (authOptions) => { if (typeof runtime.getAuthorizers()[authOptions.transport] === "undefined") { throw `'${authOptions.transport}' is not a recognized auth transport`; } return (params, callback) => { const query = composeChannelQuery(params, authOptions); runtime.getAuthorizers()[authOptions.transport](runtime, query, authOptions, AuthRequestType.UserAuthentication, callback); }; }; var user_authenticator = UserAuthenticator; const channel_authorizer_composeChannelQuery = (params, authOptions) => { var query = "socket_id=" + encodeURIComponent(params.socketId); query += "&channel_name=" + encodeURIComponent(params.channelName); for (var key in authOptions.params) { query += "&" + encodeURIComponent(key) + "=" + encodeURIComponent(authOptions.params[key]); } if (authOptions.paramsProvider != null) { let dynamicParams = authOptions.paramsProvider(); for (var key in dynamicParams) { query += "&" + encodeURIComponent(key) + "=" + encodeURIComponent(dynamicParams[key]); } } return query; }; const ChannelAuthorizer = (authOptions) => { if (typeof runtime.getAuthorizers()[authOptions.transport] === "undefined") { throw `'${authOptions.transport}' is not a recognized auth transport`; } return (params, callback) => { const query = channel_authorizer_composeChannelQuery(params, authOptions); runtime.getAuthorizers()[authOptions.transport](runtime, query, authOptions, AuthRequestType.ChannelAuthorization, callback); }; }; var channel_authorizer = ChannelAuthorizer; const ChannelAuthorizerProxy = (pusher, authOptions, channelAuthorizerGenerator) => { const deprecatedAuthorizerOptions = { authTransport: authOptions.transport, authEndpoint: authOptions.endpoint, auth: { params: authOptions.params, headers: authOptions.headers } }; return (params, callback) => { const channel = pusher.channel(params.channelName); const channelAuthorizer = channelAuthorizerGenerator(channel, deprecatedAuthorizerOptions); channelAuthorizer.authorize(params.socketId, callback); }; }; function getConfig(opts, pusher) { let config = { activityTimeout: opts.activityTimeout || defaults.activityTimeout, cluster: opts.cluster, httpPath: opts.httpPath || defaults.httpPath, httpPort: opts.httpPort || defaults.httpPort, httpsPort: opts.httpsPort || defaults.httpsPort, pongTimeout: opts.pongTimeout || defaults.pongTimeout, statsHost: opts.statsHost || defaults.stats_host, unavailableTimeout: opts.unavailableTimeout || defaults.unavailableTimeout, wsPath: opts.wsPath || defaults.wsPath, wsPort: opts.wsPort || defaults.wsPort, wssPort: opts.wssPort || defaults.wssPort, enableStats: getEnableStatsConfig(opts), httpHost: getHttpHost(opts), useTLS: shouldUseTLS(opts), wsHost: getWebsocketHost(opts), userAuthenticator: buildUserAuthenticator(opts), channelAuthorizer: buildChannelAuthorizer(opts, pusher) }; if ("disabledTransports" in opts) config.disabledTransports = opts.disabledTransports; if ("enabledTransports" in opts) config.enabledTransports = opts.enabledTransports; if ("ignoreNullOrigin" in opts) config.ignoreNullOrigin = opts.ignoreNullOrigin; if ("timelineParams" in opts) config.timelineParams = opts.timelineParams; if ("nacl" in opts) { config.nacl = opts.nacl; } return config; } function getHttpHost(opts) { if (opts.httpHost) { return opts.httpHost; } if (opts.cluster) { return `sockjs-${opts.cluster}.pusher.com`; } return defaults.httpHost; } function getWebsocketHost(opts) { if (opts.wsHost) { return opts.wsHost; } return getWebsocketHostFromCluster(opts.cluster); } function getWebsocketHostFromCluster(cluster) { return `ws-${cluster}.pusher.com`; } function shouldUseTLS(opts) { if (runtime.getProtocol() === "https:") { return true; } else if (opts.forceTLS === false) { return false; } return true; } function getEnableStatsConfig(opts) { if ("enableStats" in opts) { return opts.enableStats; } if ("disableStats" in opts) { return !opts.disableStats; } return false; } const hasCustomHandler = (auth) => { return "customHandler" in auth && auth["customHandler"] != null; }; function buildUserAuthenticator(opts) { const userAuthentication = Object.assign(Object.assign({}, defaults.userAuthentication), opts.userAuthentication); if (hasCustomHandler(userAuthentication)) { return userAuthentication["customHandler"]; } return user_authenticator(userAuthentication); } function buildChannelAuth(opts, pusher) { let channelAuthorization; if ("channelAuthorization" in opts) { channelAuthorization = Object.assign(Object.assign({}, defaults.channelAuthorization), opts.channelAuthorization); } else { channelAuthorization = { transport: opts.authTransport || defaults.authTransport, endpoint: opts.authEndpoint || defaults.authEndpoint }; if ("auth" in opts) { if ("params" in opts.auth) channelAuthorization.params = opts.auth.params; if ("headers" in opts.auth) channelAuthorization.headers = opts.auth.headers; } if ("authorizer" in opts) { return { customHandler: ChannelAuthorizerProxy(pusher, channelAuthorization, opts.authorizer) }; } } return channelAuthorization; } function buildChannelAuthorizer(opts, pusher) { const channelAuthorization = buildChannelAuth(opts, pusher); if (hasCustomHandler(channelAuthorization)) { return channelAuthorization["customHandler"]; } return channel_authorizer(channelAuthorization); } class watchlist_WatchlistFacade extends dispatcher_Dispatcher { constructor(pusher) { super(function(eventName, data) { logger40.debug(`No callbacks on watchlist events for ${eventName}`); }); this.pusher = pusher; this.bindWatchlistInternalEvent(); } handleEvent(pusherEvent) { pusherEvent.data.events.forEach((watchlistEvent) => { this.emit(watchlistEvent.name, watchlistEvent); }); } bindWatchlistInternalEvent() { this.pusher.connection.bind("message", (pusherEvent) => { var eventName = pusherEvent.event; if (eventName === "pusher_internal:watchlist_events") { this.handleEvent(pusherEvent); } }); } } function flatPromise() { let resolve, reject; const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); return { promise, resolve, reject }; } var flat_promise = flatPromise; class user_UserFacade extends dispatcher_Dispatcher { constructor(pusher) { super(function(eventName, data) { logger40.debug("No callbacks on user for " + eventName); }); this.signin_requested = false; this.user_data = null; this.serverToUserChannel = null; this.signinDonePromise = null; this._signinDoneResolve = null; this._onAuthorize = (err, authData) => { if (err) { logger40.warn(`Error during signin: ${err}`); this._cleanup(); return; } this.pusher.send_event("pusher:signin", { auth: authData.auth, user_data: authData.user_data }); }; this.pusher = pusher; this.pusher.connection.bind("state_change", ({ previous, current }) => { if (previous !== "connected" && current === "connected") { this._signin(); } if (previous === "connected" && current !== "connected") { this._cleanup(); this._newSigninPromiseIfNeeded(); } }); this.watchlist = new watchlist_WatchlistFacade(pusher); this.pusher.connection.bind("message", (event) => { var eventName = event.event; if (eventName === "pusher:signin_success") { this._onSigninSuccess(event.data); } if (this.serverToUserChannel && this.serverToUserChannel.name === event.channel) { this.serverToUserChannel.handleEvent(event); } }); } signin() { if (this.signin_requested) { return; } this.signin_requested = true; this._signin(); } _signin() { if (!this.signin_requested) { return; } this._newSigninPromiseIfNeeded(); if (this.pusher.connection.state !== "connected") { return; } this.pusher.config.userAuthenticator({ socketId: this.pusher.connection.socket_id }, this._onAuthorize); } _onSigninSuccess(data) { try { this.user_data = JSON.parse(data.user_data); } catch (e) { logger40.error(`Failed parsing user data after signin: ${data.user_data}`); this._cleanup(); return; } if (typeof this.user_data.id !== "string" || this.user_data.id === "") { logger40.error(`user_data doesn't contain an id. user_data: ${this.user_data}`); this._cleanup(); return; } this._signinDoneResolve(); this._subscribeChannels(); } _subscribeChannels() { const ensure_subscribed = (channel) => { if (channel.subscriptionPending && channel.subscriptionCancelled) { channel.reinstateSubscription(); } else if (!channel.subscriptionPending && this.pusher.connection.state === "connected") { channel.subscribe(); } }; this.serverToUserChannel = new channel_Channel(`#server-to-user-${this.user_data.id}`, this.pusher); this.serverToUserChannel.bind_global((eventName, data) => { if (eventName.indexOf("pusher_internal:") === 0 || eventName.indexOf("pusher:") === 0) { return; } this.emit(eventName, data); }); ensure_subscribed(this.serverToUserChannel); } _cleanup() { this.user_data = null; if (this.serverToUserChannel) { this.serverToUserChannel.unbind_all(); this.serverToUserChannel.disconnect(); this.serverToUserChannel = null; } if (this.signin_requested) { this._signinDoneResolve(); } } _newSigninPromiseIfNeeded() { if (!this.signin_requested) { return; } if (this.signinDonePromise && !this.signinDonePromise.done) { return; } const { promise, resolve, reject: _ } = flat_promise(); promise.done = false; const setDone = () => { promise.done = true; }; promise.then(setDone).catch(setDone); this.signinDonePromise = promise; this._signinDoneResolve = resolve; } } class pusher_Pusher { static ready() { pusher_Pusher.isReady = true; for (var i = 0, l2 = pusher_Pusher.instances.length; i < l2; i++) { pusher_Pusher.instances[i].connect(); } } static getClientFeatures() { return keys(filterObject({ ws: runtime.Transports.ws }, function(t) { return t.isSupported({}); })); } constructor(app_key, options) { checkAppKey(app_key); validateOptions(options); this.key = app_key; this.options = options; this.config = getConfig(this.options, this); this.channels = factory.createChannels(); this.global_emitter = new dispatcher_Dispatcher(); this.sessionID = runtime.randomInt(1e9); this.timeline = new timeline_Timeline(this.key, this.sessionID, { cluster: this.config.cluster, features: pusher_Pusher.getClientFeatures(), params: this.config.timelineParams || {}, limit: 50, level: timeline_level.INFO, version: defaults.VERSION }); if (this.config.enableStats) { this.timelineSender = factory.createTimelineSender(this.timeline, { host: this.config.statsHost, path: "/timeline/v2/" + runtime.TimelineTransport.name }); } var getStrategy = (options2) => { return runtime.getDefaultStrategy(this.config, options2, strategy_builder_defineTransport); }; this.connection = factory.createConnectionManager(this.key, { getStrategy, timeline: this.timeline, activityTimeout: this.config.activityTimeout, pongTimeout: this.config.pongTimeout, unavailableTimeout: this.config.unavailableTimeout, useTLS: Boolean(this.config.useTLS) }); this.connection.bind("connected", () => { this.subscribeAll(); if (this.timelineSender) { this.timelineSender.send(this.connection.isUsingTLS()); } }); this.connection.bind("message", (event) => { var eventName = event.event; var internal = eventName.indexOf("pusher_internal:") === 0; if (event.channel) { var channel = this.channel(event.channel); if (channel) { channel.handleEvent(event); } } if (!internal) { this.global_emitter.emit(event.event, event.data); } }); this.connection.bind("connecting", () => { this.channels.disconnect(); }); this.connection.bind("disconnected", () => { this.channels.disconnect(); }); this.connection.bind("error", (err) => { logger40.warn(err); }); pusher_Pusher.instances.push(this); this.timeline.info({ instances: pusher_Pusher.instances.length }); this.user = new user_UserFacade(this); if (pusher_Pusher.isReady) { this.connect(); } } switchCluster(options) { const { appKey, cluster } = options; this.key = appKey; this.options = Object.assign(Object.assign({}, this.options), { cluster }); this.config = getConfig(this.options, this); this.connection.switchCluster(this.key); } channel(name) { return this.channels.find(name); } allChannels() { return this.channels.all(); } connect() { this.connection.connect(); if (this.timelineSender) { if (!this.timelineSenderTimer) { var usingTLS = this.connection.isUsingTLS(); var timelineSender = this.timelineSender; this.timelineSenderTimer = new timers_PeriodicTimer(6e4, function() { timelineSender.send(usingTLS); }); } } } disconnect() { this.connection.disconnect(); if (this.timelineSenderTimer) { this.timelineSenderTimer.ensureAborted(); this.timelineSenderTimer = null; } } bind(event_name, callback, context) { this.global_emitter.bind(event_name, callback, context); return this; } unbind(event_name, callback, context) { this.global_emitter.unbind(event_name, callback, context); return this; } bind_global(callback) { this.global_emitter.bind_global(callback); return this; } unbind_global(callback) { this.global_emitter.unbind_global(callback); return this; } unbind_all(callback) { this.global_emitter.unbind_all(); return this; } subscribeAll() { var channelName; for (channelName in this.channels.channels) { if (this.channels.channels.hasOwnProperty(channelName)) { this.subscribe(channelName); } } } subscribe(channel_name) { var channel = this.channels.add(channel_name, this); if (channel.subscriptionPending && channel.subscriptionCancelled) { channel.reinstateSubscription(); } else if (!channel.subscriptionPending && this.connection.state === "connected") { channel.subscribe(); } return channel; } unsubscribe(channel_name) { var channel = this.channels.find(channel_name); if (channel && channel.subscriptionPending) { channel.cancelSubscription(); } else { channel = this.channels.remove(channel_name); if (channel && channel.subscribed) { channel.unsubscribe(); } } } send_event(event_name, data, channel) { return this.connection.send_event(event_name, data, channel); } shouldUseTLS() { return this.config.useTLS; } signin() { this.user.signin(); } } pusher_Pusher.instances = []; pusher_Pusher.isReady = false; pusher_Pusher.logToConsole = false; pusher_Pusher.Runtime = runtime; pusher_Pusher.ScriptReceivers = runtime.ScriptReceivers; pusher_Pusher.DependenciesReceivers = runtime.DependenciesReceivers; pusher_Pusher.auth_callbacks = runtime.auth_callbacks; var core_pusher = __webpack_exports__["default"] = pusher_Pusher; function checkAppKey(key) { if (key === null || key === void 0) { throw "You must pass your app key when you instantiate Pusher."; } } runtime.setup(pusher_Pusher); } /******/ ]) ); }); } }); // node_modules/@twemoji/parser/dist/lib/regex.js var require_regex = __commonJS({ "node_modules/@twemoji/parser/dist/lib/regex.js"(exports) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = /(?:\ud83d\udc68\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffc-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb\udffd-\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb\udffc\udffe\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb-\udffd\udfff]|\ud83e\uddd1\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb-\udffe]|\ud83d\udc68\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc68\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc68\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffe]|\ud83e\uddd1\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffc-\udfff]|\ud83e\uddd1\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb\udffd-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb\udffc\udffe\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb-\udffd\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb-\udffe]|\ud83e\uddd1\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d[\udc68\udc69]|\ud83e\udef1\ud83c\udffb\u200d\ud83e\udef2\ud83c[\udffc-\udfff]|\ud83e\udef1\ud83c\udffc\u200d\ud83e\udef2\ud83c[\udffb\udffd-\udfff]|\ud83e\udef1\ud83c\udffd\u200d\ud83e\udef2\ud83c[\udffb\udffc\udffe\udfff]|\ud83e\udef1\ud83c\udffe\u200d\ud83e\udef2\ud83c[\udffb-\udffd\udfff]|\ud83e\udef1\ud83c\udfff\u200d\ud83e\udef2\ud83c[\udffb-\udffe]|\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc68|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d[\udc68\udc69]|\ud83e\uddd1\u200d\ud83e\udd1d\u200d\ud83e\uddd1|\ud83d\udc6b\ud83c[\udffb-\udfff]|\ud83d\udc6c\ud83c[\udffb-\udfff]|\ud83d\udc6d\ud83c[\udffb-\udfff]|\ud83d\udc8f\ud83c[\udffb-\udfff]|\ud83d\udc91\ud83c[\udffb-\udfff]|\ud83e\udd1d\ud83c[\udffb-\udfff]|\ud83d[\udc6b-\udc6d\udc8f\udc91]|\ud83e\udd1d)|(?:\ud83d[\udc68\udc69]|\ud83e\uddd1)(?:\ud83c[\udffb-\udfff])?\u200d(?:\u2695\ufe0f|\u2696\ufe0f|\u2708\ufe0f|\ud83c[\udf3e\udf73\udf7c\udf84\udf93\udfa4\udfa8\udfeb\udfed]|\ud83d[\udcbb\udcbc\udd27\udd2c\ude80\ude92]|\ud83e[\uddaf-\uddb3\uddbc\uddbd])(?:\u200d\u27a1\ufe0f)?|(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75]|\u26f9)((?:\ud83c[\udffb-\udfff]|\ufe0f)\u200d[\u2640\u2642]\ufe0f(?:\u200d\u27a1\ufe0f)?)|(?:\ud83c[\udfc3\udfc4\udfca]|\ud83d[\udc6e\udc70\udc71\udc73\udc77\udc81\udc82\udc86\udc87\ude45-\ude47\ude4b\ude4d\ude4e\udea3\udeb4-\udeb6]|\ud83e[\udd26\udd35\udd37-\udd39\udd3d\udd3e\uddb8\uddb9\uddcd-\uddcf\uddd4\uddd6-\udddd])(?:\ud83c[\udffb-\udfff])?\u200d[\u2640\u2642]\ufe0f(?:\u200d\u27a1\ufe0f)?|(?:\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83e\uddd1\u200d\ud83e\uddd1\u200d\ud83e\uddd2\u200d\ud83e\uddd2|\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83e\uddd1\u200d\ud83e\uddd1\u200d\ud83e\uddd2|\ud83e\uddd1\u200d\ud83e\uddd2\u200d\ud83e\uddd2|\ud83c\udff3\ufe0f\u200d\u26a7\ufe0f|\ud83c\udff3\ufe0f\u200d\ud83c\udf08|\ud83d\ude36\u200d\ud83c\udf2b\ufe0f|\u26d3\ufe0f\u200d\ud83d\udca5|\u2764\ufe0f\u200d\ud83d\udd25|\u2764\ufe0f\u200d\ud83e\ude79|\ud83c\udf44\u200d\ud83d\udfeb|\ud83c\udf4b\u200d\ud83d\udfe9|\ud83c\udff4\u200d\u2620\ufe0f|\ud83d\udc15\u200d\ud83e\uddba|\ud83d\udc26\u200d\ud83d\udd25|\ud83d\udc3b\u200d\u2744\ufe0f|\ud83d\udc41\u200d\ud83d\udde8|\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc6f\u200d\u2640\ufe0f|\ud83d\udc6f\u200d\u2642\ufe0f|\ud83d\ude2e\u200d\ud83d\udca8|\ud83d\ude35\u200d\ud83d\udcab|\ud83d\ude42\u200d\u2194\ufe0f|\ud83d\ude42\u200d\u2195\ufe0f|\ud83e\udd3c\u200d\u2640\ufe0f|\ud83e\udd3c\u200d\u2642\ufe0f|\ud83e\uddd1\u200d\ud83e\uddd2|\ud83e\uddde\u200d\u2640\ufe0f|\ud83e\uddde\u200d\u2642\ufe0f|\ud83e\udddf\u200d\u2640\ufe0f|\ud83e\udddf\u200d\u2642\ufe0f|\ud83d\udc08\u200d\u2b1b|\ud83d\udc26\u200d\u2b1b)|[#*0-9]\ufe0f?\u20e3|(?:[©®\u2122\u265f]\ufe0f)|(?:\ud83c[\udc04\udd70\udd71\udd7e\udd7f\ude02\ude1a\ude2f\ude37\udf21\udf24-\udf2c\udf36\udf7d\udf96\udf97\udf99-\udf9b\udf9e\udf9f\udfcd\udfce\udfd4-\udfdf\udff3\udff5\udff7]|\ud83d[\udc3f\udc41\udcfd\udd49\udd4a\udd6f\udd70\udd73\udd76-\udd79\udd87\udd8a-\udd8d\udda5\udda8\uddb1\uddb2\uddbc\uddc2-\uddc4\uddd1-\uddd3\udddc-\uddde\udde1\udde3\udde8\uddef\uddf3\uddfa\udecb\udecd-\udecf\udee0-\udee5\udee9\udef0\udef3]|[\u203c\u2049\u2139\u2194-\u2199\u21a9\u21aa\u231a\u231b\u2328\u23cf\u23ed-\u23ef\u23f1\u23f2\u23f8-\u23fa\u24c2\u25aa\u25ab\u25b6\u25c0\u25fb-\u25fe\u2600-\u2604\u260e\u2611\u2614\u2615\u2618\u2620\u2622\u2623\u2626\u262a\u262e\u262f\u2638-\u263a\u2640\u2642\u2648-\u2653\u2660\u2663\u2665\u2666\u2668\u267b\u267f\u2692-\u2697\u2699\u269b\u269c\u26a0\u26a1\u26a7\u26aa\u26ab\u26b0\u26b1\u26bd\u26be\u26c4\u26c5\u26c8\u26cf\u26d1\u26d3\u26d4\u26e9\u26ea\u26f0-\u26f5\u26f8\u26fa\u26fd\u2702\u2708\u2709\u270f\u2712\u2714\u2716\u271d\u2721\u2733\u2734\u2744\u2747\u2757\u2763\u2764\u27a1\u2934\u2935\u2b05-\u2b07\u2b1b\u2b1c\u2b50\u2b55\u3030\u303d\u3297\u3299])(?:\ufe0f|(?!\ufe0e))|(?:(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75\udd90]|\ud83e\udef0|[\u261d\u26f7\u26f9\u270c\u270d])(?:\ufe0f|(?!\ufe0e))|(?:\ud83c\udfc3|\ud83d\udeb6|\ud83e\uddce)(?:\ud83c[\udffb-\udfff])?(?:\u200d\u27a1\ufe0f)?|(?:\ud83c[\udf85\udfc2\udfc4\udfc7\udfca]|\ud83d[\udc42\udc43\udc46-\udc50\udc66-\udc69\udc6e\udc70-\udc78\udc7c\udc81-\udc83\udc85-\udc87\udcaa\udd7a\udd95\udd96\ude45-\ude47\ude4b-\ude4f\udea3\udeb4\udeb5\udec0\udecc]|\ud83e[\udd0c\udd0f\udd18-\udd1c\udd1e\udd1f\udd26\udd30-\udd39\udd3d\udd3e\udd77\uddb5\uddb6\uddb8\uddb9\uddbb\uddcd\uddcf\uddd1-\udddd\udec3-\udec5\udef1-\udef8]|[\u270a\u270b]))(?:\ud83c[\udffb-\udfff])?|(?:\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc65\udb40\udc6e\udb40\udc67\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc73\udb40\udc63\udb40\udc74\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f|\ud83c\udde6\ud83c[\udde8-\uddec\uddee\uddf1\uddf2\uddf4\uddf6-\uddfa\uddfc\uddfd\uddff]|\ud83c\udde7\ud83c[\udde6\udde7\udde9-\uddef\uddf1-\uddf4\uddf6-\uddf9\uddfb\uddfc\uddfe\uddff]|\ud83c\udde8\ud83c[\udde6\udde8\udde9\uddeb-\uddee\uddf0-\uddf5\uddf7\uddfa-\uddff]|\ud83c\udde9\ud83c[\uddea\uddec\uddef\uddf0\uddf2\uddf4\uddff]|\ud83c\uddea\ud83c[\udde6\udde8\uddea\uddec\udded\uddf7-\uddfa]|\ud83c\uddeb\ud83c[\uddee-\uddf0\uddf2\uddf4\uddf7]|\ud83c\uddec\ud83c[\udde6\udde7\udde9-\uddee\uddf1-\uddf3\uddf5-\uddfa\uddfc\uddfe]|\ud83c\udded\ud83c[\uddf0\uddf2\uddf3\uddf7\uddf9\uddfa]|\ud83c\uddee\ud83c[\udde8-\uddea\uddf1-\uddf4\uddf6-\uddf9]|\ud83c\uddef\ud83c[\uddea\uddf2\uddf4\uddf5]|\ud83c\uddf0\ud83c[\uddea\uddec-\uddee\uddf2\uddf3\uddf5\uddf7\uddfc\uddfe\uddff]|\ud83c\uddf1\ud83c[\udde6-\udde8\uddee\uddf0\uddf7-\uddfb\uddfe]|\ud83c\uddf2\ud83c[\udde6\udde8-\udded\uddf0-\uddff]|\ud83c\uddf3\ud83c[\udde6\udde8\uddea-\uddec\uddee\uddf1\uddf4\uddf5\uddf7\uddfa\uddff]|\ud83c\uddf4\ud83c\uddf2|\ud83c\uddf5\ud83c[\udde6\uddea-\udded\uddf0-\uddf3\uddf7-\uddf9\uddfc\uddfe]|\ud83c\uddf6\ud83c\udde6|\ud83c\uddf7\ud83c[\uddea\uddf4\uddf8\uddfa\uddfc]|\ud83c\uddf8\ud83c[\udde6-\uddea\uddec-\uddf4\uddf7-\uddf9\uddfb\uddfd-\uddff]|\ud83c\uddf9\ud83c[\udde6\udde8\udde9\uddeb-\udded\uddef-\uddf4\uddf7\uddf9\uddfb\uddfc\uddff]|\ud83c\uddfa\ud83c[\udde6\uddec\uddf2\uddf3\uddf8\uddfe\uddff]|\ud83c\uddfb\ud83c[\udde6\udde8\uddea\uddec\uddee\uddf3\uddfa]|\ud83c\uddfc\ud83c[\uddeb\uddf8]|\ud83c\uddfd\ud83c\uddf0|\ud83c\uddfe\ud83c[\uddea\uddf9]|\ud83c\uddff\ud83c[\udde6\uddf2\uddfc]|\ud83c[\udccf\udd8e\udd91-\udd9a\udde6-\uddff\ude01\ude32-\ude36\ude38-\ude3a\ude50\ude51\udf00-\udf20\udf2d-\udf35\udf37-\udf7c\udf7e-\udf84\udf86-\udf93\udfa0-\udfc1\udfc5\udfc6\udfc8\udfc9\udfcf-\udfd3\udfe0-\udff0\udff4\udff8-\udfff]|\ud83d[\udc00-\udc3e\udc40\udc44\udc45\udc51-\udc65\udc6a\udc6f\udc79-\udc7b\udc7d-\udc80\udc84\udc88-\udc8e\udc90\udc92-\udca9\udcab-\udcfc\udcff-\udd3d\udd4b-\udd4e\udd50-\udd67\udda4\uddfb-\ude44\ude48-\ude4a\ude80-\udea2\udea4-\udeb3\udeb7-\udebf\udec1-\udec5\uded0-\uded2\uded5-\uded7\udedc-\udedf\udeeb\udeec\udef4-\udefc\udfe0-\udfeb\udff0]|\ud83e[\udd0d\udd0e\udd10-\udd17\udd20-\udd25\udd27-\udd2f\udd3a\udd3c\udd3f-\udd45\udd47-\udd76\udd78-\uddb4\uddb7\uddba\uddbc-\uddcc\uddd0\uddde-\uddff\ude70-\ude7c\ude80-\ude88\ude90-\udebd\udebf-\udec2\udece-\udedb\udee0-\udee8]|[\u23e9-\u23ec\u23f0\u23f3\u267e\u26ce\u2705\u2728\u274c\u274e\u2753-\u2755\u2795-\u2797\u27b0\u27bf\ue50a])|\ufe0f/g; } }); // node_modules/@twemoji/parser/dist/index.js var require_dist = __commonJS({ "node_modules/@twemoji/parser/dist/index.js"(exports) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TypeName = void 0; exports.parse = parse2; exports.toCodePoints = toCodePoints; var _regex = require_regex(); var _regex2 = _interopRequireDefault(_regex); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } var TypeName = exports.TypeName = "emoji"; function parse2(text, options) { var assetType = options && options.assetType ? options.assetType : "svg"; var getTwemojiUrl = options && options.buildUrl ? options.buildUrl : function(codepoints2, assetType2) { return assetType2 === "png" ? "https://cdn.jsdelivr.net/gh/jdecked/twemoji@latest/assets/72x72/" + codepoints2 + ".png" : "https://cdn.jsdelivr.net/gh/jdecked/twemoji@latest/assets/svg/" + codepoints2 + ".svg"; }; var entities = []; _regex2.default.lastIndex = 0; while (true) { var result = _regex2.default.exec(text); if (!result) { break; } var emojiText = result[0]; var codepoints = toCodePoints(removeVS16s(emojiText)).join("-"); entities.push({ url: codepoints ? getTwemojiUrl(codepoints, assetType) : "", indices: [result.index, _regex2.default.lastIndex], text: emojiText, type: TypeName }); } return entities; } var vs16RegExp = /\uFE0F/g; var zeroWidthJoiner = String.fromCharCode(8205); var removeVS16s = function removeVS16s2(rawEmoji) { return rawEmoji.indexOf(zeroWidthJoiner) < 0 ? rawEmoji.replace(vs16RegExp, "") : rawEmoji; }; function toCodePoints(unicodeSurrogates) { var points = []; var char = 0; var previous = 0; var i = 0; while (i < unicodeSurrogates.length) { char = unicodeSurrogates.charCodeAt(i++); if (previous) { points.push((65536 + (previous - 55296 << 10) + (char - 56320)).toString(16)); previous = 0; } else if (char > 55296 && char <= 56319) { previous = char; } else { points.push(char.toString(16)); } } return points; } } }); // node_modules/dexie/dist/dexie.js var require_dexie = __commonJS({ "node_modules/dexie/dist/dexie.js"(exports, module) { "use strict"; (function(global2, factory) { typeof exports === "object" && typeof module !== "undefined" ? module.exports = factory() : typeof define === "function" && define.amd ? define(factory) : (global2 = typeof globalThis !== "undefined" ? globalThis : global2 || self, global2.Dexie = factory()); })(exports, function() { "use strict"; var extendStatics = function(d, b) { extendStatics = Object.setPrototypeOf || { __proto__: [] } instanceof Array && function(d2, b2) { d2.__proto__ = b2; } || function(d2, b2) { for (var p in b2) if (Object.prototype.hasOwnProperty.call(b2, p)) d2[p] = b2[p]; }; return extendStatics(d, b); }; function __extends(d, b) { if (typeof b !== "function" && b !== null) throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); } var __assign = function() { __assign = Object.assign || function __assign2(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; function __spreadArray(to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); } var _global = typeof globalThis !== "undefined" ? globalThis : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : global; var keys = Object.keys; var isArray2 = Array.isArray; if (typeof Promise !== "undefined" && !_global.Promise) { _global.Promise = Promise; } function extend(obj, extension) { if (typeof extension !== "object") return obj; keys(extension).forEach(function(key) { obj[key] = extension[key]; }); return obj; } var getProto = Object.getPrototypeOf; var _hasOwn = {}.hasOwnProperty; function hasOwn2(obj, prop) { return _hasOwn.call(obj, prop); } function props(proto, extension) { if (typeof extension === "function") extension = extension(getProto(proto)); (typeof Reflect === "undefined" ? keys : Reflect.ownKeys)(extension).forEach(function(key) { setProp(proto, key, extension[key]); }); } var defineProperty = Object.defineProperty; function setProp(obj, prop, functionOrGetSet, options) { defineProperty(obj, prop, extend(functionOrGetSet && hasOwn2(functionOrGetSet, "get") && typeof functionOrGetSet.get === "function" ? { get: functionOrGetSet.get, set: functionOrGetSet.set, configurable: true } : { value: functionOrGetSet, configurable: true, writable: true }, options)); } function derive(Child) { return { from: function(Parent) { Child.prototype = Object.create(Parent.prototype); setProp(Child.prototype, "constructor", Child); return { extend: props.bind(null, Child.prototype) }; } }; } var getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; function getPropertyDescriptor(obj, prop) { var pd = getOwnPropertyDescriptor(obj, prop); var proto; return pd || (proto = getProto(obj)) && getPropertyDescriptor(proto, prop); } var _slice = [].slice; function slice(args, start, end) { return _slice.call(args, start, end); } function override(origFunc, overridedFactory) { return overridedFactory(origFunc); } function assert(b) { if (!b) throw new Error("Assertion Failed"); } function asap$1(fn) { if (_global.setImmediate) setImmediate(fn); else setTimeout(fn, 0); } function arrayToObject(array, extractor) { return array.reduce(function(result, item, i) { var nameAndValue = extractor(item, i); if (nameAndValue) result[nameAndValue[0]] = nameAndValue[1]; return result; }, {}); } function getByKeyPath(obj, keyPath) { if (typeof keyPath === "string" && hasOwn2(obj, keyPath)) return obj[keyPath]; if (!keyPath) return obj; if (typeof keyPath !== "string") { var rv = []; for (var i = 0, l = keyPath.length; i < l; ++i) { var val = getByKeyPath(obj, keyPath[i]); rv.push(val); } return rv; } var period = keyPath.indexOf("."); if (period !== -1) { var innerObj = obj[keyPath.substr(0, period)]; return innerObj == null ? void 0 : getByKeyPath(innerObj, keyPath.substr(period + 1)); } return void 0; } function setByKeyPath(obj, keyPath, value) { if (!obj || keyPath === void 0) return; if ("isFrozen" in Object && Object.isFrozen(obj)) return; if (typeof keyPath !== "string" && "length" in keyPath) { assert(typeof value !== "string" && "length" in value); for (var i = 0, l = keyPath.length; i < l; ++i) { setByKeyPath(obj, keyPath[i], value[i]); } } else { var period = keyPath.indexOf("."); if (period !== -1) { var currentKeyPath = keyPath.substr(0, period); var remainingKeyPath = keyPath.substr(period + 1); if (remainingKeyPath === "") if (value === void 0) { if (isArray2(obj) && !isNaN(parseInt(currentKeyPath))) obj.splice(currentKeyPath, 1); else delete obj[currentKeyPath]; } else obj[currentKeyPath] = value; else { var innerObj = obj[currentKeyPath]; if (!innerObj || !hasOwn2(obj, currentKeyPath)) innerObj = obj[currentKeyPath] = {}; setByKeyPath(innerObj, remainingKeyPath, value); } } else { if (value === void 0) { if (isArray2(obj) && !isNaN(parseInt(keyPath))) obj.splice(keyPath, 1); else delete obj[keyPath]; } else obj[keyPath] = value; } } } function delByKeyPath(obj, keyPath) { if (typeof keyPath === "string") setByKeyPath(obj, keyPath, void 0); else if ("length" in keyPath) [].map.call(keyPath, function(kp) { setByKeyPath(obj, kp, void 0); }); } function shallowClone(obj) { var rv = {}; for (var m in obj) { if (hasOwn2(obj, m)) rv[m] = obj[m]; } return rv; } var concat = [].concat; function flatten(a) { return concat.apply([], a); } var intrinsicTypeNames = "BigUint64Array,BigInt64Array,Array,Boolean,String,Date,RegExp,Blob,File,FileList,FileSystemFileHandle,FileSystemDirectoryHandle,ArrayBuffer,DataView,Uint8ClampedArray,ImageBitmap,ImageData,Map,Set,CryptoKey".split(",").concat(flatten([8, 16, 32, 64].map(function(num) { return ["Int", "Uint", "Float"].map(function(t) { return t + num + "Array"; }); }))).filter(function(t) { return _global[t]; }); var intrinsicTypes = new Set(intrinsicTypeNames.map(function(t) { return _global[t]; })); function cloneSimpleObjectTree(o) { var rv = {}; for (var k in o) if (hasOwn2(o, k)) { var v = o[k]; rv[k] = !v || typeof v !== "object" || intrinsicTypes.has(v.constructor) ? v : cloneSimpleObjectTree(v); } return rv; } function objectIsEmpty(o) { for (var k in o) if (hasOwn2(o, k)) return false; return true; } var circularRefs = null; function deepClone(any) { circularRefs = /* @__PURE__ */ new WeakMap(); var rv = innerDeepClone(any); circularRefs = null; return rv; } function innerDeepClone(x) { if (!x || typeof x !== "object") return x; var rv = circularRefs.get(x); if (rv) return rv; if (isArray2(x)) { rv = []; circularRefs.set(x, rv); for (var i = 0, l = x.length; i < l; ++i) { rv.push(innerDeepClone(x[i])); } } else if (intrinsicTypes.has(x.constructor)) { rv = x; } else { var proto = getProto(x); rv = proto === Object.prototype ? {} : Object.create(proto); circularRefs.set(x, rv); for (var prop in x) { if (hasOwn2(x, prop)) { rv[prop] = innerDeepClone(x[prop]); } } } return rv; } var toString2 = {}.toString; function toStringTag(o) { return toString2.call(o).slice(8, -1); } var iteratorSymbol = typeof Symbol !== "undefined" ? Symbol.iterator : "@@iterator"; var getIteratorOf = typeof iteratorSymbol === "symbol" ? function(x) { var i; return x != null && (i = x[iteratorSymbol]) && i.apply(x); } : function() { return null; }; function delArrayItem(a, x) { var i = a.indexOf(x); if (i >= 0) a.splice(i, 1); return i >= 0; } var NO_CHAR_ARRAY = {}; function getArrayOf(arrayLike) { var i, a, x, it; if (arguments.length === 1) { if (isArray2(arrayLike)) return arrayLike.slice(); if (this === NO_CHAR_ARRAY && typeof arrayLike === "string") return [arrayLike]; if (it = getIteratorOf(arrayLike)) { a = []; while (x = it.next(), !x.done) a.push(x.value); return a; } if (arrayLike == null) return [arrayLike]; i = arrayLike.length; if (typeof i === "number") { a = new Array(i); while (i--) a[i] = arrayLike[i]; return a; } return [arrayLike]; } i = arguments.length; a = new Array(i); while (i--) a[i] = arguments[i]; return a; } var isAsyncFunction = typeof Symbol !== "undefined" ? function(fn) { return fn[Symbol.toStringTag] === "AsyncFunction"; } : function() { return false; }; var dexieErrorNames = [ "Modify", "Bulk", "OpenFailed", "VersionChange", "Schema", "Upgrade", "InvalidTable", "MissingAPI", "NoSuchDatabase", "InvalidArgument", "SubTransaction", "Unsupported", "Internal", "DatabaseClosed", "PrematureCommit", "ForeignAwait" ]; var idbDomErrorNames = [ "Unknown", "Constraint", "Data", "TransactionInactive", "ReadOnly", "Version", "NotFound", "InvalidState", "InvalidAccess", "Abort", "Timeout", "QuotaExceeded", "Syntax", "DataClone" ]; var errorList = dexieErrorNames.concat(idbDomErrorNames); var defaultTexts = { VersionChanged: "Database version changed by other database connection", DatabaseClosed: "Database has been closed", Abort: "Transaction aborted", TransactionInactive: "Transaction has already completed or failed", MissingAPI: "IndexedDB API missing. Please visit https://tinyurl.com/y2uuvskb" }; function DexieError(name, msg) { this.name = name; this.message = msg; } derive(DexieError).from(Error).extend({ toString: function() { return this.name + ": " + this.message; } }); function getMultiErrorMessage(msg, failures) { return msg + ". Errors: " + Object.keys(failures).map(function(key) { return failures[key].toString(); }).filter(function(v, i, s) { return s.indexOf(v) === i; }).join("\n"); } function ModifyError(msg, failures, successCount, failedKeys) { this.failures = failures; this.failedKeys = failedKeys; this.successCount = successCount; this.message = getMultiErrorMessage(msg, failures); } derive(ModifyError).from(DexieError); function BulkError(msg, failures) { this.name = "BulkError"; this.failures = Object.keys(failures).map(function(pos) { return failures[pos]; }); this.failuresByPos = failures; this.message = getMultiErrorMessage(msg, this.failures); } derive(BulkError).from(DexieError); var errnames = errorList.reduce(function(obj, name) { return obj[name] = name + "Error", obj; }, {}); var BaseException = DexieError; var exceptions = errorList.reduce(function(obj, name) { var fullName = name + "Error"; function DexieError2(msgOrInner, inner) { this.name = fullName; if (!msgOrInner) { this.message = defaultTexts[name] || fullName; this.inner = null; } else if (typeof msgOrInner === "string") { this.message = "".concat(msgOrInner).concat(!inner ? "" : "\n " + inner); this.inner = inner || null; } else if (typeof msgOrInner === "object") { this.message = "".concat(msgOrInner.name, " ").concat(msgOrInner.message); this.inner = msgOrInner; } } derive(DexieError2).from(BaseException); obj[name] = DexieError2; return obj; }, {}); exceptions.Syntax = SyntaxError; exceptions.Type = TypeError; exceptions.Range = RangeError; var exceptionMap = idbDomErrorNames.reduce(function(obj, name) { obj[name + "Error"] = exceptions[name]; return obj; }, {}); function mapError(domError, message) { if (!domError || domError instanceof DexieError || domError instanceof TypeError || domError instanceof SyntaxError || !domError.name || !exceptionMap[domError.name]) return domError; var rv = new exceptionMap[domError.name](message || domError.message, domError); if ("stack" in domError) { setProp(rv, "stack", { get: function() { return this.inner.stack; } }); } return rv; } var fullNameExceptions = errorList.reduce(function(obj, name) { if (["Syntax", "Type", "Range"].indexOf(name) === -1) obj[name + "Error"] = exceptions[name]; return obj; }, {}); fullNameExceptions.ModifyError = ModifyError; fullNameExceptions.DexieError = DexieError; fullNameExceptions.BulkError = BulkError; function nop() { } function mirror(val) { return val; } function pureFunctionChain(f1, f2) { if (f1 == null || f1 === mirror) return f2; return function(val) { return f2(f1(val)); }; } function callBoth(on1, on2) { return function() { on1.apply(this, arguments); on2.apply(this, arguments); }; } function hookCreatingChain(f1, f2) { if (f1 === nop) return f2; return function() { var res = f1.apply(this, arguments); if (res !== void 0) arguments[0] = res; var onsuccess = this.onsuccess, onerror = this.onerror; this.onsuccess = null; this.onerror = null; var res2 = f2.apply(this, arguments); if (onsuccess) this.onsuccess = this.onsuccess ? callBoth(onsuccess, this.onsuccess) : onsuccess; if (onerror) this.onerror = this.onerror ? callBoth(onerror, this.onerror) : onerror; return res2 !== void 0 ? res2 : res; }; } function hookDeletingChain(f1, f2) { if (f1 === nop) return f2; return function() { f1.apply(this, arguments); var onsuccess = this.onsuccess, onerror = this.onerror; this.onsuccess = this.onerror = null; f2.apply(this, arguments); if (onsuccess) this.onsuccess = this.onsuccess ? callBoth(onsuccess, this.onsuccess) : onsuccess; if (onerror) this.onerror = this.onerror ? callBoth(onerror, this.onerror) : onerror; }; } function hookUpdatingChain(f1, f2) { if (f1 === nop) return f2; return function(modifications) { var res = f1.apply(this, arguments); extend(modifications, res); var onsuccess = this.onsuccess, onerror = this.onerror; this.onsuccess = null; this.onerror = null; var res2 = f2.apply(this, arguments); if (onsuccess) this.onsuccess = this.onsuccess ? callBoth(onsuccess, this.onsuccess) : onsuccess; if (onerror) this.onerror = this.onerror ? callBoth(onerror, this.onerror) : onerror; return res === void 0 ? res2 === void 0 ? void 0 : res2 : extend(res, res2); }; } function reverseStoppableEventChain(f1, f2) { if (f1 === nop) return f2; return function() { if (f2.apply(this, arguments) === false) return false; return f1.apply(this, arguments); }; } function promisableChain(f1, f2) { if (f1 === nop) return f2; return function() { var res = f1.apply(this, arguments); if (res && typeof res.then === "function") { var thiz = this, i = arguments.length, args = new Array(i); while (i--) args[i] = arguments[i]; return res.then(function() { return f2.apply(thiz, args); }); } return f2.apply(this, arguments); }; } var debug = typeof location !== "undefined" && /^(http|https):\/\/(localhost|127\.0\.0\.1)/.test(location.href); function setDebug(value, filter) { debug = value; } var INTERNAL = {}; var ZONE_ECHO_LIMIT = 100, _a$1 = typeof Promise === "undefined" ? [] : function() { var globalP = Promise.resolve(); if (typeof crypto === "undefined" || !crypto.subtle) return [globalP, getProto(globalP), globalP]; var nativeP = crypto.subtle.digest("SHA-512", new Uint8Array([0])); return [ nativeP, getProto(nativeP), globalP ]; }(), resolvedNativePromise = _a$1[0], nativePromiseProto = _a$1[1], resolvedGlobalPromise = _a$1[2], nativePromiseThen = nativePromiseProto && nativePromiseProto.then; var NativePromise = resolvedNativePromise && resolvedNativePromise.constructor; var patchGlobalPromise = !!resolvedGlobalPromise; function schedulePhysicalTick() { queueMicrotask(physicalTick); } var asap = function(callback, args) { microtickQueue.push([callback, args]); if (needsNewPhysicalTick) { schedulePhysicalTick(); needsNewPhysicalTick = false; } }; var isOutsideMicroTick = true, needsNewPhysicalTick = true, unhandledErrors = [], rejectingErrors = [], rejectionMapper = mirror; var globalPSD = { id: "global", global: true, ref: 0, unhandleds: [], onunhandled: nop, pgp: false, env: {}, finalize: nop }; var PSD = globalPSD; var microtickQueue = []; var numScheduledCalls = 0; var tickFinalizers = []; function DexiePromise(fn) { if (typeof this !== "object") throw new TypeError("Promises must be constructed via new"); this._listeners = []; this._lib = false; var psd = this._PSD = PSD; if (typeof fn !== "function") { if (fn !== INTERNAL) throw new TypeError("Not a function"); this._state = arguments[1]; this._value = arguments[2]; if (this._state === false) handleRejection(this, this._value); return; } this._state = null; this._value = null; ++psd.ref; executePromiseTask(this, fn); } var thenProp = { get: function() { var psd = PSD, microTaskId = totalEchoes; function then(onFulfilled, onRejected) { var _this = this; var possibleAwait = !psd.global && (psd !== PSD || microTaskId !== totalEchoes); var cleanup = possibleAwait && !decrementExpectedAwaits(); var rv = new DexiePromise(function(resolve, reject) { propagateToListener(_this, new Listener(nativeAwaitCompatibleWrap(onFulfilled, psd, possibleAwait, cleanup), nativeAwaitCompatibleWrap(onRejected, psd, possibleAwait, cleanup), resolve, reject, psd)); }); if (this._consoleTask) rv._consoleTask = this._consoleTask; return rv; } then.prototype = INTERNAL; return then; }, set: function(value) { setProp(this, "then", value && value.prototype === INTERNAL ? thenProp : { get: function() { return value; }, set: thenProp.set }); } }; props(DexiePromise.prototype, { then: thenProp, _then: function(onFulfilled, onRejected) { propagateToListener(this, new Listener(null, null, onFulfilled, onRejected, PSD)); }, catch: function(onRejected) { if (arguments.length === 1) return this.then(null, onRejected); var type2 = arguments[0], handler = arguments[1]; return typeof type2 === "function" ? this.then(null, function(err) { return err instanceof type2 ? handler(err) : PromiseReject(err); }) : this.then(null, function(err) { return err && err.name === type2 ? handler(err) : PromiseReject(err); }); }, finally: function(onFinally) { return this.then(function(value) { return DexiePromise.resolve(onFinally()).then(function() { return value; }); }, function(err) { return DexiePromise.resolve(onFinally()).then(function() { return PromiseReject(err); }); }); }, timeout: function(ms, msg) { var _this = this; return ms < Infinity ? new DexiePromise(function(resolve, reject) { var handle = setTimeout(function() { return reject(new exceptions.Timeout(msg)); }, ms); _this.then(resolve, reject).finally(clearTimeout.bind(null, handle)); }) : this; } }); if (typeof Symbol !== "undefined" && Symbol.toStringTag) setProp(DexiePromise.prototype, Symbol.toStringTag, "Dexie.Promise"); globalPSD.env = snapShot(); function Listener(onFulfilled, onRejected, resolve, reject, zone) { this.onFulfilled = typeof onFulfilled === "function" ? onFulfilled : null; this.onRejected = typeof onRejected === "function" ? onRejected : null; this.resolve = resolve; this.reject = reject; this.psd = zone; } props(DexiePromise, { all: function() { var values = getArrayOf.apply(null, arguments).map(onPossibleParallellAsync); return new DexiePromise(function(resolve, reject) { if (values.length === 0) resolve([]); var remaining = values.length; values.forEach(function(a, i) { return DexiePromise.resolve(a).then(function(x) { values[i] = x; if (!--remaining) resolve(values); }, reject); }); }); }, resolve: function(value) { if (value instanceof DexiePromise) return value; if (value && typeof value.then === "function") return new DexiePromise(function(resolve, reject) { value.then(resolve, reject); }); var rv = new DexiePromise(INTERNAL, true, value); return rv; }, reject: PromiseReject, race: function() { var values = getArrayOf.apply(null, arguments).map(onPossibleParallellAsync); return new DexiePromise(function(resolve, reject) { values.map(function(value) { return DexiePromise.resolve(value).then(resolve, reject); }); }); }, PSD: { get: function() { return PSD; }, set: function(value) { return PSD = value; } }, totalEchoes: { get: function() { return totalEchoes; } }, newPSD: newScope, usePSD, scheduler: { get: function() { return asap; }, set: function(value) { asap = value; } }, rejectionMapper: { get: function() { return rejectionMapper; }, set: function(value) { rejectionMapper = value; } }, follow: function(fn, zoneProps) { return new DexiePromise(function(resolve, reject) { return newScope(function(resolve2, reject2) { var psd = PSD; psd.unhandleds = []; psd.onunhandled = reject2; psd.finalize = callBoth(function() { var _this = this; run_at_end_of_this_or_next_physical_tick(function() { _this.unhandleds.length === 0 ? resolve2() : reject2(_this.unhandleds[0]); }); }, psd.finalize); fn(); }, zoneProps, resolve, reject); }); } }); if (NativePromise) { if (NativePromise.allSettled) setProp(DexiePromise, "allSettled", function() { var possiblePromises = getArrayOf.apply(null, arguments).map(onPossibleParallellAsync); return new DexiePromise(function(resolve) { if (possiblePromises.length === 0) resolve([]); var remaining = possiblePromises.length; var results = new Array(remaining); possiblePromises.forEach(function(p, i) { return DexiePromise.resolve(p).then(function(value) { return results[i] = { status: "fulfilled", value }; }, function(reason) { return results[i] = { status: "rejected", reason }; }).then(function() { return --remaining || resolve(results); }); }); }); }); if (NativePromise.any && typeof AggregateError !== "undefined") setProp(DexiePromise, "any", function() { var possiblePromises = getArrayOf.apply(null, arguments).map(onPossibleParallellAsync); return new DexiePromise(function(resolve, reject) { if (possiblePromises.length === 0) reject(new AggregateError([])); var remaining = possiblePromises.length; var failures = new Array(remaining); possiblePromises.forEach(function(p, i) { return DexiePromise.resolve(p).then(function(value) { return resolve(value); }, function(failure) { failures[i] = failure; if (!--remaining) reject(new AggregateError(failures)); }); }); }); }); } function executePromiseTask(promise, fn) { try { fn(function(value) { if (promise._state !== null) return; if (value === promise) throw new TypeError("A promise cannot be resolved with itself."); var shouldExecuteTick = promise._lib && beginMicroTickScope(); if (value && typeof value.then === "function") { executePromiseTask(promise, function(resolve, reject) { value instanceof DexiePromise ? value._then(resolve, reject) : value.then(resolve, reject); }); } else { promise._state = true; promise._value = value; propagateAllListeners(promise); } if (shouldExecuteTick) endMicroTickScope(); }, handleRejection.bind(null, promise)); } catch (ex) { handleRejection(promise, ex); } } function handleRejection(promise, reason) { rejectingErrors.push(reason); if (promise._state !== null) return; var shouldExecuteTick = promise._lib && beginMicroTickScope(); reason = rejectionMapper(reason); promise._state = false; promise._value = reason; addPossiblyUnhandledError(promise); propagateAllListeners(promise); if (shouldExecuteTick) endMicroTickScope(); } function propagateAllListeners(promise) { var listeners = promise._listeners; promise._listeners = []; for (var i = 0, len = listeners.length; i < len; ++i) { propagateToListener(promise, listeners[i]); } var psd = promise._PSD; --psd.ref || psd.finalize(); if (numScheduledCalls === 0) { ++numScheduledCalls; asap(function() { if (--numScheduledCalls === 0) finalizePhysicalTick(); }, []); } } function propagateToListener(promise, listener) { if (promise._state === null) { promise._listeners.push(listener); return; } var cb = promise._state ? listener.onFulfilled : listener.onRejected; if (cb === null) { return (promise._state ? listener.resolve : listener.reject)(promise._value); } ++listener.psd.ref; ++numScheduledCalls; asap(callListener, [cb, promise, listener]); } function callListener(cb, promise, listener) { try { var ret, value = promise._value; if (!promise._state && rejectingErrors.length) rejectingErrors = []; ret = debug && promise._consoleTask ? promise._consoleTask.run(function() { return cb(value); }) : cb(value); if (!promise._state && rejectingErrors.indexOf(value) === -1) { markErrorAsHandled(promise); } listener.resolve(ret); } catch (e) { listener.reject(e); } finally { if (--numScheduledCalls === 0) finalizePhysicalTick(); --listener.psd.ref || listener.psd.finalize(); } } function physicalTick() { usePSD(globalPSD, function() { beginMicroTickScope() && endMicroTickScope(); }); } function beginMicroTickScope() { var wasRootExec = isOutsideMicroTick; isOutsideMicroTick = false; needsNewPhysicalTick = false; return wasRootExec; } function endMicroTickScope() { var callbacks, i, l; do { while (microtickQueue.length > 0) { callbacks = microtickQueue; microtickQueue = []; l = callbacks.length; for (i = 0; i < l; ++i) { var item = callbacks[i]; item[0].apply(null, item[1]); } } } while (microtickQueue.length > 0); isOutsideMicroTick = true; needsNewPhysicalTick = true; } function finalizePhysicalTick() { var unhandledErrs = unhandledErrors; unhandledErrors = []; unhandledErrs.forEach(function(p) { p._PSD.onunhandled.call(null, p._value, p); }); var finalizers = tickFinalizers.slice(0); var i = finalizers.length; while (i) finalizers[--i](); } function run_at_end_of_this_or_next_physical_tick(fn) { function finalizer() { fn(); tickFinalizers.splice(tickFinalizers.indexOf(finalizer), 1); } tickFinalizers.push(finalizer); ++numScheduledCalls; asap(function() { if (--numScheduledCalls === 0) finalizePhysicalTick(); }, []); } function addPossiblyUnhandledError(promise) { if (!unhandledErrors.some(function(p) { return p._value === promise._value; })) unhandledErrors.push(promise); } function markErrorAsHandled(promise) { var i = unhandledErrors.length; while (i) if (unhandledErrors[--i]._value === promise._value) { unhandledErrors.splice(i, 1); return; } } function PromiseReject(reason) { return new DexiePromise(INTERNAL, false, reason); } function wrap(fn, errorCatcher) { var psd = PSD; return function() { var wasRootExec = beginMicroTickScope(), outerScope = PSD; try { switchToZone(psd, true); return fn.apply(this, arguments); } catch (e) { errorCatcher && errorCatcher(e); } finally { switchToZone(outerScope, false); if (wasRootExec) endMicroTickScope(); } }; } var task = { awaits: 0, echoes: 0, id: 0 }; var taskCounter = 0; var zoneStack = []; var zoneEchoes = 0; var totalEchoes = 0; var zone_id_counter = 0; function newScope(fn, props2, a1, a2) { var parent = PSD, psd = Object.create(parent); psd.parent = parent; psd.ref = 0; psd.global = false; psd.id = ++zone_id_counter; globalPSD.env; psd.env = patchGlobalPromise ? { Promise: DexiePromise, PromiseProp: { value: DexiePromise, configurable: true, writable: true }, all: DexiePromise.all, race: DexiePromise.race, allSettled: DexiePromise.allSettled, any: DexiePromise.any, resolve: DexiePromise.resolve, reject: DexiePromise.reject } : {}; if (props2) extend(psd, props2); ++parent.ref; psd.finalize = function() { --this.parent.ref || this.parent.finalize(); }; var rv = usePSD(psd, fn, a1, a2); if (psd.ref === 0) psd.finalize(); return rv; } function incrementExpectedAwaits() { if (!task.id) task.id = ++taskCounter; ++task.awaits; task.echoes += ZONE_ECHO_LIMIT; return task.id; } function decrementExpectedAwaits() { if (!task.awaits) return false; if (--task.awaits === 0) task.id = 0; task.echoes = task.awaits * ZONE_ECHO_LIMIT; return true; } if (("" + nativePromiseThen).indexOf("[native code]") === -1) { incrementExpectedAwaits = decrementExpectedAwaits = nop; } function onPossibleParallellAsync(possiblePromise) { if (task.echoes && possiblePromise && possiblePromise.constructor === NativePromise) { incrementExpectedAwaits(); return possiblePromise.then(function(x) { decrementExpectedAwaits(); return x; }, function(e) { decrementExpectedAwaits(); return rejection(e); }); } return possiblePromise; } function zoneEnterEcho(targetZone) { ++totalEchoes; if (!task.echoes || --task.echoes === 0) { task.echoes = task.awaits = task.id = 0; } zoneStack.push(PSD); switchToZone(targetZone, true); } function zoneLeaveEcho() { var zone = zoneStack[zoneStack.length - 1]; zoneStack.pop(); switchToZone(zone, false); } function switchToZone(targetZone, bEnteringZone) { var currentZone = PSD; if (bEnteringZone ? task.echoes && (!zoneEchoes++ || targetZone !== PSD) : zoneEchoes && (!--zoneEchoes || targetZone !== PSD)) { queueMicrotask(bEnteringZone ? zoneEnterEcho.bind(null, targetZone) : zoneLeaveEcho); } if (targetZone === PSD) return; PSD = targetZone; if (currentZone === globalPSD) globalPSD.env = snapShot(); if (patchGlobalPromise) { var GlobalPromise = globalPSD.env.Promise; var targetEnv = targetZone.env; if (currentZone.global || targetZone.global) { Object.defineProperty(_global, "Promise", targetEnv.PromiseProp); GlobalPromise.all = targetEnv.all; GlobalPromise.race = targetEnv.race; GlobalPromise.resolve = targetEnv.resolve; GlobalPromise.reject = targetEnv.reject; if (targetEnv.allSettled) GlobalPromise.allSettled = targetEnv.allSettled; if (targetEnv.any) GlobalPromise.any = targetEnv.any; } } } function snapShot() { var GlobalPromise = _global.Promise; return patchGlobalPromise ? { Promise: GlobalPromise, PromiseProp: Object.getOwnPropertyDescriptor(_global, "Promise"), all: GlobalPromise.all, race: GlobalPromise.race, allSettled: GlobalPromise.allSettled, any: GlobalPromise.any, resolve: GlobalPromise.resolve, reject: GlobalPromise.reject } : {}; } function usePSD(psd, fn, a1, a2, a3) { var outerScope = PSD; try { switchToZone(psd, true); return fn(a1, a2, a3); } finally { switchToZone(outerScope, false); } } function nativeAwaitCompatibleWrap(fn, zone, possibleAwait, cleanup) { return typeof fn !== "function" ? fn : function() { var outerZone = PSD; if (possibleAwait) incrementExpectedAwaits(); switchToZone(zone, true); try { return fn.apply(this, arguments); } finally { switchToZone(outerZone, false); if (cleanup) queueMicrotask(decrementExpectedAwaits); } }; } function execInGlobalContext(cb) { if (Promise === NativePromise && task.echoes === 0) { if (zoneEchoes === 0) { cb(); } else { enqueueNativeMicroTask(cb); } } else { setTimeout(cb, 0); } } var rejection = DexiePromise.reject; function tempTransaction(db, mode, storeNames, fn) { if (!db.idbdb || !db._state.openComplete && (!PSD.letThrough && !db._vip)) { if (db._state.openComplete) { return rejection(new exceptions.DatabaseClosed(db._state.dbOpenError)); } if (!db._state.isBeingOpened) { if (!db._state.autoOpen) return rejection(new exceptions.DatabaseClosed()); db.open().catch(nop); } return db._state.dbReadyPromise.then(function() { return tempTransaction(db, mode, storeNames, fn); }); } else { var trans = db._createTransaction(mode, storeNames, db._dbSchema); try { trans.create(); db._state.PR1398_maxLoop = 3; } catch (ex) { if (ex.name === errnames.InvalidState && db.isOpen() && --db._state.PR1398_maxLoop > 0) { console.warn("Dexie: Need to reopen db"); db.close({ disableAutoOpen: false }); return db.open().then(function() { return tempTransaction(db, mode, storeNames, fn); }); } return rejection(ex); } return trans._promise(mode, function(resolve, reject) { return newScope(function() { PSD.trans = trans; return fn(resolve, reject, trans); }); }).then(function(result) { if (mode === "readwrite") try { trans.idbtrans.commit(); } catch (_a2) { } return mode === "readonly" ? result : trans._completion.then(function() { return result; }); }); } } var DEXIE_VERSION = "4.0.8"; var maxString = String.fromCharCode(65535); var minKey = -Infinity; var INVALID_KEY_ARGUMENT = "Invalid key provided. Keys must be of type string, number, Date or Array."; var STRING_EXPECTED = "String expected."; var connections = []; var DBNAMES_DB = "__dbnames"; var READONLY = "readonly"; var READWRITE = "readwrite"; function combine(filter1, filter2) { return filter1 ? filter2 ? function() { return filter1.apply(this, arguments) && filter2.apply(this, arguments); } : filter1 : filter2; } var AnyRange = { type: 3, lower: -Infinity, lowerOpen: false, upper: [[]], upperOpen: false }; function workaroundForUndefinedPrimKey(keyPath) { return typeof keyPath === "string" && !/\./.test(keyPath) ? function(obj) { if (obj[keyPath] === void 0 && keyPath in obj) { obj = deepClone(obj); delete obj[keyPath]; } return obj; } : function(obj) { return obj; }; } function Entity2() { throw exceptions.Type(); } function cmp2(a, b) { try { var ta = type(a); var tb = type(b); if (ta !== tb) { if (ta === "Array") return 1; if (tb === "Array") return -1; if (ta === "binary") return 1; if (tb === "binary") return -1; if (ta === "string") return 1; if (tb === "string") return -1; if (ta === "Date") return 1; if (tb !== "Date") return NaN; return -1; } switch (ta) { case "number": case "Date": case "string": return a > b ? 1 : a < b ? -1 : 0; case "binary": { return compareUint8Arrays(getUint8Array(a), getUint8Array(b)); } case "Array": return compareArrays(a, b); } } catch (_a2) { } return NaN; } function compareArrays(a, b) { var al = a.length; var bl = b.length; var l = al < bl ? al : bl; for (var i = 0; i < l; ++i) { var res = cmp2(a[i], b[i]); if (res !== 0) return res; } return al === bl ? 0 : al < bl ? -1 : 1; } function compareUint8Arrays(a, b) { var al = a.length; var bl = b.length; var l = al < bl ? al : bl; for (var i = 0; i < l; ++i) { if (a[i] !== b[i]) return a[i] < b[i] ? -1 : 1; } return al === bl ? 0 : al < bl ? -1 : 1; } function type(x) { var t = typeof x; if (t !== "object") return t; if (ArrayBuffer.isView(x)) return "binary"; var tsTag = toStringTag(x); return tsTag === "ArrayBuffer" ? "binary" : tsTag; } function getUint8Array(a) { if (a instanceof Uint8Array) return a; if (ArrayBuffer.isView(a)) return new Uint8Array(a.buffer, a.byteOffset, a.byteLength); return new Uint8Array(a); } var Table2 = function() { function Table3() { } Table3.prototype._trans = function(mode, fn, writeLocked) { var trans = this._tx || PSD.trans; var tableName = this.name; var task2 = debug && typeof console !== "undefined" && console.createTask && console.createTask("Dexie: ".concat(mode === "readonly" ? "read" : "write", " ").concat(this.name)); function checkTableInTransaction(resolve, reject, trans2) { if (!trans2.schema[tableName]) throw new exceptions.NotFound("Table " + tableName + " not part of transaction"); return fn(trans2.idbtrans, trans2); } var wasRootExec = beginMicroTickScope(); try { var p = trans && trans.db._novip === this.db._novip ? trans === PSD.trans ? trans._promise(mode, checkTableInTransaction, writeLocked) : newScope(function() { return trans._promise(mode, checkTableInTransaction, writeLocked); }, { trans, transless: PSD.transless || PSD }) : tempTransaction(this.db, mode, [this.name], checkTableInTransaction); if (task2) { p._consoleTask = task2; p = p.catch(function(err) { console.trace(err); return rejection(err); }); } return p; } finally { if (wasRootExec) endMicroTickScope(); } }; Table3.prototype.get = function(keyOrCrit, cb) { var _this = this; if (keyOrCrit && keyOrCrit.constructor === Object) return this.where(keyOrCrit).first(cb); if (keyOrCrit == null) return rejection(new exceptions.Type("Invalid argument to Table.get()")); return this._trans("readonly", function(trans) { return _this.core.get({ trans, key: keyOrCrit }).then(function(res) { return _this.hook.reading.fire(res); }); }).then(cb); }; Table3.prototype.where = function(indexOrCrit) { if (typeof indexOrCrit === "string") return new this.db.WhereClause(this, indexOrCrit); if (isArray2(indexOrCrit)) return new this.db.WhereClause(this, "[".concat(indexOrCrit.join("+"), "]")); var keyPaths = keys(indexOrCrit); if (keyPaths.length === 1) return this.where(keyPaths[0]).equals(indexOrCrit[keyPaths[0]]); var compoundIndex = this.schema.indexes.concat(this.schema.primKey).filter(function(ix) { if (ix.compound && keyPaths.every(function(keyPath) { return ix.keyPath.indexOf(keyPath) >= 0; })) { for (var i = 0; i < keyPaths.length; ++i) { if (keyPaths.indexOf(ix.keyPath[i]) === -1) return false; } return true; } return false; }).sort(function(a, b) { return a.keyPath.length - b.keyPath.length; })[0]; if (compoundIndex && this.db._maxKey !== maxString) { var keyPathsInValidOrder = compoundIndex.keyPath.slice(0, keyPaths.length); return this.where(keyPathsInValidOrder).equals(keyPathsInValidOrder.map(function(kp) { return indexOrCrit[kp]; })); } if (!compoundIndex && debug) console.warn("The query ".concat(JSON.stringify(indexOrCrit), " on ").concat(this.name, " would benefit from a ") + "compound index [".concat(keyPaths.join("+"), "]")); var idxByName = this.schema.idxByName; var idb = this.db._deps.indexedDB; function equals(a, b) { return idb.cmp(a, b) === 0; } var _a2 = keyPaths.reduce(function(_a3, keyPath) { var prevIndex = _a3[0], prevFilterFn = _a3[1]; var index = idxByName[keyPath]; var value = indexOrCrit[keyPath]; return [ prevIndex || index, prevIndex || !index ? combine(prevFilterFn, index && index.multi ? function(x) { var prop = getByKeyPath(x, keyPath); return isArray2(prop) && prop.some(function(item) { return equals(value, item); }); } : function(x) { return equals(value, getByKeyPath(x, keyPath)); }) : prevFilterFn ]; }, [null, null]), idx = _a2[0], filterFunction = _a2[1]; return idx ? this.where(idx.name).equals(indexOrCrit[idx.keyPath]).filter(filterFunction) : compoundIndex ? this.filter(filterFunction) : this.where(keyPaths).equals(""); }; Table3.prototype.filter = function(filterFunction) { return this.toCollection().and(filterFunction); }; Table3.prototype.count = function(thenShortcut) { return this.toCollection().count(thenShortcut); }; Table3.prototype.offset = function(offset) { return this.toCollection().offset(offset); }; Table3.prototype.limit = function(numRows) { return this.toCollection().limit(numRows); }; Table3.prototype.each = function(callback) { return this.toCollection().each(callback); }; Table3.prototype.toArray = function(thenShortcut) { return this.toCollection().toArray(thenShortcut); }; Table3.prototype.toCollection = function() { return new this.db.Collection(new this.db.WhereClause(this)); }; Table3.prototype.orderBy = function(index) { return new this.db.Collection(new this.db.WhereClause(this, isArray2(index) ? "[".concat(index.join("+"), "]") : index)); }; Table3.prototype.reverse = function() { return this.toCollection().reverse(); }; Table3.prototype.mapToClass = function(constructor) { var _a2 = this, db = _a2.db, tableName = _a2.name; this.schema.mappedClass = constructor; if (constructor.prototype instanceof Entity2) { constructor = function(_super) { __extends(class_1, _super); function class_1() { return _super !== null && _super.apply(this, arguments) || this; } Object.defineProperty(class_1.prototype, "db", { get: function() { return db; }, enumerable: false, configurable: true }); class_1.prototype.table = function() { return tableName; }; return class_1; }(constructor); } var inheritedProps = /* @__PURE__ */ new Set(); for (var proto = constructor.prototype; proto; proto = getProto(proto)) { Object.getOwnPropertyNames(proto).forEach(function(propName) { return inheritedProps.add(propName); }); } var readHook = function(obj) { if (!obj) return obj; var res = Object.create(constructor.prototype); for (var m in obj) if (!inheritedProps.has(m)) try { res[m] = obj[m]; } catch (_) { } return res; }; if (this.schema.readHook) { this.hook.reading.unsubscribe(this.schema.readHook); } this.schema.readHook = readHook; this.hook("reading", readHook); return constructor; }; Table3.prototype.defineClass = function() { function Class(content) { extend(this, content); } return this.mapToClass(Class); }; Table3.prototype.add = function(obj, key) { var _this = this; var _a2 = this.schema.primKey, auto = _a2.auto, keyPath = _a2.keyPath; var objToAdd = obj; if (keyPath && auto) { objToAdd = workaroundForUndefinedPrimKey(keyPath)(obj); } return this._trans("readwrite", function(trans) { return _this.core.mutate({ trans, type: "add", keys: key != null ? [key] : null, values: [objToAdd] }); }).then(function(res) { return res.numFailures ? DexiePromise.reject(res.failures[0]) : res.lastResult; }).then(function(lastResult) { if (keyPath) { try { setByKeyPath(obj, keyPath, lastResult); } catch (_) { } } return lastResult; }); }; Table3.prototype.update = function(keyOrObject, modifications) { if (typeof keyOrObject === "object" && !isArray2(keyOrObject)) { var key = getByKeyPath(keyOrObject, this.schema.primKey.keyPath); if (key === void 0) return rejection(new exceptions.InvalidArgument("Given object does not contain its primary key")); return this.where(":id").equals(key).modify(modifications); } else { return this.where(":id").equals(keyOrObject).modify(modifications); } }; Table3.prototype.put = function(obj, key) { var _this = this; var _a2 = this.schema.primKey, auto = _a2.auto, keyPath = _a2.keyPath; var objToAdd = obj; if (keyPath && auto) { objToAdd = workaroundForUndefinedPrimKey(keyPath)(obj); } return this._trans("readwrite", function(trans) { return _this.core.mutate({ trans, type: "put", values: [objToAdd], keys: key != null ? [key] : null }); }).then(function(res) { return res.numFailures ? DexiePromise.reject(res.failures[0]) : res.lastResult; }).then(function(lastResult) { if (keyPath) { try { setByKeyPath(obj, keyPath, lastResult); } catch (_) { } } return lastResult; }); }; Table3.prototype.delete = function(key) { var _this = this; return this._trans("readwrite", function(trans) { return _this.core.mutate({ trans, type: "delete", keys: [key] }); }).then(function(res) { return res.numFailures ? DexiePromise.reject(res.failures[0]) : void 0; }); }; Table3.prototype.clear = function() { var _this = this; return this._trans("readwrite", function(trans) { return _this.core.mutate({ trans, type: "deleteRange", range: AnyRange }); }).then(function(res) { return res.numFailures ? DexiePromise.reject(res.failures[0]) : void 0; }); }; Table3.prototype.bulkGet = function(keys2) { var _this = this; return this._trans("readonly", function(trans) { return _this.core.getMany({ keys: keys2, trans }).then(function(result) { return result.map(function(res) { return _this.hook.reading.fire(res); }); }); }); }; Table3.prototype.bulkAdd = function(objects, keysOrOptions, options) { var _this = this; var keys2 = Array.isArray(keysOrOptions) ? keysOrOptions : void 0; options = options || (keys2 ? void 0 : keysOrOptions); var wantResults = options ? options.allKeys : void 0; return this._trans("readwrite", function(trans) { var _a2 = _this.schema.primKey, auto = _a2.auto, keyPath = _a2.keyPath; if (keyPath && keys2) throw new exceptions.InvalidArgument("bulkAdd(): keys argument invalid on tables with inbound keys"); if (keys2 && keys2.length !== objects.length) throw new exceptions.InvalidArgument("Arguments objects and keys must have the same length"); var numObjects = objects.length; var objectsToAdd = keyPath && auto ? objects.map(workaroundForUndefinedPrimKey(keyPath)) : objects; return _this.core.mutate({ trans, type: "add", keys: keys2, values: objectsToAdd, wantResults }).then(function(_a3) { var numFailures = _a3.numFailures, results = _a3.results, lastResult = _a3.lastResult, failures = _a3.failures; var result = wantResults ? results : lastResult; if (numFailures === 0) return result; throw new BulkError("".concat(_this.name, ".bulkAdd(): ").concat(numFailures, " of ").concat(numObjects, " operations failed"), failures); }); }); }; Table3.prototype.bulkPut = function(objects, keysOrOptions, options) { var _this = this; var keys2 = Array.isArray(keysOrOptions) ? keysOrOptions : void 0; options = options || (keys2 ? void 0 : keysOrOptions); var wantResults = options ? options.allKeys : void 0; return this._trans("readwrite", function(trans) { var _a2 = _this.schema.primKey, auto = _a2.auto, keyPath = _a2.keyPath; if (keyPath && keys2) throw new exceptions.InvalidArgument("bulkPut(): keys argument invalid on tables with inbound keys"); if (keys2 && keys2.length !== objects.length) throw new exceptions.InvalidArgument("Arguments objects and keys must have the same length"); var numObjects = objects.length; var objectsToPut = keyPath && auto ? objects.map(workaroundForUndefinedPrimKey(keyPath)) : objects; return _this.core.mutate({ trans, type: "put", keys: keys2, values: objectsToPut, wantResults }).then(function(_a3) { var numFailures = _a3.numFailures, results = _a3.results, lastResult = _a3.lastResult, failures = _a3.failures; var result = wantResults ? results : lastResult; if (numFailures === 0) return result; throw new BulkError("".concat(_this.name, ".bulkPut(): ").concat(numFailures, " of ").concat(numObjects, " operations failed"), failures); }); }); }; Table3.prototype.bulkUpdate = function(keysAndChanges) { var _this = this; var coreTable = this.core; var keys2 = keysAndChanges.map(function(entry) { return entry.key; }); var changeSpecs = keysAndChanges.map(function(entry) { return entry.changes; }); var offsetMap = []; return this._trans("readwrite", function(trans) { return coreTable.getMany({ trans, keys: keys2, cache: "clone" }).then(function(objs) { var resultKeys = []; var resultObjs = []; keysAndChanges.forEach(function(_a2, idx) { var key = _a2.key, changes = _a2.changes; var obj = objs[idx]; if (obj) { for (var _i = 0, _b = Object.keys(changes); _i < _b.length; _i++) { var keyPath = _b[_i]; var value = changes[keyPath]; if (keyPath === _this.schema.primKey.keyPath) { if (cmp2(value, key) !== 0) { throw new exceptions.Constraint("Cannot update primary key in bulkUpdate()"); } } else { setByKeyPath(obj, keyPath, value); } } offsetMap.push(idx); resultKeys.push(key); resultObjs.push(obj); } }); var numEntries = resultKeys.length; return coreTable.mutate({ trans, type: "put", keys: resultKeys, values: resultObjs, updates: { keys: keys2, changeSpecs } }).then(function(_a2) { var numFailures = _a2.numFailures, failures = _a2.failures; if (numFailures === 0) return numEntries; for (var _i = 0, _b = Object.keys(failures); _i < _b.length; _i++) { var offset = _b[_i]; var mappedOffset = offsetMap[Number(offset)]; if (mappedOffset != null) { var failure = failures[offset]; delete failures[offset]; failures[mappedOffset] = failure; } } throw new BulkError("".concat(_this.name, ".bulkUpdate(): ").concat(numFailures, " of ").concat(numEntries, " operations failed"), failures); }); }); }); }; Table3.prototype.bulkDelete = function(keys2) { var _this = this; var numKeys = keys2.length; return this._trans("readwrite", function(trans) { return _this.core.mutate({ trans, type: "delete", keys: keys2 }); }).then(function(_a2) { var numFailures = _a2.numFailures, lastResult = _a2.lastResult, failures = _a2.failures; if (numFailures === 0) return lastResult; throw new BulkError("".concat(_this.name, ".bulkDelete(): ").concat(numFailures, " of ").concat(numKeys, " operations failed"), failures); }); }; return Table3; }(); function Events(ctx) { var evs = {}; var rv = function(eventName, subscriber) { if (subscriber) { var i2 = arguments.length, args = new Array(i2 - 1); while (--i2) args[i2 - 1] = arguments[i2]; evs[eventName].subscribe.apply(null, args); return ctx; } else if (typeof eventName === "string") { return evs[eventName]; } }; rv.addEventType = add3; for (var i = 1, l = arguments.length; i < l; ++i) { add3(arguments[i]); } return rv; function add3(eventName, chainFunction, defaultFunction) { if (typeof eventName === "object") return addConfiguredEvents(eventName); if (!chainFunction) chainFunction = reverseStoppableEventChain; if (!defaultFunction) defaultFunction = nop; var context = { subscribers: [], fire: defaultFunction, subscribe: function(cb) { if (context.subscribers.indexOf(cb) === -1) { context.subscribers.push(cb); context.fire = chainFunction(context.fire, cb); } }, unsubscribe: function(cb) { context.subscribers = context.subscribers.filter(function(fn) { return fn !== cb; }); context.fire = context.subscribers.reduce(chainFunction, defaultFunction); } }; evs[eventName] = rv[eventName] = context; return context; } function addConfiguredEvents(cfg) { keys(cfg).forEach(function(eventName) { var args = cfg[eventName]; if (isArray2(args)) { add3(eventName, cfg[eventName][0], cfg[eventName][1]); } else if (args === "asap") { var context = add3(eventName, mirror, function fire() { var i2 = arguments.length, args2 = new Array(i2); while (i2--) args2[i2] = arguments[i2]; context.subscribers.forEach(function(fn) { asap$1(function fireEvent() { fn.apply(null, args2); }); }); }); } else throw new exceptions.InvalidArgument("Invalid event config"); }); } } function makeClassConstructor(prototype, constructor) { derive(constructor).from({ prototype }); return constructor; } function createTableConstructor(db) { return makeClassConstructor(Table2.prototype, function Table3(name, tableSchema, trans) { this.db = db; this._tx = trans; this.name = name; this.schema = tableSchema; this.hook = db._allTables[name] ? db._allTables[name].hook : Events(null, { "creating": [hookCreatingChain, nop], "reading": [pureFunctionChain, mirror], "updating": [hookUpdatingChain, nop], "deleting": [hookDeletingChain, nop] }); }); } function isPlainKeyRange(ctx, ignoreLimitFilter) { return !(ctx.filter || ctx.algorithm || ctx.or) && (ignoreLimitFilter ? ctx.justLimit : !ctx.replayFilter); } function addFilter(ctx, fn) { ctx.filter = combine(ctx.filter, fn); } function addReplayFilter(ctx, factory, isLimitFilter) { var curr = ctx.replayFilter; ctx.replayFilter = curr ? function() { return combine(curr(), factory()); } : factory; ctx.justLimit = isLimitFilter && !curr; } function addMatchFilter(ctx, fn) { ctx.isMatch = combine(ctx.isMatch, fn); } function getIndexOrStore(ctx, coreSchema) { if (ctx.isPrimKey) return coreSchema.primaryKey; var index = coreSchema.getIndexByKeyPath(ctx.index); if (!index) throw new exceptions.Schema("KeyPath " + ctx.index + " on object store " + coreSchema.name + " is not indexed"); return index; } function openCursor(ctx, coreTable, trans) { var index = getIndexOrStore(ctx, coreTable.schema); return coreTable.openCursor({ trans, values: !ctx.keysOnly, reverse: ctx.dir === "prev", unique: !!ctx.unique, query: { index, range: ctx.range } }); } function iter(ctx, fn, coreTrans, coreTable) { var filter = ctx.replayFilter ? combine(ctx.filter, ctx.replayFilter()) : ctx.filter; if (!ctx.or) { return iterate(openCursor(ctx, coreTable, coreTrans), combine(ctx.algorithm, filter), fn, !ctx.keysOnly && ctx.valueMapper); } else { var set_1 = {}; var union = function(item, cursor, advance) { if (!filter || filter(cursor, advance, function(result) { return cursor.stop(result); }, function(err) { return cursor.fail(err); })) { var primaryKey = cursor.primaryKey; var key = "" + primaryKey; if (key === "[object ArrayBuffer]") key = "" + new Uint8Array(primaryKey); if (!hasOwn2(set_1, key)) { set_1[key] = true; fn(item, cursor, advance); } } }; return Promise.all([ ctx.or._iterate(union, coreTrans), iterate(openCursor(ctx, coreTable, coreTrans), ctx.algorithm, union, !ctx.keysOnly && ctx.valueMapper) ]); } } function iterate(cursorPromise, filter, fn, valueMapper) { var mappedFn = valueMapper ? function(x, c, a) { return fn(valueMapper(x), c, a); } : fn; var wrappedFn = wrap(mappedFn); return cursorPromise.then(function(cursor) { if (cursor) { return cursor.start(function() { var c = function() { return cursor.continue(); }; if (!filter || filter(cursor, function(advancer) { return c = advancer; }, function(val) { cursor.stop(val); c = nop; }, function(e) { cursor.fail(e); c = nop; })) wrappedFn(cursor.value, cursor, function(advancer) { return c = advancer; }); c(); }); } }); } var PropModSymbol2 = Symbol(); var PropModification2 = function() { function PropModification3(spec) { Object.assign(this, spec); } PropModification3.prototype.execute = function(value) { var _a2; if (this.add !== void 0) { var term = this.add; if (isArray2(term)) { return __spreadArray(__spreadArray([], isArray2(value) ? value : [], true), term, true).sort(); } if (typeof term === "number") return (Number(value) || 0) + term; if (typeof term === "bigint") { try { return BigInt(value) + term; } catch (_b) { return BigInt(0) + term; } } throw new TypeError("Invalid term ".concat(term)); } if (this.remove !== void 0) { var subtrahend_1 = this.remove; if (isArray2(subtrahend_1)) { return isArray2(value) ? value.filter(function(item) { return !subtrahend_1.includes(item); }).sort() : []; } if (typeof subtrahend_1 === "number") return Number(value) - subtrahend_1; if (typeof subtrahend_1 === "bigint") { try { return BigInt(value) - subtrahend_1; } catch (_c) { return BigInt(0) - subtrahend_1; } } throw new TypeError("Invalid subtrahend ".concat(subtrahend_1)); } var prefixToReplace = (_a2 = this.replacePrefix) === null || _a2 === void 0 ? void 0 : _a2[0]; if (prefixToReplace && typeof value === "string" && value.startsWith(prefixToReplace)) { return this.replacePrefix[1] + value.substring(prefixToReplace.length); } return value; }; return PropModification3; }(); var Collection = function() { function Collection2() { } Collection2.prototype._read = function(fn, cb) { var ctx = this._ctx; return ctx.error ? ctx.table._trans(null, rejection.bind(null, ctx.error)) : ctx.table._trans("readonly", fn).then(cb); }; Collection2.prototype._write = function(fn) { var ctx = this._ctx; return ctx.error ? ctx.table._trans(null, rejection.bind(null, ctx.error)) : ctx.table._trans("readwrite", fn, "locked"); }; Collection2.prototype._addAlgorithm = function(fn) { var ctx = this._ctx; ctx.algorithm = combine(ctx.algorithm, fn); }; Collection2.prototype._iterate = function(fn, coreTrans) { return iter(this._ctx, fn, coreTrans, this._ctx.table.core); }; Collection2.prototype.clone = function(props2) { var rv = Object.create(this.constructor.prototype), ctx = Object.create(this._ctx); if (props2) extend(ctx, props2); rv._ctx = ctx; return rv; }; Collection2.prototype.raw = function() { this._ctx.valueMapper = null; return this; }; Collection2.prototype.each = function(fn) { var ctx = this._ctx; return this._read(function(trans) { return iter(ctx, fn, trans, ctx.table.core); }); }; Collection2.prototype.count = function(cb) { var _this = this; return this._read(function(trans) { var ctx = _this._ctx; var coreTable = ctx.table.core; if (isPlainKeyRange(ctx, true)) { return coreTable.count({ trans, query: { index: getIndexOrStore(ctx, coreTable.schema), range: ctx.range } }).then(function(count2) { return Math.min(count2, ctx.limit); }); } else { var count = 0; return iter(ctx, function() { ++count; return false; }, trans, coreTable).then(function() { return count; }); } }).then(cb); }; Collection2.prototype.sortBy = function(keyPath, cb) { var parts = keyPath.split(".").reverse(), lastPart = parts[0], lastIndex = parts.length - 1; function getval(obj, i) { if (i) return getval(obj[parts[i]], i - 1); return obj[lastPart]; } var order = this._ctx.dir === "next" ? 1 : -1; function sorter(a, b) { var aVal = getval(a, lastIndex), bVal = getval(b, lastIndex); return aVal < bVal ? -order : aVal > bVal ? order : 0; } return this.toArray(function(a) { return a.sort(sorter); }).then(cb); }; Collection2.prototype.toArray = function(cb) { var _this = this; return this._read(function(trans) { var ctx = _this._ctx; if (ctx.dir === "next" && isPlainKeyRange(ctx, true) && ctx.limit > 0) { var valueMapper_1 = ctx.valueMapper; var index = getIndexOrStore(ctx, ctx.table.core.schema); return ctx.table.core.query({ trans, limit: ctx.limit, values: true, query: { index, range: ctx.range } }).then(function(_a2) { var result = _a2.result; return valueMapper_1 ? result.map(valueMapper_1) : result; }); } else { var a_1 = []; return iter(ctx, function(item) { return a_1.push(item); }, trans, ctx.table.core).then(function() { return a_1; }); } }, cb); }; Collection2.prototype.offset = function(offset) { var ctx = this._ctx; if (offset <= 0) return this; ctx.offset += offset; if (isPlainKeyRange(ctx)) { addReplayFilter(ctx, function() { var offsetLeft = offset; return function(cursor, advance) { if (offsetLeft === 0) return true; if (offsetLeft === 1) { --offsetLeft; return false; } advance(function() { cursor.advance(offsetLeft); offsetLeft = 0; }); return false; }; }); } else { addReplayFilter(ctx, function() { var offsetLeft = offset; return function() { return --offsetLeft < 0; }; }); } return this; }; Collection2.prototype.limit = function(numRows) { this._ctx.limit = Math.min(this._ctx.limit, numRows); addReplayFilter(this._ctx, function() { var rowsLeft = numRows; return function(cursor, advance, resolve) { if (--rowsLeft <= 0) advance(resolve); return rowsLeft >= 0; }; }, true); return this; }; Collection2.prototype.until = function(filterFunction, bIncludeStopEntry) { addFilter(this._ctx, function(cursor, advance, resolve) { if (filterFunction(cursor.value)) { advance(resolve); return bIncludeStopEntry; } else { return true; } }); return this; }; Collection2.prototype.first = function(cb) { return this.limit(1).toArray(function(a) { return a[0]; }).then(cb); }; Collection2.prototype.last = function(cb) { return this.reverse().first(cb); }; Collection2.prototype.filter = function(filterFunction) { addFilter(this._ctx, function(cursor) { return filterFunction(cursor.value); }); addMatchFilter(this._ctx, filterFunction); return this; }; Collection2.prototype.and = function(filter) { return this.filter(filter); }; Collection2.prototype.or = function(indexName) { return new this.db.WhereClause(this._ctx.table, indexName, this); }; Collection2.prototype.reverse = function() { this._ctx.dir = this._ctx.dir === "prev" ? "next" : "prev"; if (this._ondirectionchange) this._ondirectionchange(this._ctx.dir); return this; }; Collection2.prototype.desc = function() { return this.reverse(); }; Collection2.prototype.eachKey = function(cb) { var ctx = this._ctx; ctx.keysOnly = !ctx.isMatch; return this.each(function(val, cursor) { cb(cursor.key, cursor); }); }; Collection2.prototype.eachUniqueKey = function(cb) { this._ctx.unique = "unique"; return this.eachKey(cb); }; Collection2.prototype.eachPrimaryKey = function(cb) { var ctx = this._ctx; ctx.keysOnly = !ctx.isMatch; return this.each(function(val, cursor) { cb(cursor.primaryKey, cursor); }); }; Collection2.prototype.keys = function(cb) { var ctx = this._ctx; ctx.keysOnly = !ctx.isMatch; var a = []; return this.each(function(item, cursor) { a.push(cursor.key); }).then(function() { return a; }).then(cb); }; Collection2.prototype.primaryKeys = function(cb) { var ctx = this._ctx; if (ctx.dir === "next" && isPlainKeyRange(ctx, true) && ctx.limit > 0) { return this._read(function(trans) { var index = getIndexOrStore(ctx, ctx.table.core.schema); return ctx.table.core.query({ trans, values: false, limit: ctx.limit, query: { index, range: ctx.range } }); }).then(function(_a2) { var result = _a2.result; return result; }).then(cb); } ctx.keysOnly = !ctx.isMatch; var a = []; return this.each(function(item, cursor) { a.push(cursor.primaryKey); }).then(function() { return a; }).then(cb); }; Collection2.prototype.uniqueKeys = function(cb) { this._ctx.unique = "unique"; return this.keys(cb); }; Collection2.prototype.firstKey = function(cb) { return this.limit(1).keys(function(a) { return a[0]; }).then(cb); }; Collection2.prototype.lastKey = function(cb) { return this.reverse().firstKey(cb); }; Collection2.prototype.distinct = function() { var ctx = this._ctx, idx = ctx.index && ctx.table.schema.idxByName[ctx.index]; if (!idx || !idx.multi) return this; var set = {}; addFilter(this._ctx, function(cursor) { var strKey = cursor.primaryKey.toString(); var found = hasOwn2(set, strKey); set[strKey] = true; return !found; }); return this; }; Collection2.prototype.modify = function(changes) { var _this = this; var ctx = this._ctx; return this._write(function(trans) { var modifyer; if (typeof changes === "function") { modifyer = changes; } else { var keyPaths = keys(changes); var numKeys = keyPaths.length; modifyer = function(item) { var anythingModified = false; for (var i = 0; i < numKeys; ++i) { var keyPath = keyPaths[i]; var val = changes[keyPath]; var origVal = getByKeyPath(item, keyPath); if (val instanceof PropModification2) { setByKeyPath(item, keyPath, val.execute(origVal)); anythingModified = true; } else if (origVal !== val) { setByKeyPath(item, keyPath, val); anythingModified = true; } } return anythingModified; }; } var coreTable = ctx.table.core; var _a2 = coreTable.schema.primaryKey, outbound = _a2.outbound, extractKey = _a2.extractKey; var limit = _this.db._options.modifyChunkSize || 200; var totalFailures = []; var successCount = 0; var failedKeys = []; var applyMutateResult = function(expectedCount, res) { var failures = res.failures, numFailures = res.numFailures; successCount += expectedCount - numFailures; for (var _i = 0, _a3 = keys(failures); _i < _a3.length; _i++) { var pos = _a3[_i]; totalFailures.push(failures[pos]); } }; return _this.clone().primaryKeys().then(function(keys2) { var criteria = isPlainKeyRange(ctx) && ctx.limit === Infinity && (typeof changes !== "function" || changes === deleteCallback) && { index: ctx.index, range: ctx.range }; var nextChunk = function(offset) { var count = Math.min(limit, keys2.length - offset); return coreTable.getMany({ trans, keys: keys2.slice(offset, offset + count), cache: "immutable" }).then(function(values) { var addValues = []; var putValues = []; var putKeys = outbound ? [] : null; var deleteKeys = []; for (var i = 0; i < count; ++i) { var origValue = values[i]; var ctx_1 = { value: deepClone(origValue), primKey: keys2[offset + i] }; if (modifyer.call(ctx_1, ctx_1.value, ctx_1) !== false) { if (ctx_1.value == null) { deleteKeys.push(keys2[offset + i]); } else if (!outbound && cmp2(extractKey(origValue), extractKey(ctx_1.value)) !== 0) { deleteKeys.push(keys2[offset + i]); addValues.push(ctx_1.value); } else { putValues.push(ctx_1.value); if (outbound) putKeys.push(keys2[offset + i]); } } } return Promise.resolve(addValues.length > 0 && coreTable.mutate({ trans, type: "add", values: addValues }).then(function(res) { for (var pos in res.failures) { deleteKeys.splice(parseInt(pos), 1); } applyMutateResult(addValues.length, res); })).then(function() { return (putValues.length > 0 || criteria && typeof changes === "object") && coreTable.mutate({ trans, type: "put", keys: putKeys, values: putValues, criteria, changeSpec: typeof changes !== "function" && changes, isAdditionalChunk: offset > 0 }).then(function(res) { return applyMutateResult(putValues.length, res); }); }).then(function() { return (deleteKeys.length > 0 || criteria && changes === deleteCallback) && coreTable.mutate({ trans, type: "delete", keys: deleteKeys, criteria, isAdditionalChunk: offset > 0 }).then(function(res) { return applyMutateResult(deleteKeys.length, res); }); }).then(function() { return keys2.length > offset + count && nextChunk(offset + limit); }); }); }; return nextChunk(0).then(function() { if (totalFailures.length > 0) throw new ModifyError("Error modifying one or more objects", totalFailures, successCount, failedKeys); return keys2.length; }); }); }); }; Collection2.prototype.delete = function() { var ctx = this._ctx, range = ctx.range; if (isPlainKeyRange(ctx) && (ctx.isPrimKey || range.type === 3)) { return this._write(function(trans) { var primaryKey = ctx.table.core.schema.primaryKey; var coreRange = range; return ctx.table.core.count({ trans, query: { index: primaryKey, range: coreRange } }).then(function(count) { return ctx.table.core.mutate({ trans, type: "deleteRange", range: coreRange }).then(function(_a2) { var failures = _a2.failures; _a2.lastResult; _a2.results; var numFailures = _a2.numFailures; if (numFailures) throw new ModifyError("Could not delete some values", Object.keys(failures).map(function(pos) { return failures[pos]; }), count - numFailures); return count - numFailures; }); }); }); } return this.modify(deleteCallback); }; return Collection2; }(); var deleteCallback = function(value, ctx) { return ctx.value = null; }; function createCollectionConstructor(db) { return makeClassConstructor(Collection.prototype, function Collection2(whereClause, keyRangeGenerator) { this.db = db; var keyRange = AnyRange, error40 = null; if (keyRangeGenerator) try { keyRange = keyRangeGenerator(); } catch (ex) { error40 = ex; } var whereCtx = whereClause._ctx; var table = whereCtx.table; var readingHook = table.hook.reading.fire; this._ctx = { table, index: whereCtx.index, isPrimKey: !whereCtx.index || table.schema.primKey.keyPath && whereCtx.index === table.schema.primKey.name, range: keyRange, keysOnly: false, dir: "next", unique: "", algorithm: null, filter: null, replayFilter: null, justLimit: true, isMatch: null, offset: 0, limit: Infinity, error: error40, or: whereCtx.or, valueMapper: readingHook !== mirror ? readingHook : null }; }); } function simpleCompare(a, b) { return a < b ? -1 : a === b ? 0 : 1; } function simpleCompareReverse(a, b) { return a > b ? -1 : a === b ? 0 : 1; } function fail(collectionOrWhereClause, err, T) { var collection = collectionOrWhereClause instanceof WhereClause ? new collectionOrWhereClause.Collection(collectionOrWhereClause) : collectionOrWhereClause; collection._ctx.error = T ? new T(err) : new TypeError(err); return collection; } function emptyCollection(whereClause) { return new whereClause.Collection(whereClause, function() { return rangeEqual(""); }).limit(0); } function upperFactory(dir) { return dir === "next" ? function(s) { return s.toUpperCase(); } : function(s) { return s.toLowerCase(); }; } function lowerFactory(dir) { return dir === "next" ? function(s) { return s.toLowerCase(); } : function(s) { return s.toUpperCase(); }; } function nextCasing(key, lowerKey, upperNeedle, lowerNeedle, cmp3, dir) { var length = Math.min(key.length, lowerNeedle.length); var llp = -1; for (var i = 0; i < length; ++i) { var lwrKeyChar = lowerKey[i]; if (lwrKeyChar !== lowerNeedle[i]) { if (cmp3(key[i], upperNeedle[i]) < 0) return key.substr(0, i) + upperNeedle[i] + upperNeedle.substr(i + 1); if (cmp3(key[i], lowerNeedle[i]) < 0) return key.substr(0, i) + lowerNeedle[i] + upperNeedle.substr(i + 1); if (llp >= 0) return key.substr(0, llp) + lowerKey[llp] + upperNeedle.substr(llp + 1); return null; } if (cmp3(key[i], lwrKeyChar) < 0) llp = i; } if (length < lowerNeedle.length && dir === "next") return key + upperNeedle.substr(key.length); if (length < key.length && dir === "prev") return key.substr(0, upperNeedle.length); return llp < 0 ? null : key.substr(0, llp) + lowerNeedle[llp] + upperNeedle.substr(llp + 1); } function addIgnoreCaseAlgorithm(whereClause, match, needles, suffix) { var upper, lower, compare, upperNeedles, lowerNeedles, direction, nextKeySuffix, needlesLen = needles.length; if (!needles.every(function(s) { return typeof s === "string"; })) { return fail(whereClause, STRING_EXPECTED); } function initDirection(dir) { upper = upperFactory(dir); lower = lowerFactory(dir); compare = dir === "next" ? simpleCompare : simpleCompareReverse; var needleBounds = needles.map(function(needle) { return { lower: lower(needle), upper: upper(needle) }; }).sort(function(a, b) { return compare(a.lower, b.lower); }); upperNeedles = needleBounds.map(function(nb) { return nb.upper; }); lowerNeedles = needleBounds.map(function(nb) { return nb.lower; }); direction = dir; nextKeySuffix = dir === "next" ? "" : suffix; } initDirection("next"); var c = new whereClause.Collection(whereClause, function() { return createRange(upperNeedles[0], lowerNeedles[needlesLen - 1] + suffix); }); c._ondirectionchange = function(direction2) { initDirection(direction2); }; var firstPossibleNeedle = 0; c._addAlgorithm(function(cursor, advance, resolve) { var key = cursor.key; if (typeof key !== "string") return false; var lowerKey = lower(key); if (match(lowerKey, lowerNeedles, firstPossibleNeedle)) { return true; } else { var lowestPossibleCasing = null; for (var i = firstPossibleNeedle; i < needlesLen; ++i) { var casing = nextCasing(key, lowerKey, upperNeedles[i], lowerNeedles[i], compare, direction); if (casing === null && lowestPossibleCasing === null) firstPossibleNeedle = i + 1; else if (lowestPossibleCasing === null || compare(lowestPossibleCasing, casing) > 0) { lowestPossibleCasing = casing; } } if (lowestPossibleCasing !== null) { advance(function() { cursor.continue(lowestPossibleCasing + nextKeySuffix); }); } else { advance(resolve); } return false; } }); return c; } function createRange(lower, upper, lowerOpen, upperOpen) { return { type: 2, lower, upper, lowerOpen, upperOpen }; } function rangeEqual(value) { return { type: 1, lower: value, upper: value }; } var WhereClause = function() { function WhereClause2() { } Object.defineProperty(WhereClause2.prototype, "Collection", { get: function() { return this._ctx.table.db.Collection; }, enumerable: false, configurable: true }); WhereClause2.prototype.between = function(lower, upper, includeLower, includeUpper) { includeLower = includeLower !== false; includeUpper = includeUpper === true; try { if (this._cmp(lower, upper) > 0 || this._cmp(lower, upper) === 0 && (includeLower || includeUpper) && !(includeLower && includeUpper)) return emptyCollection(this); return new this.Collection(this, function() { return createRange(lower, upper, !includeLower, !includeUpper); }); } catch (e) { return fail(this, INVALID_KEY_ARGUMENT); } }; WhereClause2.prototype.equals = function(value) { if (value == null) return fail(this, INVALID_KEY_ARGUMENT); return new this.Collection(this, function() { return rangeEqual(value); }); }; WhereClause2.prototype.above = function(value) { if (value == null) return fail(this, INVALID_KEY_ARGUMENT); return new this.Collection(this, function() { return createRange(value, void 0, true); }); }; WhereClause2.prototype.aboveOrEqual = function(value) { if (value == null) return fail(this, INVALID_KEY_ARGUMENT); return new this.Collection(this, function() { return createRange(value, void 0, false); }); }; WhereClause2.prototype.below = function(value) { if (value == null) return fail(this, INVALID_KEY_ARGUMENT); return new this.Collection(this, function() { return createRange(void 0, value, false, true); }); }; WhereClause2.prototype.belowOrEqual = function(value) { if (value == null) return fail(this, INVALID_KEY_ARGUMENT); return new this.Collection(this, function() { return createRange(void 0, value); }); }; WhereClause2.prototype.startsWith = function(str) { if (typeof str !== "string") return fail(this, STRING_EXPECTED); return this.between(str, str + maxString, true, true); }; WhereClause2.prototype.startsWithIgnoreCase = function(str) { if (str === "") return this.startsWith(str); return addIgnoreCaseAlgorithm(this, function(x, a) { return x.indexOf(a[0]) === 0; }, [str], maxString); }; WhereClause2.prototype.equalsIgnoreCase = function(str) { return addIgnoreCaseAlgorithm(this, function(x, a) { return x === a[0]; }, [str], ""); }; WhereClause2.prototype.anyOfIgnoreCase = function() { var set = getArrayOf.apply(NO_CHAR_ARRAY, arguments); if (set.length === 0) return emptyCollection(this); return addIgnoreCaseAlgorithm(this, function(x, a) { return a.indexOf(x) !== -1; }, set, ""); }; WhereClause2.prototype.startsWithAnyOfIgnoreCase = function() { var set = getArrayOf.apply(NO_CHAR_ARRAY, arguments); if (set.length === 0) return emptyCollection(this); return addIgnoreCaseAlgorithm(this, function(x, a) { return a.some(function(n) { return x.indexOf(n) === 0; }); }, set, maxString); }; WhereClause2.prototype.anyOf = function() { var _this = this; var set = getArrayOf.apply(NO_CHAR_ARRAY, arguments); var compare = this._cmp; try { set.sort(compare); } catch (e) { return fail(this, INVALID_KEY_ARGUMENT); } if (set.length === 0) return emptyCollection(this); var c = new this.Collection(this, function() { return createRange(set[0], set[set.length - 1]); }); c._ondirectionchange = function(direction) { compare = direction === "next" ? _this._ascending : _this._descending; set.sort(compare); }; var i = 0; c._addAlgorithm(function(cursor, advance, resolve) { var key = cursor.key; while (compare(key, set[i]) > 0) { ++i; if (i === set.length) { advance(resolve); return false; } } if (compare(key, set[i]) === 0) { return true; } else { advance(function() { cursor.continue(set[i]); }); return false; } }); return c; }; WhereClause2.prototype.notEqual = function(value) { return this.inAnyRange([[minKey, value], [value, this.db._maxKey]], { includeLowers: false, includeUppers: false }); }; WhereClause2.prototype.noneOf = function() { var set = getArrayOf.apply(NO_CHAR_ARRAY, arguments); if (set.length === 0) return new this.Collection(this); try { set.sort(this._ascending); } catch (e) { return fail(this, INVALID_KEY_ARGUMENT); } var ranges = set.reduce(function(res, val) { return res ? res.concat([[res[res.length - 1][1], val]]) : [[minKey, val]]; }, null); ranges.push([set[set.length - 1], this.db._maxKey]); return this.inAnyRange(ranges, { includeLowers: false, includeUppers: false }); }; WhereClause2.prototype.inAnyRange = function(ranges, options) { var _this = this; var cmp3 = this._cmp, ascending = this._ascending, descending = this._descending, min = this._min, max = this._max; if (ranges.length === 0) return emptyCollection(this); if (!ranges.every(function(range) { return range[0] !== void 0 && range[1] !== void 0 && ascending(range[0], range[1]) <= 0; })) { return fail(this, "First argument to inAnyRange() must be an Array of two-value Arrays [lower,upper] where upper must not be lower than lower", exceptions.InvalidArgument); } var includeLowers = !options || options.includeLowers !== false; var includeUppers = options && options.includeUppers === true; function addRange2(ranges2, newRange) { var i = 0, l = ranges2.length; for (; i < l; ++i) { var range = ranges2[i]; if (cmp3(newRange[0], range[1]) < 0 && cmp3(newRange[1], range[0]) > 0) { range[0] = min(range[0], newRange[0]); range[1] = max(range[1], newRange[1]); break; } } if (i === l) ranges2.push(newRange); return ranges2; } var sortDirection = ascending; function rangeSorter(a, b) { return sortDirection(a[0], b[0]); } var set; try { set = ranges.reduce(addRange2, []); set.sort(rangeSorter); } catch (ex) { return fail(this, INVALID_KEY_ARGUMENT); } var rangePos = 0; var keyIsBeyondCurrentEntry = includeUppers ? function(key) { return ascending(key, set[rangePos][1]) > 0; } : function(key) { return ascending(key, set[rangePos][1]) >= 0; }; var keyIsBeforeCurrentEntry = includeLowers ? function(key) { return descending(key, set[rangePos][0]) > 0; } : function(key) { return descending(key, set[rangePos][0]) >= 0; }; function keyWithinCurrentRange(key) { return !keyIsBeyondCurrentEntry(key) && !keyIsBeforeCurrentEntry(key); } var checkKey = keyIsBeyondCurrentEntry; var c = new this.Collection(this, function() { return createRange(set[0][0], set[set.length - 1][1], !includeLowers, !includeUppers); }); c._ondirectionchange = function(direction) { if (direction === "next") { checkKey = keyIsBeyondCurrentEntry; sortDirection = ascending; } else { checkKey = keyIsBeforeCurrentEntry; sortDirection = descending; } set.sort(rangeSorter); }; c._addAlgorithm(function(cursor, advance, resolve) { var key = cursor.key; while (checkKey(key)) { ++rangePos; if (rangePos === set.length) { advance(resolve); return false; } } if (keyWithinCurrentRange(key)) { return true; } else if (_this._cmp(key, set[rangePos][1]) === 0 || _this._cmp(key, set[rangePos][0]) === 0) { return false; } else { advance(function() { if (sortDirection === ascending) cursor.continue(set[rangePos][0]); else cursor.continue(set[rangePos][1]); }); return false; } }); return c; }; WhereClause2.prototype.startsWithAnyOf = function() { var set = getArrayOf.apply(NO_CHAR_ARRAY, arguments); if (!set.every(function(s) { return typeof s === "string"; })) { return fail(this, "startsWithAnyOf() only works with strings"); } if (set.length === 0) return emptyCollection(this); return this.inAnyRange(set.map(function(str) { return [str, str + maxString]; })); }; return WhereClause2; }(); function createWhereClauseConstructor(db) { return makeClassConstructor(WhereClause.prototype, function WhereClause2(table, index, orCollection) { this.db = db; this._ctx = { table, index: index === ":id" ? null : index, or: orCollection }; this._cmp = this._ascending = cmp2; this._descending = function(a, b) { return cmp2(b, a); }; this._max = function(a, b) { return cmp2(a, b) > 0 ? a : b; }; this._min = function(a, b) { return cmp2(a, b) < 0 ? a : b; }; this._IDBKeyRange = db._deps.IDBKeyRange; if (!this._IDBKeyRange) throw new exceptions.MissingAPI(); }); } function eventRejectHandler(reject) { return wrap(function(event) { preventDefault(event); reject(event.target.error); return false; }); } function preventDefault(event) { if (event.stopPropagation) event.stopPropagation(); if (event.preventDefault) event.preventDefault(); } var DEXIE_STORAGE_MUTATED_EVENT_NAME = "storagemutated"; var STORAGE_MUTATED_DOM_EVENT_NAME = "x-storagemutated-1"; var globalEvents = Events(null, DEXIE_STORAGE_MUTATED_EVENT_NAME); var Transaction = function() { function Transaction2() { } Transaction2.prototype._lock = function() { assert(!PSD.global); ++this._reculock; if (this._reculock === 1 && !PSD.global) PSD.lockOwnerFor = this; return this; }; Transaction2.prototype._unlock = function() { assert(!PSD.global); if (--this._reculock === 0) { if (!PSD.global) PSD.lockOwnerFor = null; while (this._blockedFuncs.length > 0 && !this._locked()) { var fnAndPSD = this._blockedFuncs.shift(); try { usePSD(fnAndPSD[1], fnAndPSD[0]); } catch (e) { } } } return this; }; Transaction2.prototype._locked = function() { return this._reculock && PSD.lockOwnerFor !== this; }; Transaction2.prototype.create = function(idbtrans) { var _this = this; if (!this.mode) return this; var idbdb = this.db.idbdb; var dbOpenError = this.db._state.dbOpenError; assert(!this.idbtrans); if (!idbtrans && !idbdb) { switch (dbOpenError && dbOpenError.name) { case "DatabaseClosedError": throw new exceptions.DatabaseClosed(dbOpenError); case "MissingAPIError": throw new exceptions.MissingAPI(dbOpenError.message, dbOpenError); default: throw new exceptions.OpenFailed(dbOpenError); } } if (!this.active) throw new exceptions.TransactionInactive(); assert(this._completion._state === null); idbtrans = this.idbtrans = idbtrans || (this.db.core ? this.db.core.transaction(this.storeNames, this.mode, { durability: this.chromeTransactionDurability }) : idbdb.transaction(this.storeNames, this.mode, { durability: this.chromeTransactionDurability })); idbtrans.onerror = wrap(function(ev) { preventDefault(ev); _this._reject(idbtrans.error); }); idbtrans.onabort = wrap(function(ev) { preventDefault(ev); _this.active && _this._reject(new exceptions.Abort(idbtrans.error)); _this.active = false; _this.on("abort").fire(ev); }); idbtrans.oncomplete = wrap(function() { _this.active = false; _this._resolve(); if ("mutatedParts" in idbtrans) { globalEvents.storagemutated.fire(idbtrans["mutatedParts"]); } }); return this; }; Transaction2.prototype._promise = function(mode, fn, bWriteLock) { var _this = this; if (mode === "readwrite" && this.mode !== "readwrite") return rejection(new exceptions.ReadOnly("Transaction is readonly")); if (!this.active) return rejection(new exceptions.TransactionInactive()); if (this._locked()) { return new DexiePromise(function(resolve, reject) { _this._blockedFuncs.push([function() { _this._promise(mode, fn, bWriteLock).then(resolve, reject); }, PSD]); }); } else if (bWriteLock) { return newScope(function() { var p2 = new DexiePromise(function(resolve, reject) { _this._lock(); var rv = fn(resolve, reject, _this); if (rv && rv.then) rv.then(resolve, reject); }); p2.finally(function() { return _this._unlock(); }); p2._lib = true; return p2; }); } else { var p = new DexiePromise(function(resolve, reject) { var rv = fn(resolve, reject, _this); if (rv && rv.then) rv.then(resolve, reject); }); p._lib = true; return p; } }; Transaction2.prototype._root = function() { return this.parent ? this.parent._root() : this; }; Transaction2.prototype.waitFor = function(promiseLike) { var root = this._root(); var promise = DexiePromise.resolve(promiseLike); if (root._waitingFor) { root._waitingFor = root._waitingFor.then(function() { return promise; }); } else { root._waitingFor = promise; root._waitingQueue = []; var store = root.idbtrans.objectStore(root.storeNames[0]); (function spin() { ++root._spinCount; while (root._waitingQueue.length) root._waitingQueue.shift()(); if (root._waitingFor) store.get(-Infinity).onsuccess = spin; })(); } var currentWaitPromise = root._waitingFor; return new DexiePromise(function(resolve, reject) { promise.then(function(res) { return root._waitingQueue.push(wrap(resolve.bind(null, res))); }, function(err) { return root._waitingQueue.push(wrap(reject.bind(null, err))); }).finally(function() { if (root._waitingFor === currentWaitPromise) { root._waitingFor = null; } }); }); }; Transaction2.prototype.abort = function() { if (this.active) { this.active = false; if (this.idbtrans) this.idbtrans.abort(); this._reject(new exceptions.Abort()); } }; Transaction2.prototype.table = function(tableName) { var memoizedTables = this._memoizedTables || (this._memoizedTables = {}); if (hasOwn2(memoizedTables, tableName)) return memoizedTables[tableName]; var tableSchema = this.schema[tableName]; if (!tableSchema) { throw new exceptions.NotFound("Table " + tableName + " not part of transaction"); } var transactionBoundTable = new this.db.Table(tableName, tableSchema, this); transactionBoundTable.core = this.db.core.table(tableName); memoizedTables[tableName] = transactionBoundTable; return transactionBoundTable; }; return Transaction2; }(); function createTransactionConstructor(db) { return makeClassConstructor(Transaction.prototype, function Transaction2(mode, storeNames, dbschema, chromeTransactionDurability, parent) { var _this = this; this.db = db; this.mode = mode; this.storeNames = storeNames; this.schema = dbschema; this.chromeTransactionDurability = chromeTransactionDurability; this.idbtrans = null; this.on = Events(this, "complete", "error", "abort"); this.parent = parent || null; this.active = true; this._reculock = 0; this._blockedFuncs = []; this._resolve = null; this._reject = null; this._waitingFor = null; this._waitingQueue = null; this._spinCount = 0; this._completion = new DexiePromise(function(resolve, reject) { _this._resolve = resolve; _this._reject = reject; }); this._completion.then(function() { _this.active = false; _this.on.complete.fire(); }, function(e) { var wasActive = _this.active; _this.active = false; _this.on.error.fire(e); _this.parent ? _this.parent._reject(e) : wasActive && _this.idbtrans && _this.idbtrans.abort(); return rejection(e); }); }); } function createIndexSpec(name, keyPath, unique, multi, auto, compound, isPrimKey) { return { name, keyPath, unique, multi, auto, compound, src: (unique && !isPrimKey ? "&" : "") + (multi ? "*" : "") + (auto ? "++" : "") + nameFromKeyPath(keyPath) }; } function nameFromKeyPath(keyPath) { return typeof keyPath === "string" ? keyPath : keyPath ? "[" + [].join.call(keyPath, "+") + "]" : ""; } function createTableSchema(name, primKey, indexes) { return { name, primKey, indexes, mappedClass: null, idxByName: arrayToObject(indexes, function(index) { return [index.name, index]; }) }; } function safariMultiStoreFix(storeNames) { return storeNames.length === 1 ? storeNames[0] : storeNames; } var getMaxKey = function(IdbKeyRange) { try { IdbKeyRange.only([[]]); getMaxKey = function() { return [[]]; }; return [[]]; } catch (e) { getMaxKey = function() { return maxString; }; return maxString; } }; function getKeyExtractor(keyPath) { if (keyPath == null) { return function() { return void 0; }; } else if (typeof keyPath === "string") { return getSinglePathKeyExtractor(keyPath); } else { return function(obj) { return getByKeyPath(obj, keyPath); }; } } function getSinglePathKeyExtractor(keyPath) { var split = keyPath.split("."); if (split.length === 1) { return function(obj) { return obj[keyPath]; }; } else { return function(obj) { return getByKeyPath(obj, keyPath); }; } } function arrayify(arrayLike) { return [].slice.call(arrayLike); } var _id_counter = 0; function getKeyPathAlias(keyPath) { return keyPath == null ? ":id" : typeof keyPath === "string" ? keyPath : "[".concat(keyPath.join("+"), "]"); } function createDBCore(db, IdbKeyRange, tmpTrans) { function extractSchema(db2, trans) { var tables2 = arrayify(db2.objectStoreNames); return { schema: { name: db2.name, tables: tables2.map(function(table) { return trans.objectStore(table); }).map(function(store) { var keyPath = store.keyPath, autoIncrement = store.autoIncrement; var compound = isArray2(keyPath); var outbound = keyPath == null; var indexByKeyPath = {}; var result = { name: store.name, primaryKey: { name: null, isPrimaryKey: true, outbound, compound, keyPath, autoIncrement, unique: true, extractKey: getKeyExtractor(keyPath) }, indexes: arrayify(store.indexNames).map(function(indexName) { return store.index(indexName); }).map(function(index) { var name = index.name, unique = index.unique, multiEntry = index.multiEntry, keyPath2 = index.keyPath; var compound2 = isArray2(keyPath2); var result2 = { name, compound: compound2, keyPath: keyPath2, unique, multiEntry, extractKey: getKeyExtractor(keyPath2) }; indexByKeyPath[getKeyPathAlias(keyPath2)] = result2; return result2; }), getIndexByKeyPath: function(keyPath2) { return indexByKeyPath[getKeyPathAlias(keyPath2)]; } }; indexByKeyPath[":id"] = result.primaryKey; if (keyPath != null) { indexByKeyPath[getKeyPathAlias(keyPath)] = result.primaryKey; } return result; }) }, hasGetAll: tables2.length > 0 && "getAll" in trans.objectStore(tables2[0]) && !(typeof navigator !== "undefined" && /Safari/.test(navigator.userAgent) && !/(Chrome\/|Edge\/)/.test(navigator.userAgent) && [].concat(navigator.userAgent.match(/Safari\/(\d*)/))[1] < 604) }; } function makeIDBKeyRange(range) { if (range.type === 3) return null; if (range.type === 4) throw new Error("Cannot convert never type to IDBKeyRange"); var lower = range.lower, upper = range.upper, lowerOpen = range.lowerOpen, upperOpen = range.upperOpen; var idbRange = lower === void 0 ? upper === void 0 ? null : IdbKeyRange.upperBound(upper, !!upperOpen) : upper === void 0 ? IdbKeyRange.lowerBound(lower, !!lowerOpen) : IdbKeyRange.bound(lower, upper, !!lowerOpen, !!upperOpen); return idbRange; } function createDbCoreTable(tableSchema) { var tableName = tableSchema.name; function mutate(_a3) { var trans = _a3.trans, type2 = _a3.type, keys2 = _a3.keys, values = _a3.values, range = _a3.range; return new Promise(function(resolve, reject) { resolve = wrap(resolve); var store = trans.objectStore(tableName); var outbound = store.keyPath == null; var isAddOrPut = type2 === "put" || type2 === "add"; if (!isAddOrPut && type2 !== "delete" && type2 !== "deleteRange") throw new Error("Invalid operation type: " + type2); var length = (keys2 || values || { length: 1 }).length; if (keys2 && values && keys2.length !== values.length) { throw new Error("Given keys array must have same length as given values array."); } if (length === 0) return resolve({ numFailures: 0, failures: {}, results: [], lastResult: void 0 }); var req; var reqs = []; var failures = []; var numFailures = 0; var errorHandler = function(event) { ++numFailures; preventDefault(event); }; if (type2 === "deleteRange") { if (range.type === 4) return resolve({ numFailures, failures, results: [], lastResult: void 0 }); if (range.type === 3) reqs.push(req = store.clear()); else reqs.push(req = store.delete(makeIDBKeyRange(range))); } else { var _a4 = isAddOrPut ? outbound ? [values, keys2] : [values, null] : [keys2, null], args1 = _a4[0], args2 = _a4[1]; if (isAddOrPut) { for (var i = 0; i < length; ++i) { reqs.push(req = args2 && args2[i] !== void 0 ? store[type2](args1[i], args2[i]) : store[type2](args1[i])); req.onerror = errorHandler; } } else { for (var i = 0; i < length; ++i) { reqs.push(req = store[type2](args1[i])); req.onerror = errorHandler; } } } var done = function(event) { var lastResult = event.target.result; reqs.forEach(function(req2, i2) { return req2.error != null && (failures[i2] = req2.error); }); resolve({ numFailures, failures, results: type2 === "delete" ? keys2 : reqs.map(function(req2) { return req2.result; }), lastResult }); }; req.onerror = function(event) { errorHandler(event); done(event); }; req.onsuccess = done; }); } function openCursor2(_a3) { var trans = _a3.trans, values = _a3.values, query2 = _a3.query, reverse = _a3.reverse, unique = _a3.unique; return new Promise(function(resolve, reject) { resolve = wrap(resolve); var index = query2.index, range = query2.range; var store = trans.objectStore(tableName); var source = index.isPrimaryKey ? store : store.index(index.name); var direction = reverse ? unique ? "prevunique" : "prev" : unique ? "nextunique" : "next"; var req = values || !("openKeyCursor" in source) ? source.openCursor(makeIDBKeyRange(range), direction) : source.openKeyCursor(makeIDBKeyRange(range), direction); req.onerror = eventRejectHandler(reject); req.onsuccess = wrap(function(ev) { var cursor = req.result; if (!cursor) { resolve(null); return; } cursor.___id = ++_id_counter; cursor.done = false; var _cursorContinue = cursor.continue.bind(cursor); var _cursorContinuePrimaryKey = cursor.continuePrimaryKey; if (_cursorContinuePrimaryKey) _cursorContinuePrimaryKey = _cursorContinuePrimaryKey.bind(cursor); var _cursorAdvance = cursor.advance.bind(cursor); var doThrowCursorIsNotStarted = function() { throw new Error("Cursor not started"); }; var doThrowCursorIsStopped = function() { throw new Error("Cursor not stopped"); }; cursor.trans = trans; cursor.stop = cursor.continue = cursor.continuePrimaryKey = cursor.advance = doThrowCursorIsNotStarted; cursor.fail = wrap(reject); cursor.next = function() { var _this = this; var gotOne = 1; return this.start(function() { return gotOne-- ? _this.continue() : _this.stop(); }).then(function() { return _this; }); }; cursor.start = function(callback) { var iterationPromise = new Promise(function(resolveIteration, rejectIteration) { resolveIteration = wrap(resolveIteration); req.onerror = eventRejectHandler(rejectIteration); cursor.fail = rejectIteration; cursor.stop = function(value) { cursor.stop = cursor.continue = cursor.continuePrimaryKey = cursor.advance = doThrowCursorIsStopped; resolveIteration(value); }; }); var guardedCallback = function() { if (req.result) { try { callback(); } catch (err) { cursor.fail(err); } } else { cursor.done = true; cursor.start = function() { throw new Error("Cursor behind last entry"); }; cursor.stop(); } }; req.onsuccess = wrap(function(ev2) { req.onsuccess = guardedCallback; guardedCallback(); }); cursor.continue = _cursorContinue; cursor.continuePrimaryKey = _cursorContinuePrimaryKey; cursor.advance = _cursorAdvance; guardedCallback(); return iterationPromise; }; resolve(cursor); }, reject); }); } function query(hasGetAll2) { return function(request) { return new Promise(function(resolve, reject) { resolve = wrap(resolve); var trans = request.trans, values = request.values, limit = request.limit, query2 = request.query; var nonInfinitLimit = limit === Infinity ? void 0 : limit; var index = query2.index, range = query2.range; var store = trans.objectStore(tableName); var source = index.isPrimaryKey ? store : store.index(index.name); var idbKeyRange = makeIDBKeyRange(range); if (limit === 0) return resolve({ result: [] }); if (hasGetAll2) { var req = values ? source.getAll(idbKeyRange, nonInfinitLimit) : source.getAllKeys(idbKeyRange, nonInfinitLimit); req.onsuccess = function(event) { return resolve({ result: event.target.result }); }; req.onerror = eventRejectHandler(reject); } else { var count_1 = 0; var req_1 = values || !("openKeyCursor" in source) ? source.openCursor(idbKeyRange) : source.openKeyCursor(idbKeyRange); var result_1 = []; req_1.onsuccess = function(event) { var cursor = req_1.result; if (!cursor) return resolve({ result: result_1 }); result_1.push(values ? cursor.value : cursor.primaryKey); if (++count_1 === limit) return resolve({ result: result_1 }); cursor.continue(); }; req_1.onerror = eventRejectHandler(reject); } }); }; } return { name: tableName, schema: tableSchema, mutate, getMany: function(_a3) { var trans = _a3.trans, keys2 = _a3.keys; return new Promise(function(resolve, reject) { resolve = wrap(resolve); var store = trans.objectStore(tableName); var length = keys2.length; var result = new Array(length); var keyCount = 0; var callbackCount = 0; var req; var successHandler = function(event) { var req2 = event.target; if ((result[req2._pos] = req2.result) != null) ; if (++callbackCount === keyCount) resolve(result); }; var errorHandler = eventRejectHandler(reject); for (var i = 0; i < length; ++i) { var key = keys2[i]; if (key != null) { req = store.get(keys2[i]); req._pos = i; req.onsuccess = successHandler; req.onerror = errorHandler; ++keyCount; } } if (keyCount === 0) resolve(result); }); }, get: function(_a3) { var trans = _a3.trans, key = _a3.key; return new Promise(function(resolve, reject) { resolve = wrap(resolve); var store = trans.objectStore(tableName); var req = store.get(key); req.onsuccess = function(event) { return resolve(event.target.result); }; req.onerror = eventRejectHandler(reject); }); }, query: query(hasGetAll), openCursor: openCursor2, count: function(_a3) { var query2 = _a3.query, trans = _a3.trans; var index = query2.index, range = query2.range; return new Promise(function(resolve, reject) { var store = trans.objectStore(tableName); var source = index.isPrimaryKey ? store : store.index(index.name); var idbKeyRange = makeIDBKeyRange(range); var req = idbKeyRange ? source.count(idbKeyRange) : source.count(); req.onsuccess = wrap(function(ev) { return resolve(ev.target.result); }); req.onerror = eventRejectHandler(reject); }); } }; } var _a2 = extractSchema(db, tmpTrans), schema = _a2.schema, hasGetAll = _a2.hasGetAll; var tables = schema.tables.map(function(tableSchema) { return createDbCoreTable(tableSchema); }); var tableMap = {}; tables.forEach(function(table) { return tableMap[table.name] = table; }); return { stack: "dbcore", transaction: db.transaction.bind(db), table: function(name) { var result = tableMap[name]; if (!result) throw new Error("Table '".concat(name, "' not found")); return tableMap[name]; }, MIN_KEY: -Infinity, MAX_KEY: getMaxKey(IdbKeyRange), schema }; } function createMiddlewareStack(stackImpl, middlewares) { return middlewares.reduce(function(down, _a2) { var create = _a2.create; return __assign(__assign({}, down), create(down)); }, stackImpl); } function createMiddlewareStacks(middlewares, idbdb, _a2, tmpTrans) { var IDBKeyRange = _a2.IDBKeyRange; _a2.indexedDB; var dbcore = createMiddlewareStack(createDBCore(idbdb, IDBKeyRange, tmpTrans), middlewares.dbcore); return { dbcore }; } function generateMiddlewareStacks(db, tmpTrans) { var idbdb = tmpTrans.db; var stacks = createMiddlewareStacks(db._middlewares, idbdb, db._deps, tmpTrans); db.core = stacks.dbcore; db.tables.forEach(function(table) { var tableName = table.name; if (db.core.schema.tables.some(function(tbl) { return tbl.name === tableName; })) { table.core = db.core.table(tableName); if (db[tableName] instanceof db.Table) { db[tableName].core = table.core; } } }); } function setApiOnPlace(db, objs, tableNames, dbschema) { tableNames.forEach(function(tableName) { var schema = dbschema[tableName]; objs.forEach(function(obj) { var propDesc = getPropertyDescriptor(obj, tableName); if (!propDesc || "value" in propDesc && propDesc.value === void 0) { if (obj === db.Transaction.prototype || obj instanceof db.Transaction) { setProp(obj, tableName, { get: function() { return this.table(tableName); }, set: function(value) { defineProperty(this, tableName, { value, writable: true, configurable: true, enumerable: true }); } }); } else { obj[tableName] = new db.Table(tableName, schema); } } }); }); } function removeTablesApi(db, objs) { objs.forEach(function(obj) { for (var key in obj) { if (obj[key] instanceof db.Table) delete obj[key]; } }); } function lowerVersionFirst(a, b) { return a._cfg.version - b._cfg.version; } function runUpgraders(db, oldVersion, idbUpgradeTrans, reject) { var globalSchema = db._dbSchema; if (idbUpgradeTrans.objectStoreNames.contains("$meta") && !globalSchema.$meta) { globalSchema.$meta = createTableSchema("$meta", parseIndexSyntax("")[0], []); db._storeNames.push("$meta"); } var trans = db._createTransaction("readwrite", db._storeNames, globalSchema); trans.create(idbUpgradeTrans); trans._completion.catch(reject); var rejectTransaction = trans._reject.bind(trans); var transless = PSD.transless || PSD; newScope(function() { PSD.trans = trans; PSD.transless = transless; if (oldVersion === 0) { keys(globalSchema).forEach(function(tableName) { createTable(idbUpgradeTrans, tableName, globalSchema[tableName].primKey, globalSchema[tableName].indexes); }); generateMiddlewareStacks(db, idbUpgradeTrans); DexiePromise.follow(function() { return db.on.populate.fire(trans); }).catch(rejectTransaction); } else { generateMiddlewareStacks(db, idbUpgradeTrans); return getExistingVersion(db, trans, oldVersion).then(function(oldVersion2) { return updateTablesAndIndexes(db, oldVersion2, trans, idbUpgradeTrans); }).catch(rejectTransaction); } }); } function patchCurrentVersion(db, idbUpgradeTrans) { createMissingTables(db._dbSchema, idbUpgradeTrans); if (idbUpgradeTrans.db.version % 10 === 0 && !idbUpgradeTrans.objectStoreNames.contains("$meta")) { idbUpgradeTrans.db.createObjectStore("$meta").add(Math.ceil(idbUpgradeTrans.db.version / 10 - 1), "version"); } var globalSchema = buildGlobalSchema(db, db.idbdb, idbUpgradeTrans); adjustToExistingIndexNames(db, db._dbSchema, idbUpgradeTrans); var diff = getSchemaDiff(globalSchema, db._dbSchema); var _loop_1 = function(tableChange2) { if (tableChange2.change.length || tableChange2.recreate) { console.warn("Unable to patch indexes of table ".concat(tableChange2.name, " because it has changes on the type of index or primary key.")); return { value: void 0 }; } var store = idbUpgradeTrans.objectStore(tableChange2.name); tableChange2.add.forEach(function(idx) { if (debug) console.debug("Dexie upgrade patch: Creating missing index ".concat(tableChange2.name, ".").concat(idx.src)); addIndex(store, idx); }); }; for (var _i = 0, _a2 = diff.change; _i < _a2.length; _i++) { var tableChange = _a2[_i]; var state_1 = _loop_1(tableChange); if (typeof state_1 === "object") return state_1.value; } } function getExistingVersion(db, trans, oldVersion) { if (trans.storeNames.includes("$meta")) { return trans.table("$meta").get("version").then(function(metaVersion) { return metaVersion != null ? metaVersion : oldVersion; }); } else { return DexiePromise.resolve(oldVersion); } } function updateTablesAndIndexes(db, oldVersion, trans, idbUpgradeTrans) { var queue = []; var versions = db._versions; var globalSchema = db._dbSchema = buildGlobalSchema(db, db.idbdb, idbUpgradeTrans); var versToRun = versions.filter(function(v) { return v._cfg.version >= oldVersion; }); if (versToRun.length === 0) { return DexiePromise.resolve(); } versToRun.forEach(function(version) { queue.push(function() { var oldSchema = globalSchema; var newSchema = version._cfg.dbschema; adjustToExistingIndexNames(db, oldSchema, idbUpgradeTrans); adjustToExistingIndexNames(db, newSchema, idbUpgradeTrans); globalSchema = db._dbSchema = newSchema; var diff = getSchemaDiff(oldSchema, newSchema); diff.add.forEach(function(tuple) { createTable(idbUpgradeTrans, tuple[0], tuple[1].primKey, tuple[1].indexes); }); diff.change.forEach(function(change) { if (change.recreate) { throw new exceptions.Upgrade("Not yet support for changing primary key"); } else { var store_1 = idbUpgradeTrans.objectStore(change.name); change.add.forEach(function(idx) { return addIndex(store_1, idx); }); change.change.forEach(function(idx) { store_1.deleteIndex(idx.name); addIndex(store_1, idx); }); change.del.forEach(function(idxName) { return store_1.deleteIndex(idxName); }); } }); var contentUpgrade = version._cfg.contentUpgrade; if (contentUpgrade && version._cfg.version > oldVersion) { generateMiddlewareStacks(db, idbUpgradeTrans); trans._memoizedTables = {}; var upgradeSchema_1 = shallowClone(newSchema); diff.del.forEach(function(table) { upgradeSchema_1[table] = oldSchema[table]; }); removeTablesApi(db, [db.Transaction.prototype]); setApiOnPlace(db, [db.Transaction.prototype], keys(upgradeSchema_1), upgradeSchema_1); trans.schema = upgradeSchema_1; var contentUpgradeIsAsync_1 = isAsyncFunction(contentUpgrade); if (contentUpgradeIsAsync_1) { incrementExpectedAwaits(); } var returnValue_1; var promiseFollowed = DexiePromise.follow(function() { returnValue_1 = contentUpgrade(trans); if (returnValue_1) { if (contentUpgradeIsAsync_1) { var decrementor = decrementExpectedAwaits.bind(null, null); returnValue_1.then(decrementor, decrementor); } } }); return returnValue_1 && typeof returnValue_1.then === "function" ? DexiePromise.resolve(returnValue_1) : promiseFollowed.then(function() { return returnValue_1; }); } }); queue.push(function(idbtrans) { var newSchema = version._cfg.dbschema; deleteRemovedTables(newSchema, idbtrans); removeTablesApi(db, [db.Transaction.prototype]); setApiOnPlace(db, [db.Transaction.prototype], db._storeNames, db._dbSchema); trans.schema = db._dbSchema; }); queue.push(function(idbtrans) { if (db.idbdb.objectStoreNames.contains("$meta")) { if (Math.ceil(db.idbdb.version / 10) === version._cfg.version) { db.idbdb.deleteObjectStore("$meta"); delete db._dbSchema.$meta; db._storeNames = db._storeNames.filter(function(name) { return name !== "$meta"; }); } else { idbtrans.objectStore("$meta").put(version._cfg.version, "version"); } } }); }); function runQueue() { return queue.length ? DexiePromise.resolve(queue.shift()(trans.idbtrans)).then(runQueue) : DexiePromise.resolve(); } return runQueue().then(function() { createMissingTables(globalSchema, idbUpgradeTrans); }); } function getSchemaDiff(oldSchema, newSchema) { var diff = { del: [], add: [], change: [] }; var table; for (table in oldSchema) { if (!newSchema[table]) diff.del.push(table); } for (table in newSchema) { var oldDef = oldSchema[table], newDef = newSchema[table]; if (!oldDef) { diff.add.push([table, newDef]); } else { var change = { name: table, def: newDef, recreate: false, del: [], add: [], change: [] }; if ("" + (oldDef.primKey.keyPath || "") !== "" + (newDef.primKey.keyPath || "") || oldDef.primKey.auto !== newDef.primKey.auto) { change.recreate = true; diff.change.push(change); } else { var oldIndexes = oldDef.idxByName; var newIndexes = newDef.idxByName; var idxName = void 0; for (idxName in oldIndexes) { if (!newIndexes[idxName]) change.del.push(idxName); } for (idxName in newIndexes) { var oldIdx = oldIndexes[idxName], newIdx = newIndexes[idxName]; if (!oldIdx) change.add.push(newIdx); else if (oldIdx.src !== newIdx.src) change.change.push(newIdx); } if (change.del.length > 0 || change.add.length > 0 || change.change.length > 0) { diff.change.push(change); } } } } return diff; } function createTable(idbtrans, tableName, primKey, indexes) { var store = idbtrans.db.createObjectStore(tableName, primKey.keyPath ? { keyPath: primKey.keyPath, autoIncrement: primKey.auto } : { autoIncrement: primKey.auto }); indexes.forEach(function(idx) { return addIndex(store, idx); }); return store; } function createMissingTables(newSchema, idbtrans) { keys(newSchema).forEach(function(tableName) { if (!idbtrans.db.objectStoreNames.contains(tableName)) { if (debug) console.debug("Dexie: Creating missing table", tableName); createTable(idbtrans, tableName, newSchema[tableName].primKey, newSchema[tableName].indexes); } }); } function deleteRemovedTables(newSchema, idbtrans) { [].slice.call(idbtrans.db.objectStoreNames).forEach(function(storeName) { return newSchema[storeName] == null && idbtrans.db.deleteObjectStore(storeName); }); } function addIndex(store, idx) { store.createIndex(idx.name, idx.keyPath, { unique: idx.unique, multiEntry: idx.multi }); } function buildGlobalSchema(db, idbdb, tmpTrans) { var globalSchema = {}; var dbStoreNames = slice(idbdb.objectStoreNames, 0); dbStoreNames.forEach(function(storeName) { var store = tmpTrans.objectStore(storeName); var keyPath = store.keyPath; var primKey = createIndexSpec(nameFromKeyPath(keyPath), keyPath || "", true, false, !!store.autoIncrement, keyPath && typeof keyPath !== "string", true); var indexes = []; for (var j = 0; j < store.indexNames.length; ++j) { var idbindex = store.index(store.indexNames[j]); keyPath = idbindex.keyPath; var index = createIndexSpec(idbindex.name, keyPath, !!idbindex.unique, !!idbindex.multiEntry, false, keyPath && typeof keyPath !== "string", false); indexes.push(index); } globalSchema[storeName] = createTableSchema(storeName, primKey, indexes); }); return globalSchema; } function readGlobalSchema(db, idbdb, tmpTrans) { db.verno = idbdb.version / 10; var globalSchema = db._dbSchema = buildGlobalSchema(db, idbdb, tmpTrans); db._storeNames = slice(idbdb.objectStoreNames, 0); setApiOnPlace(db, [db._allTables], keys(globalSchema), globalSchema); } function verifyInstalledSchema(db, tmpTrans) { var installedSchema = buildGlobalSchema(db, db.idbdb, tmpTrans); var diff = getSchemaDiff(installedSchema, db._dbSchema); return !(diff.add.length || diff.change.some(function(ch) { return ch.add.length || ch.change.length; })); } function adjustToExistingIndexNames(db, schema, idbtrans) { var storeNames = idbtrans.db.objectStoreNames; for (var i = 0; i < storeNames.length; ++i) { var storeName = storeNames[i]; var store = idbtrans.objectStore(storeName); db._hasGetAll = "getAll" in store; for (var j = 0; j < store.indexNames.length; ++j) { var indexName = store.indexNames[j]; var keyPath = store.index(indexName).keyPath; var dexieName = typeof keyPath === "string" ? keyPath : "[" + slice(keyPath).join("+") + "]"; if (schema[storeName]) { var indexSpec = schema[storeName].idxByName[dexieName]; if (indexSpec) { indexSpec.name = indexName; delete schema[storeName].idxByName[dexieName]; schema[storeName].idxByName[indexName] = indexSpec; } } } } if (typeof navigator !== "undefined" && /Safari/.test(navigator.userAgent) && !/(Chrome\/|Edge\/)/.test(navigator.userAgent) && _global.WorkerGlobalScope && _global instanceof _global.WorkerGlobalScope && [].concat(navigator.userAgent.match(/Safari\/(\d*)/))[1] < 604) { db._hasGetAll = false; } } function parseIndexSyntax(primKeyAndIndexes) { return primKeyAndIndexes.split(",").map(function(index, indexNum) { index = index.trim(); var name = index.replace(/([&*]|\+\+)/g, ""); var keyPath = /^\[/.test(name) ? name.match(/^\[(.*)\]$/)[1].split("+") : name; return createIndexSpec(name, keyPath || null, /\&/.test(index), /\*/.test(index), /\+\+/.test(index), isArray2(keyPath), indexNum === 0); }); } var Version = function() { function Version2() { } Version2.prototype._parseStoresSpec = function(stores, outSchema) { keys(stores).forEach(function(tableName) { if (stores[tableName] !== null) { var indexes = parseIndexSyntax(stores[tableName]); var primKey = indexes.shift(); primKey.unique = true; if (primKey.multi) throw new exceptions.Schema("Primary key cannot be multi-valued"); indexes.forEach(function(idx) { if (idx.auto) throw new exceptions.Schema("Only primary key can be marked as autoIncrement (++)"); if (!idx.keyPath) throw new exceptions.Schema("Index must have a name and cannot be an empty string"); }); outSchema[tableName] = createTableSchema(tableName, primKey, indexes); } }); }; Version2.prototype.stores = function(stores) { var db = this.db; this._cfg.storesSource = this._cfg.storesSource ? extend(this._cfg.storesSource, stores) : stores; var versions = db._versions; var storesSpec = {}; var dbschema = {}; versions.forEach(function(version) { extend(storesSpec, version._cfg.storesSource); dbschema = version._cfg.dbschema = {}; version._parseStoresSpec(storesSpec, dbschema); }); db._dbSchema = dbschema; removeTablesApi(db, [db._allTables, db, db.Transaction.prototype]); setApiOnPlace(db, [db._allTables, db, db.Transaction.prototype, this._cfg.tables], keys(dbschema), dbschema); db._storeNames = keys(dbschema); return this; }; Version2.prototype.upgrade = function(upgradeFunction) { this._cfg.contentUpgrade = promisableChain(this._cfg.contentUpgrade || nop, upgradeFunction); return this; }; return Version2; }(); function createVersionConstructor(db) { return makeClassConstructor(Version.prototype, function Version2(versionNumber) { this.db = db; this._cfg = { version: versionNumber, storesSource: null, dbschema: {}, tables: {}, contentUpgrade: null }; }); } function getDbNamesTable(indexedDB2, IDBKeyRange) { var dbNamesDB = indexedDB2["_dbNamesDB"]; if (!dbNamesDB) { dbNamesDB = indexedDB2["_dbNamesDB"] = new Dexie$1(DBNAMES_DB, { addons: [], indexedDB: indexedDB2, IDBKeyRange }); dbNamesDB.version(1).stores({ dbnames: "name" }); } return dbNamesDB.table("dbnames"); } function hasDatabasesNative(indexedDB2) { return indexedDB2 && typeof indexedDB2.databases === "function"; } function getDatabaseNames(_a2) { var indexedDB2 = _a2.indexedDB, IDBKeyRange = _a2.IDBKeyRange; return hasDatabasesNative(indexedDB2) ? Promise.resolve(indexedDB2.databases()).then(function(infos) { return infos.map(function(info37) { return info37.name; }).filter(function(name) { return name !== DBNAMES_DB; }); }) : getDbNamesTable(indexedDB2, IDBKeyRange).toCollection().primaryKeys(); } function _onDatabaseCreated(_a2, name) { var indexedDB2 = _a2.indexedDB, IDBKeyRange = _a2.IDBKeyRange; !hasDatabasesNative(indexedDB2) && name !== DBNAMES_DB && getDbNamesTable(indexedDB2, IDBKeyRange).put({ name }).catch(nop); } function _onDatabaseDeleted(_a2, name) { var indexedDB2 = _a2.indexedDB, IDBKeyRange = _a2.IDBKeyRange; !hasDatabasesNative(indexedDB2) && name !== DBNAMES_DB && getDbNamesTable(indexedDB2, IDBKeyRange).delete(name).catch(nop); } function vip(fn) { return newScope(function() { PSD.letThrough = true; return fn(); }); } function idbReady() { var isSafari = !navigator.userAgentData && /Safari\//.test(navigator.userAgent) && !/Chrom(e|ium)\//.test(navigator.userAgent); if (!isSafari || !indexedDB.databases) return Promise.resolve(); var intervalId; return new Promise(function(resolve) { var tryIdb = function() { return indexedDB.databases().finally(resolve); }; intervalId = setInterval(tryIdb, 100); tryIdb(); }).finally(function() { return clearInterval(intervalId); }); } var _a; function isEmptyRange(node) { return !("from" in node); } var RangeSet2 = function(fromOrTree, to) { if (this) { extend(this, arguments.length ? { d: 1, from: fromOrTree, to: arguments.length > 1 ? to : fromOrTree } : { d: 0 }); } else { var rv = new RangeSet2(); if (fromOrTree && "d" in fromOrTree) { extend(rv, fromOrTree); } return rv; } }; props(RangeSet2.prototype, (_a = { add: function(rangeSet) { mergeRanges2(this, rangeSet); return this; }, addKey: function(key) { addRange(this, key, key); return this; }, addKeys: function(keys2) { var _this = this; keys2.forEach(function(key) { return addRange(_this, key, key); }); return this; }, hasKey: function(key) { var node = getRangeSetIterator(this).next(key).value; return node && cmp2(node.from, key) <= 0 && cmp2(node.to, key) >= 0; } }, _a[iteratorSymbol] = function() { return getRangeSetIterator(this); }, _a)); function addRange(target, from, to) { var diff = cmp2(from, to); if (isNaN(diff)) return; if (diff > 0) throw RangeError(); if (isEmptyRange(target)) return extend(target, { from, to, d: 1 }); var left = target.l; var right = target.r; if (cmp2(to, target.from) < 0) { left ? addRange(left, from, to) : target.l = { from, to, d: 1, l: null, r: null }; return rebalance(target); } if (cmp2(from, target.to) > 0) { right ? addRange(right, from, to) : target.r = { from, to, d: 1, l: null, r: null }; return rebalance(target); } if (cmp2(from, target.from) < 0) { target.from = from; target.l = null; target.d = right ? right.d + 1 : 1; } if (cmp2(to, target.to) > 0) { target.to = to; target.r = null; target.d = target.l ? target.l.d + 1 : 1; } var rightWasCutOff = !target.r; if (left && !target.l) { mergeRanges2(target, left); } if (right && rightWasCutOff) { mergeRanges2(target, right); } } function mergeRanges2(target, newSet) { function _addRangeSet(target2, _a2) { var from = _a2.from, to = _a2.to, l = _a2.l, r = _a2.r; addRange(target2, from, to); if (l) _addRangeSet(target2, l); if (r) _addRangeSet(target2, r); } if (!isEmptyRange(newSet)) _addRangeSet(target, newSet); } function rangesOverlap2(rangeSet1, rangeSet2) { var i1 = getRangeSetIterator(rangeSet2); var nextResult1 = i1.next(); if (nextResult1.done) return false; var a = nextResult1.value; var i2 = getRangeSetIterator(rangeSet1); var nextResult2 = i2.next(a.from); var b = nextResult2.value; while (!nextResult1.done && !nextResult2.done) { if (cmp2(b.from, a.to) <= 0 && cmp2(b.to, a.from) >= 0) return true; cmp2(a.from, b.from) < 0 ? a = (nextResult1 = i1.next(b.from)).value : b = (nextResult2 = i2.next(a.from)).value; } return false; } function getRangeSetIterator(node) { var state = isEmptyRange(node) ? null : { s: 0, n: node }; return { next: function(key) { var keyProvided = arguments.length > 0; while (state) { switch (state.s) { case 0: state.s = 1; if (keyProvided) { while (state.n.l && cmp2(key, state.n.from) < 0) state = { up: state, n: state.n.l, s: 1 }; } else { while (state.n.l) state = { up: state, n: state.n.l, s: 1 }; } case 1: state.s = 2; if (!keyProvided || cmp2(key, state.n.to) <= 0) return { value: state.n, done: false }; case 2: if (state.n.r) { state.s = 3; state = { up: state, n: state.n.r, s: 0 }; continue; } case 3: state = state.up; } } return { done: true }; } }; } function rebalance(target) { var _a2, _b; var diff = (((_a2 = target.r) === null || _a2 === void 0 ? void 0 : _a2.d) || 0) - (((_b = target.l) === null || _b === void 0 ? void 0 : _b.d) || 0); var r = diff > 1 ? "r" : diff < -1 ? "l" : ""; if (r) { var l = r === "r" ? "l" : "r"; var rootClone = __assign({}, target); var oldRootRight = target[r]; target.from = oldRootRight.from; target.to = oldRootRight.to; target[r] = oldRootRight[r]; rootClone[r] = oldRootRight[l]; target[l] = rootClone; rootClone.d = computeDepth(rootClone); } target.d = computeDepth(target); } function computeDepth(_a2) { var r = _a2.r, l = _a2.l; return (r ? l ? Math.max(r.d, l.d) : r.d : l ? l.d : 0) + 1; } function extendObservabilitySet(target, newSet) { keys(newSet).forEach(function(part) { if (target[part]) mergeRanges2(target[part], newSet[part]); else target[part] = cloneSimpleObjectTree(newSet[part]); }); return target; } function obsSetsOverlap(os1, os2) { return os1.all || os2.all || Object.keys(os1).some(function(key) { return os2[key] && rangesOverlap2(os2[key], os1[key]); }); } var cache = {}; var unsignaledParts = {}; var isTaskEnqueued = false; function signalSubscribersLazily(part, optimistic) { extendObservabilitySet(unsignaledParts, part); if (!isTaskEnqueued) { isTaskEnqueued = true; setTimeout(function() { isTaskEnqueued = false; var parts = unsignaledParts; unsignaledParts = {}; signalSubscribersNow(parts, false); }, 0); } } function signalSubscribersNow(updatedParts, deleteAffectedCacheEntries) { if (deleteAffectedCacheEntries === void 0) { deleteAffectedCacheEntries = false; } var queriesToSignal = /* @__PURE__ */ new Set(); if (updatedParts.all) { for (var _i = 0, _a2 = Object.values(cache); _i < _a2.length; _i++) { var tblCache = _a2[_i]; collectTableSubscribers(tblCache, updatedParts, queriesToSignal, deleteAffectedCacheEntries); } } else { for (var key in updatedParts) { var parts = /^idb\:\/\/(.*)\/(.*)\//.exec(key); if (parts) { var dbName = parts[1], tableName = parts[2]; var tblCache = cache["idb://".concat(dbName, "/").concat(tableName)]; if (tblCache) collectTableSubscribers(tblCache, updatedParts, queriesToSignal, deleteAffectedCacheEntries); } } } queriesToSignal.forEach(function(requery) { return requery(); }); } function collectTableSubscribers(tblCache, updatedParts, outQueriesToSignal, deleteAffectedCacheEntries) { var updatedEntryLists = []; for (var _i = 0, _a2 = Object.entries(tblCache.queries.query); _i < _a2.length; _i++) { var _b = _a2[_i], indexName = _b[0], entries = _b[1]; var filteredEntries = []; for (var _c = 0, entries_1 = entries; _c < entries_1.length; _c++) { var entry = entries_1[_c]; if (obsSetsOverlap(updatedParts, entry.obsSet)) { entry.subscribers.forEach(function(requery) { return outQueriesToSignal.add(requery); }); } else if (deleteAffectedCacheEntries) { filteredEntries.push(entry); } } if (deleteAffectedCacheEntries) updatedEntryLists.push([indexName, filteredEntries]); } if (deleteAffectedCacheEntries) { for (var _d = 0, updatedEntryLists_1 = updatedEntryLists; _d < updatedEntryLists_1.length; _d++) { var _e = updatedEntryLists_1[_d], indexName = _e[0], filteredEntries = _e[1]; tblCache.queries.query[indexName] = filteredEntries; } } } function dexieOpen(db) { var state = db._state; var indexedDB2 = db._deps.indexedDB; if (state.isBeingOpened || db.idbdb) return state.dbReadyPromise.then(function() { return state.dbOpenError ? rejection(state.dbOpenError) : db; }); state.isBeingOpened = true; state.dbOpenError = null; state.openComplete = false; var openCanceller = state.openCanceller; var nativeVerToOpen = Math.round(db.verno * 10); var schemaPatchMode = false; function throwIfCancelled() { if (state.openCanceller !== openCanceller) throw new exceptions.DatabaseClosed("db.open() was cancelled"); } var resolveDbReady = state.dbReadyResolve, upgradeTransaction = null, wasCreated = false; var tryOpenDB = function() { return new DexiePromise(function(resolve, reject) { throwIfCancelled(); if (!indexedDB2) throw new exceptions.MissingAPI(); var dbName = db.name; var req = state.autoSchema || !nativeVerToOpen ? indexedDB2.open(dbName) : indexedDB2.open(dbName, nativeVerToOpen); if (!req) throw new exceptions.MissingAPI(); req.onerror = eventRejectHandler(reject); req.onblocked = wrap(db._fireOnBlocked); req.onupgradeneeded = wrap(function(e) { upgradeTransaction = req.transaction; if (state.autoSchema && !db._options.allowEmptyDB) { req.onerror = preventDefault; upgradeTransaction.abort(); req.result.close(); var delreq = indexedDB2.deleteDatabase(dbName); delreq.onsuccess = delreq.onerror = wrap(function() { reject(new exceptions.NoSuchDatabase("Database ".concat(dbName, " doesnt exist"))); }); } else { upgradeTransaction.onerror = eventRejectHandler(reject); var oldVer = e.oldVersion > Math.pow(2, 62) ? 0 : e.oldVersion; wasCreated = oldVer < 1; db.idbdb = req.result; if (schemaPatchMode) { patchCurrentVersion(db, upgradeTransaction); } runUpgraders(db, oldVer / 10, upgradeTransaction, reject); } }, reject); req.onsuccess = wrap(function() { upgradeTransaction = null; var idbdb = db.idbdb = req.result; var objectStoreNames = slice(idbdb.objectStoreNames); if (objectStoreNames.length > 0) try { var tmpTrans = idbdb.transaction(safariMultiStoreFix(objectStoreNames), "readonly"); if (state.autoSchema) readGlobalSchema(db, idbdb, tmpTrans); else { adjustToExistingIndexNames(db, db._dbSchema, tmpTrans); if (!verifyInstalledSchema(db, tmpTrans) && !schemaPatchMode) { console.warn("Dexie SchemaDiff: Schema was extended without increasing the number passed to db.version(). Dexie will add missing parts and increment native version number to workaround this."); idbdb.close(); nativeVerToOpen = idbdb.version + 1; schemaPatchMode = true; return resolve(tryOpenDB()); } } generateMiddlewareStacks(db, tmpTrans); } catch (e) { } connections.push(db); idbdb.onversionchange = wrap(function(ev) { state.vcFired = true; db.on("versionchange").fire(ev); }); idbdb.onclose = wrap(function(ev) { db.on("close").fire(ev); }); if (wasCreated) _onDatabaseCreated(db._deps, dbName); resolve(); }, reject); }).catch(function(err) { switch (err === null || err === void 0 ? void 0 : err.name) { case "UnknownError": if (state.PR1398_maxLoop > 0) { state.PR1398_maxLoop--; console.warn("Dexie: Workaround for Chrome UnknownError on open()"); return tryOpenDB(); } break; case "VersionError": if (nativeVerToOpen > 0) { nativeVerToOpen = 0; return tryOpenDB(); } break; } return DexiePromise.reject(err); }); }; return DexiePromise.race([ openCanceller, (typeof navigator === "undefined" ? DexiePromise.resolve() : idbReady()).then(tryOpenDB) ]).then(function() { throwIfCancelled(); state.onReadyBeingFired = []; return DexiePromise.resolve(vip(function() { return db.on.ready.fire(db.vip); })).then(function fireRemainders() { if (state.onReadyBeingFired.length > 0) { var remainders_1 = state.onReadyBeingFired.reduce(promisableChain, nop); state.onReadyBeingFired = []; return DexiePromise.resolve(vip(function() { return remainders_1(db.vip); })).then(fireRemainders); } }); }).finally(function() { if (state.openCanceller === openCanceller) { state.onReadyBeingFired = null; state.isBeingOpened = false; } }).catch(function(err) { state.dbOpenError = err; try { upgradeTransaction && upgradeTransaction.abort(); } catch (_a2) { } if (openCanceller === state.openCanceller) { db._close(); } return rejection(err); }).finally(function() { state.openComplete = true; resolveDbReady(); }).then(function() { if (wasCreated) { var everything_1 = {}; db.tables.forEach(function(table) { table.schema.indexes.forEach(function(idx) { if (idx.name) everything_1["idb://".concat(db.name, "/").concat(table.name, "/").concat(idx.name)] = new RangeSet2(-Infinity, [[[]]]); }); everything_1["idb://".concat(db.name, "/").concat(table.name, "/")] = everything_1["idb://".concat(db.name, "/").concat(table.name, "/:dels")] = new RangeSet2(-Infinity, [[[]]]); }); globalEvents(DEXIE_STORAGE_MUTATED_EVENT_NAME).fire(everything_1); signalSubscribersNow(everything_1, true); } return db; }); } function awaitIterator(iterator) { var callNext = function(result) { return iterator.next(result); }, doThrow = function(error40) { return iterator.throw(error40); }, onSuccess = step(callNext), onError = step(doThrow); function step(getNext) { return function(val) { var next = getNext(val), value = next.value; return next.done ? value : !value || typeof value.then !== "function" ? isArray2(value) ? Promise.all(value).then(onSuccess, onError) : onSuccess(value) : value.then(onSuccess, onError); }; } return step(callNext)(); } function extractTransactionArgs(mode, _tableArgs_, scopeFunc) { var i = arguments.length; if (i < 2) throw new exceptions.InvalidArgument("Too few arguments"); var args = new Array(i - 1); while (--i) args[i - 1] = arguments[i]; scopeFunc = args.pop(); var tables = flatten(args); return [mode, tables, scopeFunc]; } function enterTransactionScope(db, mode, storeNames, parentTransaction, scopeFunc) { return DexiePromise.resolve().then(function() { var transless = PSD.transless || PSD; var trans = db._createTransaction(mode, storeNames, db._dbSchema, parentTransaction); trans.explicit = true; var zoneProps = { trans, transless }; if (parentTransaction) { trans.idbtrans = parentTransaction.idbtrans; } else { try { trans.create(); trans.idbtrans._explicit = true; db._state.PR1398_maxLoop = 3; } catch (ex) { if (ex.name === errnames.InvalidState && db.isOpen() && --db._state.PR1398_maxLoop > 0) { console.warn("Dexie: Need to reopen db"); db.close({ disableAutoOpen: false }); return db.open().then(function() { return enterTransactionScope(db, mode, storeNames, null, scopeFunc); }); } return rejection(ex); } } var scopeFuncIsAsync = isAsyncFunction(scopeFunc); if (scopeFuncIsAsync) { incrementExpectedAwaits(); } var returnValue; var promiseFollowed = DexiePromise.follow(function() { returnValue = scopeFunc.call(trans, trans); if (returnValue) { if (scopeFuncIsAsync) { var decrementor = decrementExpectedAwaits.bind(null, null); returnValue.then(decrementor, decrementor); } else if (typeof returnValue.next === "function" && typeof returnValue.throw === "function") { returnValue = awaitIterator(returnValue); } } }, zoneProps); return (returnValue && typeof returnValue.then === "function" ? DexiePromise.resolve(returnValue).then(function(x) { return trans.active ? x : rejection(new exceptions.PrematureCommit("Transaction committed too early. See http://bit.ly/2kdckMn")); }) : promiseFollowed.then(function() { return returnValue; })).then(function(x) { if (parentTransaction) trans._resolve(); return trans._completion.then(function() { return x; }); }).catch(function(e) { trans._reject(e); return rejection(e); }); }); } function pad(a, value, count) { var result = isArray2(a) ? a.slice() : [a]; for (var i = 0; i < count; ++i) result.push(value); return result; } function createVirtualIndexMiddleware(down) { return __assign(__assign({}, down), { table: function(tableName) { var table = down.table(tableName); var schema = table.schema; var indexLookup = {}; var allVirtualIndexes = []; function addVirtualIndexes(keyPath, keyTail, lowLevelIndex) { var keyPathAlias = getKeyPathAlias(keyPath); var indexList = indexLookup[keyPathAlias] = indexLookup[keyPathAlias] || []; var keyLength = keyPath == null ? 0 : typeof keyPath === "string" ? 1 : keyPath.length; var isVirtual = keyTail > 0; var virtualIndex = __assign(__assign({}, lowLevelIndex), { name: isVirtual ? "".concat(keyPathAlias, "(virtual-from:").concat(lowLevelIndex.name, ")") : lowLevelIndex.name, lowLevelIndex, isVirtual, keyTail, keyLength, extractKey: getKeyExtractor(keyPath), unique: !isVirtual && lowLevelIndex.unique }); indexList.push(virtualIndex); if (!virtualIndex.isPrimaryKey) { allVirtualIndexes.push(virtualIndex); } if (keyLength > 1) { var virtualKeyPath = keyLength === 2 ? keyPath[0] : keyPath.slice(0, keyLength - 1); addVirtualIndexes(virtualKeyPath, keyTail + 1, lowLevelIndex); } indexList.sort(function(a, b) { return a.keyTail - b.keyTail; }); return virtualIndex; } var primaryKey = addVirtualIndexes(schema.primaryKey.keyPath, 0, schema.primaryKey); indexLookup[":id"] = [primaryKey]; for (var _i = 0, _a2 = schema.indexes; _i < _a2.length; _i++) { var index = _a2[_i]; addVirtualIndexes(index.keyPath, 0, index); } function findBestIndex(keyPath) { var result2 = indexLookup[getKeyPathAlias(keyPath)]; return result2 && result2[0]; } function translateRange(range, keyTail) { return { type: range.type === 1 ? 2 : range.type, lower: pad(range.lower, range.lowerOpen ? down.MAX_KEY : down.MIN_KEY, keyTail), lowerOpen: true, upper: pad(range.upper, range.upperOpen ? down.MIN_KEY : down.MAX_KEY, keyTail), upperOpen: true }; } function translateRequest(req) { var index2 = req.query.index; return index2.isVirtual ? __assign(__assign({}, req), { query: { index: index2.lowLevelIndex, range: translateRange(req.query.range, index2.keyTail) } }) : req; } var result = __assign(__assign({}, table), { schema: __assign(__assign({}, schema), { primaryKey, indexes: allVirtualIndexes, getIndexByKeyPath: findBestIndex }), count: function(req) { return table.count(translateRequest(req)); }, query: function(req) { return table.query(translateRequest(req)); }, openCursor: function(req) { var _a3 = req.query.index, keyTail = _a3.keyTail, isVirtual = _a3.isVirtual, keyLength = _a3.keyLength; if (!isVirtual) return table.openCursor(req); function createVirtualCursor(cursor) { function _continue(key) { key != null ? cursor.continue(pad(key, req.reverse ? down.MAX_KEY : down.MIN_KEY, keyTail)) : req.unique ? cursor.continue(cursor.key.slice(0, keyLength).concat(req.reverse ? down.MIN_KEY : down.MAX_KEY, keyTail)) : cursor.continue(); } var virtualCursor = Object.create(cursor, { continue: { value: _continue }, continuePrimaryKey: { value: function(key, primaryKey2) { cursor.continuePrimaryKey(pad(key, down.MAX_KEY, keyTail), primaryKey2); } }, primaryKey: { get: function() { return cursor.primaryKey; } }, key: { get: function() { var key = cursor.key; return keyLength === 1 ? key[0] : key.slice(0, keyLength); } }, value: { get: function() { return cursor.value; } } }); return virtualCursor; } return table.openCursor(translateRequest(req)).then(function(cursor) { return cursor && createVirtualCursor(cursor); }); } }); return result; } }); } var virtualIndexMiddleware = { stack: "dbcore", name: "VirtualIndexMiddleware", level: 1, create: createVirtualIndexMiddleware }; function getObjectDiff(a, b, rv, prfx) { rv = rv || {}; prfx = prfx || ""; keys(a).forEach(function(prop) { if (!hasOwn2(b, prop)) { rv[prfx + prop] = void 0; } else { var ap = a[prop], bp = b[prop]; if (typeof ap === "object" && typeof bp === "object" && ap && bp) { var apTypeName = toStringTag(ap); var bpTypeName = toStringTag(bp); if (apTypeName !== bpTypeName) { rv[prfx + prop] = b[prop]; } else if (apTypeName === "Object") { getObjectDiff(ap, bp, rv, prfx + prop + "."); } else if (ap !== bp) { rv[prfx + prop] = b[prop]; } } else if (ap !== bp) rv[prfx + prop] = b[prop]; } }); keys(b).forEach(function(prop) { if (!hasOwn2(a, prop)) { rv[prfx + prop] = b[prop]; } }); return rv; } function getEffectiveKeys(primaryKey, req) { if (req.type === "delete") return req.keys; return req.keys || req.values.map(primaryKey.extractKey); } var hooksMiddleware = { stack: "dbcore", name: "HooksMiddleware", level: 2, create: function(downCore) { return __assign(__assign({}, downCore), { table: function(tableName) { var downTable = downCore.table(tableName); var primaryKey = downTable.schema.primaryKey; var tableMiddleware = __assign(__assign({}, downTable), { mutate: function(req) { var dxTrans = PSD.trans; var _a2 = dxTrans.table(tableName).hook, deleting = _a2.deleting, creating = _a2.creating, updating = _a2.updating; switch (req.type) { case "add": if (creating.fire === nop) break; return dxTrans._promise("readwrite", function() { return addPutOrDelete(req); }, true); case "put": if (creating.fire === nop && updating.fire === nop) break; return dxTrans._promise("readwrite", function() { return addPutOrDelete(req); }, true); case "delete": if (deleting.fire === nop) break; return dxTrans._promise("readwrite", function() { return addPutOrDelete(req); }, true); case "deleteRange": if (deleting.fire === nop) break; return dxTrans._promise("readwrite", function() { return deleteRange(req); }, true); } return downTable.mutate(req); function addPutOrDelete(req2) { var dxTrans2 = PSD.trans; var keys2 = req2.keys || getEffectiveKeys(primaryKey, req2); if (!keys2) throw new Error("Keys missing"); req2 = req2.type === "add" || req2.type === "put" ? __assign(__assign({}, req2), { keys: keys2 }) : __assign({}, req2); if (req2.type !== "delete") req2.values = __spreadArray([], req2.values, true); if (req2.keys) req2.keys = __spreadArray([], req2.keys, true); return getExistingValues(downTable, req2, keys2).then(function(existingValues) { var contexts = keys2.map(function(key, i) { var existingValue = existingValues[i]; var ctx = { onerror: null, onsuccess: null }; if (req2.type === "delete") { deleting.fire.call(ctx, key, existingValue, dxTrans2); } else if (req2.type === "add" || existingValue === void 0) { var generatedPrimaryKey = creating.fire.call(ctx, key, req2.values[i], dxTrans2); if (key == null && generatedPrimaryKey != null) { key = generatedPrimaryKey; req2.keys[i] = key; if (!primaryKey.outbound) { setByKeyPath(req2.values[i], primaryKey.keyPath, key); } } } else { var objectDiff = getObjectDiff(existingValue, req2.values[i]); var additionalChanges_1 = updating.fire.call(ctx, objectDiff, key, existingValue, dxTrans2); if (additionalChanges_1) { var requestedValue_1 = req2.values[i]; Object.keys(additionalChanges_1).forEach(function(keyPath) { if (hasOwn2(requestedValue_1, keyPath)) { requestedValue_1[keyPath] = additionalChanges_1[keyPath]; } else { setByKeyPath(requestedValue_1, keyPath, additionalChanges_1[keyPath]); } }); } } return ctx; }); return downTable.mutate(req2).then(function(_a3) { var failures = _a3.failures, results = _a3.results, numFailures = _a3.numFailures, lastResult = _a3.lastResult; for (var i = 0; i < keys2.length; ++i) { var primKey = results ? results[i] : keys2[i]; var ctx = contexts[i]; if (primKey == null) { ctx.onerror && ctx.onerror(failures[i]); } else { ctx.onsuccess && ctx.onsuccess( req2.type === "put" && existingValues[i] ? req2.values[i] : primKey ); } } return { failures, results, numFailures, lastResult }; }).catch(function(error40) { contexts.forEach(function(ctx) { return ctx.onerror && ctx.onerror(error40); }); return Promise.reject(error40); }); }); } function deleteRange(req2) { return deleteNextChunk(req2.trans, req2.range, 1e4); } function deleteNextChunk(trans, range, limit) { return downTable.query({ trans, values: false, query: { index: primaryKey, range }, limit }).then(function(_a3) { var result = _a3.result; return addPutOrDelete({ type: "delete", keys: result, trans }).then(function(res) { if (res.numFailures > 0) return Promise.reject(res.failures[0]); if (result.length < limit) { return { failures: [], numFailures: 0, lastResult: void 0 }; } else { return deleteNextChunk(trans, __assign(__assign({}, range), { lower: result[result.length - 1], lowerOpen: true }), limit); } }); }); } } }); return tableMiddleware; } }); } }; function getExistingValues(table, req, effectiveKeys) { return req.type === "add" ? Promise.resolve([]) : table.getMany({ trans: req.trans, keys: effectiveKeys, cache: "immutable" }); } function getFromTransactionCache(keys2, cache2, clone) { try { if (!cache2) return null; if (cache2.keys.length < keys2.length) return null; var result = []; for (var i = 0, j = 0; i < cache2.keys.length && j < keys2.length; ++i) { if (cmp2(cache2.keys[i], keys2[j]) !== 0) continue; result.push(clone ? deepClone(cache2.values[i]) : cache2.values[i]); ++j; } return result.length === keys2.length ? result : null; } catch (_a2) { return null; } } var cacheExistingValuesMiddleware = { stack: "dbcore", level: -1, create: function(core) { return { table: function(tableName) { var table = core.table(tableName); return __assign(__assign({}, table), { getMany: function(req) { if (!req.cache) { return table.getMany(req); } var cachedResult = getFromTransactionCache(req.keys, req.trans["_cache"], req.cache === "clone"); if (cachedResult) { return DexiePromise.resolve(cachedResult); } return table.getMany(req).then(function(res) { req.trans["_cache"] = { keys: req.keys, values: req.cache === "clone" ? deepClone(res) : res }; return res; }); }, mutate: function(req) { if (req.type !== "add") req.trans["_cache"] = null; return table.mutate(req); } }); } }; } }; function isCachableContext(ctx, table) { return ctx.trans.mode === "readonly" && !!ctx.subscr && !ctx.trans.explicit && ctx.trans.db._options.cache !== "disabled" && !table.schema.primaryKey.outbound; } function isCachableRequest(type2, req) { switch (type2) { case "query": return req.values && !req.unique; case "get": return false; case "getMany": return false; case "count": return false; case "openCursor": return false; } } var observabilityMiddleware = { stack: "dbcore", level: 0, name: "Observability", create: function(core) { var dbName = core.schema.name; var FULL_RANGE = new RangeSet2(core.MIN_KEY, core.MAX_KEY); return __assign(__assign({}, core), { transaction: function(stores, mode, options) { if (PSD.subscr && mode !== "readonly") { throw new exceptions.ReadOnly("Readwrite transaction in liveQuery context. Querier source: ".concat(PSD.querier)); } return core.transaction(stores, mode, options); }, table: function(tableName) { var table = core.table(tableName); var schema = table.schema; var primaryKey = schema.primaryKey, indexes = schema.indexes; var extractKey = primaryKey.extractKey, outbound = primaryKey.outbound; var indexesWithAutoIncPK = primaryKey.autoIncrement && indexes.filter(function(index) { return index.compound && index.keyPath.includes(primaryKey.keyPath); }); var tableClone = __assign(__assign({}, table), { mutate: function(req) { var trans = req.trans; var mutatedParts = req.mutatedParts || (req.mutatedParts = {}); var getRangeSet = function(indexName) { var part = "idb://".concat(dbName, "/").concat(tableName, "/").concat(indexName); return mutatedParts[part] || (mutatedParts[part] = new RangeSet2()); }; var pkRangeSet = getRangeSet(""); var delsRangeSet = getRangeSet(":dels"); var type2 = req.type; var _a2 = req.type === "deleteRange" ? [req.range] : req.type === "delete" ? [req.keys] : req.values.length < 50 ? [getEffectiveKeys(primaryKey, req).filter(function(id) { return id; }), req.values] : [], keys2 = _a2[0], newObjs = _a2[1]; var oldCache = req.trans["_cache"]; if (isArray2(keys2)) { pkRangeSet.addKeys(keys2); var oldObjs = type2 === "delete" || keys2.length === newObjs.length ? getFromTransactionCache(keys2, oldCache) : null; if (!oldObjs) { delsRangeSet.addKeys(keys2); } if (oldObjs || newObjs) { trackAffectedIndexes(getRangeSet, schema, oldObjs, newObjs); } } else if (keys2) { var range = { from: keys2.lower, to: keys2.upper }; delsRangeSet.add(range); pkRangeSet.add(range); } else { pkRangeSet.add(FULL_RANGE); delsRangeSet.add(FULL_RANGE); schema.indexes.forEach(function(idx) { return getRangeSet(idx.name).add(FULL_RANGE); }); } return table.mutate(req).then(function(res) { if (keys2 && (req.type === "add" || req.type === "put")) { pkRangeSet.addKeys(res.results); if (indexesWithAutoIncPK) { indexesWithAutoIncPK.forEach(function(idx) { var idxVals = req.values.map(function(v) { return idx.extractKey(v); }); var pkPos = idx.keyPath.findIndex(function(prop) { return prop === primaryKey.keyPath; }); res.results.forEach(function(pk) { return idxVals[pkPos] = pk; }); getRangeSet(idx.name).addKeys(idxVals); }); } } trans.mutatedParts = extendObservabilitySet(trans.mutatedParts || {}, mutatedParts); return res; }); } }); var getRange = function(_a2) { var _b, _c; var _d = _a2.query, index = _d.index, range = _d.range; return [ index, new RangeSet2((_b = range.lower) !== null && _b !== void 0 ? _b : core.MIN_KEY, (_c = range.upper) !== null && _c !== void 0 ? _c : core.MAX_KEY) ]; }; var readSubscribers = { get: function(req) { return [primaryKey, new RangeSet2(req.key)]; }, getMany: function(req) { return [primaryKey, new RangeSet2().addKeys(req.keys)]; }, count: getRange, query: getRange, openCursor: getRange }; keys(readSubscribers).forEach(function(method) { tableClone[method] = function(req) { var subscr = PSD.subscr; var isLiveQuery = !!subscr; var cachable = isCachableContext(PSD, table) && isCachableRequest(method, req); var obsSet = cachable ? req.obsSet = {} : subscr; if (isLiveQuery) { var getRangeSet = function(indexName) { var part = "idb://".concat(dbName, "/").concat(tableName, "/").concat(indexName); return obsSet[part] || (obsSet[part] = new RangeSet2()); }; var pkRangeSet_1 = getRangeSet(""); var delsRangeSet_1 = getRangeSet(":dels"); var _a2 = readSubscribers[method](req), queriedIndex = _a2[0], queriedRanges = _a2[1]; if (method === "query" && queriedIndex.isPrimaryKey && !req.values) { delsRangeSet_1.add(queriedRanges); } else { getRangeSet(queriedIndex.name || "").add(queriedRanges); } if (!queriedIndex.isPrimaryKey) { if (method === "count") { delsRangeSet_1.add(FULL_RANGE); } else { var keysPromise_1 = method === "query" && outbound && req.values && table.query(__assign(__assign({}, req), { values: false })); return table[method].apply(this, arguments).then(function(res) { if (method === "query") { if (outbound && req.values) { return keysPromise_1.then(function(_a3) { var resultingKeys = _a3.result; pkRangeSet_1.addKeys(resultingKeys); return res; }); } var pKeys = req.values ? res.result.map(extractKey) : res.result; if (req.values) { pkRangeSet_1.addKeys(pKeys); } else { delsRangeSet_1.addKeys(pKeys); } } else if (method === "openCursor") { var cursor_1 = res; var wantValues_1 = req.values; return cursor_1 && Object.create(cursor_1, { key: { get: function() { delsRangeSet_1.addKey(cursor_1.primaryKey); return cursor_1.key; } }, primaryKey: { get: function() { var pkey = cursor_1.primaryKey; delsRangeSet_1.addKey(pkey); return pkey; } }, value: { get: function() { wantValues_1 && pkRangeSet_1.addKey(cursor_1.primaryKey); return cursor_1.value; } } }); } return res; }); } } } return table[method].apply(this, arguments); }; }); return tableClone; } }); } }; function trackAffectedIndexes(getRangeSet, schema, oldObjs, newObjs) { function addAffectedIndex(ix) { var rangeSet = getRangeSet(ix.name || ""); function extractKey(obj) { return obj != null ? ix.extractKey(obj) : null; } var addKeyOrKeys = function(key) { return ix.multiEntry && isArray2(key) ? key.forEach(function(key2) { return rangeSet.addKey(key2); }) : rangeSet.addKey(key); }; (oldObjs || newObjs).forEach(function(_, i) { var oldKey = oldObjs && extractKey(oldObjs[i]); var newKey = newObjs && extractKey(newObjs[i]); if (cmp2(oldKey, newKey) !== 0) { if (oldKey != null) addKeyOrKeys(oldKey); if (newKey != null) addKeyOrKeys(newKey); } }); } schema.indexes.forEach(addAffectedIndex); } function adjustOptimisticFromFailures(tblCache, req, res) { if (res.numFailures === 0) return req; if (req.type === "deleteRange") { return null; } var numBulkOps = req.keys ? req.keys.length : "values" in req && req.values ? req.values.length : 1; if (res.numFailures === numBulkOps) { return null; } var clone = __assign({}, req); if (isArray2(clone.keys)) { clone.keys = clone.keys.filter(function(_, i) { return !(i in res.failures); }); } if ("values" in clone && isArray2(clone.values)) { clone.values = clone.values.filter(function(_, i) { return !(i in res.failures); }); } return clone; } function isAboveLower(key, range) { return range.lower === void 0 ? true : range.lowerOpen ? cmp2(key, range.lower) > 0 : cmp2(key, range.lower) >= 0; } function isBelowUpper(key, range) { return range.upper === void 0 ? true : range.upperOpen ? cmp2(key, range.upper) < 0 : cmp2(key, range.upper) <= 0; } function isWithinRange(key, range) { return isAboveLower(key, range) && isBelowUpper(key, range); } function applyOptimisticOps(result, req, ops, table, cacheEntry, immutable) { if (!ops || ops.length === 0) return result; var index = req.query.index; var multiEntry = index.multiEntry; var queryRange = req.query.range; var primaryKey = table.schema.primaryKey; var extractPrimKey = primaryKey.extractKey; var extractIndex = index.extractKey; var extractLowLevelIndex = (index.lowLevelIndex || index).extractKey; var finalResult = ops.reduce(function(result2, op) { var modifedResult = result2; var includedValues = []; if (op.type === "add" || op.type === "put") { var includedPKs = new RangeSet2(); for (var i = op.values.length - 1; i >= 0; --i) { var value = op.values[i]; var pk = extractPrimKey(value); if (includedPKs.hasKey(pk)) continue; var key = extractIndex(value); if (multiEntry && isArray2(key) ? key.some(function(k) { return isWithinRange(k, queryRange); }) : isWithinRange(key, queryRange)) { includedPKs.addKey(pk); includedValues.push(value); } } } switch (op.type) { case "add": modifedResult = result2.concat(req.values ? includedValues : includedValues.map(function(v) { return extractPrimKey(v); })); break; case "put": var keySet_1 = new RangeSet2().addKeys(op.values.map(function(v) { return extractPrimKey(v); })); modifedResult = result2.filter( function(item) { return !keySet_1.hasKey(req.values ? extractPrimKey(item) : item); } ).concat( req.values ? includedValues : includedValues.map(function(v) { return extractPrimKey(v); }) ); break; case "delete": var keysToDelete_1 = new RangeSet2().addKeys(op.keys); modifedResult = result2.filter(function(item) { return !keysToDelete_1.hasKey(req.values ? extractPrimKey(item) : item); }); break; case "deleteRange": var range_1 = op.range; modifedResult = result2.filter(function(item) { return !isWithinRange(extractPrimKey(item), range_1); }); break; } return modifedResult; }, result); if (finalResult === result) return result; finalResult.sort(function(a, b) { return cmp2(extractLowLevelIndex(a), extractLowLevelIndex(b)) || cmp2(extractPrimKey(a), extractPrimKey(b)); }); if (req.limit && req.limit < Infinity) { if (finalResult.length > req.limit) { finalResult.length = req.limit; } else if (result.length === req.limit && finalResult.length < req.limit) { cacheEntry.dirty = true; } } return immutable ? Object.freeze(finalResult) : finalResult; } function areRangesEqual(r1, r2) { return cmp2(r1.lower, r2.lower) === 0 && cmp2(r1.upper, r2.upper) === 0 && !!r1.lowerOpen === !!r2.lowerOpen && !!r1.upperOpen === !!r2.upperOpen; } function compareLowers(lower1, lower2, lowerOpen1, lowerOpen2) { if (lower1 === void 0) return lower2 !== void 0 ? -1 : 0; if (lower2 === void 0) return 1; var c = cmp2(lower1, lower2); if (c === 0) { if (lowerOpen1 && lowerOpen2) return 0; if (lowerOpen1) return 1; if (lowerOpen2) return -1; } return c; } function compareUppers(upper1, upper2, upperOpen1, upperOpen2) { if (upper1 === void 0) return upper2 !== void 0 ? 1 : 0; if (upper2 === void 0) return -1; var c = cmp2(upper1, upper2); if (c === 0) { if (upperOpen1 && upperOpen2) return 0; if (upperOpen1) return -1; if (upperOpen2) return 1; } return c; } function isSuperRange(r1, r2) { return compareLowers(r1.lower, r2.lower, r1.lowerOpen, r2.lowerOpen) <= 0 && compareUppers(r1.upper, r2.upper, r1.upperOpen, r2.upperOpen) >= 0; } function findCompatibleQuery(dbName, tableName, type2, req) { var tblCache = cache["idb://".concat(dbName, "/").concat(tableName)]; if (!tblCache) return []; var queries = tblCache.queries[type2]; if (!queries) return [null, false, tblCache, null]; var indexName = req.query ? req.query.index.name : null; var entries = queries[indexName || ""]; if (!entries) return [null, false, tblCache, null]; switch (type2) { case "query": var equalEntry = entries.find(function(entry) { return entry.req.limit === req.limit && entry.req.values === req.values && areRangesEqual(entry.req.query.range, req.query.range); }); if (equalEntry) return [ equalEntry, true, tblCache, entries ]; var superEntry = entries.find(function(entry) { var limit = "limit" in entry.req ? entry.req.limit : Infinity; return limit >= req.limit && (req.values ? entry.req.values : true) && isSuperRange(entry.req.query.range, req.query.range); }); return [superEntry, false, tblCache, entries]; case "count": var countQuery = entries.find(function(entry) { return areRangesEqual(entry.req.query.range, req.query.range); }); return [countQuery, !!countQuery, tblCache, entries]; } } function subscribeToCacheEntry(cacheEntry, container, requery, signal) { cacheEntry.subscribers.add(requery); signal.addEventListener("abort", function() { cacheEntry.subscribers.delete(requery); if (cacheEntry.subscribers.size === 0) { enqueForDeletion(cacheEntry, container); } }); } function enqueForDeletion(cacheEntry, container) { setTimeout(function() { if (cacheEntry.subscribers.size === 0) { delArrayItem(container, cacheEntry); } }, 3e3); } var cacheMiddleware = { stack: "dbcore", level: 0, name: "Cache", create: function(core) { var dbName = core.schema.name; var coreMW = __assign(__assign({}, core), { transaction: function(stores, mode, options) { var idbtrans = core.transaction(stores, mode, options); if (mode === "readwrite") { var ac_1 = new AbortController(); var signal = ac_1.signal; var endTransaction = function(wasCommitted) { return function() { ac_1.abort(); if (mode === "readwrite") { var affectedSubscribers_1 = /* @__PURE__ */ new Set(); for (var _i = 0, stores_1 = stores; _i < stores_1.length; _i++) { var storeName = stores_1[_i]; var tblCache = cache["idb://".concat(dbName, "/").concat(storeName)]; if (tblCache) { var table = core.table(storeName); var ops = tblCache.optimisticOps.filter(function(op) { return op.trans === idbtrans; }); if (idbtrans._explicit && wasCommitted && idbtrans.mutatedParts) { for (var _a2 = 0, _b = Object.values(tblCache.queries.query); _a2 < _b.length; _a2++) { var entries = _b[_a2]; for (var _c = 0, _d = entries.slice(); _c < _d.length; _c++) { var entry = _d[_c]; if (obsSetsOverlap(entry.obsSet, idbtrans.mutatedParts)) { delArrayItem(entries, entry); entry.subscribers.forEach(function(requery) { return affectedSubscribers_1.add(requery); }); } } } } else if (ops.length > 0) { tblCache.optimisticOps = tblCache.optimisticOps.filter(function(op) { return op.trans !== idbtrans; }); for (var _e = 0, _f = Object.values(tblCache.queries.query); _e < _f.length; _e++) { var entries = _f[_e]; for (var _g = 0, _h = entries.slice(); _g < _h.length; _g++) { var entry = _h[_g]; if (entry.res != null && idbtrans.mutatedParts) { if (wasCommitted && !entry.dirty) { var freezeResults = Object.isFrozen(entry.res); var modRes = applyOptimisticOps(entry.res, entry.req, ops, table, entry, freezeResults); if (entry.dirty) { delArrayItem(entries, entry); entry.subscribers.forEach(function(requery) { return affectedSubscribers_1.add(requery); }); } else if (modRes !== entry.res) { entry.res = modRes; entry.promise = DexiePromise.resolve({ result: modRes }); } } else { if (entry.dirty) { delArrayItem(entries, entry); } entry.subscribers.forEach(function(requery) { return affectedSubscribers_1.add(requery); }); } } } } } } } affectedSubscribers_1.forEach(function(requery) { return requery(); }); } }; }; idbtrans.addEventListener("abort", endTransaction(false), { signal }); idbtrans.addEventListener("error", endTransaction(false), { signal }); idbtrans.addEventListener("complete", endTransaction(true), { signal }); } return idbtrans; }, table: function(tableName) { var downTable = core.table(tableName); var primKey = downTable.schema.primaryKey; var tableMW = __assign(__assign({}, downTable), { mutate: function(req) { var trans = PSD.trans; if (primKey.outbound || trans.db._options.cache === "disabled" || trans.explicit) { return downTable.mutate(req); } var tblCache = cache["idb://".concat(dbName, "/").concat(tableName)]; if (!tblCache) return downTable.mutate(req); var promise = downTable.mutate(req); if ((req.type === "add" || req.type === "put") && (req.values.length >= 50 || getEffectiveKeys(primKey, req).some(function(key) { return key == null; }))) { promise.then(function(res) { var reqWithResolvedKeys = __assign(__assign({}, req), { values: req.values.map(function(value, i) { var _a2; var valueWithKey = ((_a2 = primKey.keyPath) === null || _a2 === void 0 ? void 0 : _a2.includes(".")) ? deepClone(value) : __assign({}, value); setByKeyPath(valueWithKey, primKey.keyPath, res.results[i]); return valueWithKey; }) }); var adjustedReq = adjustOptimisticFromFailures(tblCache, reqWithResolvedKeys, res); tblCache.optimisticOps.push(adjustedReq); queueMicrotask(function() { return req.mutatedParts && signalSubscribersLazily(req.mutatedParts); }); }); } else { tblCache.optimisticOps.push(req); req.mutatedParts && signalSubscribersLazily(req.mutatedParts); promise.then(function(res) { if (res.numFailures > 0) { delArrayItem(tblCache.optimisticOps, req); var adjustedReq = adjustOptimisticFromFailures(tblCache, req, res); if (adjustedReq) { tblCache.optimisticOps.push(adjustedReq); } req.mutatedParts && signalSubscribersLazily(req.mutatedParts); } }); promise.catch(function() { delArrayItem(tblCache.optimisticOps, req); req.mutatedParts && signalSubscribersLazily(req.mutatedParts); }); } return promise; }, query: function(req) { var _a2; if (!isCachableContext(PSD, downTable) || !isCachableRequest("query", req)) return downTable.query(req); var freezeResults = ((_a2 = PSD.trans) === null || _a2 === void 0 ? void 0 : _a2.db._options.cache) === "immutable"; var _b = PSD, requery = _b.requery, signal = _b.signal; var _c = findCompatibleQuery(dbName, tableName, "query", req), cacheEntry = _c[0], exactMatch = _c[1], tblCache = _c[2], container = _c[3]; if (cacheEntry && exactMatch) { cacheEntry.obsSet = req.obsSet; } else { var promise = downTable.query(req).then(function(res) { var result = res.result; if (cacheEntry) cacheEntry.res = result; if (freezeResults) { for (var i = 0, l = result.length; i < l; ++i) { Object.freeze(result[i]); } Object.freeze(result); } else { res.result = deepClone(result); } return res; }).catch(function(error40) { if (container && cacheEntry) delArrayItem(container, cacheEntry); return Promise.reject(error40); }); cacheEntry = { obsSet: req.obsSet, promise, subscribers: /* @__PURE__ */ new Set(), type: "query", req, dirty: false }; if (container) { container.push(cacheEntry); } else { container = [cacheEntry]; if (!tblCache) { tblCache = cache["idb://".concat(dbName, "/").concat(tableName)] = { queries: { query: {}, count: {} }, objs: /* @__PURE__ */ new Map(), optimisticOps: [], unsignaledParts: {} }; } tblCache.queries.query[req.query.index.name || ""] = container; } } subscribeToCacheEntry(cacheEntry, container, requery, signal); return cacheEntry.promise.then(function(res) { return { result: applyOptimisticOps(res.result, req, tblCache === null || tblCache === void 0 ? void 0 : tblCache.optimisticOps, downTable, cacheEntry, freezeResults) }; }); } }); return tableMW; } }); return coreMW; } }; function vipify(target, vipDb) { return new Proxy(target, { get: function(target2, prop, receiver) { if (prop === "db") return vipDb; return Reflect.get(target2, prop, receiver); } }); } var Dexie$1 = function() { function Dexie3(name, options) { var _this = this; this._middlewares = {}; this.verno = 0; var deps = Dexie3.dependencies; this._options = options = __assign({ addons: Dexie3.addons, autoOpen: true, indexedDB: deps.indexedDB, IDBKeyRange: deps.IDBKeyRange, cache: "cloned" }, options); this._deps = { indexedDB: options.indexedDB, IDBKeyRange: options.IDBKeyRange }; var addons = options.addons; this._dbSchema = {}; this._versions = []; this._storeNames = []; this._allTables = {}; this.idbdb = null; this._novip = this; var state = { dbOpenError: null, isBeingOpened: false, onReadyBeingFired: null, openComplete: false, dbReadyResolve: nop, dbReadyPromise: null, cancelOpen: nop, openCanceller: null, autoSchema: true, PR1398_maxLoop: 3, autoOpen: options.autoOpen }; state.dbReadyPromise = new DexiePromise(function(resolve) { state.dbReadyResolve = resolve; }); state.openCanceller = new DexiePromise(function(_, reject) { state.cancelOpen = reject; }); this._state = state; this.name = name; this.on = Events(this, "populate", "blocked", "versionchange", "close", { ready: [promisableChain, nop] }); this.on.ready.subscribe = override(this.on.ready.subscribe, function(subscribe) { return function(subscriber, bSticky) { Dexie3.vip(function() { var state2 = _this._state; if (state2.openComplete) { if (!state2.dbOpenError) DexiePromise.resolve().then(subscriber); if (bSticky) subscribe(subscriber); } else if (state2.onReadyBeingFired) { state2.onReadyBeingFired.push(subscriber); if (bSticky) subscribe(subscriber); } else { subscribe(subscriber); var db_1 = _this; if (!bSticky) subscribe(function unsubscribe() { db_1.on.ready.unsubscribe(subscriber); db_1.on.ready.unsubscribe(unsubscribe); }); } }); }; }); this.Collection = createCollectionConstructor(this); this.Table = createTableConstructor(this); this.Transaction = createTransactionConstructor(this); this.Version = createVersionConstructor(this); this.WhereClause = createWhereClauseConstructor(this); this.on("versionchange", function(ev) { if (ev.newVersion > 0) console.warn("Another connection wants to upgrade database '".concat(_this.name, "'. Closing db now to resume the upgrade.")); else console.warn("Another connection wants to delete database '".concat(_this.name, "'. Closing db now to resume the delete request.")); _this.close({ disableAutoOpen: false }); }); this.on("blocked", function(ev) { if (!ev.newVersion || ev.newVersion < ev.oldVersion) console.warn("Dexie.delete('".concat(_this.name, "') was blocked")); else console.warn("Upgrade '".concat(_this.name, "' blocked by other connection holding version ").concat(ev.oldVersion / 10)); }); this._maxKey = getMaxKey(options.IDBKeyRange); this._createTransaction = function(mode, storeNames, dbschema, parentTransaction) { return new _this.Transaction(mode, storeNames, dbschema, _this._options.chromeTransactionDurability, parentTransaction); }; this._fireOnBlocked = function(ev) { _this.on("blocked").fire(ev); connections.filter(function(c) { return c.name === _this.name && c !== _this && !c._state.vcFired; }).map(function(c) { return c.on("versionchange").fire(ev); }); }; this.use(cacheExistingValuesMiddleware); this.use(cacheMiddleware); this.use(observabilityMiddleware); this.use(virtualIndexMiddleware); this.use(hooksMiddleware); var vipDB = new Proxy(this, { get: function(_, prop, receiver) { if (prop === "_vip") return true; if (prop === "table") return function(tableName) { return vipify(_this.table(tableName), vipDB); }; var rv = Reflect.get(_, prop, receiver); if (rv instanceof Table2) return vipify(rv, vipDB); if (prop === "tables") return rv.map(function(t) { return vipify(t, vipDB); }); if (prop === "_createTransaction") return function() { var tx = rv.apply(this, arguments); return vipify(tx, vipDB); }; return rv; } }); this.vip = vipDB; addons.forEach(function(addon) { return addon(_this); }); } Dexie3.prototype.version = function(versionNumber) { if (isNaN(versionNumber) || versionNumber < 0.1) throw new exceptions.Type("Given version is not a positive number"); versionNumber = Math.round(versionNumber * 10) / 10; if (this.idbdb || this._state.isBeingOpened) throw new exceptions.Schema("Cannot add version when database is open"); this.verno = Math.max(this.verno, versionNumber); var versions = this._versions; var versionInstance = versions.filter(function(v) { return v._cfg.version === versionNumber; })[0]; if (versionInstance) return versionInstance; versionInstance = new this.Version(versionNumber); versions.push(versionInstance); versions.sort(lowerVersionFirst); versionInstance.stores({}); this._state.autoSchema = false; return versionInstance; }; Dexie3.prototype._whenReady = function(fn) { var _this = this; return this.idbdb && (this._state.openComplete || PSD.letThrough || this._vip) ? fn() : new DexiePromise(function(resolve, reject) { if (_this._state.openComplete) { return reject(new exceptions.DatabaseClosed(_this._state.dbOpenError)); } if (!_this._state.isBeingOpened) { if (!_this._state.autoOpen) { reject(new exceptions.DatabaseClosed()); return; } _this.open().catch(nop); } _this._state.dbReadyPromise.then(resolve, reject); }).then(fn); }; Dexie3.prototype.use = function(_a2) { var stack = _a2.stack, create = _a2.create, level = _a2.level, name = _a2.name; if (name) this.unuse({ stack, name }); var middlewares = this._middlewares[stack] || (this._middlewares[stack] = []); middlewares.push({ stack, create, level: level == null ? 10 : level, name }); middlewares.sort(function(a, b) { return a.level - b.level; }); return this; }; Dexie3.prototype.unuse = function(_a2) { var stack = _a2.stack, name = _a2.name, create = _a2.create; if (stack && this._middlewares[stack]) { this._middlewares[stack] = this._middlewares[stack].filter(function(mw) { return create ? mw.create !== create : name ? mw.name !== name : false; }); } return this; }; Dexie3.prototype.open = function() { var _this = this; return usePSD( globalPSD, function() { return dexieOpen(_this); } ); }; Dexie3.prototype._close = function() { var state = this._state; var idx = connections.indexOf(this); if (idx >= 0) connections.splice(idx, 1); if (this.idbdb) { try { this.idbdb.close(); } catch (e) { } this.idbdb = null; } if (!state.isBeingOpened) { state.dbReadyPromise = new DexiePromise(function(resolve) { state.dbReadyResolve = resolve; }); state.openCanceller = new DexiePromise(function(_, reject) { state.cancelOpen = reject; }); } }; Dexie3.prototype.close = function(_a2) { var _b = _a2 === void 0 ? { disableAutoOpen: true } : _a2, disableAutoOpen = _b.disableAutoOpen; var state = this._state; if (disableAutoOpen) { if (state.isBeingOpened) { state.cancelOpen(new exceptions.DatabaseClosed()); } this._close(); state.autoOpen = false; state.dbOpenError = new exceptions.DatabaseClosed(); } else { this._close(); state.autoOpen = this._options.autoOpen || state.isBeingOpened; state.openComplete = false; state.dbOpenError = null; } }; Dexie3.prototype.delete = function(closeOptions) { var _this = this; if (closeOptions === void 0) { closeOptions = { disableAutoOpen: true }; } var hasInvalidArguments = arguments.length > 0 && typeof arguments[0] !== "object"; var state = this._state; return new DexiePromise(function(resolve, reject) { var doDelete = function() { _this.close(closeOptions); var req = _this._deps.indexedDB.deleteDatabase(_this.name); req.onsuccess = wrap(function() { _onDatabaseDeleted(_this._deps, _this.name); resolve(); }); req.onerror = eventRejectHandler(reject); req.onblocked = _this._fireOnBlocked; }; if (hasInvalidArguments) throw new exceptions.InvalidArgument("Invalid closeOptions argument to db.delete()"); if (state.isBeingOpened) { state.dbReadyPromise.then(doDelete); } else { doDelete(); } }); }; Dexie3.prototype.backendDB = function() { return this.idbdb; }; Dexie3.prototype.isOpen = function() { return this.idbdb !== null; }; Dexie3.prototype.hasBeenClosed = function() { var dbOpenError = this._state.dbOpenError; return dbOpenError && dbOpenError.name === "DatabaseClosed"; }; Dexie3.prototype.hasFailed = function() { return this._state.dbOpenError !== null; }; Dexie3.prototype.dynamicallyOpened = function() { return this._state.autoSchema; }; Object.defineProperty(Dexie3.prototype, "tables", { get: function() { var _this = this; return keys(this._allTables).map(function(name) { return _this._allTables[name]; }); }, enumerable: false, configurable: true }); Dexie3.prototype.transaction = function() { var args = extractTransactionArgs.apply(this, arguments); return this._transaction.apply(this, args); }; Dexie3.prototype._transaction = function(mode, tables, scopeFunc) { var _this = this; var parentTransaction = PSD.trans; if (!parentTransaction || parentTransaction.db !== this || mode.indexOf("!") !== -1) parentTransaction = null; var onlyIfCompatible = mode.indexOf("?") !== -1; mode = mode.replace("!", "").replace("?", ""); var idbMode, storeNames; try { storeNames = tables.map(function(table) { var storeName = table instanceof _this.Table ? table.name : table; if (typeof storeName !== "string") throw new TypeError("Invalid table argument to Dexie.transaction(). Only Table or String are allowed"); return storeName; }); if (mode == "r" || mode === READONLY) idbMode = READONLY; else if (mode == "rw" || mode == READWRITE) idbMode = READWRITE; else throw new exceptions.InvalidArgument("Invalid transaction mode: " + mode); if (parentTransaction) { if (parentTransaction.mode === READONLY && idbMode === READWRITE) { if (onlyIfCompatible) { parentTransaction = null; } else throw new exceptions.SubTransaction("Cannot enter a sub-transaction with READWRITE mode when parent transaction is READONLY"); } if (parentTransaction) { storeNames.forEach(function(storeName) { if (parentTransaction && parentTransaction.storeNames.indexOf(storeName) === -1) { if (onlyIfCompatible) { parentTransaction = null; } else throw new exceptions.SubTransaction("Table " + storeName + " not included in parent transaction."); } }); } if (onlyIfCompatible && parentTransaction && !parentTransaction.active) { parentTransaction = null; } } } catch (e) { return parentTransaction ? parentTransaction._promise(null, function(_, reject) { reject(e); }) : rejection(e); } var enterTransaction = enterTransactionScope.bind(null, this, idbMode, storeNames, parentTransaction, scopeFunc); return parentTransaction ? parentTransaction._promise(idbMode, enterTransaction, "lock") : PSD.trans ? usePSD(PSD.transless, function() { return _this._whenReady(enterTransaction); }) : this._whenReady(enterTransaction); }; Dexie3.prototype.table = function(tableName) { if (!hasOwn2(this._allTables, tableName)) { throw new exceptions.InvalidTable("Table ".concat(tableName, " does not exist")); } return this._allTables[tableName]; }; return Dexie3; }(); var symbolObservable = typeof Symbol !== "undefined" && "observable" in Symbol ? Symbol.observable : "@@observable"; var Observable = function() { function Observable2(subscribe) { this._subscribe = subscribe; } Observable2.prototype.subscribe = function(x, error40, complete) { return this._subscribe(!x || typeof x === "function" ? { next: x, error: error40, complete } : x); }; Observable2.prototype[symbolObservable] = function() { return this; }; return Observable2; }(); var domDeps; try { domDeps = { indexedDB: _global.indexedDB || _global.mozIndexedDB || _global.webkitIndexedDB || _global.msIndexedDB, IDBKeyRange: _global.IDBKeyRange || _global.webkitIDBKeyRange }; } catch (e) { domDeps = { indexedDB: null, IDBKeyRange: null }; } function liveQuery2(querier) { var hasValue = false; var currentValue; var observable = new Observable(function(observer) { var scopeFuncIsAsync = isAsyncFunction(querier); function execute(ctx) { var wasRootExec = beginMicroTickScope(); try { if (scopeFuncIsAsync) { incrementExpectedAwaits(); } var rv = newScope(querier, ctx); if (scopeFuncIsAsync) { rv = rv.finally(decrementExpectedAwaits); } return rv; } finally { wasRootExec && endMicroTickScope(); } } var closed = false; var abortController; var accumMuts = {}; var currentObs = {}; var subscription = { get closed() { return closed; }, unsubscribe: function() { if (closed) return; closed = true; if (abortController) abortController.abort(); if (startedListening) globalEvents.storagemutated.unsubscribe(mutationListener); } }; observer.start && observer.start(subscription); var startedListening = false; var doQuery = function() { return execInGlobalContext(_doQuery); }; function shouldNotify() { return obsSetsOverlap(currentObs, accumMuts); } var mutationListener = function(parts) { extendObservabilitySet(accumMuts, parts); if (shouldNotify()) { doQuery(); } }; var _doQuery = function() { if (closed || !domDeps.indexedDB) { return; } accumMuts = {}; var subscr = {}; if (abortController) abortController.abort(); abortController = new AbortController(); var ctx = { subscr, signal: abortController.signal, requery: doQuery, querier, trans: null }; var ret = execute(ctx); Promise.resolve(ret).then(function(result) { hasValue = true; currentValue = result; if (closed || ctx.signal.aborted) { return; } accumMuts = {}; currentObs = subscr; if (!objectIsEmpty(currentObs) && !startedListening) { globalEvents(DEXIE_STORAGE_MUTATED_EVENT_NAME, mutationListener); startedListening = true; } execInGlobalContext(function() { return !closed && observer.next && observer.next(result); }); }, function(err) { hasValue = false; if (!["DatabaseClosedError", "AbortError"].includes(err === null || err === void 0 ? void 0 : err.name)) { if (!closed) execInGlobalContext(function() { if (closed) return; observer.error && observer.error(err); }); } }); }; setTimeout(doQuery, 0); return subscription; }); observable.hasValue = function() { return hasValue; }; observable.getValue = function() { return currentValue; }; return observable; } var Dexie2 = Dexie$1; props(Dexie2, __assign(__assign({}, fullNameExceptions), { delete: function(databaseName) { var db = new Dexie2(databaseName, { addons: [] }); return db.delete(); }, exists: function(name) { return new Dexie2(name, { addons: [] }).open().then(function(db) { db.close(); return true; }).catch("NoSuchDatabaseError", function() { return false; }); }, getDatabaseNames: function(cb) { try { return getDatabaseNames(Dexie2.dependencies).then(cb); } catch (_a2) { return rejection(new exceptions.MissingAPI()); } }, defineClass: function() { function Class(content) { extend(this, content); } return Class; }, ignoreTransaction: function(scopeFunc) { return PSD.trans ? usePSD(PSD.transless, scopeFunc) : scopeFunc(); }, vip, async: function(generatorFn) { return function() { try { var rv = awaitIterator(generatorFn.apply(this, arguments)); if (!rv || typeof rv.then !== "function") return DexiePromise.resolve(rv); return rv; } catch (e) { return rejection(e); } }; }, spawn: function(generatorFn, args, thiz) { try { var rv = awaitIterator(generatorFn.apply(thiz, args || [])); if (!rv || typeof rv.then !== "function") return DexiePromise.resolve(rv); return rv; } catch (e) { return rejection(e); } }, currentTransaction: { get: function() { return PSD.trans || null; } }, waitFor: function(promiseOrFunction, optionalTimeout) { var promise = DexiePromise.resolve(typeof promiseOrFunction === "function" ? Dexie2.ignoreTransaction(promiseOrFunction) : promiseOrFunction).timeout(optionalTimeout || 6e4); return PSD.trans ? PSD.trans.waitFor(promise) : promise; }, Promise: DexiePromise, debug: { get: function() { return debug; }, set: function(value) { setDebug(value); } }, derive, extend, props, override, Events, on: globalEvents, liveQuery: liveQuery2, extendObservabilitySet, getByKeyPath, setByKeyPath, delByKeyPath, shallowClone, deepClone, getObjectDiff, cmp: cmp2, asap: asap$1, minKey, addons: [], connections, errnames, dependencies: domDeps, cache, semVer: DEXIE_VERSION, version: DEXIE_VERSION.split(".").map(function(n) { return parseInt(n); }).reduce(function(p, c, i) { return p + c / Math.pow(10, i * 2); }) })); Dexie2.maxKey = getMaxKey(Dexie2.dependencies.IDBKeyRange); if (typeof dispatchEvent !== "undefined" && typeof addEventListener !== "undefined") { globalEvents(DEXIE_STORAGE_MUTATED_EVENT_NAME, function(updatedParts) { if (!propagatingLocally) { var event_1; event_1 = new CustomEvent(STORAGE_MUTATED_DOM_EVENT_NAME, { detail: updatedParts }); propagatingLocally = true; dispatchEvent(event_1); propagatingLocally = false; } }); addEventListener(STORAGE_MUTATED_DOM_EVENT_NAME, function(_a2) { var detail = _a2.detail; if (!propagatingLocally) { propagateLocally(detail); } }); } function propagateLocally(updateParts) { var wasMe = propagatingLocally; try { propagatingLocally = true; globalEvents.storagemutated.fire(updateParts); signalSubscribersNow(updateParts, true); } finally { propagatingLocally = wasMe; } } var propagatingLocally = false; var bc; var createBC = function() { }; if (typeof BroadcastChannel !== "undefined") { createBC = function() { bc = new BroadcastChannel(STORAGE_MUTATED_DOM_EVENT_NAME); bc.onmessage = function(ev) { return ev.data && propagateLocally(ev.data); }; }; createBC(); if (typeof bc.unref === "function") { bc.unref(); } globalEvents(DEXIE_STORAGE_MUTATED_EVENT_NAME, function(changedParts) { if (!propagatingLocally) { bc.postMessage(changedParts); } }); } if (typeof addEventListener !== "undefined") { addEventListener("pagehide", function(event) { if (!Dexie$1.disableBfCache && event.persisted) { if (debug) console.debug("Dexie: handling persisted pagehide"); bc === null || bc === void 0 ? void 0 : bc.close(); for (var _i = 0, connections_1 = connections; _i < connections_1.length; _i++) { var db = connections_1[_i]; db.close({ disableAutoOpen: false }); } } }); addEventListener("pageshow", function(event) { if (!Dexie$1.disableBfCache && event.persisted) { if (debug) console.debug("Dexie: handling persisted pageshow"); createBC(); propagateLocally({ all: new RangeSet2(-Infinity, [[]]) }); } }); } function add2(value) { return new PropModification2({ add: value }); } function remove2(value) { return new PropModification2({ remove: value }); } function replacePrefix2(a, b) { return new PropModification2({ replacePrefix: [a, b] }); } DexiePromise.rejectionMapper = mapError; setDebug(debug); var namedExports = /* @__PURE__ */ Object.freeze({ __proto__: null, Dexie: Dexie$1, liveQuery: liveQuery2, Entity: Entity2, cmp: cmp2, PropModSymbol: PropModSymbol2, PropModification: PropModification2, replacePrefix: replacePrefix2, add: add2, remove: remove2, "default": Dexie$1, RangeSet: RangeSet2, mergeRanges: mergeRanges2, rangesOverlap: rangesOverlap2 }); __assign(Dexie$1, namedExports, { default: Dexie$1 }); return Dexie$1; }); } }); // src/Core/Input/Execution/InputExecutionStrategyRegister.ts var InputExecutionStrategyRegister = class { strategies = []; registerStrategy(strategy) { this.strategies.unshift(strategy); } unregisterStrategy(strategy) { const index = this.strategies.indexOf(strategy); if (index > -1) { this.strategies.splice(index, 1); } } findApplicableStrategy(inputIntentDTO) { return this.strategies.find((x) => x.shouldUseStrategy(inputIntentDTO)) || this.strategies[0]; } async routeInput(contentEditableEditor, inputIntentDTO, dontClearInput) { return this.findApplicableStrategy(inputIntentDTO).route(contentEditableEditor, inputIntentDTO, dontClearInput); } }; // src/Core/Common/Logger.ts var Logger = class { namespaceStyle = "background: #441000; color: #fff; border-top-left-radius: 5px; border-bottom-left-radius: 5px;"; defaultCategoryStyle = "background: #6D4C41; color: #fff;"; defaultScopeStyle = "background: white; color: black;"; sharedStyle = ` vertical-align: middle; padding: 0.1em 0.6em; font-size: 1.2em;`; colorStyles = { CORE: "background: #4E342E; color: #fff;", // Dark brown INIT: "background: #00796B; color: #fff;", // Teal for initialization SETUP: "background: #546E7A; color: #fff;", // Medium blue-gray for setup MAIN: "background: #37474F; color: #fff;", // Darker blue-gray for main/core processes UI: "background: #388E3C; color: #fff;", // Green for UI NET: "background: #0288D1; color: #fff;", // Light blue for network operations EVENT: "background: #8D6E63; color: #fff;", // Lighter brown ROOT: "background: #EF6C00; color: #000;", // Darker orange SESSION: "background: #EF6C00; color: #000;", // Amber "EMOT:STORE": "background: #7E57C2; color: #fff;", // Purple for emote storage "EMOT:MGR": "background: #673AB7; color: #fff;", // Darker purple for emote management "EMOT:PROV": "background: #9575CD; color: #fff;", // Lighter purple for emote providers // SITES KICK: "background: #53fc18; color: #000;", // SevenTV "EXT:STV": "background: #29d8f6; color: #000;", EVENTAPI: "background: #0288D1; color: #fff;", // Slightly darker blue // Botrix "EXT:BTX": "background: #3ab36b; color: #000;" }; logWithStyles(category, scope, ...args) { console.log( `%cNTV%c${category.padEnd(7, " ")}%c${scope.padEnd(9, " ")}`, this.namespaceStyle + this.sharedStyle, (this.colorStyles[category] || this.defaultCategoryStyle) + this.sharedStyle, // category color or default (this.colorStyles[scope] || this.defaultScopeStyle) + this.sharedStyle + "margin-right: 0.333em;", ...args ); } log(category, scope, ...args) { this.logWithStyles(category, scope, ...args); } logNow(category, scope, ...args) { this.log(category, scope, ...structuredClone(args)); } info(category, scope, ...args) { this.logWithStyles(category, scope, ...args); } success(category, scope, ...args) { this.logWithStyles(category, scope, ...args); } warning(category, scope, ...args) { console.warn( `%cNTV%c${category.padEnd(7, " ")}%c${scope.padEnd(9, " ")}`, this.namespaceStyle + this.sharedStyle, (this.colorStyles[category] || this.defaultCategoryStyle) + this.sharedStyle, (this.colorStyles[scope] || "color: #FFA726;") + this.sharedStyle + "margin-right: 0.333em;", ...args ); } // Error method that throws a proper console error error(category, scope, ...args) { console.error( `%cNTV%c${category.padEnd(7, " ")}%c${scope.padEnd(9, " ")}`, this.namespaceStyle + this.sharedStyle, (this.colorStyles[category] || this.defaultCategoryStyle) + this.sharedStyle, (this.colorStyles[scope] || this.defaultScopeStyle) + this.sharedStyle + "margin-right: 0.333em;", ...args ); } destruct() { return { log: this.log.bind(this), logNow: this.logNow.bind(this), info: this.info.bind(this), success: this.success.bind(this), warning: this.warning.bind(this), error: this.error.bind(this) }; } }; // src/Core/Input/Execution/Strategies/DefaultExecutionStrategy.ts var logger = new Logger(); var { log, info, error } = logger.destruct(); var DefaultExecutionStrategy = class { constructor(rootContext, session) { this.rootContext = rootContext; this.session = session; } shouldUseStrategy(inputIntentDTO) { return true; } async route(contentEditableEditor, inputIntentDTO, dontClearInput) { const { session } = this; const { networkInterface } = session; dontClearInput || contentEditableEditor.clearInput(); if (inputIntentDTO.celebrationRefs) { await networkInterface.sendCelebrationAction(inputIntentDTO.celebrationRefs.id, inputIntentDTO.input); } else if (inputIntentDTO.isReply) { if (!inputIntentDTO.replyRefs) throw new Error("ReplyRefs are required for reply messages."); await networkInterface.sendReply( inputIntentDTO.input, inputIntentDTO.replyRefs.messageId, inputIntentDTO.replyRefs.messageContent, inputIntentDTO.replyRefs.senderId, inputIntentDTO.replyRefs.senderUsername, session.channelData.chatroom?.emotesMode?.enabled ); } else { await networkInterface.sendMessage(inputIntentDTO.input, session.channelData.chatroom?.emotesMode?.enabled); } } }; // src/Core/Common/constants.ts var BROWSER_ENUM = /* @__PURE__ */ ((BROWSER_ENUM2) => { BROWSER_ENUM2[BROWSER_ENUM2["NULL"] = 0] = "NULL"; BROWSER_ENUM2[BROWSER_ENUM2["CHROME"] = 1] = "CHROME"; BROWSER_ENUM2[BROWSER_ENUM2["FIREFOX"] = 2] = "FIREFOX"; BROWSER_ENUM2[BROWSER_ENUM2["OPERAGX"] = 3] = "OPERAGX"; BROWSER_ENUM2[BROWSER_ENUM2["EDGE"] = 4] = "EDGE"; BROWSER_ENUM2[BROWSER_ENUM2["SAFARI"] = 5] = "SAFARI"; return BROWSER_ENUM2; })(BROWSER_ENUM || {}); var U_TAG_LATIN_A = String.fromCodePoint(917569); var U_TAG_LATIN_B = String.fromCodePoint(917570); var U_TAG_LATIN_C = String.fromCodePoint(917571); var U_TAG_LATIN_D = String.fromCodePoint(917572); var U_TAG_LATIN_E = String.fromCodePoint(917573); var U_TAG_LATIN_F = String.fromCodePoint(917574); var U_TAG_LATIN_G = String.fromCodePoint(917575); var U_TAG_LATIN_H = String.fromCodePoint(917576); var U_TAG_LATIN_I = String.fromCodePoint(917577); var U_TAG_LATIN_J = String.fromCodePoint(917578); var U_TAG_LATIN_K = String.fromCodePoint(917579); var U_TAG_LATIN_L = String.fromCodePoint(917580); var U_TAG_LATIN_M = String.fromCodePoint(917581); var U_TAG_LATIN_N = String.fromCodePoint(917582); var U_TAG_LATIN_O = String.fromCodePoint(917583); var U_TAG_LATIN_P = String.fromCodePoint(917584); var U_TAG_LATIN_Q = String.fromCodePoint(917585); var U_TAG_LATIN_R = String.fromCodePoint(917586); var U_TAG_LATIN_S = String.fromCodePoint(917587); var U_TAG_LATIN_T = String.fromCodePoint(917588); var U_TAG_LATIN_U = String.fromCodePoint(917589); var U_TAG_LATIN_V = String.fromCodePoint(917590); var U_TAG_LATIN_W = String.fromCodePoint(917591); var U_TAG_LATIN_X = String.fromCodePoint(917592); var U_TAG_LATIN_Y = String.fromCodePoint(917593); var U_TAG_LATIN_Z = String.fromCodePoint(917594); var U_TAG_DIGIT_0 = String.fromCodePoint(917552); var U_TAG_DIGIT_1 = String.fromCodePoint(917553); var U_TAG_DIGIT_2 = String.fromCodePoint(917554); var U_TAG_DIGIT_3 = String.fromCodePoint(917555); var U_TAG_DIGIT_4 = String.fromCodePoint(917556); var U_TAG_DIGIT_5 = String.fromCodePoint(917557); var U_TAG_DIGIT_6 = String.fromCodePoint(917558); var U_TAG_DIGIT_7 = String.fromCodePoint(917559); var U_TAG_DIGIT_8 = String.fromCodePoint(917560); var U_TAG_DIGIT_9 = String.fromCodePoint(917561); var U_TAG_SPACE = String.fromCodePoint(917536); var U_TAG_EXCLAMATION_MARK = String.fromCodePoint(917537); var U_TAG_COMMERCIAL_AT = String.fromCodePoint(917568); var U_TAG_CANCEL = String.fromCodePoint(917631); var U_TAG_NTV = U_TAG_LATIN_N + U_TAG_LATIN_T + U_TAG_LATIN_V; var U_TAG_NTV_AFFIX = U_TAG_EXCLAMATION_MARK; // src/Core/Common/LexicalEditor.ts var logger2 = new Logger(); var { log: log2, error: error2 } = logger2.destruct(); var LexicalEditor = class { constructor(editor) { this.editor = editor; this.commandSignatureKeywords = { submit: ["preventDefault", "dispatchCommand", "shiftKey"], // Robust: Core event properties and methods for Enter/Shift+Enter clear: [".clear()", "!0"], // Targets .clear() method call and common `return true` removeText: [".removeText()", "!0"], // Targets .removeText() and `return true` deleteCharacter: [".deleteCharacter(", "!0"], // Targets .deleteCharacter( method call and `return true` deleteWord: [".deleteWord(", "!0"], // Targets .deleteWord( method call and `return true` deleteLine: [".deleteLine(", "!0"], // Targets .deleteLine( method call and `return true` insertText: [".insertText(", '"string"==typeof'], // Targets .insertText( and the string type check formatText: [".formatText(", "!0"], // Targets .formatText( method call and `return true` insertLineBreak: [".insertLineBreak(", "!0"], // Targets .insertLineBreak( method call and `return true` insertParagraph: [".insertParagraph()", "!0"], // Targets .insertParagraph() method call and `return true` undo: ["undoStack", ".pop()", "redoStack"], // Targets core undo logic: undoStack, pop, redoStack interaction redo: ["redoStack", ".pop()", "undoStack"], // Targets core redo logic: redoStack, pop, undoStack interaction resendMessage: ["lastSentMessage", "preventDefault"], // Specific state property and common event method cancelReply: ["currentReplyingMessage", "setReplyingMessage"], // Specific state properties/methods for reply feature blur: [".blur()", "!0"], // Targets .blur() method call and `return true` slashCommandEnter: /^(\w+)=>!!\1&&(\w+)\(\1\)$/ // Matches e => !!e && n(e) }; this.mappedCommands = {}; for (const commandName in this.commandSignatureKeywords) { const pattern = this.commandSignatureKeywords[commandName]; if (pattern instanceof Array && pattern.length === 0) { log2("COMMON", "LEXICAL", `Skipping placeholder command '${commandName}'.`); continue; } const foundCommand = this.findCommandByPattern(pattern); if (foundCommand) { this.mappedCommands[commandName] = foundCommand; log2("COMMON", "LEXICAL", `Mapped command '${commandName}':`, foundCommand); } else { if (commandName !== "slashCommandEnter") { error2("COMMON", "LEXICAL", `Could not find and map command '${commandName}'.`); } else { error2("COMMON", "LEXICAL", `Optional command '${commandName}' not found or not mapped.`); } } } if (typeof editor.getRootElement === "function") { this.editorRootElement = editor.getRootElement(); if (this.editorRootElement) { log2("COMMON", "LEXICAL", "Editor root element cached:", this.editorRootElement); } else { throw new Error("Editor root element is null. Cannot proceed with operations that require it."); } } else { throw new Error( "Editor does not have a getRootElement() method. Cannot proceed with operations that require it." ); } } commandSignatureKeywords; mappedCommands; editorRootElement = null; appendText(text, addSpace = false) { const editor = this.editor; editor.focus(() => { editor.update(() => { const state = editor.getEditorState(); const root = state._nodeMap.get("root"); const TextNodeClass = editor._nodes.get("text")?.klass; const ParagraphNodeClass = editor._nodes.get("paragraph")?.klass; if (!TextNodeClass || !ParagraphNodeClass) { throw new Error("TextNode or ParagraphNode class not found."); } const lastRootChild = root.getLastChild(); if (lastRootChild instanceof ParagraphNodeClass) { const currentParagraph = lastRootChild; const lastNodeInParagraph = currentParagraph.getLastChild(); if (lastNodeInParagraph instanceof TextNodeClass) { const existingTextNode = lastNodeInParagraph; const currentText = existingTextNode.getTextContent(); let textToSet = ""; if (addSpace && currentText.length > 0 && !currentText.endsWith(" ")) { textToSet += " "; } textToSet += text; existingTextNode.setTextContent(currentText + textToSet); currentParagraph.selectEnd(); } else { let textToSet = ""; const paragraphContentBeforeAppend = currentParagraph.getTextContent(); if (addSpace && paragraphContentBeforeAppend.length > 0 && !paragraphContentBeforeAppend.endsWith(" ")) { textToSet += " "; } textToSet += text; const newNode = new TextNodeClass(textToSet); currentParagraph.append(newNode); currentParagraph.selectEnd(); } } else { const newParagraph = new ParagraphNodeClass(); const newNode = new TextNodeClass(text); newParagraph.append(newNode); root.append(newParagraph); newParagraph.selectEnd(); } }); }); editor.blur(); } setTextContent(text) { const editor = this.editor; editor.focus(() => { editor.update(() => { const state = editor.getEditorState(); const root = state._nodeMap.get("root"); const TextNode = editor._nodes.get("text")?.klass; const ParagraphNode = editor._nodes.get("paragraph")?.klass; if (!TextNode || !ParagraphNode) { throw new Error("TextNode or ParagraphNode class not found."); } while (root.getFirstChild()) { root.getFirstChild().remove(); } const paragraph = new ParagraphNode(); const newNode = new TextNode(text); paragraph.append(newNode); paragraph.selectEnd(); root.append(paragraph); }); }); editor.blur(); } /** * Find a command whose listener function source matches the specified pattern (keywords or RegExp). */ findCommandByPattern(pattern) { const editor = this.editor; for (const [command, listenersSets] of editor._commands.entries()) { for (const listeners of listenersSets) { for (const listener of listeners) { const fnStr = listener.toString(); if (pattern instanceof RegExp) { if (pattern.test(fnStr)) { return command; } } else { if (pattern.every((kw) => fnStr.includes(kw))) { return command; } } } } } return null; } /** * Programmatically submit the editor content as if Enter was pressed. */ submit() { const editor = this.editor; const command = this.mappedCommands.submit; if (command) { const mockEnterEvent = new KeyboardEvent("keydown", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true, cancelable: true }); const eventTarget = this.editorRootElement; if (eventTarget) { Object.defineProperty(mockEnterEvent, "target", { value: eventTarget, enumerable: true }); Object.defineProperty(mockEnterEvent, "currentTarget", { value: eventTarget, enumerable: true }); } else { error2( "COMMON", "LEXICAL", "Could not determine event target for submit(). Event will be dispatched without a specific target." ); } editor.dispatchCommand(command, mockEnterEvent); log2("COMMON", "LEXICAL", "Dispatched Enter/submit command with mock event:", command); } else { throw new Error("Submit command not mapped. Cannot dispatch."); } } /** * Programmatically executes a slash command (e.g., "/clear") by setting the editor content * and then dispatching the 'slashCommandEnter' Lexical command. * @param slashCommandText The slash command string (e.g., "/clear"). */ async executeSlashCommand(slashCommandText) { const editor = this.editor; const command = this.mappedCommands.slashCommandEnter; if (!command) { throw new Error("'slashCommandEnter' command not mapped. Cannot execute slash command."); } this.setTextContent("/"); await new Promise((resolve) => setTimeout(resolve, 0)); this.appendText(slashCommandText); await new Promise((resolve) => setTimeout(resolve, 0)); const mockEnterEvent = new KeyboardEvent("keydown", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true, cancelable: true }); const eventTarget = this.editorRootElement; if (eventTarget) { Object.defineProperty(mockEnterEvent, "target", { value: eventTarget, enumerable: true }); Object.defineProperty(mockEnterEvent, "currentTarget", { value: eventTarget, enumerable: true }); } else { error2( "COMMON", "LEXICAL", "Could not determine event target for executeSlashCommand. Event will be dispatched without a specific target." ); } editor.dispatchCommand(command, mockEnterEvent); log2("COMMON", "LEXICAL", `Dispatched 'slashCommandEnter' for text "${slashCommandText}":`, command); } }; // src/Core/Common/utils.ts var logger3 = new Logger(); var { error: error3 } = logger3.destruct(); var CHAR_ZWSP = "\uFEFF"; var assertArgument = (arg, type) => { if (typeof arg !== type) { throw new Error(`Invalid argument, expected ${type} but got ${typeof arg}`); } }; var assertArray = (arg) => { if (!Array.isArray(arg)) { throw new Error("Invalid argument, expected array"); } }; var assertArgDefined = (arg) => { if (void 0 === arg) { throw new Error("Invalid argument, expected defined value"); } }; function getPlatformId() { if (location.hostname.match(/(?:www\.)?kick\.com/)) { return "kick" /* KICK */; } else if (location.hostname.match(/(?:www\.)?twitch\.tv/)) { return "twitch" /* TWITCH */; } else if (location.hostname.match(/(?:www\.)?youtube\.com/)) { return "youtube" /* YOUTUBE */; } return "null" /* NULL */; } var REST = class { static get(url) { return this.fetch(url); } static post(url, data) { if (data) { return this.fetch(url, { method: "POST", body: JSON.stringify(data) }); } else { return this.fetch(url, { method: "POST" }); } } static put(url, data) { return this.fetch(url, { method: "PUT", body: JSON.stringify(data) }); } static delete(url) { return this.fetch(url, { method: "DELETE" }); } static fetch(url, options = {}) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open(options.method || "GET", url, true); xhr.setRequestHeader("accept", "application/json"); if (options.body || options.method !== "GET") { xhr.setRequestHeader("Content-Type", "application/json"); options.headers = Object.assign(options.headers || {}, { "Content-Type": "application/json" // Accept: 'application/json, text/plain, */*' }); } const currentDomain = window.location.host.split(".").slice(-2).join("."); const urlDomain = new URL(url).host.split(".").slice(-2).join("."); if (currentDomain === urlDomain) { xhr.withCredentials = true; xhr.setRequestHeader("site", "v2"); options.headers = Object.assign(options.headers || {}, { site: "v2" }); const sessionToken = getCookie("session_token"); if (sessionToken) { xhr.setRequestHeader("Authorization", "Bearer " + sessionToken); options.headers = Object.assign(options.headers || {}, { Authorization: "Bearer " + sessionToken }); } } xhr.onload = function() { if (xhr.status >= 200 && xhr.status < 300) { if (xhr.responseText) { try { const data = JSON.parse(xhr.responseText); if (data["error"] || data["errors"]) reject(data); else resolve(data); } catch (e) { reject(); } } else resolve(void 0); } else { if (xhr.responseText) { try { reject(JSON.parse(xhr.responseText)); } catch (e) { reject(); } } else reject("Request failed with status code " + xhr.status); } }; xhr.onerror = function() { if (xhr.responseText) reject(JSON.parse(xhr.responseText)); else reject("Request failed"); }; xhr.onabort = function() { reject("Request aborted"); }; xhr.ontimeout = function() { reject("Request timed out"); }; xhr.timeout = 25e3; if (options.body) xhr.send(options.body); else xhr.send(); }); } }; var RESTFromMain = class { requestID = 0; promiseMap = /* @__PURE__ */ new Map(); constructor() { if (false) { document.addEventListener("ntv_upstream", (evt) => { const data = JSON.parse(evt.detail); const { rID, xhr } = data; const { resolve, reject } = this.promiseMap.get(rID); this.promiseMap.delete(rID); if (xhr.status >= 200 && xhr.status < 300) { if (xhr.text) { try { resolve(JSON.parse(xhr.text)); } catch (e) { reject(); } } else resolve(void 0); } else { if (xhr.text) { try { reject(JSON.parse(xhr.text)); } catch (e) { reject(); } } else reject("Request failed with status code " + xhr.status); } }); } } get(url) { return this.fetch(url); } post(url, data) { if (data) { return this.fetch(url, { method: "POST", body: JSON.stringify(data) }); } else { return this.fetch(url, { method: "POST" }); } } put(url, data) { return this.fetch(url, { method: "PUT", body: JSON.stringify(data) }); } delete(url) { return this.fetch(url, { method: "DELETE" }); } fetch(url, options = {}) { if (false) { return new Promise((resolve, reject) => { const rID = ++this.requestID; this.promiseMap.set(rID, { resolve, reject }); document.dispatchEvent( new CustomEvent("ntv_downstream", { detail: JSON.stringify({ rID, url, options }) }) ); }); } else { return REST.fetch(url, options); } } }; var ReactivePropsFromMain2 = class { requestID = 0; promiseMap = /* @__PURE__ */ new Map(); constructor() { if (false) { document.addEventListener("ntv_upstream_reactive_props", (evt) => { const data = JSON.parse(evt.detail); const { rID, props } = data; const { resolve, reject } = this.promiseMap.get(rID); this.promiseMap.delete(rID); if (props) resolve(props); else reject(); }); } } getByClassName(className) { return new Promise((resolve, reject) => { if (false) { const rID = ++this.requestID; this.promiseMap.set(rID, { resolve, reject }); document.dispatchEvent( new CustomEvent("ntv_downstream_reactive_props", { detail: JSON.stringify({ rID, className }) }) ); } else { const els = document.getElementsByClassName(className); if (els.length === 0 || els.length > 1) return reject(); const el = els[0]; el.classList.remove(className); const reactivePropsKey = Object.keys(el).find((key) => key.startsWith("__reactProps$")); if (!reactivePropsKey) return reject(); const reactivePropsHandle = el[reactivePropsKey]; const reactiveProps = reactivePropsHandle.children?.props; if (!reactiveProps) return reject(); resolve(reactiveProps); } }); } }; var LexicalCommandFromMain2 = class { requestID = 0; promiseMap = /* @__PURE__ */ new Map(); constructor() { if (false) { document.addEventListener("ntv_upstream_lexical_command", (evt) => { const data = JSON.parse(evt.detail); const { rID, error: error40 } = data; const { resolve, reject } = this.promiseMap.get(rID); this.promiseMap.delete(rID); if (error40) reject(error40); else resolve(); }); } } executeCommand(command) { return new Promise((resolve, reject) => { if (false) { const rID = ++this.requestID; this.promiseMap.set(rID, { resolve, reject }); document.dispatchEvent( new CustomEvent("ntv_downstream_lexical_command", { detail: JSON.stringify({ rID, command }) }) ); } else { const editorDOMElement = document.querySelector(".editor-input"); const editor = editorDOMElement?.__lexicalEditor; if (!editor) { reject("Lexical editor not found."); } const lexicalEditor = new LexicalEditor(editor); lexicalEditor.executeSlashCommand(command); resolve(); } }); } }; function isStringNumber(value) { return !isNaN(Number(value)); } function isEmpty(obj) { for (var x in obj) { return false; } return true; } function getCookie(name) { const c = document.cookie.split("; ").find((v) => v.startsWith(name))?.split(/=(.*)/s); return c && c[1] ? decodeURIComponent(c[1]) : null; } function eventKeyIsLetterDigitPuncSpaceChar(event) { if (event.key.length === 1 && !event.ctrlKey && !event.altKey && !event.metaKey) return true; return false; } function debounce(fn, delay) { let timeout; return function(...args) { clearTimeout(timeout); timeout = setTimeout(() => fn(...args), delay); }; } function hex2rgb(hex) { if (hex.length === 4) { let r2 = hex.slice(1, 2); let g2 = hex.slice(2, 3); let b2 = hex.slice(3, 4); return [parseInt(r2 + r2, 16), parseInt(g2 + g2, 16), parseInt(b2 + b2, 16)]; } const r = parseInt(hex.slice(1, 3), 16); const g = parseInt(hex.slice(3, 5), 16); const b = parseInt(hex.slice(5, 7), 16); return [r, g, b]; } function isElementInDOM(node) { const doc = node.ownerDocument; if (!doc) return false; if (doc.documentElement === document.documentElement) return true; return false; } function waitForElements(selectors, timeout = 1e4, signal = null) { return new Promise((resolve, reject) => { let interval; let timeoutTimestamp = Date.now() + timeout; const checkElements = function() { if (selectors.every((selector) => document.querySelector(selector))) { clearInterval(interval); resolve(selectors.map((selector) => document.querySelector(selector))); } else if (Date.now() > timeoutTimestamp) { clearInterval(interval); reject(new Error("Timeout")); } }; interval = setInterval(checkElements, 100); checkElements(); if (signal) { signal.addEventListener("abort", () => { clearInterval(interval); reject(new DOMException("Aborted", "AbortError")); }); } }); } function waitForTargetedElements(target, selectors, timeout = 1e4, signal = null) { return new Promise((resolve, reject) => { let interval; let timeoutTimestamp = Date.now() + timeout; const checkElements = function() { if (selectors.every((selector) => target.querySelector(selector))) { clearInterval(interval); resolve(selectors.map((selector) => target.querySelector(selector))); } else if (Date.now() > timeoutTimestamp) { clearInterval(interval); reject(new Error("Timeout")); } }; interval = setInterval(checkElements, 100); checkElements(); if (signal) { signal.addEventListener("abort", () => { clearInterval(interval); reject(new DOMException("Aborted", "AbortError")); }); } }); } function findNodeWithTextContent(element, text) { return document.evaluate( `//*[text()='${text}']`, element || document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null ).singleNodeValue; } function parseHTML(html, firstElement = false) { const template = document.createElement("template"); template.innerHTML = html; if (firstElement) { return template.content.childNodes[0]; } else { return template.content; } } function cleanupHTML(html) { return html.trim().replaceAll(/\s\s|\r\n|\r|\n| /gm, ""); } function countStringOccurrences(str, substr) { let count = 0, sl = substr.length, post = str.indexOf(substr); while (post !== -1) { count++; post = str.indexOf(substr, post + sl); } return count; } async function getBrowser() { return async function(agent) { if (navigator.brave && await navigator.brave.isBrave() || false) return BROWSER_ENUM.BRAVE; switch (true) { case agent.indexOf("edge") > -1: return 4 /* EDGE */; case agent.indexOf("edg") > -1: return 4 /* EDGE */; case (agent.indexOf("opr") > -1 && !!window.opr): return 3 /* OPERAGX */; case (agent.indexOf("chrome") > -1 && !!window.chrome): return 1 /* CHROME */; case agent.indexOf("firefox") > -1: return 2 /* FIREFOX */; case agent.indexOf("safari") > -1: return 5 /* SAFARI */; default: return 0 /* NULL */; } }(window.navigator.userAgent.toLowerCase()); } function getDevice() { if (typeof screen.orientation !== "undefined") return 2 /* MOBILE_OR_TABLET */; const navString = navigator.userAgent || navigator.vendor || window.opera; const check = /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test( navString ) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test( navString.substr(0, 4) ); if (check) return 2 /* MOBILE_OR_TABLET */; return 1 /* DESKTOP */; } function hasSupportForAvif() { return new Promise((resolve) => { const img = new Image(); img.src = "data:image/avif;base64,AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUEAAAD5bWV0YQAAAAAAAAAvaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAFBpY3R1cmVIYW5kbGVyAAAAAA5waXRtAAAAAAABAAAAHmlsb2MAAAAARAAAAQABAAAAAQAAASEAAAAlAAAAKGlpbmYAAAAAAAEAAAAaaW5mZQIAAAAAAQAAYXYwMUNvbG9yAAAAAGppcHJwAAAAS2lwY28AAAAUaXNwZQAAAAAAAAAQAAAAEAAAABBwaXhpAAAAAAMICAgAAAAMYXYxQ4EgAAAAAAATY29scm5jbHgAAQANAACAAAAAF2lwbWEAAAAAAAAAAQABBAECgwQAAAAtbWRhdAoMIAAAAZ/5//NAQ0AIMhUQAIAAABS5uMDf00UXignEqOuPKGo="; img.onload = () => resolve(img.width === 16 && img.height === 16); img.onerror = () => resolve(false); }); } function splitEmoteName(name, minPartLength) { if (name.length < minPartLength || name === name.toLowerCase() || name === name.toUpperCase()) { return [name]; } const parts = []; let buffer = name[0]; let lowerCharCount = 1; for (let i = 1; i < name.length; i++) { const char = name[i]; if (char === char.toUpperCase()) { const prevChar = buffer[buffer.length - 1]; if (prevChar && prevChar === prevChar.toUpperCase()) { buffer += char; } else if (lowerCharCount < minPartLength) { buffer += char; } else { parts.push(buffer); buffer = char; } lowerCharCount = 0; } else { buffer += char; lowerCharCount++; } } if (buffer.length) { if (parts.length && buffer.length < minPartLength) { parts[parts.length - 1] += buffer; } else { parts.push(buffer); } } return parts; } function md5(inputString) { var hc = "0123456789abcdef"; function rh(n) { var j, s = ""; for (j = 0; j <= 3; j++) s += hc.charAt(n >> j * 8 + 4 & 15) + hc.charAt(n >> j * 8 & 15); return s; } function ad(x2, y) { var l = (x2 & 65535) + (y & 65535); var m = (x2 >> 16) + (y >> 16) + (l >> 16); return m << 16 | l & 65535; } function rl(n, c2) { return n << c2 | n >>> 32 - c2; } function cm(q, a2, b2, x2, s, t) { return ad(rl(ad(ad(a2, q), ad(x2, t)), s), b2); } function ff(a2, b2, c2, d2, x2, s, t) { return cm(b2 & c2 | ~b2 & d2, a2, b2, x2, s, t); } function gg(a2, b2, c2, d2, x2, s, t) { return cm(b2 & d2 | c2 & ~d2, a2, b2, x2, s, t); } function hh(a2, b2, c2, d2, x2, s, t) { return cm(b2 ^ c2 ^ d2, a2, b2, x2, s, t); } function ii(a2, b2, c2, d2, x2, s, t) { return cm(c2 ^ (b2 | ~d2), a2, b2, x2, s, t); } function sb(x2) { var i2; var nblk = (x2.length + 8 >> 6) + 1; var blks = new Array(nblk * 16); for (i2 = 0; i2 < nblk * 16; i2++) blks[i2] = 0; for (i2 = 0; i2 < x2.length; i2++) blks[i2 >> 2] |= x2.charCodeAt(i2) << i2 % 4 * 8; blks[i2 >> 2] |= 128 << i2 % 4 * 8; blks[nblk * 16 - 2] = x2.length * 8; return blks; } var i, x = sb("" + inputString), a = 1732584193, b = -271733879, c = -1732584194, d = 271733878, olda, oldb, oldc, oldd; for (i = 0; i < x.length; i += 16) { olda = a; oldb = b; oldc = c; oldd = d; a = ff(a, b, c, d, x[i + 0], 7, -680876936); d = ff(d, a, b, c, x[i + 1], 12, -389564586); c = ff(c, d, a, b, x[i + 2], 17, 606105819); b = ff(b, c, d, a, x[i + 3], 22, -1044525330); a = ff(a, b, c, d, x[i + 4], 7, -176418897); d = ff(d, a, b, c, x[i + 5], 12, 1200080426); c = ff(c, d, a, b, x[i + 6], 17, -1473231341); b = ff(b, c, d, a, x[i + 7], 22, -45705983); a = ff(a, b, c, d, x[i + 8], 7, 1770035416); d = ff(d, a, b, c, x[i + 9], 12, -1958414417); c = ff(c, d, a, b, x[i + 10], 17, -42063); b = ff(b, c, d, a, x[i + 11], 22, -1990404162); a = ff(a, b, c, d, x[i + 12], 7, 1804603682); d = ff(d, a, b, c, x[i + 13], 12, -40341101); c = ff(c, d, a, b, x[i + 14], 17, -1502002290); b = ff(b, c, d, a, x[i + 15], 22, 1236535329); a = gg(a, b, c, d, x[i + 1], 5, -165796510); d = gg(d, a, b, c, x[i + 6], 9, -1069501632); c = gg(c, d, a, b, x[i + 11], 14, 643717713); b = gg(b, c, d, a, x[i + 0], 20, -373897302); a = gg(a, b, c, d, x[i + 5], 5, -701558691); d = gg(d, a, b, c, x[i + 10], 9, 38016083); c = gg(c, d, a, b, x[i + 15], 14, -660478335); b = gg(b, c, d, a, x[i + 4], 20, -405537848); a = gg(a, b, c, d, x[i + 9], 5, 568446438); d = gg(d, a, b, c, x[i + 14], 9, -1019803690); c = gg(c, d, a, b, x[i + 3], 14, -187363961); b = gg(b, c, d, a, x[i + 8], 20, 1163531501); a = gg(a, b, c, d, x[i + 13], 5, -1444681467); d = gg(d, a, b, c, x[i + 2], 9, -51403784); c = gg(c, d, a, b, x[i + 7], 14, 1735328473); b = gg(b, c, d, a, x[i + 12], 20, -1926607734); a = hh(a, b, c, d, x[i + 5], 4, -378558); d = hh(d, a, b, c, x[i + 8], 11, -2022574463); c = hh(c, d, a, b, x[i + 11], 16, 1839030562); b = hh(b, c, d, a, x[i + 14], 23, -35309556); a = hh(a, b, c, d, x[i + 1], 4, -1530992060); d = hh(d, a, b, c, x[i + 4], 11, 1272893353); c = hh(c, d, a, b, x[i + 7], 16, -155497632); b = hh(b, c, d, a, x[i + 10], 23, -1094730640); a = hh(a, b, c, d, x[i + 13], 4, 681279174); d = hh(d, a, b, c, x[i + 0], 11, -358537222); c = hh(c, d, a, b, x[i + 3], 16, -722521979); b = hh(b, c, d, a, x[i + 6], 23, 76029189); a = hh(a, b, c, d, x[i + 9], 4, -640364487); d = hh(d, a, b, c, x[i + 12], 11, -421815835); c = hh(c, d, a, b, x[i + 15], 16, 530742520); b = hh(b, c, d, a, x[i + 2], 23, -995338651); a = ii(a, b, c, d, x[i + 0], 6, -198630844); d = ii(d, a, b, c, x[i + 7], 10, 1126891415); c = ii(c, d, a, b, x[i + 14], 15, -1416354905); b = ii(b, c, d, a, x[i + 5], 21, -57434055); a = ii(a, b, c, d, x[i + 12], 6, 1700485571); d = ii(d, a, b, c, x[i + 3], 10, -1894986606); c = ii(c, d, a, b, x[i + 10], 15, -1051523); b = ii(b, c, d, a, x[i + 1], 21, -2054922799); a = ii(a, b, c, d, x[i + 8], 6, 1873313359); d = ii(d, a, b, c, x[i + 15], 10, -30611744); c = ii(c, d, a, b, x[i + 6], 15, -1560198380); b = ii(b, c, d, a, x[i + 13], 21, 1309151649); a = ii(a, b, c, d, x[i + 4], 6, -145523070); d = ii(d, a, b, c, x[i + 11], 10, -1120210379); c = ii(c, d, a, b, x[i + 2], 15, 718787259); b = ii(b, c, d, a, x[i + 9], 21, -343485551); a = ad(a, olda); b = ad(b, oldb); c = ad(c, oldc); d = ad(d, oldd); } return rh(a) + rh(b) + rh(c) + rh(d); } var relativeTimeFormatter = new Intl.RelativeTimeFormat("en", { numeric: "auto" }); var RELATIVE_TIME_DIVISIONS = [ { amount: 60, name: "seconds" }, { amount: 60, name: "minutes" }, { amount: 24, name: "hours" }, { amount: 7, name: "days" }, { amount: 4.34524, name: "weeks" }, { amount: 12, name: "months" }, { amount: Number.POSITIVE_INFINITY, name: "years" } ]; function formatRelativeTime(date) { let duration = (+date - Date.now()) / 1e3; for (let i = 0; i < RELATIVE_TIME_DIVISIONS.length; i++) { const division = RELATIVE_TIME_DIVISIONS[i]; if (Math.abs(duration) < division.amount) { return relativeTimeFormatter.format(Math.round(duration), division.name); } duration /= division.amount; } error3("UTILS", "-", "Unable to format relative time", date); return "error"; } // src/Sites/Kick/KickCommands.ts var logger4 = new Logger(); var { log: log3, info: info2, error: error4 } = logger4.destruct(); var KICK_COMMANDS = [ { name: "timeout", params: " [reason]", minAllowedRole: "moderator", description: "Temporarily ban an user from chat.", argValidators: { "": (arg) => !!arg ? arg.length > 2 ? null : "Username is too short" : "Username is required", "": (arg) => { if (!isStringNumber(arg)) return "Minutes must be a number"; const m = parseInt(arg, 10); return !Number.isNaN(m) && m > 0 && m < 10080 ? null : "Minutes must be a number between 1 and 10080 (7 days)"; } }, api: { protocol: "http", method: "post", uri: (channelName, args) => `https://kick.com/api/v2/channels/${channelName}/bans`, data: (args) => ({ banned_username: args[0], duration: args[1], reason: args.slice(2).join(" "), permanent: false }), errorMessage: "Failed to timeout user.", successMessage: "User has been timed out." } }, { name: "ban", params: " [reason]", minAllowedRole: "moderator", description: "Permanently ban an user from chat.", argValidators: { // Not doing a length check > 2 here because Kick doesn't do it.. "": (arg) => !!arg ? null : "Username is required" }, api: { protocol: "http", method: "post", uri: (channelName, args) => `https://kick.com/api/v2/channels/${channelName}/bans`, data: (args) => { const data = { banned_username: args[0], permanent: true }; if (args[1]) data.reason = args.slice(1).join(" "); return data; }, errorMessage: "Failed to ban user.", successMessage: "User has been banned." } }, { name: "user", params: "", // minAllowedRole: 'moderator', description: "Display user information.", argValidators: { "": (arg) => !!arg ? arg.length > 2 ? null : "Username is too short" : "Username is required" }, execute: async (deps, args) => { log3("KICK", "COMMANDS", "User command executed with args:", args); const { eventBus } = deps; eventBus.publish("ntv.ui.show_modal.user_info", { username: args[0] }); } }, { name: "title", params: "", minAllowedRole: "moderator", description: "Set the stream title.", argValidators: { "<title>": (arg) => !!arg ? null : "Title is required" }, api: { protocol: "http", method: "post", uri: (channelName, args) => `https://kick.com/api/v2/channels/${channelName}/chat-commands`, data: (args) => ({ command: "title", parameter: args.join(" ") }), errorMessage: "Failed to set title.", successMessage: "Title has been set." } }, { name: "category", minAllowedRole: "moderator", description: "Change the category of the stream.", execute: async (deps, args) => { return LexicalCommandFromMain.executeCommand("category"); } }, { name: "prediction", minAllowedRole: "moderator", description: "Create a prediction.", execute: async (deps, args) => { return LexicalCommandFromMain.executeCommand("prediction"); } }, { name: "poll", minAllowedRole: "moderator", description: "Create a poll.", execute: async (deps, args) => { const { eventBus } = deps; eventBus.publish("ntv.ui.show_modal.poll"); } }, { name: "polldelete", minAllowedRole: "moderator", description: "Delete the current poll.", api: { protocol: "http", method: "delete", uri: (channelName, args) => `https://kick.com/api/v2/channels/${channelName}/polls`, errorMessage: "Failed to delete poll.", successMessage: "Poll has been deleted." } }, // { // name: 'category', // command: 'category', // minAllowedRole: 'broadcaster', // description: 'Sets the stream category.' // }, { name: "slow", params: "<on_off> [seconds]", minAllowedRole: "moderator", description: "Enable slow mode for chat. Message interval in seconds.", argValidators: { "<on_off>": (arg) => arg === "on" || arg === "off" ? null : '<on_off> must be either "on" or "off"', "[seconds]": (arg) => !arg || isStringNumber(arg) && parseInt(arg, 10) > 0 ? null : "Seconds must be a number greater than 0" }, api: { protocol: "http", method: "put", uri: (channelName, args) => `https://kick.com/api/v2/channels/${channelName}/chatroom`, data: (args) => ({ slow_mode: args[0] === "on", message_interval: args[1] || 6 }), errorMessage: "Failed to set slow mode.", successMessage: "Succesfully toggled slow mode." } }, { name: "emoteonly", params: "<on_off>", minAllowedRole: "moderator", description: "Enable emote party mode for chat.", argValidators: { "<on_off>": (arg) => arg === "on" || arg === "off" ? null : '<on_off> must be either "on" or "off"' }, api: { protocol: "http", method: "put", uri: (channelName, args) => `https://kick.com/api/v2/channels/${channelName}/chatroom`, data: (args) => ({ emotes_mode: args[0] === "on" }), errorMessage: "Failed to set emote only mode.", successMessage: "Successfully toggled emote only mode." } }, { name: "followonly", params: "<on_off> [minutes]", minAllowedRole: "moderator", description: "Enable followers only mode for chat.", argValidators: { "<on_off>": (arg) => arg === "on" || arg === "off" ? null : '<on_off> must be either "on" or "off"', "[minutes]": (arg) => !arg || isStringNumber(arg) && parseInt(arg, 10) > 0 ? null : "Minutes must be a number greater than 0" }, api: { protocol: "http", method: "put", uri: (channelName, args) => `https://kick.com/api/v2/channels/${channelName}/chatroom`, data: (args) => { if (args[1]) { return { followers_mode: args[0] === "on", following_min_duration: parseInt(args[1], 10) }; } else { return { followers_mode: args[0] === "on" }; } }, errorMessage: "Failed to set followers only mode.", successMessage: "Succesfully toggled followers only mode." } }, { name: "raid", alias: "host", description: "Raid someone's channel (alias for /host)" }, { name: "timer", params: "<seconds/minutes/hours> [description]", description: "Start a timer to keep track of the duration of something. Specify time like 30s, 2m or 1h.", argValidators: { "<seconds/minutes/hours>": (arg) => { const time = arg.match(/^(\d+)(s|m|h)$/i); if (!time) return "Invalid time format. Use e.g. 30s, 2m or 1h."; const value = parseInt(time[1], 10); if (time[2] === "s" && value > 0 && value <= 3600) return null; if (time[2] === "m" && value > 0 && value <= 300) return null; if (time[2] === "h" && value > 0 && value <= 20) return null; return "Invalid time format. Use e.g. 30s, 2m or 1h."; } }, execute: async (deps, args) => { const { eventBus } = deps; eventBus.publish("ntv.ui.timers.add", { duration: args[0], description: args[1] }); log3("KICK", "COMMANDS", "Timer command executed with args:", args); } }, { name: "clear", minAllowedRole: "moderator", description: "Clear the chat.", api: { protocol: "http", method: "post", uri: (channelName, args) => `https://kick.com/api/v2/channels/${channelName}/chat-commands`, data: () => ({ command: "clear" }), errorMessage: "Failed to clear chat.", successMessage: "Chat has been cleared." } }, { name: "subonly", params: "<on_off>", minAllowedRole: "moderator", description: "Enable subscribers only mode for chat.", argValidators: { "<on_off>": (arg) => arg === "on" || arg === "off" ? null : '<on_off> must be either "on" or "off"' }, api: { protocol: "http", method: "put", uri: (channelName, args) => `https://kick.com/api/v2/channels/${channelName}/chatroom`, data: (args) => ({ subscribers_mode: args[0] === "on" }), errorMessage: "Failed to set subscribers only mode.", successMessage: "Successfully toggled subscribers only mode." } }, { name: "unban", params: "<username>", minAllowedRole: "moderator", description: "Unban an user from chat.", argValidators: { "<username>": (arg) => !!arg ? arg.length > 2 ? null : "Username is too short" : "Username is required" }, api: { protocol: "http", method: "delete", uri: (channelName, args) => `https://kick.com/api/v2/channels/${channelName}/bans/${args[0]}`, errorMessage: "Failed to unban user.", successMessage: "User has been unbanned." } }, { name: "vip", params: "<username>", minAllowedRole: "broadcaster", description: "Add an user to your VIP list.", argValidators: { "<username>": (arg) => !!arg ? arg.length > 2 ? null : "Username is too short" : "Username is required" }, api: { protocol: "http", method: "post", uri: (channelName, args) => `https://kick.com/api/internal/v1/channels/${channelName}/community/vips`, data: (args) => ({ username: args[0] }), errorMessage: "Failed to add user as VIP.", successMessage: "User has been added as VIP." } }, { name: "unvip", params: "<username>", minAllowedRole: "broadcaster", description: "Remove an user from your VIP list.", argValidators: { "<username>": (arg) => !!arg ? arg.length > 2 ? null : "Username is too short" : "Username is required" }, api: { protocol: "http", method: "delete", uri: (channelName, args) => `https://kick.com/api/internal/v1/channels/${channelName}/community/vips/${args[0]}`, errorMessage: "Failed to remove user from VIPs.", successMessage: "User has been removed from VIPs." } }, { name: "mod", params: "<username>", minAllowedRole: "broadcaster", description: "Add an user to your moderator list.", argValidators: { "<username>": (arg) => !!arg ? arg.length > 2 ? null : "Username is too short" : "Username is required" }, api: { protocol: "http", method: "post", uri: (channelName, args) => `https://kick.com/api/internal/v1/channels/${channelName}/community/moderators`, data: (args) => ({ username: args[0] }), errorMessage: "Failed to add user as moderator.", successMessage: "User has been added as moderator." } }, { name: "unmod", params: "<username>", minAllowedRole: "broadcaster", description: "Remove an user from your moderator list.", argValidators: { "<username>": (arg) => !!arg ? arg.length > 2 ? null : "Username is too short" : "Username is required" }, api: { protocol: "http", method: "delete", uri: (channelName, args) => `https://kick.com/api/internal/v1/channels/${channelName}/community/moderators/${args[0]}`, errorMessage: "Failed to remove user from moderators.", successMessage: "User has been removed from moderators." } }, { name: "og", params: "<username>", minAllowedRole: "broadcaster", description: "Add an user to your OG list.", argValidators: { "<username>": (arg) => !!arg ? arg.length > 2 ? null : "Username is too short" : "Username is required" }, api: { protocol: "http", method: "post", uri: (channelName, args) => `https://kick.com/api/internal/v1/channels/${channelName}/community/ogs`, data: (args) => ({ username: args[0] }), errorMessage: "Failed to add user as OG.", successMessage: "User has been added as OG." } }, { name: "unog", params: "<username>", minAllowedRole: "broadcaster", description: "Remove an user from your OG list", argValidators: { "<username>": (arg) => !!arg ? arg.length > 2 ? null : "Username is too short" : "Username is required" }, api: { protocol: "http", method: "delete", uri: (channelName, args) => `https://kick.com/api/internal/v1/channels/${channelName}/community/ogs/${args[0]}`, errorMessage: "Failed to remove user from OG.", successMessage: "User has been removed from OG." } }, { name: "follow", params: "<username>", description: "Follow an user.", argValidators: { "<username>": (arg) => !!arg ? arg.length > 2 ? null : "Username is too short" : "Username is required" }, execute: async (deps, args) => { const { networkInterface, channelData } = deps; const userInfo = await networkInterface.getUserChannelInfo(channelData.channelName, "" + args[0]).catch((err) => { throw new Error("Failed to follow user. " + (err.message || "")); }); if (!userInfo) throw new Error("Failed to follow user. User not found"); return networkInterface.followUser(userInfo.slug).then(() => "Following user").catch((err) => { throw new Error("Failed to follow user. " + (err.message || "")); }); } }, { name: "unfollow", params: "<username>", description: "Unfollow an user.", argValidators: { "<username>": (arg) => !!arg ? arg.length > 2 ? null : "Username is too short" : "Username is required" }, execute: async (deps, args) => { const { networkInterface, channelData } = deps; const userInfo = await networkInterface.getUserChannelInfo(channelData.channelName, "" + args[0]).catch((err) => { throw new Error("Failed to follow user. " + (err.message || "")); }); if (!userInfo) throw new Error("Failed to follow user. User not found"); return networkInterface.unfollowUser(userInfo.slug).then(() => "User unfollowed.").catch((err) => { throw new Error("Failed to unfollow user. " + (err.message || "")); }); } }, { name: "mute", params: "<username>", description: "Mute an user.", argValidators: { "<username>": (arg) => !!arg ? arg.length > 2 ? null : "Username is too short" : "Username is required" }, execute: async (deps, args) => { const { usersManager } = deps; const user = usersManager.getUserByName("" + args[0]); if (!user) throw new Error("User not found"); else if (user.muted) throw new Error("User is already muted"); else usersManager.muteUserById(user.id, deps.channelData.channelId); return "User has been muted."; } }, { name: "unmute", params: "<username>", description: "Unmute an user.", argValidators: { "<username>": (arg) => !!arg ? arg.length > 2 ? null : "Username is too short" : "Username is required" }, execute: async (deps, args) => { const { usersManager } = deps; const user = usersManager.getUserByName("" + args[0]); if (!user) throw new Error("User not found"); else if (!user.muted) throw new Error("User is not muted"); else usersManager.unmuteUserById(user.id); return "User has been unmuted."; } }, { name: "host", params: "<username>", minAllowedRole: "broadcaster", description: "Host someone's channel", argValidators: { "<username>": (arg) => !!arg ? arg.length > 2 ? null : "Username is too short" : "Username is required" }, api: { protocol: "http", method: "post", uri: (channelName, args) => `https://kick.com/api/v2/channels/${channelName}/chat-commands`, data: (args) => ({ command: "host", parameter: args[0] }), errorMessage: "Failed to host channel.", successMessage: "Channel has been hosted." } } // { // name: 'multistream', // params: '<on_off>', // minAllowedRole: 'broadcaster', // description: 'Enable/disable multistream mode.', // argValidators: { // '<on_off>': arg => (arg === 'on' || arg === 'off' ? null : '<on_off> must be either "on" or "off"') // }, // execute: async (deps: RootContext & Session, args) => { // // TODO: Implement this command // } // } ]; // src/Core/Input/Execution/Strategies/CommandExecutionStrategy.ts var logger5 = new Logger(); var { log: log4, info: info3, error: error5 } = logger5.destruct(); var CommandExecutionStrategy = class { constructor(rootContext, session) { this.rootContext = rootContext; this.session = session; } shouldUseStrategy(inputIntentDTO) { return inputIntentDTO.input[0] === "/"; } async route(contentEditableEditor, inputIntentDTO, dontClearInput) { const inputString = inputIntentDTO.input; const isInvalid = this.validateInputCommand(inputString.substring(1)); const [commandData, commandEntry] = this.getParsedInputCommand(inputString.substring(1)); if (isInvalid) { throw new Error(isInvalid); } else if (!commandData) { throw new Error("Command not found."); } const { networkInterface } = this.session; if (commandEntry && typeof commandEntry.execute === "function") { return commandEntry.execute({ ...this.rootContext, ...this.session }, commandData.args).then(() => { dontClearInput || contentEditableEditor.clearInput(); }); } else { log4("CORE", "COMEXS", "Executing command", commandData); return networkInterface.executeCommand(commandData.name, this.session.channelData.channelName, commandData.args).then(() => { dontClearInput || contentEditableEditor.clearInput(); if (commandEntry?.api?.protocol === "http" && commandEntry.api.successMessage) return commandEntry.api.successMessage; }).catch(() => { if (commandEntry?.api?.protocol === "http" && commandEntry.api.errorMessage) throw new Error(commandEntry.api.errorMessage); }); } } validateInputCommand(command) { const inputParts = command.split(" "); const inputCommandName = inputParts[0]; if (!inputCommandName) return "No command provided."; const availableCommands = this.getAvailableCommands(); let commandEntry = availableCommands.find((n) => n.name === inputCommandName); if (!commandEntry) return "Command not found."; if (commandEntry.alias) { commandEntry = availableCommands.find((n) => n.name === commandEntry.name); if (!commandEntry) return "Command alias not found."; } const args = commandEntry.params?.split(" "); const argValidators = commandEntry.argValidators; if (!argValidators || !args) return null; const inputArgs = inputParts.slice(1); for (let i = 0; i < args.length; i++) { const arg = args[i]; const inputArg = inputArgs[i] || ""; const argValidator = argValidators[arg]; if (argValidator) { const argIsInvalid = argValidator(inputArg); if (argIsInvalid) return `Invalid argument ${arg}. ${argIsInvalid}.`; } } return null; } getAvailableCommands() { const channelData = this.session.channelData; const is_broadcaster = channelData?.me?.isBroadcaster || false; const is_moderator = channelData?.me?.isModerator || false; return KICK_COMMANDS.filter((commandEntry) => { if (commandEntry.minAllowedRole === "broadcaster") return is_broadcaster; if (commandEntry.minAllowedRole === "moderator") return is_moderator || is_broadcaster; return true; }); } getParsedInputCommand(inputString) { const inputParts = inputString.split(" ").filter((v) => v !== ""); const inputCommandName = inputParts[0]; const availableCommands = this.getAvailableCommands(); let commandEntry = availableCommands.find((n) => n.name === inputCommandName); if (!commandEntry) return [error5("CORE", "COMEXS", "Command not found.")]; if (commandEntry.alias) { commandEntry = availableCommands.find((n) => n.name === commandEntry.name); if (!commandEntry) return [error5("CORE", "COMEXS", "Command alias not found.")]; } const argCount = countStringOccurrences(commandEntry.params || "", "<"); if (inputParts.length - 1 > argCount) { const start = inputParts.slice(1, argCount + 1); const rest = inputParts.slice(argCount + 1).join(" "); return [ { name: commandEntry.name, alias: commandEntry.alias, args: start.concat(rest) }, commandEntry ]; } return [ { name: commandEntry.name, alias: commandEntry.alias, args: inputParts.slice(1, argCount + 1) }, commandEntry ]; } }; // src/Core/Input/Completion/InputCompletionStrategyRegister.ts var InputCompletionStrategyRegister = class { fullLineStrategies = []; inlineStrategies = []; registerStrategy(strategy) { if (strategy.isFullLineStrategy) { if (this.fullLineStrategies.find((x) => x.constructor === strategy.constructor)) throw new Error("Full line strategy already registered"); this.fullLineStrategies.push(strategy); } else { if (this.inlineStrategies.find((x) => x.constructor === strategy.constructor)) throw new Error("Inline strategy already registered"); this.inlineStrategies.push(strategy); } } unregisterStrategy(strategy) { if (strategy.isFullLineStrategy) { const index = this.fullLineStrategies.findIndex((x) => x.constructor === strategy.constructor); if (index === -1) throw new Error("Full line strategy not registered"); this.fullLineStrategies.splice(index, 1); } else { const index = this.inlineStrategies.findIndex((x) => x.constructor === strategy.constructor); if (index === -1) throw new Error("Inline strategy not registered"); this.inlineStrategies.splice(index, 1); } } findApplicableFullLineStrategy(event, editor) { return this.fullLineStrategies.find((x) => x.shouldUseStrategy(event, editor)); } findApplicableInlineStrategy(event, editor) { return this.inlineStrategies.find((x) => x.shouldUseStrategy(event, editor)); } }; // src/Database/DatabaseProxy.ts var extensionProxyHandler = { /** * [dbName] gets passed as the first argument to the Proxy constructor * to initialize the database name, then stack is tacked on as * empty array to keep track of the callstack. */ get(target, prop, receiver) { if (target.prototype === void 0) { const db = target[0]; target = function() { }; target.stack = [prop]; target.db = db; } else { target.stack.push(prop); } return new Proxy(target, extensionProxyHandler); }, apply(target, thisArg, args) { return new Promise((resolve, reject) => { browser.runtime.sendMessage({ action: "database", db: target.db, stack: target.stack, args }).then((r) => { !r || "error" in r ? reject(r && r.error) : resolve(r.data); }); }); } }; var DatabaseProxyFactory = class { static create(dbName, database) { if (database) return database; else return new Proxy([dbName], extensionProxyHandler); } }; // src/Core/Common/RenderMessagePipeline.ts var RenderMessagePipeline = class { middlewares = []; use(middleware) { this.middlewares.push(middleware); return middleware; } process(message, badgesEl, usernameEl, messageParts) { const stack = [...this.middlewares]; const execute = () => { const nextMiddleware = stack.shift(); if (nextMiddleware) { nextMiddleware(message, badgesEl, usernameEl, messageParts, execute); } }; execute(); } remove(middleware) { const index = this.middlewares.indexOf(middleware); if (index !== -1) { this.middlewares.splice(index, 1); } } clear() { this.middlewares = []; } }; // src/Core/UI/Components/AbstractComponent.ts var AbstractComponent = class { // Method to render the component render() { throw new Error("render() method is not implemented yet"); } // Method to attach event handlers attachEventHandlers() { throw new Error("attachEventHandlers() method is not implemented yet"); } // Method to initialize the component init() { if (this.render.constructor.name === "AsyncFunction") { ; this.render().then(this.attachEventHandlers.bind(this)); } else { this.render(); this.attachEventHandlers(); } return this; } }; // src/Core/UI/Modals/AbstractModal.ts var AbstractModal = class extends AbstractComponent { eventTarget = new EventTarget(); className; geometry; element; modalHeaderBodyEl; modalBodyEl; modalCloseBtn; destroyed = false; constructor(className, geometry) { super(); this.className = className; this.geometry = geometry; const position = this.geometry?.position; let positionStyle = ""; if (position === "chat-top") { positionStyle = "right:0;top:43px;"; } else if (position === "coordinates" && this.geometry?.coords) { const coords = this.geometry.coords; positionStyle = `left:${coords.x}px;top:${coords.y}px;`; } const widthStyle = this.geometry?.width ? `width:${this.geometry.width}` : ""; const styleAttribute = `style="${widthStyle};${positionStyle}"`; this.element = parseHTML( cleanupHTML( `<div class="ntv__modal ${this.className ? `ntv__${this.className}-modal` : ""}" ${styleAttribute}> <div class="ntv__modal__header"> <div class="ntv__modal__header__body"></div> <button class="ntv__modal__close-btn">\u{1F7A8}</button> </div> <div class="ntv__modal__body"></div> </div>` ), true ); const modalHeaderEl = this.element.querySelector(".ntv__modal__header"); this.modalHeaderBodyEl = this.element.querySelector(".ntv__modal__header__body"); this.modalBodyEl = this.element.querySelector(".ntv__modal__body"); this.modalCloseBtn = this.element.querySelector(".ntv__modal__close-btn"); this.modalCloseBtn.addEventListener("click", () => { this.destroy(); this.eventTarget.dispatchEvent(new Event("close")); }); modalHeaderEl.addEventListener("mousedown", this.handleModalDrag.bind(this)); if (this.geometry?.position === "center") { window.addEventListener("resize", this.centerModal.bind(this)); } else { window.addEventListener("resize", this.keepModalInsideViewport.bind(this)); } } init() { super.init(); return this; } // Renders the modal container, header and body render() { document.body.appendChild(this.element); if (this.geometry?.position === "center") this.centerModal(); } addEventListener(type, listener) { this.eventTarget.addEventListener(type, listener); } attachEventHandlers() { } destroy() { this.element.remove(); this.destroyed = true; this.eventTarget.dispatchEvent(new Event("destroy")); } isDestroyed() { return this.destroyed; } centerModal() { const windowHeight = window.innerHeight; const windowWidth = window.innerWidth; this.element.style.left = windowWidth / 2 + "px"; this.element.style.top = windowHeight / 2 + "px"; this.element.style.removeProperty("right"); this.element.style.removeProperty("bottom"); this.element.style.transform = "translate(-50%, -50%)"; } keepModalInsideViewport() { const modal = this.element; const modalOffset = modal.getBoundingClientRect(); const windowHeight = window.innerHeight; const windowWidth = window.innerWidth; const modalWidth = modal.clientWidth; const modalHeight = modal.clientHeight; let x = modalOffset.left; let y = modalOffset.top; if (x < 0) x = 0; if (y < 0) y = 0; if (x + modalWidth > windowWidth) x = windowWidth - modalWidth; if (y + modalHeight > windowHeight) y = windowHeight - modalHeight; modal.style.left = `${x}px`; modal.style.top = `${y}px`; } handleModalDrag(event) { const modal = this.element; const modalOffset = modal.getBoundingClientRect(); const cursorOffsetX = event.pageX - modalOffset.left; const cursorOffsetY = event.pageY - modalOffset.top; const windowHeight = window.innerHeight; const windowWidth = window.innerWidth; const modalWidth = modal.clientWidth; const modalHeight = modal.clientHeight; const handleDrag = (evt) => { let x = evt.pageX - cursorOffsetX; let y = evt.pageY - cursorOffsetY; if (x < 0) x = 0; if (y < 0) y = 0; if (x + modalWidth > windowWidth) x = windowWidth - modalWidth; if (y + modalHeight > windowHeight) y = windowHeight - modalHeight; modal.style.left = `${x}px`; modal.style.top = `${y}px`; this.element.style.removeProperty("transform"); }; const handleDragEnd = () => { document.removeEventListener("mousemove", handleDrag); document.removeEventListener("mouseup", handleDragEnd); }; document.addEventListener("mousemove", handleDrag); document.addEventListener("mouseup", handleDragEnd); } }; // src/Core/UI/Modals/AnnouncementModal.ts var AnnouncementModal = class extends AbstractModal { constructor(announcement) { const geometry = { // width: '400px', position: "center" }; super("announcement", geometry); this.announcement = announcement; } eventTarget = new EventTarget(); init() { super.init(); return this; } async render() { super.render(); const { announcement } = this; const { id: announcementId, title: announcementTitle } = announcement; const modalBodyEl = this.modalBodyEl; this.element.setAttribute("data-announcement-id", announcementId); const modalHeaderEl = this.modalHeaderBodyEl; modalHeaderEl.prepend( parseHTML( cleanupHTML(` <svg class="ntv__modal__logo" alt="NipahTV" width="32" height="32" viewBox="0 0 16 16" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path style="opacity:1;fill:#ff3e64;fill-opacity:1;fill-rule:nonzero;stroke-width:0.0230583" d="M 0.2512317,15.995848 0.2531577,6.8477328 C 0.24943124,6.3032776 0.7989812,5.104041 1.8304975,4.5520217 2.4476507,4.2217505 2.9962666,4.1106784 4.0212875,4.0887637 5.0105274,4.067611 5.5052433,4.2710769 5.6829817,4.3608374 c 0.4879421,0.2263549 0.995257,0.7009925 1.134824,0.9054343 l 4.4137403,6.8270373 c 0.262057,0.343592 0.582941,0.565754 0.919866,0.529874 0.284783,-0.0234 0.4358,-0.268186 0.437049,-0.491242 l 0.003,-9.2749904 L 0.25,2.8575985 0.25004315,0 15.747898,2.1455645e-4 15.747791,13.08679 c -0.0055,0.149056 -0.09606,1.191174 -1.033875,2.026391 -0.839525,0.807902 -2.269442,0.879196 -2.269442,0.879196 -0.601658,0.0088 -1.057295,0.02361 -1.397155,-0.04695 -0.563514,-0.148465 -0.807905,-0.274059 -1.274522,-0.607992 -0.4091245,-0.311857 -0.6818768,-0.678904 -0.9118424,-0.98799 0,0 -1.0856521,-1.86285 -1.8718165,-3.044031 C 6.3863506,10.352753 5.3651096,8.7805786 4.8659674,8.1123589 4.4859461,7.5666062 4.2214229,7.4478431 4.0798053,7.3975803 3.9287117,7.3478681 3.7624996,7.39252 3.6345251,7.4474753 3.5213234,7.5006891 3.4249644,7.5987165 3.4078407,7.7314301 l 7.632e-4,8.2653999 z"/></svg><h2>${announcementTitle ? announcementTitle : "Announcement"}</h2>`) ) ); modalBodyEl.appendChild(parseHTML(cleanupHTML(announcement.message))); const buttonContainerEl = document.createElement("div"); buttonContainerEl.classList.add("ntv__announcement-modal__button-container"); if (announcement.showDontShowAgainButton) { const buttonEl = parseHTML(`<button class="ntv__button">Don't show again</button>`, true); buttonEl.addEventListener("click", () => this.closePermanently()); buttonContainerEl.appendChild(buttonEl); } if (announcement.showCloseButton) { const buttonEl = parseHTML(`<button class="ntv__button">Close</button>`, true); buttonEl.addEventListener("click", () => this.close()); buttonContainerEl.appendChild(buttonEl); } modalBodyEl.appendChild(buttonContainerEl); } closePermanently() { this.eventTarget.dispatchEvent(new Event("acknowledged_announcement")); this.close(); } close() { super.destroy(); } }; // src/Core/Services/AnnouncementService.ts var logger6 = new Logger(); var { log: log5, info: info4, error: error6 } = logger6.destruct(); var AnnouncementService = class { constructor(rootEventBus, settingsManager) { this.rootEventBus = rootEventBus; this.settingsManager = settingsManager; settingsManager.registerSetting(NTV_PLATFORM, "shared", "announcements", {}); this.rootEventBus.subscribe("ntv.settings.loaded", this.initialize.bind(this), true); } announcements = {}; closedAnnouncements = {}; queuedAnnouncements = []; currentAnnouncement = null; async initialize() { const { settingsManager } = this; this.closedAnnouncements = settingsManager.getSetting("shared", "announcements"); } registerAnnouncement(announcement) { if (this.announcements[announcement.id]) return log5("SERVICE", "ANNOUNCE", "Announcement already registered:", announcement.id); const isClosed = this.closedAnnouncements[announcement.id] !== void 0; if (isClosed) return; if (announcement.dateTimeRange) { const now = /* @__PURE__ */ new Date(); if (announcement.dateTimeRange.length === 1) { const [end] = announcement.dateTimeRange; if (now > end) return; } else { const [start, end] = announcement.dateTimeRange; if (now < start || now > end) return; } } if (void 0 === announcement.showDontShowAgainButton) { announcement.showDontShowAgainButton = true; } this.announcements[announcement.id] = announcement; } hasAnnouncement(id) { return this.announcements[id] !== void 0; } showNextAnnouncement() { if (this.queuedAnnouncements.length) { this.currentAnnouncement = this.queuedAnnouncements.shift(); this.displayAnnouncement(this.currentAnnouncement.id); } else { this.currentAnnouncement = null; } } async closeAnnouncement(id) { this.closedAnnouncements[id] = /* @__PURE__ */ new Date(); await this.settingsManager.setSetting(NTV_PLATFORM, "shared", "announcements", this.closedAnnouncements); } displayAnnouncement(id) { if (this.closedAnnouncements[id]) return; if (this.currentAnnouncement && this.currentAnnouncement.id !== id) { this.queuedAnnouncements.push(this.announcements[id]); return; } const oldModal = document.querySelectorAll(`.ntv__modal[data-announcement-id="${id}"]`); if (oldModal.length) { Array.from(oldModal).forEach((el) => el.remove()); } this.currentAnnouncement = this.announcements[id]; const announcement = this.currentAnnouncement; const modal = new AnnouncementModal(announcement).init(); modal.addEventListener("acknowledged_announcement", () => this.closeAnnouncement(announcement.id)); modal.addEventListener("destroy", () => { this.currentAnnouncement = null; this.showNextAnnouncement(); }); } }; // src/Sites/Twitch/TwitchEventService.ts var TwitchEventService = class { connect(channelData) { } subToChatroomEvents(channelData) { } addEventListener(channelData, event, callback) { } disconnect(channelData) { } disconnectAll() { } }; // src/Core/UI/Components/SteppedInputSliderLabeledComponent.ts var SteppedInputSliderLabeledComponent = class extends AbstractComponent { value; labels; steps; event = new EventTarget(); element; constructor(labels, steps, label, value) { super(); this.labels = labels; this.steps = steps; const defaultIndex = typeof value !== "undefined" && steps.indexOf(value) || 0; this.element = parseHTML( cleanupHTML(` <div class="ntv__stepped-input-slider ntv__stepped-input-slider-labeled"> <label>${label}</label> <div class="ntv__stepped-input-slider-labeled__slider"> <input type="range" min="0" max="${this.steps.length - 1}" step="1" value="${defaultIndex}"> <div class="ntv__stepped-input-slider-labeled__labels"> ${labels.map((label2, index) => `<span>${label2}</span>`).join("")} </div> </div> </div> `), true ); this.value = value || steps[0]; } render() { } attachEventHandlers() { if (!this.element) return; const inputEl = this.element.querySelector("input"); const labelsEl = this.element.querySelector(".ntv__stepped-input-slider-labeled__labels"); const index = parseInt(inputEl.value); labelsEl.children[index].setAttribute("data-active", "true"); inputEl.addEventListener("input", () => { const index2 = parseInt(inputEl.value); labelsEl.querySelectorAll("span[data-active]").forEach((el) => el.removeAttribute("data-active")); labelsEl.children[index2].setAttribute("data-active", "true"); this.value = this.steps[index2] || this.steps[0]; this.event.dispatchEvent(new Event("change")); }); } addEventListener(event, callback) { this.event.addEventListener(event, callback); } getValue() { return this.value; } }; // src/Core/UI/Components/CheckboxComponent.ts var CheckboxComponent = class extends AbstractComponent { checked; label; id; event = new EventTarget(); element; constructor(id, label, checked = false) { super(); this.id = id; this.label = label; this.checked = checked; this.element = parseHTML( cleanupHTML(` <div class="ntv__checkbox"> <input type="checkbox" id="${this.id}" ${this.checked ? "checked" : ""}> <label for="${this.id}">${this.label}</label> </div> `), true ); } render() { } attachEventHandlers() { const inputEl = this.element?.querySelector("input"); inputEl.addEventListener("change", (event) => { this.checked = event.target.checked; this.event.dispatchEvent(new Event("change")); }); } addEventListener(event, callback) { this.event.addEventListener(event, callback); } getValue() { return this.checked; } }; // src/Core/UI/Components/DropdownComponent.ts var DropdownComponent = class extends AbstractComponent { id; label; options; selectedOption; selectEl; event = new EventTarget(); element; constructor(id, label, options, selectedOption = null) { super(); this.id = id; this.label = label; this.options = options; this.selectedOption = selectedOption; this.element = parseHTML( cleanupHTML(` <div class="ntv__dropdown"> <label for="${this.id}">${this.label}</label> <select id="${this.id}"> ${this.options.map((option) => { const selected = this.selectedOption && option.value === this.selectedOption ? "selected" : ""; return `<option value="${option.value}" ${selected}>${option.label}</option>`; }).join("")} </select> </div> `), true ); this.selectEl = this.element?.querySelector("select"); } render() { } attachEventHandlers() { this.selectEl.addEventListener("change", (event) => { this.event.dispatchEvent(new Event("change")); }); } addEventListener(event, callback) { this.event.addEventListener(event, callback); } getValue() { return this.selectEl.value; } }; // src/Core/UI/Components/NumberComponent.ts var NumberComponent = class extends AbstractComponent { id; label; value; min; max; step; event = new EventTarget(); element; constructor(id, label, value = 0, min = 0, max = 10, step = 1) { super(); this.id = id; this.label = label; this.value = value; this.min = min; this.max = max; this.step = step; this.element = parseHTML( cleanupHTML(` <div class="ntv__number"> <label for="${this.id}">${this.label}</label> <input type="number" id="${this.id}" name="${this.id}" value="${this.value}" min="${this.min}" max="${this.max}" step="${this.step}"> </div> `), true ); } render() { } attachEventHandlers() { const inputEl = this.element.querySelector("input"); inputEl.addEventListener("input", (event) => { this.value = +event.target.value; this.event.dispatchEvent(new Event("change")); }); } addEventListener(event, callback) { this.event.addEventListener(event, callback); } getValue() { return this.value; } }; // src/Core/UI/Components/ColorComponent.ts var ColorComponent = class extends AbstractComponent { value; label; id; event = new EventTarget(); element; constructor(id, label, value = "#000000") { super(); this.id = id; this.label = label; this.value = value; this.element = parseHTML( cleanupHTML(` <div class="ntv__color"> <label for="${this.id}">${this.label}</label> <input type="color" id="${this.id}" value="${this.value}"> </div> `), true ); } render() { } attachEventHandlers() { const inputEl = this.element.querySelector("input"); inputEl.addEventListener("change", (event) => { this.value = event.target.value; this.event.dispatchEvent(new Event("change")); }); } addEventListener(event, callback) { this.event.addEventListener(event, callback); } getValue() { return this.value; } }; // src/changelog.ts var CHANGELOG = [ { version: "1.5.85", date: "2026-01-25", description: ` Fix: Setting channel emote added/removed messages in chat requiring page refresh to apply ` }, { version: "1.5.84", date: "2026-01-24", description: ` Feat: 7TV channel emote changes now show added/removed messages in chat Feat: Added setting to disable channel emote added/removed messages in chat ` }, { version: "1.5.83", date: "2025-12-27", description: ` Just a little fix before closing off the year 2025. Happy holidays everyone! Fix: NipahTV not loading when in mobile split screen mode ` }, { version: "1.5.82", date: "2025-12-11", description: ` Fix: Celebration share button styling ` }, { version: "1.5.81", date: "2025-12-02", description: ` Feat: Make user info modal username handle better compatible with other extensions @igorovh ` }, { version: "1.5.80", date: "2025-11-29", description: ` Feat: Add visual cues of favorited emotes in emote menu Fix: Kick website update has broken chat overlay mode transparency ` }, { version: "1.5.79", date: "2025-11-27", description: ` Fix: Kick website changes to chat has broken UI ` }, { version: "1.5.78", date: "2025-09-14", description: ` The issues with channel points menu are now fixed. Fix: UI components not getting restored correctly when nuked Fix: Share celebrations not showing correctly when quick emotes holder is disabled Fix: Channel points menu causing chat footer to become fully transparent in theatre mode Chore: Improved UI reload handling when UI context is lost ` }, { version: "1.5.77", date: "2025-09-11", description: ` Kick's new website update causes NTV to reload itself in attempt of restoring state when the channel points menu is opened. A temporary workaround has been applied but it's not a perfect fix yet. At least it doesn't cause messages to be duplicated anymore because of it. Fix: NTV not loading on moderator dashboard Fix: Applied a temporary workaround for channel points menu causing NTV to reload ` }, { version: "1.5.75", date: "2025-08-28", description: ` Fix: Kick randomly throwing "Page not found." errors on settings pages when navigating too fast Fix: Emote tooltips sometimes getting stuck on screen Fix: Emote menu not refreshing emoteset after emote override has been registered #228 ` }, { version: "1.5.74", date: "2025-06-14", description: ` Fix: User message history not loading correctly Fix: Settings modal showing under emote menu Chore: Improved commands menu UI Chore: Refine setting description ` }, { version: "1.5.73", date: "2025-06-05", description: ` Feat: Added support for command /prediction Feat: Added support for command /category ` }, { version: "1.5.72", date: "2025-04-14", description: ` Fix: Kick's new channel points button swallowing our menu button ` }, { version: "1.5.71", date: "2025-04-09", description: ` Fix: Kick broke share anniversary celebrations ` }, { version: "1.5.70", date: "2025-03-14", description: ` Fix: Creator dashboard view no longer working Chore: Decreased NTV badge size a tiny bit ` }, { version: "1.5.69", date: "2025-02-28", description: ` Fix: Kick's chat submit button no longer hiding due to Kick website changes ` }, { version: "1.5.68", date: "2025-01-28", description: ` Looks like Kick made some unfathomably dumb change again where someone in their infinite wisdom decided it would be a good idea to steal focus away from the chat input when you press the arrow keys, just to scrub the stream back and forth. I patched it so it doesn't steal focus anymore from chat input. Fix: Kick stealing focus on arrow key during input ` }, { version: "1.5.67", date: "2025-01-21", description: ` Turns out I done a bit of an oopsie and nobody told me about it. I accidentally commented out the share celebration netcode, that's why sharing celebrations didn't actually send the message.. Whoops! It's fixed now, so go ahead and share those celebrations! \u{1F389} Please do report issues or bugs you encounter! I can't fix issues nobody bothers to report. \u{1F605} Fix: Uncommented the share celebration netcode ` }, { version: "1.5.66", date: "2025-01-11", description: ` Added partial support for sub anniversary celebrations. Still need more test data to fully implement this feature. If you have this feature on any of your subbed channels, please reach out to me on the NTV Discord Community server (https://discord.gg/u2gEQZrB6h) if you're willing to help provide some screenshots. This will help me implement support for this feature in NTV. Thanks in advance! Feat: Partial support for sub anniversary celebrations ` }, { version: "1.5.65", date: "2025-01-11", description: ` Temporarily hidden the share subscription anniversary feature for now because it only gets in the way. Now I got the necessary data to implement it, I'll ship it tomorrow. Fix: Temporarily hiding the share celebration feature Fix: Native Kick quick emotes holder sometimes not hiding properly on page load ` }, { version: "1.5.63", date: "2025-01-04", description: ` It's a new year and a new update! I hope you all had a great holiday season and are ready for a fresh new year. I'll kick it off with some fixes as I get back to picking up the pace. The next milestone still remains the complete rewrite of the extension with the new UI framework as version 2.0. As you can imagine this is quite a bit of work so don't think I'm dead if you don't see updates for a little while. I'm still here, just working on the big stuff! I'll still be picking up important bug fixes in the meantime, so don't hesitate to report any issues you encounter. ===! HELP WANTED !=== That being said, Kick has recently rolled out a new update that allows you to share your sub anniversary in chat. As I'm currently unable to reproduce this, I'm looking for someone who can help me out with this. If you currently have this button on any of your subbed channels, please reach out to me on the NTV Discord Community server (https://discord.gg/u2gEQZrB6h) if you're willing to help provide some screenshots. This will help me implement support for this feature in NTV. Thanks in advance! ===! HELP WANTED !=== Feat: Re-implement user mute/block feature after Kick broke it, now actually remembers muted users #191 Fix: New Kick update breaking admin dashboard again #195 Fix: Unable to copy emojis in chat to clipboard #194 Fix: Broken 7TV emoteset icon #197 Fix: Stream buffering overlay showing on top of chat #159 ` }, { version: "1.5.62", date: "2024-11-07", description: ` Fix: Video alignment setting not working Fix: Video control bar overlapping left side positioned chat Fix: Raised network request timeout to absurdly high levels ` }, { version: "1.5.61", date: "2024-11-07", description: ` Fix: Re-arranging favorites resulting in randomized order Fix: 7TV event API not reconnecting Fix: Issue in toggling quick emote holder settings Chore: Minor changes to settings menu structure ` }, { version: "1.5.60", date: "2024-11-05", description: ` Feat: New settings option to move chat to left side of layout ` }, { version: "1.5.59", date: "2024-11-05", description: ` Fix: 7TV emotes without files breaking runtime Fix: Emote sets sometimes not loading due to network/UI racing conditions Fix: Emote tooltip images not working for other channel Kick emotes you are not subscribed to Fix: Settings menu not scrolling to top correctly Chore: Adjusted event API reconnect max duration to 2 hours Chore: Turned my doorbell into a time machine ` }, { version: "1.5.57", date: "2024-11-03", description: ` Fix: Clip links not working in chat messages Fix: Another attempt at fixing the async session reload issues ` }, { version: "1.5.56", date: "2024-11-03", description: ` Fix: Lack of previous session sometimes breaking navigation ` }, { version: "1.5.55", date: "2024-11-03", description: ` I'm currently preparing for a big rewrite of the UI framework, which is a big part of the codebase, because I want to get rid of technical debt and keep the codebase more maintainable. There's currently a lot of legacy code due to decisions made during the rapid prototyping and version iterating of the early stages of NipahTV. This will take some time, but it will allow me to better support features like translating NipahTV to other languages and finally do a full replace of the chat, instead of the dirty injection method it's doing now (causing all kinds of unsolvable issues). But before that, enjoy the new features and fixes! We now have support for 7TV nametag paints and 7TV supporter badges. Feat: Added support for 7TV namepaint cosmetics Feat: Added support for 7TV supporter badge cosmetics Feat: Extensions can now manage private database Fix: Settings menu modal not scrolling to top on category panel change Fix: Page navigation resulting in sessions firing too many session destoyed events Fix: Temporary UI reload fix re-running too fast Fix: Double message history after page navigation Fix: Weird names breaking name selectors Fix: False positives for VOD URIs Chore: Refactored 7TV Event API from SSE to websockets Chore: Added boilerplate example extension ` }, { version: "1.5.54", date: "2024-10-24", description: ` Fix: Zero width emotes overflowing emote container Fix: Windows emoji bar unrecognized input events Fix: Short UI init timeouts sporadically resulting in partially loaded UI states Fix: Input requiring more checks for empty content events Fix: Compositioned input events not inserting content correctly into input Fix: Re-aligned the stars to improve system performance ` }, { version: "1.5.53", date: "2024-10-23", description: ` Fix: Raised request timeout from 7s to 15s ` }, { version: "1.5.52", date: "2024-10-22", description: ` Fix: Emote sets re-rendering all emote sets for every set added Fix: 7TV user not found showing unnecessary error toast Fix: Emote completions not closing nav window on cursor click ` }, { version: "1.5.51", date: "2024-10-20", description: ` Fix: Quick emote holder favorites not correctly updating new loaded emote sets ` }, { version: "1.5.50", date: "2024-10-20", description: ` Fix: Allow faster asynchronous non-blocking loading of emote providers Fix: Emote provider outtage potentially resulting in data loss of recently used emotes and stored favorites due to scheduled database cleanup ` }, { version: "1.5.48", date: "2024-10-20", description: ` Feat: Added new settings options for message styling Feat: Added new settings options for emotes styling Fix: Default message and emote styling not matching native Kick Fix: Zero-width emotes not centering when wider than emote Fix: Clipboard copy not working for emotes in input field Style: Improved settings modal looks Refactor: Re-identified settings keys ` }, { version: "1.5.47", date: "2024-10-13", description: ` Fix: Incorrect quick emote holder height calculation ` }, { version: "1.5.46", date: "2024-10-13", description: ` Fix: Emotes getting slightly clipped due to overflow Fix: Being able to click sub emotes in chat of current channel when not subbed Fix: Slightly improved new messages showing out of view Fix: Chat overlay mode buttons not translucent #180 Fix: Low quality emote tooltip images Refactor: Isolated 7TV integration as extension Refactor: Full restructure of project from pattern-based to semantic-based organization Style: Quick emote holder emotes are now same size as in chat Style: Changed default highlight color Style: Tightened spacing of chat footer and message input Style: Larger emote tooltip images Style: Fixed the alternating background colors of message input on focus states Chore: Turned my doorbell into a time machine Chore: Added warning announcement for lost stragglers on the old Kick popout chatroom page ` }, { version: "1.5.45", date: "2024-10-11", description: ` Fix: Empty favorites for channels where favorited emotes do not apply Fix: Subscriber emotes showing in quick emotes holder when not subscribed Fix: Subscriber emotes showing in input completion when not subscribed Fix: Emote provider setting being mixed up with wrong setting key Fix: Kick emote provider current channel setting not applying Fix: Quick emote holder and input completors not respecting emote provider enabled emote settings Style: Reduced message lines and emote spacing Style: Reduced message badge sizes ` }, { version: "1.5.44", date: "2024-10-06", description: ` Fix: Quick actions not showing when timestamps are disabled for creator & moderators view Fix: Timestamps showing after quick actions for creator & moderators view Fix: Clicking reply message not focussing input #178 Fix: Certain settings like timestamps not applying to VOD pages ` }, { version: "1.5.43", date: "2024-10-04", description: ` Fix: Send emotes immediately setting shouldn't apply when replying Fix: Chat message seperator setting not updating livetime ` }, { version: "1.5.42", date: "2024-10-03", description: ` Fix: Reply message wrappers not cleaning up after reply stacking invisible height ` }, { version: "1.5.39", date: "2024-10-03", description: ` Finally fixed the reply message behaviour that got broken due to the Kick website update. For now it only has effect on channel chatrooms, and not yet the creator & moderator dashboard pages. I'm still waiting for when Kick will actually finish the update for the creator & moderator dashboard pages. But if they don't, I'll make it work with the old layout that still gets loaded on those pages as well. Fix: Broken reply message behaviour due to Kick website update Style: Removed background color on quick emote holder emotes ` }, { version: "1.5.37", date: "2024-09-30", description: ` Fix: Correct emote sizes after restructure Fix: Adjust chat message line spacing Fix: Emote menu sidebar collapses in height when searching for emotes #173 Style: Reduced Kick footer padding on top of quick emote holder ` }, { version: "1.5.36", date: "2024-09-30", description: ` Fix: Channel Zero-Width emotes not working Fix: Favorited emotes not sizing correctly in emote menu Fix: Regression of issue #131 Fix: Temporary fix to reload UI when components get nuked by Kick Chore: Adjusted emote menu emotes row and column gap distance ` }, { version: "1.5.35", date: "2024-09-29", description: ` Fix: Kick emotes not rendering in moderator dashboard view ` }, { version: "1.5.34", date: "2024-09-28", description: ` A hot new update introducing support for Zero-Width emotes! Along with bug fixes, quite a lot changed under the hood. If I missed any new bugs, as always please do report them so I can fix it. Feat: Added support for Zero-Width emotes Fix: Sanitize emote names to prevent potential XSS injections Fix: Clicking on videos tab on channel unloads NTV #165 Fix: User info card showing on top of sub gifting modal Fix: Partial page reloads causing double message behaviour Fix: Ugly temporary reply behaviour not clearing its state correctly Fix: Kick showing elements in certain chat states Fix: Ordering favorites not working correctly Fix: Unloading session not clearing render message cleanup interval Fix: Broken SVG markup Refactor: Reworked message content rendering Refactor: Solidified structure of emotes Refactor: Minor relabeling of stuff Chore: Replaced my floorboards with chocolate bars Chore: Finetuned chat message rendering performance ` }, { version: "1.5.32", date: "2024-09-26", description: ` Fix: Verified badge not showing in moderator dashboard view Fix: Kick bug where hiding pinned message cause horizontal scrollbar to show ` }, { version: "1.5.31", date: "2024-09-25", description: ` Fix: Inconsistent badge element structure in old Kick website structure causing chat rendering to crash ` }, { version: "1.5.30", date: "2024-09-25", description: ` NipahTV now works again on the moderator & creators dashboard pages! If you have any issues here, please do report them. It's now also possible to completely disable NTV on moderator dashboard & creator pages for those that want this. Feat: Added settings option whether to render chat messages Feat: Added moderator setting to disable NTV on moderator & creator dashboard pages Fix: NTV no longer loading for moderator & creator dashboard pages after Kick website update Fix: Metadata in API calls causing Kick servers to shit themselves for no reason making messages broken when pinned Fix: Disable steal focus feature for VODs Fix: REST requests not returning data correctly Refactor: Reworked native Kick chat input fallback mechanism for reply behaviour Chore: Moved settings to new moderator category Chore: Conducted a full-scale rescue mission for my missing socks ` }, { version: "1.5.29", date: "2024-09-24", description: ` Fix: Safari seems to have partial support for Avif causing issues ` }, { version: "1.5.28", date: "2024-09-24", description: ` Fix: Added WebP fallback support for browsers that do not support the modern AVIF standard ` }, { version: "1.5.27", date: "2024-09-22", description: ` Fix: Incorrect sub emote subscribers status check for other channels locking them ` }, { version: "1.5.26", date: "2024-09-22", description: ` Feat: Settings modal now shows update info and button to trigger manual update Fix: Firefox add-on updates automatically triggers reload of everything resulting in double message rendering when sending messages #153 Fix: ContentEditableEditor not reprocessing input on clipboard cut events #158 Fix: Expired faved sub emotes not showing as locked Fix: Unability to unfavorite expired faved sub emotes Fix: Recently used emotes showing expired sub emotes Fix: Not being able to drag and reorder locked faved emotes ` }, { version: "1.5.25", date: "2024-09-20", description: ` Chore: Added link to extension compatibility warning ` }, { version: "1.5.24", date: "2024-09-20", description: ` Chore: Add extension compatibility check ` }, { version: "1.5.23", date: "2024-09-18", description: ` Fix: Change announcement button label ` }, { version: "1.5.22", date: "2024-09-18", description: ` Fix: Pinned messages not rendering hyperlinks #151 Fix: NTV logo image not loading for emote menu button ` }, { version: "1.5.21", date: "2024-09-18", description: ` Fix: VODs not rendering message emotes on new Kick website Fix: Reply and mentions not highlighting messages Fix: Pinned messages not rendering on new Kick website #150 Fix: Improve mention completion behaviour #143 Fix: Improve color emote completion behaviour ` }, { version: "1.5.20", date: "2024-09-15", description: ` Fix: Add label to settings category Chore: Added Discord community server launch announcement ` }, { version: "1.5.19", date: "2024-09-15", description: ` Fix: Replying sometimes get stuck in native Kick chat input ` }, { version: "1.5.18", date: "2024-09-15", description: ` Feat: Added settings option for moderator quick actions Feat: Can now change alignment of stream video in chat overlay mode Fix: Added back in moderator quick action (delete, timeout, ban) Fix: Changing emote menu button setting not livetime updated Fix: Chat overlay mode chat position setting Fix: Deleted messages showing label out of place Chore: Added new emote menu button style setting ` }, { version: "1.5.17", date: "2024-09-15", description: ` Major issue solved, finally figured what was causing the page to crash when replying to messages. Feat: Added setting whether to show recently used emotes in the quick emotes holder Fix: Replying to messages randomly crashing the page #126 Fix: Emote tooltips getting cut off due to overflow Fix: Messages with emojis not rendering correctly Fix: Quick emotes holder spacer showing when favorited or recent emotes are empty ` }, { version: "1.5.16", date: "2024-09-14", description: ` Feat: Automatic mention completions instead of <TAB> key triggered Fix: Double processed chat messages on URL change when session UI is not destroyed #131 Fix: Favorite emotes showing saved image instead of current channel image #134 Fix: <COLON> key emote completions only triggering after first letter ` }, { version: "1.5.15", date: "2024-09-14", description: ` Feat: Make chat message lines consistent in height allowing emote overlap Fix: Sometimes unable to click emotes in chat #137 Fix: Hide quick emotes holder when replying Fix: Chat message spacing setting Fix: Prevent zalgo text from overflowing messages Chore: Prevent darkening of chat input on focus ` }, { version: "1.5.14", date: "2024-09-13", description: ` Fix: Commands not showing notification toasts Fix: Followonly command has changed definition Fix: Unreliable network requests due to invalid session token ` }, { version: "1.5.13", date: "2024-09-13", description: ` Fix: Firefox add-on works again Fix: Reply-to-me message highlighting Fix: UI elements not loading when Kick native quick emote holder disabled ` }, { version: "1.5.12", date: "2024-09-13", description: ` Looks like the chat message rendering is finally stable again now with minimal jittering and acceptable performance on fast moving chats. Fix: User mention auto-completion not working Fix: First time user highlighting Fix: Message render queue not reducing correctly Fix: Annoying jittering message movements Fix: Reversed message render order resulted in incorrect first time message highlighting Chore: Further finetuned message rendering Chore: Clarified settings ` }, { version: "1.5.11", date: "2024-09-12", description: ` Chore: Finetuning the message rendering queue system Fix: Styling on chat causing message rendering performance issues ` }, { version: "1.5.10", date: "2024-09-12", description: ` Feat: Added message rendering queue system to increase performance for fast moving chats Fix: Force truncate outrageously long usernames Fix: Translucent chat overlay mode being enabled by default ` }, { version: "1.5.9", date: "2024-09-12", description: ` Fix: Translucent chat overlay feature in theatre mode Fix: Links not rendering in messages Fix: Emote menu positioning wrong after window resize Fix: Show original Kick submit button for replies Fix: User channelId not loading Fix: NTV badge sometimes not showing Chore: Temporarily removed broken message spacing feature ` }, { version: "1.5.8", date: "2024-09-11", description: ` Fix: Shortcut <K>key would pause stream while typing in chat Fix: Command completions not updating on keys that match Kick shortcuts Fix: Updated old popout chat link Fix: Quick emote holder showing overflowing emotes Fix: Temporarily disabled mute button in user info modals until user management is fixed ` }, { version: "1.5.7", date: "2024-09-11", description: ` Feat: Add settings option whether to steal focus to chat input when typing Fix: Typing in chat triggering Kick shortcuts Fix: Emote menu not opening with <CTRL> + <SPACE> ` }, { version: "1.5.6", date: "2024-09-11", description: ` Kick Website Overhaul: I have been hard at work since Kick's major website overhaul (about 12 hours ago, 10 Sept) and am excited to share that NipahTV is back up and functional with most core functionality restored! For those who are new or out of the loop, Kick introduced a complete redesign of their site yesterday, which has affected NipahTV and other extensions. While there\u2019s still a lot more to be done, you can once again enjoy the core features of NipahTV! Current Known Issues: - Reply Functionality: Kick\u2019s overhaul made it impossible to implement the reply message feature. When replying, NipahTV falls back to the default Kick chat input as a temporary workaround. - Firefox Issues: Kick has historically had many issues with Firefox, and currently, Firefox is having trouble authenticating. - Mobile Mode Conflicts: Kick\u2019s new mobile mode activates on smaller window sizes, which currently breaks NipahTV. - Chat Scrolling Problems: Occasionally, chat gets stuck while scrolling, particularly when large messages with a lot of emotes expand. - Bans/Timeouts: Banning or timing out users causes their page to crash completely. - Feature Restoration: Some settings, such as the transparent overlay chat in theatre mode, still need to be re-implemented into Kick\u2019s new design. We are continuing to make fixes and adjustments to improve the experience and restore the features you all loved. Thank you for your patience and support as we adapt to these changes! MAJOR BREAKING CHANGES Major fix: Complete rewrite of the Kick userinterface in support of the new website Feat: Added settings for message timestamps Fix: Accommodated new changes to the Kick API Fix: Spacing issues of texts between emotes ` }, { version: "1.5.5", date: "2024-09-08", description: ` Major Kick website overhaul: As I'm sure many of you are aware by now, and perhaps you just found out by the time you read this announcement because it already happened, Kick has planned a major overhaul of the website. As reportedly planned it currently stands to be released on: - Monday 9 Sept for all of Oceania - Tuesday 10 Sept for Latin America - Wednesday 11 Sept for Europe - Thursday 12 Sept for North America It is not yet known in what ways the new website will break NipahTV, but we will do our best to keep up with the changes and provide you with the best experience possible. If it turns out to be utterly broken, simply temporarily disable the extension/userscript until we can push an update to fix it. Please be patient and allow us some time to adjust to the coming changes. Thank you for supporting NipahTV! Fix: Regression of missing plaform ID for extension database ` }, { version: "1.5.4", date: "2024-09-08", description: ` Feat: Added announcement service Feat: Added partial support for platform and channel independent context scoping of settings for more specificity and granularity control of settings Chore: Added Kick website overhaul announcement Refactor: Cleaned up platform globals ` }, { version: "1.5.2", date: "2024-09-05", description: ` Fix: VOD chat history no longer rendering emotes ` }, { version: "1.5.1", date: "2024-09-01", description: ` Notable changes: - Fixed an overdue issue of emotes not sending in emote only mode chat. - Changed the input status so that streamers and mods can always see the chat mode status without having to second guess it. It'll remain the same for regular users as ("Send message.."). - Ban/unban events are now handled much more reliably and consistently. - Only downside currently is that the follow button hasn't been implemented yet, so a page refresh is necessary after following a new channel. This will be fixed in a later update, it's low priority for now. Fix: Emotes not sending in emote only mode chat Fix: Errors not showing for commands due to incorrect error handling Feat: Privileged users can now always see chat status in input (emoteonly/followonly/subonly) Feat: Added pusher event handling service to handle ban/unban events and subonly/followonly/slowmode/emotesonly mode events Feat: Added followers minimum channel following duration handling Refactor: Reworked how ban/unban events are handled ` }, { version: "1.5.0", date: "2024-08-30", description: ` Following a major overhaul of the input completion strategy, it's now possible to trigger emote and mention completions during commands, with support for other command types coming soon. With input submission execution now decoupled from the input completer strategy, implementing support for third-party bot commands like Botrix is now possible, and command input is much more reliable. Previously, there were some edge cases, such as adding a '/' in front of pre-written or copied commands, which resulted in them being sent as regular messages. A new input completer strategy has been added to support colon emotes, allowing you to start emote completions with, for example, ":peepo". You can now also disable all input completion strategies in the settings under Chat > Input > Input completion. Do note, that colon emotes (e.g. ":peepoGaze:") are not yet supported with copy-pasting, but this will be added in a future update. Please do let me know if you value this feature, so I can prioritize it accordingly. It's a fairly significant update, so I might have missed some bugs. Please do report any issues you encounter. Feat: Added input submission execution strategy system Feat: Added support for colon emote completion suggestions Feat: Added settings options for all input completion strategies Feat: Kick command execution now confirms success before clearing input Fix: Pressing <ESCAPE> completed emote completion instead of canceling it Fix: Command messages being excluded from chat history Fix: Command completion blocking chat history navigation Fix: Sticky command UI completion after execution Fix: <CONTROL> key closing the emote completion Major refactor: Complete rewrite of input completion strategy system Refactor: Isolated command execution logic Refactor: Isolated Kick network interface API call signatures Refactor: Isolated Kick command definitions Chore: Change favorites instruction label Chore: Cleaned up changelog ` }, { version: "1.4.38", date: "2024-08-22", description: ` Feat: Added support for extensions (NTV add-ons, not browser extensions) Fix: Stale emotes showed as "undefined" in quick emote holder Fix: Hovering over emotes in quick emotes holder caused clipping Fix: Botrix commands were not working due to an unicode formatting tag codepoints bug in Botrix Chore: Added input completion strategy registry ` }, { version: "1.4.37", date: "2024-08-20", description: ` Kick changed the message element structure, causing messages to break. This is fixed now. Fix: Kick changes to element structure broke messages Chore: Watered my cotton couch ` }, { version: "1.4.36", date: "2024-08-02", description: ` Fix: Emote menu was getting shoved out of screen on small window sizes Fix: Emote tooltips were not centered on emotes #118 Fix: Input field changes height when an emote gets rendered/inserted into it, whereas it should be fixed #110 Fix: Message styling was no longer applying ` }, { version: "1.4.34", date: "2024-08-02", description: ` Feat: Added a new setting for message spacing Feat: Added a new setting for message style Fix: Emotes don't register usage engagement events when sent by submit button #115 Fix: Clicking on emotes in pinned messages won't insert them #107 ` }, { version: "1.4.33", date: "2024-07-31", description: ` Fix: Subscribers emotes showing as available even when not subscribed #114 ` }, { version: "1.4.32", date: "2024-07-30", description: ` Discovered a bug where the platform constant was not available in extension background workers, causing all emotes to be registered and loaded under the wrong platform ID. As a result, all emote usage history would appear as if it had been wiped; apologies for the inconvenience! Hotfix: Constant PLATFORM was not available in extension background workers ` }, { version: "1.4.31", date: "2024-07-30", description: ` Added a new feature to favorite emotes and sort them by drag & drop. The favorite emotes are platform-wide. Do note that other-channel emotes that are not available in current channel will, by default, be hidden. Feat: Implemented platform-wide emote favorites with drag & drop sorting capabilities Feat: Added a setting to not show non-cross-channel favorited emotes Fix: Incorrect implementation of emoteset registration ordering Fix: Emote menu styling issues Fix: Quick emote holder settings required a page refresh to take effect Major refactor: Fully rewrote database structure Refactor: Reworked quick emotes holder for better performance ` }, { version: "1.4.30", date: "2024-07-18", description: ` Fix: Copy-paste was no longer working Fix: NTV badge was not rendering for reply messages ` }, { version: "1.4.29", date: "2024-07-18", description: ` Users with NipahTV now show a NTV badge in chat. If you don't like this feature, you can disable it in the settings. Feat: Added NTV badge for NTV users Feat: Added a report bug button to emote menu Feat: Added a changelog to settings modal Fix: Commands showed false positive error toasts due to inconsistent Kick API definitions Fix: Emote tooltips showed under messages in overlay chat mode Fix: Multiple chat submit buttons appeared when navigating ` }, { version: "1.4.27", date: "2024-07-15", description: ` Added an experimental feature, a new settings option to overlay chat transparently over the stream. It can be found under Settings > Appearance > Layout. I pushed this feature in a prior version but it accidentally completely messed up the chat, so I had to revert it. Apologies for the inconvenience for the few that noticed it. Feat: Overlayed chat on top of stream transparently #66 Feat: Improved the video player control bar, removing the ugly and annoying opaque color bar that had a completely different gradient style than the entire rest of the Kick interface.. Why Kick, why? Fix: Pasting inserted at the wrong cursor position in Chrome ` }, { version: "1.4.26", date: "2024-07-10", description: ` Fix: Chat messages were not loading on providers' load Fix: Emotes could be inserted from quick emote holder when banned Fix: Chat messages were not loading with appropriate styling for VODs Fix: Annoying horizontal scrollbar appeared due to poor styling of Kick Fix: Added more spacing for timestamps ` }, { version: "1.4.24", date: "2024-07-02", description: ` Fix: Commands could be sent as plaintext messages when using chat button Fix: Pinned messages showed double message Fix: Odd-pair brackets [ were incorrectly parsed as emote text, messing up the message Fix: Rapid channel switching sometimes resulted in multiple input fields Fix: Misaligned message user identity styling Fix: Reverted truncating Kick emote names to underscore (because non-NTV users would see emote tooltips as underscore) Chore: Improved emoji spacing ` }, { version: "1.4.23", date: "2024-07-02", description: ` Fix: Input focus was getting stolen on creator's dashboard ` }, { version: "1.4.22", date: "2024-06-30", description: ` There's a new fancy command /timer that allows you to track the duration of something. Channels where you are not subbed now show unlockable subscribers' emotes. Native Kick emotes now get rendered in user info modals when viewing message history (moderators only). Input now actually shows when you are timed out or banned with the appropriate message. Before, deleted messages never really got deleted at all; message deletion now works correctly. Lastly, fixed some bugs and annoyances. Fix: Emotes were randomly not loading due to overrides Fix: Deleted messages weren't deleted #81 Fix: No visual feedback when banned or timed out #82 Fix: Message parts were overflowing to an empty next line Fix: Native Kick emotes were not rendering in user info modal #71 Feat: Added timer command to keep track of duration of something #83 Feat: Showed unlockable sub emotes for unsubbed viewers #80 Feat: Added settings option to hide subscriber emotes when not subscribed Major refactor: Moved Dexie & Fuse vendor scripts to ESM imports Refactor: Completely rewrote emotes in message rendering Refactor: Twemoji was moved from content script to ESM import Refactor: Emotes no longer render in-place Chore: Adjusted separator styling ` }, { version: "1.4.20", date: "2024-06-27", description: ` Fix: Incorrect provider emote overrides #72 Fix: Emotes sometimes didn't load for VODs due to load event racing #79 Fix: Backspace when selecting emotes deleted the following emote as well Fix: Forward delete when selecting emotes deleted the following emote as well Fix: Misaligned message usernames ` }, { version: "1.4.19", date: "2024-06-24", description: ` It's now possible possible to enable/disable emote provider emotes for the menu while still allowing them to render in chat. New 7TV emote provider settings were added to enable/disable new 7TV global emotes & 7TV channel emotes. Feat: Support for 7TV global emotes (emote picker/rendering) #72 Fix: Tiny gap in sticky emote set header ` }, { version: "1.4.18", date: "2024-06-20", description: ` Fix: Emote menu always showing sub-only Kick emotes ` }, { version: "1.4.17", date: "2024-06-20", description: ` Fix: Command completion lagged behind one key event on backspace Fix: Follow command did not use the correct username slug Fix: Relative formatted datetimes were output in the local locale instead of English ` }, { version: "1.4.14", date: "2024-06-17", description: ` I messed up, forgot to remove the test code that always gave everyone all badges including global moderator and staff badges.. Whoops. Hotfix: Everyone had all badges ` }, { version: "1.4.13", date: "2024-06-17", description: ` User info modals (when clicking on usernames) are fully replaced with new custom user info modals. The gift sub button is implemented and is fully functional as well, which was a bit hard to hack in but works well. A new chat theme was also added to the settings options. Fix: Added gift a sub button to user info modal #70 Fix: Kept user info modal within viewport on window resize Fix: User info requests failed because Kick has no consistency in username slug format Feat: Re-enabled user info modal to replace default Kick user info modal #60 Feat: Added general tooltips Feat: Added tooltips for all badges Feat: Added new rounded chat theme settings option ` }, { version: "1.4.12", date: "2024-06-16", description: ` After many hours of debugging Kasada I still managed to add in some new features as well for version 1.4.12. I already figured out how to deal with Kasada, however the solution sadly wasn't easily portable from Userscript to extensions. After trying a lot of prototype implementations it now works smoothly, and Kasada won't be a limitation anymore. Copy-pasting now works much more reliably after fixing some clipboard issues. However, Firefox support couldn't be achieved just yet because of weird bugs in the Gecko engine due to standard Web APIs not working in |globalThis| that are fine in Chromium. Feat: Added new commands /follow, /unfollow, /mute, /unmute Feat: Added a new settings option to always send quick emote holder emotes to chat immediately Feat: Ctrl + enter now allows sending messages without clearing input #49 Feat: User info modal now loads the actual channel subscribers badges instead of generic default -Feat-: Replaced Kick user info modal with our new user info modal #60 (postponed until Gift a sub button #70 is implemented) Feat: Implemented the VIP and MOD buttons for user info modal #68 Feat: /raid is now an alias for /host #53 Fix: Kasada anti-bot WAF caused API authorization issues #56 Fix: Space after mention completion did not close the completion #58 Fix: Command /poll cancel button did nothing #47 Fix: Twemoji emojis did not render in pinned messages #46 Fix: Lowered /user command minimum allowed role. Viewers can now see a few commands available. Fix: Error occurred when loading non-existing 7TV emote sets Fix: Moderator commands did not parse sentences correctly Fix: Copy & paste issues when copying from chat Fix: Added toast notifications for command errors Fix: User info modal got pushed out of screen depending on chat message click coordinates #69 ` }, { version: "1.4.11", date: "2024-06-07", description: ` Feat: Added poll command, polls now finally work! Fix: Non-sub emotes did not show for broadcasters ` }, { version: "1.4.6", date: "2024-05-25", description: ` Fix: Tooltips got stuck on screen when searching for emotes Chore: Replaced outdated screenshots and video in readme ` }, { version: "1.4.5", date: "2024-05-24", description: ` Feat: Added mute button to user info modal (actually worked now) Fix: Styling issues with settings modal sidebar were resolved Fix: Background highlight accent color worked again Fix: Settings rows of emotes did not update immediately, no longer required page refresh Fix: Tooltips stopped working after removing jquery technical debt Refactor: Major refactor of dependency injections throughout the entire codebase, now based on session contexts Chore: Removed setting to hide Kick emote menu button ` }, { version: "1.4.4", date: "2024-05-23", description: ` Fix: First user message still used name partially instead of ID ` }, { version: "1.4.3", date: "2024-05-23", description: ` Feat: Added badges to user info modal Feat: Added progressive message history loading to user info modal Feat: Added timeline labeling to user info modal Fix: First user message highlighting now use ID instead of name to prevent triggering on name change ` }, { version: "1.4.2", date: "2024-05-21", description: ` Fix: Changed emote name in Kick emotes for third party compatibility ` }, { version: "1.4.1", date: "2024-05-15", description: ` Fix: Message history no longer worked ` }, { version: "1.4.0", date: "2024-05-14", description: ` The update this time was a bit of a pain, because Kick forced scope creep where I'm progressively just completely replacing the entire website, but fixes were made, and long-standing important features that were put aside for a while were finally implemented. The primary focus of this update was to implement support for native Kick commands for moderators and improve upon the overall UX and usability of the rather lacking native commands UX of Kick. Initially, NipahTV employed shadow proxy elements that forwarded all inputs to the original Kick elements and simulated browser input events to make Kick submit the inputs to the servers. However, this proved impossible to make work with Kick commands, so a network interface for the Kick API was necessary to send messages and commands to the Kick API servers directly, completely bypassing the Kick frontend browser client. However, after doing that, some commands would pop up some window with more information like /user and /poll, which promptly led to the conclusion that all of this user interfacing needed to be implemented as well. At least now we can finally start recommending NipahTV to moderators as well because Kick commands finally work. MODERATOR NOTE: There are still a couple of missing command actions because they are low priority, they'll be added in due time. This regards the /poll command and the VIP & MOD buttons on the /user {{user}} modal. The VIP and MOD commands should work fine, however. Feat: Added Kick network interface, messages, and commands are now sent to Kick API directly. Feat: Added command completion strategy, native Kick commands are now largely supported. Feat: Added kick /user command Feat: Added ban action to user info modal Feat: Added channel user messages history to user info modal Feat: Added account creation date and profile pic to user info modal Feat: 7TV emotes in user info modal message history now rendered Feat: Added follow/unfollow actions to user info modal Feat: Added timeout action to user info modal Feat: Added status page to user info modal Feat: Added toasts for error messaging Feat: Can now reply to messages Feat: Settings menu is now mobile friendly Fix: Emotes not inserting at correct anchored position in some edge cases. Fix: Unable to log in because proxy fields blocked native Kick fields. Fix: Bug in REST requests caused 7TV load failure. Fix: User info modal showed message history in wrong chronological order Fix: Modals mouse drag events no longer working correctly and being positioned wrong Fix: Native send chat button showing double depending on options setting Fix: Typing text next to emote got pushed past the emote Fix: Stealing focus after keydown was too late, preventing input from registering the change Chore: Lubricated my second-hand Nokia 9300i Chore: Implemented abstract navigatable scrolling view component Chore: Removed jQuery technical debt Refactor: Major refactor of SCSS structure Refactor: Decoupled input completer logic to NavigatableEntriesWindowComponent class. Refactor: Decoupled input completer logic to separate strategy classes. ` }, { version: "1.3.9", date: "2024-04-16", description: ` Fix: Regression of 7tv emotes sometimes did not render correctly in pinned messages Fix: Tab completion edge case resulted in double space Fix: Submitting after typing exceptionally fast caused a racing issue with text cutting off ` }, { version: "1.3.6", date: "2024-04-06", description: ` Feat: Added Twemoji emoji rendering Fix: Ctrl+Arrow key caret navigation skipped over entire inline text node instead of word #28 Fix: Changing Kick identity made native emote holder reappear #29 Fix: Rapidly clicking 7tv emotes in chat made them disappear #30 ` }, { version: "1.3.5", date: "2024-03-28", description: ` Feat: Added settings option to enable first user message highlighting only in channels where you are a moderator Fix: Inserted space after mention tab completions ` }, { version: "1.3.4", date: "2024-03-24", description: ` Hotfix: User mentions completions did not update input controller's state ` }, { version: "1.3.1", date: "2024-03-24", description: ` A major update, many things got completely rewritten and/or added. Many annoying caret navigation issues were solved by custom overriding behaviour implementations for input handling, including mouse cursor selections. Previously Ctrl/Ctrl+Shift handling was practically unusable. This now works intuitively and smoothly. The only remaining annoyance, of which I'm not sure how to solve yet, is mouse cursor selections starting on top of emotes and between emotes, because selections work by anchor and focus (start point and drag end point). However, it is not obvious what the start point would be when clicking and dragging to start a selection with emotes. Arrow key selection handling, however, works flawlessly now. Another big change is that after endless issues, particularly due to how Kick handles spaces, I decided to just completely get rid of padding spaces altogether. There's no more need for spaces between emotes or text at all. Feat: Reworked navigation behaviour for default caret navigation Feat: Implemented new behaviour for Ctrl-key caret navigation Feat: Implemented new behaviour for Shift-key selection handling Feat: Implemented new behaviour for Ctrl+Shift-key caret selection navigation Feat: Implemented new behaviour for mouse-based emote selections Fix: Chat messages history no longer worked due to settings ID change Fix: Replacing selections with emotes caused corrupt component leftovers Fix: Updated character count limit on input paste BREAKING CHANGE: Removed all padding spaces from input components Major refactor: Decoupled all tab completion and message history logic away from UI Style: Reworked emote spacing Chore: Input emote character counting is now more efficient through debouncing ` }, { version: "1.2.17", date: "2024-03-22", description: ` Feat: Input field now dynamically showed character count limit feedback Fix: Clipboard copy/paste space padding issues Chore: Improved Delete key handling (WIP) ` }, { version: "1.2.16", date: "2024-03-22", description: ` Feat: Added a new settings option to show quick emote holder ` }, { version: "1.2.15", date: "2024-03-21", description: ` Fix: Alternating message highlighting overrode new user message highlighting Fix: In some cases, text could be written into input components Fix: Backspace behaved weirdly, deleting a lot more than it should Fix: Tab completor sometimes inserted redundant padding space Fix: Settings modal sidebar now highlights the active panel ` }, { version: "1.2.13", date: "2024-03-20", description: ` Feat: Emote rendering now works for VODs Feat: Emotes now get rendered in pinned messages ` }, { version: "1.2.11", date: "2024-03-20", description: ` Fix: Replies attached to messages wouldn't render ` }, { version: "1.2.10", date: "2024-03-20", description: ` Fix: Subscriber emotes of other channels caused NTV to stop rendering emotes Fix: NTV did not load when not logged in on Kick Fix: Selection caret got stuck when selecting in certain situations ` }, { version: "1.2.8", date: "2024-03-19", description: ` Fix: Closing tab completion with Enter or Right arrow key resulted in double spaces ` }, { version: "1.2.7", date: "2024-03-19", description: ` Fix: Closing tab completion with Space resulted in double spaces ` }, { version: "1.2.6", date: "2024-03-19", description: ` Fix: Links were not rendering in chat messages Fix: Special characters were getting HTML escaped Fix: Missing emote names for native Kick emotes in chat Fix: Improved emote spacing ` }, { version: "1.2.1", date: "2024-03-17", description: ` It took me a little while (about 5 complete rewrites) since implementing a nearly full-blown rich content text editor turned out to be quite a bit trickier than thought because of endless weird edge cases, but it's finally wrapped up and shipped. This now allowed me to fix most of the outstanding bugs. There's a couple new big features and improvements. A nearly full blown rich content text editor has been implemented for the input field for better and more reliable emote processing. It now works much better with copy pasting. It is now possible to enable first user message highlighting. It is now possible to change the color of the first user message highlighting. Manually typed emotes are now automatically completed upon closing the name with a space. Native Kick emotes have been optimized; it is now possible to send more native Kick emotes in a message than normally possible without NipahTV. Copy-pasting has been completely reworked and works much better now. Feat: Increased the possible amount of native Kick emotes sent beyond normal #21 Feat: Implemented first new user message highlighting #9 Feat: Rendered manually typed emotes #4 Feat: Implemented first user message highlighting Fix: Native Kick chat identity button was missing from shadow proxy input field #20 Fix: Arrow keys got blocked by message history when caret was right after emotes #17 Fix: Chat input text was not the same height as inline emotes, causing subtle small shifts in vertical caret positioning #16 Fix: Pasting emotes inserted them at the start of the input line instead of after the caret position #15 Fix: Username mentions lacked extra padding after tab completion #14 Fix: Tooltips no longer worked with the new structure Fix: Clicking emotes in chat no longer worked with the new emote structure Fix: Forward deletion with caret inside component did not work correctly Fix: Caret positioning right before emote component did not get pushed out Fix: Incorrect caret positioning during insertions Fix: Selection caret should not go inside input components Fix: Reimplemented clipboard copy & paste Fix: Copy/paste was blocked #13 ` }, { version: "1.1.12", date: "2024-03-03", description: ` Feat: Added a settings option to change quick emote holder rows Fix: Prevented Kick from stealing focus from chat input Chore: Changed order of Emoji emoteset to lower ` }, { version: "1.1.10", date: "2024-03-03", description: ` Fix: Emotes registered double in quick emote holder because of ID conflicts. Refactor: Major refactor of all provider-specific emote IDs to name hashes for better future provider compatibility and emote reliability. Chore: Changed database engine from LocalStorage to NoSQL IndexedDB. ` }, { version: "1.1.8", date: "2024-03-03", description: ` Feat: Added tooltips for emotes in chat Feat: Added clicking emotes to insert them into chat input Feat: Added guards against potential memory leaks Chore: Improved global visual clarity ` }, { version: "1.1.6", date: "2024-03-03", description: ` Feat: Added clipboard handling, can now copy & paste emotes Feat: Added @ user mention capability for tab-completion Feat: Added max message character limit of 500 ` }, { version: "1.1.4", date: "2024-03-03", description: ` Fix: Loading pages without chat input caused the interface to duplicate ` }, { version: "1.1.3", date: "2024-03-02", description: ` Fix: Tab completion was not doing initial scroll Fix: Tab completion Shift & Tab key did not work correctly because of @ mentions ` }, { version: "1.1.2", date: "2024-03-02", description: ` Fixed some issues, particularly regarding the tab completion and chat history behaviour and the new Kick update. The input fields and chat button were completely replaced with new proxy fields. This was done to work around Kick messing with the input field and buttons. As a result, inline emote rendering was also implemented, so picking emotes now actually shows the emotes in the input field. Typing emotes manually, however, will not automatically turn them into inline emotes just yet. @ username mentions are not implemented yet either. The emote tab completion was completely reworked with more contextually meaningful searching by splitting up all emotes into semantical groupings (e.g. nebrideLove -> ["nebride", "love"]), so that tab completions and searching for emotes can better find the most relevant emotes. A few new settings options were added, like enabling search bias for current/other channel emotes, menu button styles, etc. A good number of issues and behaviour quirks were fixed, like having to press keys twice to navigate chat history, tab completion menu navigation and scrolling, etc. ` } ]; // src/Core/Settings/Modals/SettingsModal.ts var logger7 = new Logger(); var { log: log6, info: info5, error: error7 } = logger7.destruct(); var SettingsModal = class extends AbstractModal { constructor(rootEventBus, settingsOpts) { const geometry = { position: "center" }; super("settings", geometry); this.rootEventBus = rootEventBus; this.settingsOpts = settingsOpts; } panelsEl; sidebarEl; sidebarBtnEl; init() { super.init(); return this; } render() { super.render(); log6("CORE", "SETTINGS", "Rendered settings modal.."); const uiSettings = this.settingsOpts.uiSettings; const settingsMap = this.settingsOpts.settingsMap; const modalBodyEl = this.modalBodyEl; const windowWidth = window.innerWidth; const hasUpdateAvailable = settingsMap.get("global.shared.app.update_available"); const modalHeaderEl = this.modalHeaderBodyEl; if (hasUpdateAvailable) { modalHeaderEl.prepend( parseHTML( `<span class="ntv__modal__version">v${NTV_APP_VERSION}</span><span>|</span><span class="ntv__modal__update-available">Update available</span><button class="ntv__modal__update-btn ntv__button">Update now</button>` ) ); } else { modalHeaderEl.prepend(parseHTML(`<span class="ntv__modal__version">v${NTV_APP_VERSION}</span>`)); } modalHeaderEl.prepend( parseHTML( `<svg class="ntv__modal__logo" alt="NipahTV" width="32" height="32" viewBox="0 0 16 16" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"><path style="opacity:1;fill:#ff3e64;fill-opacity:1;fill-rule:nonzero;stroke-width:0.023423" d="M 0.0045448,15.994522 0.00653212,7.0100931 C 0.00268662,6.4657282 0.56985346,5.2666906 1.6344369,4.714763 2.2713742,4.3845464 2.837577,4.2734929 3.8954568,4.2515817 4.9164087,4.2304326 5.4269836,4.4338648 5.61042,4.5236104 6.1140039,4.7499277 6.6375815,5.2244865 6.7816227,5.4288944 l 4.5552313,6.6637346 c 0.270458,0.343536 0.601629,0.565661 0.949354,0.529787 0.293913,-0.02339 0.449771,-0.268141 0.45106,-0.491161 l 0.0031,-9.2329087 -12.73699293,6.478e-4 4.453e-5,-2.8976667 15.9946434,2.145e-4 -1.11e-4,13.0844041 c -0.0057,0.149032 -0.09913,1.190976 -1.067016,2.026055 -0.866438,0.807769 -2.342194,0.87905 -2.342194,0.87905 -0.620946,0.0088 -1.091187,0.02361 -1.441943,-0.04695 -0.581657,-0.148437 -0.833883,-0.27401 -1.315459,-0.607888 -0.4222398,-0.311805 -0.7037358,-0.678791 -0.9410734,-0.987827 0,0 -1.1204546,-1.801726 -1.931821,-2.982711 C 6.3363366,10.413177 5.2823578,8.9426183 4.7672147,8.2745095 4.375011,7.7288473 4.102008,7.6101039 3.9558506,7.5598495 3.7999133,7.5101456 3.6283732,7.55479 3.4962961,7.6097361 3.3794655,7.6629412 3.2800176,7.7609522 3.2623448,7.8936439 l 7.879e-4,8.1018591 z"/></svg>` ) ); this.panelsEl = parseHTML(`<div class="ntv__settings-modal__panels"></div>`, true); this.sidebarEl = parseHTML( `<div class="ntv__settings-modal__sidebar ${windowWidth < 768 ? "" : "ntv__settings-modal__sidebar--open"}"><ul></ul></div>`, true ); this.sidebarBtnEl = parseHTML( `<div class="ntv__settings-modal__mobile-btn-wrapper"><button><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"> <path fill="currentColor" d="M2.753 18h18.5a.75.75 0 0 1 .101 1.493l-.101.007h-18.5a.75.75 0 0 1-.102-1.494zh18.5zm0-6.497h18.5a.75.75 0 0 1 .101 1.493l-.101.007h-18.5a.75.75 0 0 1-.102-1.494zh18.5zm-.001-6.5h18.5a.75.75 0 0 1 .102 1.493l-.102.007h-18.5A.75.75 0 0 1 2.65 5.01zh18.5z" /> </svg> Menu</button></div>`, true ); const sidebarList = this.sidebarEl.querySelector("ul"); for (const category of uiSettings) { const categoryEl = parseHTML( cleanupHTML(` <li class="ntv__settings-modal__category"> <span>${category.label}</span> <ul></ul> </li> `), true ); const categoryListEl = categoryEl.querySelector("ul"); sidebarList.appendChild(categoryEl); for (const subCategory of category.children) { const categoryId = `${category.label.toLowerCase()}.${subCategory.label.toLowerCase()}`; const subCategoryEl = parseHTML( cleanupHTML( `<li data-panel="${categoryId}" class="ntv__settings-modal__sub-category"> <span>${subCategory.label}</span> </li>` ), true ); categoryListEl.appendChild(subCategoryEl); } } for (const category of uiSettings) { for (const subCategory of category.children) { const categoryId = `${category.label.toLowerCase()}.${subCategory.label.toLowerCase()}`; const subCategoryPanelEl = parseHTML( `<div data-panel="${categoryId}" class="ntv__settings-modal__panel" style="display: none"></div>`, true ); if (subCategory.label === "Changelog") { const changelogListEl = parseHTML("<ul></ul>", true); subCategoryPanelEl.appendChild(changelogListEl); for (const change of CHANGELOG) { const changeEl = parseHTML( cleanupHTML( `<li> <h3>${change.version} <span>(${change.date})</span></h3> </li>` ), true ); const descriptionEl = document.createElement("p"); descriptionEl.textContent = change.description.trim().replace(/^[^\S\r\n]+/gm, ""); changeEl.appendChild(descriptionEl); changelogListEl.appendChild(changeEl); } } for (const group of subCategory.children) { const groupEl = parseHTML( cleanupHTML( `<div class="ntv__settings-modal__group"> <div class="ntv__settings-modal__group-header"> <h3>${group.label}</h3> ${group.description ? `<p>${group.description}</p>` : ""} </div> </div>` ), true ); subCategoryPanelEl.append(groupEl); for (const setting of group.children) { const settingId = `global.shared.${setting.key}`; let settingComponent; let settingValue = settingsMap.get(settingId); if (void 0 === settingValue) settingValue = setting.default || null; switch (setting.type) { case "checkbox": settingComponent = new CheckboxComponent(settingId, setting.label, settingValue); break; case "color": settingComponent = new ColorComponent(settingId, setting.label, settingValue); break; case "dropdown": settingComponent = new DropdownComponent( settingId, setting.label, setting.options, settingValue ); break; case "stepped_slider": settingComponent = new SteppedInputSliderLabeledComponent( setting.labels, setting.steps, setting.label, settingValue ); break; case "number": settingComponent = new NumberComponent( settingId, setting.label, settingValue, setting.min, setting.max, setting.step ); break; default: error7("CORE", "SETTINGS", `No component found for setting,`, setting); continue; } settingComponent?.init(); groupEl.append(settingComponent.element); settingComponent.addEventListener("change", () => { const value = settingComponent.getValue(); this.eventTarget.dispatchEvent( new CustomEvent("setting_change", { detail: { id: settingId, platformId: "global", channelId: "shared", key: setting.key, value } }) ); }); } } this.panelsEl.appendChild(subCategoryPanelEl); } } const defaultPanel = "nipahtv.changelog"; this.panelsEl.querySelector(`[data-panel="${defaultPanel}"]`)?.setAttribute("style", "display: block"); this.sidebarEl.querySelector(`[data-panel="${defaultPanel}"]`)?.classList.add("ntv__settings-modal__sub-category--active"); modalBodyEl.appendChild(this.sidebarBtnEl); modalBodyEl.appendChild(this.sidebarEl); modalBodyEl.appendChild(this.panelsEl); } getSettingElement(setting) { } attachEventHandlers() { super.attachEventHandlers(); if (!this.panelsEl || !this.sidebarEl) { error7("CORE", "SETTINGS", "SettingsModal: panelsEl or sidebarEl not found"); return; } this.sidebarEl.querySelectorAll(".ntv__settings-modal__sub-category").forEach((el1) => { el1.addEventListener("click", (evt) => { const panelId = el1.dataset.panel; this.sidebarEl.querySelectorAll(".ntv__settings-modal__sub-category").forEach((el2) => { el2.classList.remove("ntv__settings-modal__sub-category--active"); }); el1.classList.add("ntv__settings-modal__sub-category--active"); this.panelsEl.querySelectorAll(".ntv__settings-modal__panel").forEach((el2) => { ; el2.style.display = "none"; }); this.panelsEl.querySelector(`[data-panel="${panelId}"]`)?.setAttribute("style", "display: block"); this.modalBodyEl.scrollTop = 0; }); }); this.sidebarBtnEl.addEventListener("click", () => { this.sidebarEl.classList.toggle("ntv__settings-modal__sidebar--open"); }); this.modalHeaderBodyEl.querySelector(".ntv__modal__update-btn")?.addEventListener("click", () => { this.rootEventBus.publish("ntv.app.update"); }); } }; // src/Core/Settings/SettingsManager.ts var logger8 = new Logger(); var { log: log7, info: info6, error: error8 } = logger8.destruct(); var SettingsManager = class { uiSettings = [ { label: "NipahTV", children: [ { label: "Changelog", children: [] } ] }, { label: "Appearance", children: [ { label: "Layout", children: [ { label: "Layout", children: [ { label: "Chat position alignment", key: "chat.position", default: "center", type: "dropdown", options: [ { label: "Default", value: "none" }, { label: "Left", value: "left" }, { label: "Right", value: "right" } ] } ] }, { label: "Theatre mode", children: [ { label: "Alignment position of the stream video in theatre mode (only has effect if video player is smaller than screen)", key: "appearance.layout.overlay_chat.video_alignment", default: "center", type: "dropdown", options: [ { label: "Centered", value: "center" }, { label: "Aligned to left of screen", value: "aligned_left" }, { label: "Aligned to right of screen", value: "aligned_right" } ] } ] }, { label: "Translucent overlay chat", children: [ { label: "Overlay the chat transparently on top of the stream when in theatre mode (EXPERIMENTAL)", key: "appearance.layout.overlay_chat", default: "none", type: "dropdown", options: [ { label: "Disabled", value: "none" }, { label: "Very translucent", value: "very_translucent" }, { label: "Semi translucent", value: "semi_translucent" }, { label: "Dark translucent", value: "dark_translucent" } ] }, { label: "Overlay chat position in theatre mode", key: "appearance.layout.overlay_chat.position", default: "right", type: "dropdown", options: [ { label: "Right", value: "right" }, { label: "Left", value: "left" } ] } ] } ] } ] }, { label: "Chat", children: [ // { // label: 'Appearance', // children: [ // { // label: 'Appearance', // children: [] // }, // { // label: 'General', // description: 'These settings require a page refresh to take effect.', // children: [ // ] // } // ] // }, { label: "Badges", children: [ { label: "Badges", children: [ { label: "Show the NipahTV badge for NTV users", key: "chat.badges.show_ntv_badge", default: true, type: "checkbox" } ] } ] }, { label: "Emotes", children: [ { label: "General", children: [ { label: "Show emotes in chat", key: "chat.emote_providers.kick.show_emotes", default: true, type: "checkbox" } ] }, { label: "Emote appearance", children: [ { label: "Emote size", key: "chat.messages.emotes.size", default: "28px", type: "stepped_slider", labels: ["Small", "Default", "Large"], steps: ["24px", "28px", "32px"] }, { label: "Message emote overlap", key: "chat.messages.emotes.overlap", default: 3, type: "stepped_slider", labels: ["No overlap", "Slight overlap", "Moderate overlap", "Default"], steps: [0, 1, 2, 3] } ] }, { label: "In chat appearance", description: "These settings require a page refresh to take effect.", children: [ { label: "Hide subscriber emotes for channels you are not subscribed to. They will still show when other users send them", key: "chat.emotes.hide_subscriber_emotes", default: false, type: "checkbox" }, { label: "Display images in tooltips", key: "chat.tooltips.images", default: true, type: "checkbox" } ] } ] }, { label: "Emote Menu", children: [ { label: "Appearance", description: "These settings require a page refresh to take effect.", children: [ { label: "Choose the style of the emote menu button", key: "chat.emote_menu.appearance.button_style", default: "nipah", type: "dropdown", options: [ { label: "Nipah", value: "nipah" }, { label: "NipahTV", value: "nipahtv" }, { label: "Logo", value: "logo" }, { label: "NTV", value: "ntv" }, { label: "NTV 3D", value: "ntv_3d" }, { label: "NTV 3D RGB", value: "ntv_3d_rgb" }, { label: "NTV 3D Shadow", value: "ntv_3d_shadow" }, { label: "NTV 3D Shadow (beveled)", value: "ntv_3d_shadow_beveled" } ] }, // Dangerous, impossible to undo because settings button will be hidden // { // label: 'Show the navigation sidebar on the side of the menu', // key: 'chat.emote_menu.sidebar', // default: true, // type: 'checkbox' // }, { label: "Show the search box", key: "chat.emote_menu.search_box", default: true, type: "checkbox" }, { label: "Show favorited emotes in the emote menu", key: "emote_menu.show_favorites", default: true, type: "checkbox" }, { label: "Show favorited emotes of other channels that cannot be used, because they're not cross-channel emotes", key: "emote_menu.show_unavailable_favorites", default: false, type: "checkbox" } ] }, { label: "Behavior", children: [ { label: "Close the emote menu after clicking an emote", key: "chat.emote_menu.close_on_click", default: false, type: "checkbox" } ] }, { label: "Hotkeys", description: "These settings require a page refresh to take effect.", children: [ { label: "Use Ctrl+E to open the Emote Menu", key: "chat.emote_menu.open_ctrl_e", default: false, type: "checkbox" }, { label: "Use Ctrl+Spacebar to open the Emote Menu", key: "chat.emote_menu.open_ctrl_spacebar", default: true, type: "checkbox" } ] } ] }, { label: "Input Field", children: [ { label: "Behavior", children: [ { label: "Steal focus to chat input when typing without having chat input focused", key: "chat.input.steal_focus", default: false, type: "checkbox" } ] }, { label: "Recent messages", children: [ { label: "Enable navigation of chat history by pressing up/down arrow keys to recall previously sent chat messages", key: "chat.input.history.enabled", default: true, type: "checkbox" } ] }, { label: "Input completion", description: "These settings require a page refresh to take effect.", children: [ // { // label: 'Display a tooltip when using tab-completion', // key: 'chat.input.completion.tooltip', // default: true, // type: 'checkbox' // }, { label: "Enable <b></></b> key command completion suggestions", key: "chat.input.completion.commands.enabled", default: true, type: "checkbox" }, { label: "Enable <b><TAB></b> key emote completion suggestions", key: "chat.input.completion.emotes.enabled", default: true, type: "checkbox" }, { label: "Enable <b><COLON (:)></b> key emote completion suggestions", key: "chat.input.completion.colon_emotes.enabled", default: true, type: "checkbox" }, { label: "Enable <b><@></b> key username mention completion suggestions", key: "chat.input.completion.mentions.enabled", default: true, type: "checkbox" } ] } ] }, { label: "Messages", children: [ { label: "General", children: [ { label: "Enable chat message rendering", key: "chat.behavior.enable_chat_rendering", default: true, type: "checkbox" }, { label: "Enable chat smooth scrolling (currently broken after Kick update!)", key: "chat.behavior.smooth_scrolling", default: false, type: "checkbox" }, { label: "Show chat message timestamps", key: "chat.messages.show_timestamps", default: false, type: "checkbox" } ] }, { label: "Appearance", children: [ { label: "Messages font size", key: "chat.messages.font_size", default: "13px", type: "stepped_slider", labels: ["Small", "Default", "Large", "Extra Large"], steps: ["12px", "13px", "14px", "15px"] }, { label: "Messages spacing", key: "chat.messages.spacing", default: "0", type: "stepped_slider", labels: ["No spacing", "Little spacing", "Moderate spacing", "Large spacing"], steps: ["0", "0.128em", "0.256em", "0.384em"] }, { label: "Seperators", key: "chat.messages.seperators", default: "none", type: "dropdown", options: [ { label: "Disabled", value: "none" }, { label: "Basic Line (1px Solid)", value: "basic" }, { label: "3D Line (2px Groove)", value: "3d" }, { label: "3D Line (2x Groove Inset)", value: "3d-inset" }, { label: "Wide Line (2px Solid)", value: "wide" } ] }, { label: "Messages style", key: "chat.messages.style", default: "none", type: "dropdown", options: [ { label: "Default", value: "none" }, { label: "Slightly rounded", value: "rounded" }, { label: "Fully rounded", value: "rounded-2" } ] } ] }, { label: "Highlighting", children: [ { label: "Highlight first time user messages (of this chat session, not first time ever)", key: "chat.messages.highlight_first_time", default: false, type: "checkbox" }, { label: "Highlight first user messages only for channels where you are a moderator", key: "chat.messages.highlight_first_moderator", default: false, type: "checkbox" }, { label: "Highlight Color", key: "chat.messages.highlight_color", default: "#4f95ff", type: "color" }, { label: "Display lines with alternating background colors", key: "chat.messages.alternating_background", default: false, type: "checkbox" } ] }, { label: "Events", children: [ { label: "Show emote added/removed event messages in chat", key: "chat.messages.emote_updates.enabled", default: true, type: "checkbox" } ] } ] }, { label: "Quick Emote Holder", children: [ { label: "Appearance", children: [ { label: "Show quick emote holder", key: "quick_emote_holder.enabled", type: "checkbox", default: true }, { label: "Rows of emotes to display", key: "quick_emote_holder.rows", type: "number", default: 2, min: 1, max: 10 }, { label: "Show favorited emotes in the quick emote holder", key: "quick_emote_holder.show_favorites", type: "checkbox", default: true }, { label: "Show recently used emotes in the quick emote holder", key: "quick_emote_holder.show_recently_used", type: "checkbox", default: true }, { label: "Show favorited emotes of other channels that cannot be used (because they're not cross-channel emotes)", key: "quick_emote_holder.show_non_cross_channel_favorites", type: "checkbox", default: false } ] }, { label: "Behavior", children: [ { label: "Send emotes to chat immediately on click", key: "chat.quick_emote_holder.send_immediately", type: "checkbox", default: false } ] } ] }, { label: "Searching", children: [ { label: "Search behavior", description: "These settings require a page refresh to take effect.", children: [ { label: "Add bias to emotes of channels you are subscribed to when searching", key: "chat.behavior.search_bias_subscribed_channels", default: true, type: "checkbox" }, { label: "Add extra bias to emotes of the current channel you are watching the stream of when searching", key: "chat.behavior.search_bias_current_channels", default: true, type: "checkbox" } ] } ] } ] }, { label: "Kick", children: [ { label: "Emote Menu", children: [ { label: "Emote Sets", description: "These settings require a page refresh to take effect.", children: [ { label: "Show global emote set in emote menu", key: "emote_menu.emote_providers.kick.show_global", default: true, type: "checkbox" }, { label: "Show current channel emote set in emote menu", key: "emote_menu.emote_providers.kick.show_current_channel", default: true, type: "checkbox" }, { label: "Show other channel emote sets in emote menu", key: "emote_menu.emote_providers.kick.show_other_channels", default: true, type: "checkbox" }, { label: "Show Emoji emote set in emote menu", key: "emote_menu.emote_providers.kick.show_emojis", default: false, type: "checkbox" } ] } ] } ] }, { label: "Add-ons", children: [ { label: "7TV", children: [ { label: "General", description: "These settings require a page refresh to take effect.", children: [ { label: "Enable 7TV add-on", key: "ext.7tv.enabled", default: true, type: "checkbox" } ] }, { label: "Appearance", children: [ { label: "Enable username paint cosmetics in chat", key: "ext.7tv.cosmetics.paints.enabled", default: true, type: "checkbox" }, { label: "Enable shadows for username paint cosmetics in chat (potentially high performance impact)", key: "ext.7tv.cosmetics.paints.shadows.enabled", default: true, type: "checkbox" }, { label: "Enable user supporter badges in chat", key: "ext.7tv.cosmetics.badges.enabled", default: true, type: "checkbox" } ] }, { label: "Emote sets", description: "These settings require a page refresh to take effect.", children: [ { label: "Show emotes in chat", key: "chat.emote_providers.7tv.show_emotes", default: true, type: "checkbox" }, { label: "Show global emote set in emote menu", key: "emote_menu.emote_providers.7tv.show_global", default: true, type: "checkbox" }, { label: "Show current channel emote set in emote menu", key: "emote_menu.emote_providers.7tv.show_current_channel", default: true, type: "checkbox" } ] } ] }, { label: "Botrix", children: [ { label: "General", children: [] } ] } ] }, { label: "Moderators", children: [ { label: "Behavior", children: [ { label: "General", description: "These settings require a page refresh to take effect.", children: [ { label: "Completely disable NipahTV for moderator and creator dashboard pages. <br><br>WARNING: This will COMPLETELY disable NipahTV for these pages! (You can undo this setting by accessing the settings again on any frontend stream page)<br><br>USERSCRIPT USERS WARNING: Once enabled sadly this setting CANNOT be undone anymore without manually deleting the setting from database, due to Kick recently changing the domain to dashboard.kick.com. This is simply a limitation of Userscripts, I recommend using the browser extension instead of an Userscript if you want to use this setting. Join the discord if you need help undoing this setting for Userscript users.", key: "moderators.mod_creator_view.disable_ntv", default: false, type: "checkbox" } ] } ] }, { label: "Chat", children: [ { label: "Messages", children: [ { label: "Show quick actions (delete, timeout, ban)", key: "moderators.chat.show_quick_actions", default: true, type: "checkbox" } ] } ] } ] } ]; settingsMap = /* @__PURE__ */ new Map([ ["global.shared.app.version", null], ["global.shared.app.update_available", null], ["global.shared.app.announcements", {}] ]); isShowingModal = false; database; rootEventBus; modal; isLoaded = false; constructor({ database, rootEventBus }) { this.database = database; this.rootEventBus = rootEventBus; } initialize() { const { rootEventBus: eventBus } = this; for (const category of this.uiSettings) { for (const subCategory of category.children) { for (const group of subCategory.children) { for (const setting of group.children) { this.settingsMap.set(`global.shared.${setting.key}`, setting.default); } } } } eventBus.subscribe("ntv.ui.settings.toggle_show", this.handleShowModal.bind(this)); } async loadSettings() { if (this.isLoaded) return true; const { database, rootEventBus: eventBus } = this; const settingsRecords = await database.settings.getRecords(); for (const setting of settingsRecords) { const { id, value } = setting; this.settingsMap.set(id, value); } const messageEmoteOverlapSetting = this.settingsMap.get("global.shared.chat.messages.emotes.overlap"); if (typeof messageEmoteOverlapSetting === "string") { let messageEmoteOverlapValue = 0; if (messageEmoteOverlapSetting === "0") { messageEmoteOverlapValue = 0; } else if (messageEmoteOverlapSetting === "-0.2em") { messageEmoteOverlapValue = 1; } else if (messageEmoteOverlapSetting === "-0.3em") { messageEmoteOverlapValue = 2; } else if (messageEmoteOverlapSetting === "-0.4em") { messageEmoteOverlapValue = 3; } this.setSetting("global", "shared", "chat.messages.emotes.overlap", messageEmoteOverlapValue); } const quickEmoteHolderRowsSetting = this.settingsMap.get("global.shared.quick_emote_holder.rows"); if (typeof quickEmoteHolderRowsSetting === "string") { const rows = parseInt(quickEmoteHolderRowsSetting, 10); this.setSetting("global", "shared", "quick_emote_holder.rows", isNaN(rows) ? 2 : rows); } this.isLoaded = true; eventBus.publish("ntv.settings.loaded"); } registerSetting(platformId, channelId, key, defaultVal) { const id = `${platformId}.${channelId}.${key}`; if (this.settingsMap.has(id)) return error8("CORE", "SETTINGS", "Setting already registered:", id); this.settingsMap.set(id, defaultVal); } setSetting(platformId, channelId, key, value) { if (!platformId || !channelId || !key || void 0 === value) return error8("CORE", "SETTINGS", "Unable to set setting, invalid parameters:", { platformId, channelId, key, value }); const id = `${platformId}.${channelId}.${key}`; if (!this.settingsMap.has(id)) return error8("CORE", "SETTINGS", "Setting not registered:", id); this.database.settings.putRecord({ id, platformId, channelId, key, value }).catch((err) => error8("CORE", "SETTINGS", "Failed to save setting to database.", err.message)); this.settingsMap.set(id, value); } setGlobalSetting(key, value) { const id = `global.shared.${key}`; if (!this.settingsMap.has(id)) return error8("CORE", "SETTINGS", "Setting not registered:", id); this.database.settings.putRecord({ id, platformId: "global", channelId: "shared", key, value }).catch((err) => error8("CORE", "SETTINGS", "Failed to save global setting to database.", err.message)); this.settingsMap.set(id, value); } /** * Setting resolution order: * {{platformId}}.{{channelId}}.key * {{platformId}}.shared.key * global.{{channelId}}.key * global.shared.key */ getSetting(channelId, key, bubbleChannel = true, bubblePlatform = true) { const platformId = NTV_PLATFORM; const id = `${platformId}.${channelId}.${key}`; if (this.settingsMap.has(id)) return this.settingsMap.get(id); else if (bubbleChannel && channelId !== "shared" && this.settingsMap.has(`${platformId}.shared.${key}`)) return this.settingsMap.get(`${platformId}.shared.${key}`); else if (bubblePlatform && this.settingsMap.has(`global.${channelId}.${key}`)) return this.settingsMap.get(`global.${channelId}.${key}`); else if (bubblePlatform && bubbleChannel && this.settingsMap.has(`global.shared.${key}`)) return this.settingsMap.get(`global.shared.${key}`); return error8("CORE", "SETTINGS", "Setting not registered:", id); } getGlobalSetting(key) { if (this.settingsMap.has(`global.shared.${key}`)) return this.settingsMap.get(`global.shared.${key}`); return error8("CORE", "SETTINGS", "Setting not registered:", key); } async getSettingFromDatabase(key) { return this.database.settings.getRecord(key).then((res) => res !== void 0 ? res.value : this.settingsMap.get(key)); } // getSettingsForPlatform(platformId: SettingDocument['platformId']) {} // getSettingsForChannel(platformId: SettingDocument['platformId'], channelId: SettingDocument['channelId']) {} handleShowModal(evt) { this.showModal(!this.isShowingModal); } showModal(bool = true) { if (!this.isLoaded) { return error8( "CORE", "SETTINGS", "Unable to show settings modal because the settings are not loaded yet, please wait for it to load first." ); } if (!bool) { this.isShowingModal = false; if (this.modal) { this.modal.destroy(); delete this.modal; } } else { this.isShowingModal = true; if (this.modal) return; this.modal = new SettingsModal(this.rootEventBus, { uiSettings: this.uiSettings, settingsMap: this.settingsMap }); this.modal.init(); this.modal.addEventListener("close", () => { this.isShowingModal = false; delete this.modal; }); this.modal.addEventListener("setting_change", (evt) => { const settingDocument = evt.detail; const { platformId, channelId, key, value } = settingDocument; const prevValue = this.getSetting(channelId, key); this.setSetting(platformId, channelId, key, value); this.rootEventBus.publish("ntv.settings.change." + key, { value, prevValue }); }); } } }; // src/Sites/Kick/KickEventService.ts var import_pusher_js = __toESM(require_pusher()); var logger9 = new Logger(); var { log: log8, info: info7, error: error9 } = logger9.destruct(); var KickEventService = class { pusher; chatroomChannelsMap = /* @__PURE__ */ new Map(); constructor() { this.pusher = new import_pusher_js.default("32cbd69e4b950bf97679", { cluster: "us2", auth: { headers: {} }, authEndpoint: "/broadcasting/auth", userAuthentication: { endpoint: "/broadcasting/user-auth", transport: "ajax", headers: {} }, forceTLS: true }); } connect(channelData) { } subToChatroomEvents(channelData) { const { chatroom } = channelData; if (!chatroom) return error9("KICK", "EVENTS", "Chatroom data is missing from channelData"); const channelId = chatroom.id; this.chatroomChannelsMap.set(channelId, this.pusher.subscribe(`chatrooms.${channelId}.v2`)); } addEventListener(channelData, event, callback) { const { chatroom } = channelData; if (!chatroom) return error9("KICK", "EVENTS", "Chatroom data is missing from channelData"); const channel = this.chatroomChannelsMap.get(chatroom.id); if (!channel) return error9("KICK", "EVENTS", "Unable to find channel for EventService chatroom", chatroom.id); if (event === "MESSAGE") { channel.bind("App\\Events\\ChatMessageEvent", (data) => { let res; if (data.type === "message") { res = { id: "" + data.id, content: data.content, type: "MESSAGE", createdAt: data.created_at, sender: { id: "" + data.sender.id, username: data.sender.username, slug: "" + data.sender.slug } }; } else if (data.type === "reply") { res = { id: "" + data.id, content: data.content, type: "REPLY", createdAt: data.created_at, sender: { id: "" + data.sender.id, username: data.sender.username, slug: "" + data.sender.slug }, replyTo: { id: "" + data.metadata.original_message.id, content: data.metadata.original_message.content, userId: "" + data.metadata.original_sender.id, username: data.metadata.original_sender.username } }; } else { throw new Error(`Unknown message type: ${data.type}`); } callback(res); }); } else if (event === "CHATROOM_UPDATED") { channel.bind("App\\Events\\ChatroomUpdatedEvent", (data) => { callback({ id: "" + data.id, emotesMode: { enabled: !!data.emotes_mode.enabled }, subscribersMode: { enabled: !!data.subscribers_mode.enabled }, followersMode: { enabled: !!data.followers_mode.enabled, min_duration: data.followers_mode.min_duration || 0 }, slowMode: { enabled: !!data.slow_mode.enabled, messageInterval: data.slow_mode.message_interval || 6 } }); }); } else if (event === "USER_BANNED") { channel.bind("App\\Events\\UserBannedEvent", (data) => { callback({ id: "" + data.id, user: { id: "" + data.user.id, username: data.user.username || "", slug: data.user.slug || "" }, bannedBy: { id: "" + data.banned_by.id, username: data.banned_by.username || "", slug: data.banned_by.slug || "" }, permanent: !!data.permanent, duration: data.duration || 1, expiresAt: data.expires_at || "" }); }); } else if (event === "USER_UNBANNED") { channel.bind("App\\Events\\UserUnbannedEvent", (data) => { callback({ id: "" + data.id, user: { id: "" + data.user.id, username: data.user.username || "", slug: data.user.slug || "" }, unbannedBy: { id: "" + data.unbanned_by.id, username: data.unbanned_by.username || "", slug: data.unbanned_by.slug || "" }, permanent: !!data.permanent }); }); } } disconnect(channelData) { const { chatroom } = channelData; if (!chatroom) return error9("KICK", "EVENTS", "Chatroom data is missing from channelData"); const channel = this.chatroomChannelsMap.get(chatroom.id); if (channel) { this.pusher.unsubscribe(`chatrooms.${chatroom.id}.v2`); channel.unbind_all(); this.chatroomChannelsMap.delete(chatroom.id); } } disconnectAll() { for (const channel of this.chatroomChannelsMap.values()) { channel.unbind_all(); } this.pusher.disconnect(); } }; // src/Core/Emotes/AbstractEmoteProvider.ts var AbstractEmoteProvider = class { constructor(settingsManager) { this.settingsManager = settingsManager; } id = 0 /* NULL */; name = "NULL"; status = "unloaded" /* UNLOADED */; }; // src/Core/Emotes/EmotesManager.ts var import_parser = __toESM(require_dist()); // node_modules/fuse.js/dist/fuse.mjs function isArray(value) { return !Array.isArray ? getTag(value) === "[object Array]" : Array.isArray(value); } var INFINITY = 1 / 0; function baseToString(value) { if (typeof value == "string") { return value; } let result = value + ""; return result == "0" && 1 / value == -INFINITY ? "-0" : result; } function toString(value) { return value == null ? "" : baseToString(value); } function isString(value) { return typeof value === "string"; } function isNumber(value) { return typeof value === "number"; } function isBoolean(value) { return value === true || value === false || isObjectLike(value) && getTag(value) == "[object Boolean]"; } function isObject(value) { return typeof value === "object"; } function isObjectLike(value) { return isObject(value) && value !== null; } function isDefined(value) { return value !== void 0 && value !== null; } function isBlank(value) { return !value.trim().length; } function getTag(value) { return value == null ? value === void 0 ? "[object Undefined]" : "[object Null]" : Object.prototype.toString.call(value); } var INCORRECT_INDEX_TYPE = "Incorrect 'index' type"; var LOGICAL_SEARCH_INVALID_QUERY_FOR_KEY = (key) => `Invalid value for key ${key}`; var PATTERN_LENGTH_TOO_LARGE = (max) => `Pattern length exceeds max of ${max}.`; var MISSING_KEY_PROPERTY = (name) => `Missing ${name} property in key`; var INVALID_KEY_WEIGHT_VALUE = (key) => `Property 'weight' in key '${key}' must be a positive integer`; var hasOwn = Object.prototype.hasOwnProperty; var KeyStore = class { constructor(keys) { this._keys = []; this._keyMap = {}; let totalWeight = 0; keys.forEach((key) => { let obj = createKey(key); this._keys.push(obj); this._keyMap[obj.id] = obj; totalWeight += obj.weight; }); this._keys.forEach((key) => { key.weight /= totalWeight; }); } get(keyId) { return this._keyMap[keyId]; } keys() { return this._keys; } toJSON() { return JSON.stringify(this._keys); } }; function createKey(key) { let path = null; let id = null; let src = null; let weight = 1; let getFn = null; if (isString(key) || isArray(key)) { src = key; path = createKeyPath(key); id = createKeyId(key); } else { if (!hasOwn.call(key, "name")) { throw new Error(MISSING_KEY_PROPERTY("name")); } const name = key.name; src = name; if (hasOwn.call(key, "weight")) { weight = key.weight; if (weight <= 0) { throw new Error(INVALID_KEY_WEIGHT_VALUE(name)); } } path = createKeyPath(name); id = createKeyId(name); getFn = key.getFn; } return { path, id, weight, src, getFn }; } function createKeyPath(key) { return isArray(key) ? key : key.split("."); } function createKeyId(key) { return isArray(key) ? key.join(".") : key; } function get(obj, path) { let list = []; let arr = false; const deepGet = (obj2, path2, index) => { if (!isDefined(obj2)) { return; } if (!path2[index]) { list.push(obj2); } else { let key = path2[index]; const value = obj2[key]; if (!isDefined(value)) { return; } if (index === path2.length - 1 && (isString(value) || isNumber(value) || isBoolean(value))) { list.push(toString(value)); } else if (isArray(value)) { arr = true; for (let i = 0, len = value.length; i < len; i += 1) { deepGet(value[i], path2, index + 1); } } else if (path2.length) { deepGet(value, path2, index + 1); } } }; deepGet(obj, isString(path) ? path.split(".") : path, 0); return arr ? list : list[0]; } var MatchOptions = { // Whether the matches should be included in the result set. When `true`, each record in the result // set will include the indices of the matched characters. // These can consequently be used for highlighting purposes. includeMatches: false, // When `true`, the matching function will continue to the end of a search pattern even if // a perfect match has already been located in the string. findAllMatches: false, // Minimum number of characters that must be matched before a result is considered a match minMatchCharLength: 1 }; var BasicOptions = { // When `true`, the algorithm continues searching to the end of the input even if a perfect // match is found before the end of the same input. isCaseSensitive: false, // When true, the matching function will continue to the end of a search pattern even if includeScore: false, // List of properties that will be searched. This also supports nested properties. keys: [], // Whether to sort the result list, by score shouldSort: true, // Default sort function: sort by ascending score, ascending index sortFn: (a, b) => a.score === b.score ? a.idx < b.idx ? -1 : 1 : a.score < b.score ? -1 : 1 }; var FuzzyOptions = { // Approximately where in the text is the pattern expected to be found? location: 0, // At what point does the match algorithm give up. A threshold of '0.0' requires a perfect match // (of both letters and location), a threshold of '1.0' would match anything. threshold: 0.6, // Determines how close the match must be to the fuzzy location (specified above). // An exact letter match which is 'distance' characters away from the fuzzy location // would score as a complete mismatch. A distance of '0' requires the match be at // the exact location specified, a threshold of '1000' would require a perfect match // to be within 800 characters of the fuzzy location to be found using a 0.8 threshold. distance: 100 }; var AdvancedOptions = { // When `true`, it enables the use of unix-like search commands useExtendedSearch: false, // The get function to use when fetching an object's properties. // The default will search nested paths *ie foo.bar.baz* getFn: get, // When `true`, search will ignore `location` and `distance`, so it won't matter // where in the string the pattern appears. // More info: https://fusejs.io/concepts/scoring-theory.html#fuzziness-score ignoreLocation: false, // When `true`, the calculation for the relevance score (used for sorting) will // ignore the field-length norm. // More info: https://fusejs.io/concepts/scoring-theory.html#field-length-norm ignoreFieldNorm: false, // The weight to determine how much field length norm effects scoring. fieldNormWeight: 1 }; var Config = { ...BasicOptions, ...MatchOptions, ...FuzzyOptions, ...AdvancedOptions }; var SPACE = /[^ ]+/g; function norm(weight = 1, mantissa = 3) { const cache = /* @__PURE__ */ new Map(); const m = Math.pow(10, mantissa); return { get(value) { const numTokens = value.match(SPACE).length; if (cache.has(numTokens)) { return cache.get(numTokens); } const norm2 = 1 / Math.pow(numTokens, 0.5 * weight); const n = parseFloat(Math.round(norm2 * m) / m); cache.set(numTokens, n); return n; }, clear() { cache.clear(); } }; } var FuseIndex = class { constructor({ getFn = Config.getFn, fieldNormWeight = Config.fieldNormWeight } = {}) { this.norm = norm(fieldNormWeight, 3); this.getFn = getFn; this.isCreated = false; this.setIndexRecords(); } setSources(docs = []) { this.docs = docs; } setIndexRecords(records = []) { this.records = records; } setKeys(keys = []) { this.keys = keys; this._keysMap = {}; keys.forEach((key, idx) => { this._keysMap[key.id] = idx; }); } create() { if (this.isCreated || !this.docs.length) { return; } this.isCreated = true; if (isString(this.docs[0])) { this.docs.forEach((doc, docIndex) => { this._addString(doc, docIndex); }); } else { this.docs.forEach((doc, docIndex) => { this._addObject(doc, docIndex); }); } this.norm.clear(); } // Adds a doc to the end of the index add(doc) { const idx = this.size(); if (isString(doc)) { this._addString(doc, idx); } else { this._addObject(doc, idx); } } // Removes the doc at the specified index of the index removeAt(idx) { this.records.splice(idx, 1); for (let i = idx, len = this.size(); i < len; i += 1) { this.records[i].i -= 1; } } getValueForItemAtKeyId(item, keyId) { return item[this._keysMap[keyId]]; } size() { return this.records.length; } _addString(doc, docIndex) { if (!isDefined(doc) || isBlank(doc)) { return; } let record = { v: doc, i: docIndex, n: this.norm.get(doc) }; this.records.push(record); } _addObject(doc, docIndex) { let record = { i: docIndex, $: {} }; this.keys.forEach((key, keyIndex) => { let value = key.getFn ? key.getFn(doc) : this.getFn(doc, key.path); if (!isDefined(value)) { return; } if (isArray(value)) { let subRecords = []; const stack = [{ nestedArrIndex: -1, value }]; while (stack.length) { const { nestedArrIndex, value: value2 } = stack.pop(); if (!isDefined(value2)) { continue; } if (isString(value2) && !isBlank(value2)) { let subRecord = { v: value2, i: nestedArrIndex, n: this.norm.get(value2) }; subRecords.push(subRecord); } else if (isArray(value2)) { value2.forEach((item, k) => { stack.push({ nestedArrIndex: k, value: item }); }); } else ; } record.$[keyIndex] = subRecords; } else if (isString(value) && !isBlank(value)) { let subRecord = { v: value, n: this.norm.get(value) }; record.$[keyIndex] = subRecord; } }); this.records.push(record); } toJSON() { return { keys: this.keys, records: this.records }; } }; function createIndex(keys, docs, { getFn = Config.getFn, fieldNormWeight = Config.fieldNormWeight } = {}) { const myIndex = new FuseIndex({ getFn, fieldNormWeight }); myIndex.setKeys(keys.map(createKey)); myIndex.setSources(docs); myIndex.create(); return myIndex; } function parseIndex(data, { getFn = Config.getFn, fieldNormWeight = Config.fieldNormWeight } = {}) { const { keys, records } = data; const myIndex = new FuseIndex({ getFn, fieldNormWeight }); myIndex.setKeys(keys); myIndex.setIndexRecords(records); return myIndex; } function computeScore$1(pattern, { errors = 0, currentLocation = 0, expectedLocation = 0, distance = Config.distance, ignoreLocation = Config.ignoreLocation } = {}) { const accuracy = errors / pattern.length; if (ignoreLocation) { return accuracy; } const proximity = Math.abs(expectedLocation - currentLocation); if (!distance) { return proximity ? 1 : accuracy; } return accuracy + proximity / distance; } function convertMaskToIndices(matchmask = [], minMatchCharLength = Config.minMatchCharLength) { let indices = []; let start = -1; let end = -1; let i = 0; for (let len = matchmask.length; i < len; i += 1) { let match = matchmask[i]; if (match && start === -1) { start = i; } else if (!match && start !== -1) { end = i - 1; if (end - start + 1 >= minMatchCharLength) { indices.push([start, end]); } start = -1; } } if (matchmask[i - 1] && i - start >= minMatchCharLength) { indices.push([start, i - 1]); } return indices; } var MAX_BITS = 32; function search(text, pattern, patternAlphabet, { location: location2 = Config.location, distance = Config.distance, threshold = Config.threshold, findAllMatches = Config.findAllMatches, minMatchCharLength = Config.minMatchCharLength, includeMatches = Config.includeMatches, ignoreLocation = Config.ignoreLocation } = {}) { if (pattern.length > MAX_BITS) { throw new Error(PATTERN_LENGTH_TOO_LARGE(MAX_BITS)); } const patternLen = pattern.length; const textLen = text.length; const expectedLocation = Math.max(0, Math.min(location2, textLen)); let currentThreshold = threshold; let bestLocation = expectedLocation; const computeMatches = minMatchCharLength > 1 || includeMatches; const matchMask = computeMatches ? Array(textLen) : []; let index; while ((index = text.indexOf(pattern, bestLocation)) > -1) { let score = computeScore$1(pattern, { currentLocation: index, expectedLocation, distance, ignoreLocation }); currentThreshold = Math.min(score, currentThreshold); bestLocation = index + patternLen; if (computeMatches) { let i = 0; while (i < patternLen) { matchMask[index + i] = 1; i += 1; } } } bestLocation = -1; let lastBitArr = []; let finalScore = 1; let binMax = patternLen + textLen; const mask = 1 << patternLen - 1; for (let i = 0; i < patternLen; i += 1) { let binMin = 0; let binMid = binMax; while (binMin < binMid) { const score2 = computeScore$1(pattern, { errors: i, currentLocation: expectedLocation + binMid, expectedLocation, distance, ignoreLocation }); if (score2 <= currentThreshold) { binMin = binMid; } else { binMax = binMid; } binMid = Math.floor((binMax - binMin) / 2 + binMin); } binMax = binMid; let start = Math.max(1, expectedLocation - binMid + 1); let finish = findAllMatches ? textLen : Math.min(expectedLocation + binMid, textLen) + patternLen; let bitArr = Array(finish + 2); bitArr[finish + 1] = (1 << i) - 1; for (let j = finish; j >= start; j -= 1) { let currentLocation = j - 1; let charMatch = patternAlphabet[text.charAt(currentLocation)]; if (computeMatches) { matchMask[currentLocation] = +!!charMatch; } bitArr[j] = (bitArr[j + 1] << 1 | 1) & charMatch; if (i) { bitArr[j] |= (lastBitArr[j + 1] | lastBitArr[j]) << 1 | 1 | lastBitArr[j + 1]; } if (bitArr[j] & mask) { finalScore = computeScore$1(pattern, { errors: i, currentLocation, expectedLocation, distance, ignoreLocation }); if (finalScore <= currentThreshold) { currentThreshold = finalScore; bestLocation = currentLocation; if (bestLocation <= expectedLocation) { break; } start = Math.max(1, 2 * expectedLocation - bestLocation); } } } const score = computeScore$1(pattern, { errors: i + 1, currentLocation: expectedLocation, expectedLocation, distance, ignoreLocation }); if (score > currentThreshold) { break; } lastBitArr = bitArr; } const result = { isMatch: bestLocation >= 0, // Count exact matches (those with a score of 0) to be "almost" exact score: Math.max(1e-3, finalScore) }; if (computeMatches) { const indices = convertMaskToIndices(matchMask, minMatchCharLength); if (!indices.length) { result.isMatch = false; } else if (includeMatches) { result.indices = indices; } } return result; } function createPatternAlphabet(pattern) { let mask = {}; for (let i = 0, len = pattern.length; i < len; i += 1) { const char = pattern.charAt(i); mask[char] = (mask[char] || 0) | 1 << len - i - 1; } return mask; } var BitapSearch = class { constructor(pattern, { location: location2 = Config.location, threshold = Config.threshold, distance = Config.distance, includeMatches = Config.includeMatches, findAllMatches = Config.findAllMatches, minMatchCharLength = Config.minMatchCharLength, isCaseSensitive = Config.isCaseSensitive, ignoreLocation = Config.ignoreLocation } = {}) { this.options = { location: location2, threshold, distance, includeMatches, findAllMatches, minMatchCharLength, isCaseSensitive, ignoreLocation }; this.pattern = isCaseSensitive ? pattern : pattern.toLowerCase(); this.chunks = []; if (!this.pattern.length) { return; } const addChunk = (pattern2, startIndex) => { this.chunks.push({ pattern: pattern2, alphabet: createPatternAlphabet(pattern2), startIndex }); }; const len = this.pattern.length; if (len > MAX_BITS) { let i = 0; const remainder = len % MAX_BITS; const end = len - remainder; while (i < end) { addChunk(this.pattern.substr(i, MAX_BITS), i); i += MAX_BITS; } if (remainder) { const startIndex = len - MAX_BITS; addChunk(this.pattern.substr(startIndex), startIndex); } } else { addChunk(this.pattern, 0); } } searchIn(text) { const { isCaseSensitive, includeMatches } = this.options; if (!isCaseSensitive) { text = text.toLowerCase(); } if (this.pattern === text) { let result2 = { isMatch: true, score: 0 }; if (includeMatches) { result2.indices = [[0, text.length - 1]]; } return result2; } const { location: location2, distance, threshold, findAllMatches, minMatchCharLength, ignoreLocation } = this.options; let allIndices = []; let totalScore = 0; let hasMatches = false; this.chunks.forEach(({ pattern, alphabet, startIndex }) => { const { isMatch, score, indices } = search(text, pattern, alphabet, { location: location2 + startIndex, distance, threshold, findAllMatches, minMatchCharLength, includeMatches, ignoreLocation }); if (isMatch) { hasMatches = true; } totalScore += score; if (isMatch && indices) { allIndices = [...allIndices, ...indices]; } }); let result = { isMatch: hasMatches, score: hasMatches ? totalScore / this.chunks.length : 1 }; if (hasMatches && includeMatches) { result.indices = allIndices; } return result; } }; var BaseMatch = class { constructor(pattern) { this.pattern = pattern; } static isMultiMatch(pattern) { return getMatch(pattern, this.multiRegex); } static isSingleMatch(pattern) { return getMatch(pattern, this.singleRegex); } search() { } }; function getMatch(pattern, exp) { const matches = pattern.match(exp); return matches ? matches[1] : null; } var ExactMatch = class extends BaseMatch { constructor(pattern) { super(pattern); } static get type() { return "exact"; } static get multiRegex() { return /^="(.*)"$/; } static get singleRegex() { return /^=(.*)$/; } search(text) { const isMatch = text === this.pattern; return { isMatch, score: isMatch ? 0 : 1, indices: [0, this.pattern.length - 1] }; } }; var InverseExactMatch = class extends BaseMatch { constructor(pattern) { super(pattern); } static get type() { return "inverse-exact"; } static get multiRegex() { return /^!"(.*)"$/; } static get singleRegex() { return /^!(.*)$/; } search(text) { const index = text.indexOf(this.pattern); const isMatch = index === -1; return { isMatch, score: isMatch ? 0 : 1, indices: [0, text.length - 1] }; } }; var PrefixExactMatch = class extends BaseMatch { constructor(pattern) { super(pattern); } static get type() { return "prefix-exact"; } static get multiRegex() { return /^\^"(.*)"$/; } static get singleRegex() { return /^\^(.*)$/; } search(text) { const isMatch = text.startsWith(this.pattern); return { isMatch, score: isMatch ? 0 : 1, indices: [0, this.pattern.length - 1] }; } }; var InversePrefixExactMatch = class extends BaseMatch { constructor(pattern) { super(pattern); } static get type() { return "inverse-prefix-exact"; } static get multiRegex() { return /^!\^"(.*)"$/; } static get singleRegex() { return /^!\^(.*)$/; } search(text) { const isMatch = !text.startsWith(this.pattern); return { isMatch, score: isMatch ? 0 : 1, indices: [0, text.length - 1] }; } }; var SuffixExactMatch = class extends BaseMatch { constructor(pattern) { super(pattern); } static get type() { return "suffix-exact"; } static get multiRegex() { return /^"(.*)"\$$/; } static get singleRegex() { return /^(.*)\$$/; } search(text) { const isMatch = text.endsWith(this.pattern); return { isMatch, score: isMatch ? 0 : 1, indices: [text.length - this.pattern.length, text.length - 1] }; } }; var InverseSuffixExactMatch = class extends BaseMatch { constructor(pattern) { super(pattern); } static get type() { return "inverse-suffix-exact"; } static get multiRegex() { return /^!"(.*)"\$$/; } static get singleRegex() { return /^!(.*)\$$/; } search(text) { const isMatch = !text.endsWith(this.pattern); return { isMatch, score: isMatch ? 0 : 1, indices: [0, text.length - 1] }; } }; var FuzzyMatch = class extends BaseMatch { constructor(pattern, { location: location2 = Config.location, threshold = Config.threshold, distance = Config.distance, includeMatches = Config.includeMatches, findAllMatches = Config.findAllMatches, minMatchCharLength = Config.minMatchCharLength, isCaseSensitive = Config.isCaseSensitive, ignoreLocation = Config.ignoreLocation } = {}) { super(pattern); this._bitapSearch = new BitapSearch(pattern, { location: location2, threshold, distance, includeMatches, findAllMatches, minMatchCharLength, isCaseSensitive, ignoreLocation }); } static get type() { return "fuzzy"; } static get multiRegex() { return /^"(.*)"$/; } static get singleRegex() { return /^(.*)$/; } search(text) { return this._bitapSearch.searchIn(text); } }; var IncludeMatch = class extends BaseMatch { constructor(pattern) { super(pattern); } static get type() { return "include"; } static get multiRegex() { return /^'"(.*)"$/; } static get singleRegex() { return /^'(.*)$/; } search(text) { let location2 = 0; let index; const indices = []; const patternLen = this.pattern.length; while ((index = text.indexOf(this.pattern, location2)) > -1) { location2 = index + patternLen; indices.push([index, location2 - 1]); } const isMatch = !!indices.length; return { isMatch, score: isMatch ? 0 : 1, indices }; } }; var searchers = [ ExactMatch, IncludeMatch, PrefixExactMatch, InversePrefixExactMatch, InverseSuffixExactMatch, SuffixExactMatch, InverseExactMatch, FuzzyMatch ]; var searchersLen = searchers.length; var SPACE_RE = / +(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)/; var OR_TOKEN = "|"; function parseQuery(pattern, options = {}) { return pattern.split(OR_TOKEN).map((item) => { let query = item.trim().split(SPACE_RE).filter((item2) => item2 && !!item2.trim()); let results = []; for (let i = 0, len = query.length; i < len; i += 1) { const queryItem = query[i]; let found = false; let idx = -1; while (!found && ++idx < searchersLen) { const searcher = searchers[idx]; let token = searcher.isMultiMatch(queryItem); if (token) { results.push(new searcher(token, options)); found = true; } } if (found) { continue; } idx = -1; while (++idx < searchersLen) { const searcher = searchers[idx]; let token = searcher.isSingleMatch(queryItem); if (token) { results.push(new searcher(token, options)); break; } } } return results; }); } var MultiMatchSet = /* @__PURE__ */ new Set([FuzzyMatch.type, IncludeMatch.type]); var ExtendedSearch = class { constructor(pattern, { isCaseSensitive = Config.isCaseSensitive, includeMatches = Config.includeMatches, minMatchCharLength = Config.minMatchCharLength, ignoreLocation = Config.ignoreLocation, findAllMatches = Config.findAllMatches, location: location2 = Config.location, threshold = Config.threshold, distance = Config.distance } = {}) { this.query = null; this.options = { isCaseSensitive, includeMatches, minMatchCharLength, findAllMatches, ignoreLocation, location: location2, threshold, distance }; this.pattern = isCaseSensitive ? pattern : pattern.toLowerCase(); this.query = parseQuery(this.pattern, this.options); } static condition(_, options) { return options.useExtendedSearch; } searchIn(text) { const query = this.query; if (!query) { return { isMatch: false, score: 1 }; } const { includeMatches, isCaseSensitive } = this.options; text = isCaseSensitive ? text : text.toLowerCase(); let numMatches = 0; let allIndices = []; let totalScore = 0; for (let i = 0, qLen = query.length; i < qLen; i += 1) { const searchers2 = query[i]; allIndices.length = 0; numMatches = 0; for (let j = 0, pLen = searchers2.length; j < pLen; j += 1) { const searcher = searchers2[j]; const { isMatch, indices, score } = searcher.search(text); if (isMatch) { numMatches += 1; totalScore += score; if (includeMatches) { const type = searcher.constructor.type; if (MultiMatchSet.has(type)) { allIndices = [...allIndices, ...indices]; } else { allIndices.push(indices); } } } else { totalScore = 0; numMatches = 0; allIndices.length = 0; break; } } if (numMatches) { let result = { isMatch: true, score: totalScore / numMatches }; if (includeMatches) { result.indices = allIndices; } return result; } } return { isMatch: false, score: 1 }; } }; var registeredSearchers = []; function register(...args) { registeredSearchers.push(...args); } function createSearcher(pattern, options) { for (let i = 0, len = registeredSearchers.length; i < len; i += 1) { let searcherClass = registeredSearchers[i]; if (searcherClass.condition(pattern, options)) { return new searcherClass(pattern, options); } } return new BitapSearch(pattern, options); } var LogicalOperator = { AND: "$and", OR: "$or" }; var KeyType = { PATH: "$path", PATTERN: "$val" }; var isExpression = (query) => !!(query[LogicalOperator.AND] || query[LogicalOperator.OR]); var isPath = (query) => !!query[KeyType.PATH]; var isLeaf = (query) => !isArray(query) && isObject(query) && !isExpression(query); var convertToExplicit = (query) => ({ [LogicalOperator.AND]: Object.keys(query).map((key) => ({ [key]: query[key] })) }); function parse(query, options, { auto = true } = {}) { const next = (query2) => { let keys = Object.keys(query2); const isQueryPath = isPath(query2); if (!isQueryPath && keys.length > 1 && !isExpression(query2)) { return next(convertToExplicit(query2)); } if (isLeaf(query2)) { const key = isQueryPath ? query2[KeyType.PATH] : keys[0]; const pattern = isQueryPath ? query2[KeyType.PATTERN] : query2[key]; if (!isString(pattern)) { throw new Error(LOGICAL_SEARCH_INVALID_QUERY_FOR_KEY(key)); } const obj = { keyId: createKeyId(key), pattern }; if (auto) { obj.searcher = createSearcher(pattern, options); } return obj; } let node = { children: [], operator: keys[0] }; keys.forEach((key) => { const value = query2[key]; if (isArray(value)) { value.forEach((item) => { node.children.push(next(item)); }); } }); return node; }; if (!isExpression(query)) { query = convertToExplicit(query); } return next(query); } function computeScore(results, { ignoreFieldNorm = Config.ignoreFieldNorm }) { results.forEach((result) => { let totalScore = 1; result.matches.forEach(({ key, norm: norm2, score }) => { const weight = key ? key.weight : null; totalScore *= Math.pow( score === 0 && weight ? Number.EPSILON : score, (weight || 1) * (ignoreFieldNorm ? 1 : norm2) ); }); result.score = totalScore; }); } function transformMatches(result, data) { const matches = result.matches; data.matches = []; if (!isDefined(matches)) { return; } matches.forEach((match) => { if (!isDefined(match.indices) || !match.indices.length) { return; } const { indices, value } = match; let obj = { indices, value }; if (match.key) { obj.key = match.key.src; } if (match.idx > -1) { obj.refIndex = match.idx; } data.matches.push(obj); }); } function transformScore(result, data) { data.score = result.score; } function format(results, docs, { includeMatches = Config.includeMatches, includeScore = Config.includeScore } = {}) { const transformers = []; if (includeMatches) transformers.push(transformMatches); if (includeScore) transformers.push(transformScore); return results.map((result) => { const { idx } = result; const data = { item: docs[idx], refIndex: idx }; if (transformers.length) { transformers.forEach((transformer) => { transformer(result, data); }); } return data; }); } var Fuse = class { constructor(docs, options = {}, index) { this.options = { ...Config, ...options }; if (this.options.useExtendedSearch && false) { throw new Error(EXTENDED_SEARCH_UNAVAILABLE); } this._keyStore = new KeyStore(this.options.keys); this.setCollection(docs, index); } setCollection(docs, index) { this._docs = docs; if (index && !(index instanceof FuseIndex)) { throw new Error(INCORRECT_INDEX_TYPE); } this._myIndex = index || createIndex(this.options.keys, this._docs, { getFn: this.options.getFn, fieldNormWeight: this.options.fieldNormWeight }); } add(doc) { if (!isDefined(doc)) { return; } this._docs.push(doc); this._myIndex.add(doc); } remove(predicate = () => false) { const results = []; for (let i = 0, len = this._docs.length; i < len; i += 1) { const doc = this._docs[i]; if (predicate(doc, i)) { this.removeAt(i); i -= 1; len -= 1; results.push(doc); } } return results; } removeAt(idx) { this._docs.splice(idx, 1); this._myIndex.removeAt(idx); } getIndex() { return this._myIndex; } search(query, { limit = -1 } = {}) { const { includeMatches, includeScore, shouldSort, sortFn, ignoreFieldNorm } = this.options; let results = isString(query) ? isString(this._docs[0]) ? this._searchStringList(query) : this._searchObjectList(query) : this._searchLogical(query); computeScore(results, { ignoreFieldNorm }); if (shouldSort) { results.sort(sortFn); } if (isNumber(limit) && limit > -1) { results = results.slice(0, limit); } return format(results, this._docs, { includeMatches, includeScore }); } _searchStringList(query) { const searcher = createSearcher(query, this.options); const { records } = this._myIndex; const results = []; records.forEach(({ v: text, i: idx, n: norm2 }) => { if (!isDefined(text)) { return; } const { isMatch, score, indices } = searcher.searchIn(text); if (isMatch) { results.push({ item: text, idx, matches: [{ score, value: text, norm: norm2, indices }] }); } }); return results; } _searchLogical(query) { const expression = parse(query, this.options); const evaluate = (node, item, idx) => { if (!node.children) { const { keyId, searcher } = node; const matches = this._findMatches({ key: this._keyStore.get(keyId), value: this._myIndex.getValueForItemAtKeyId(item, keyId), searcher }); if (matches && matches.length) { return [ { idx, item, matches } ]; } return []; } const res = []; for (let i = 0, len = node.children.length; i < len; i += 1) { const child = node.children[i]; const result = evaluate(child, item, idx); if (result.length) { res.push(...result); } else if (node.operator === LogicalOperator.AND) { return []; } } return res; }; const records = this._myIndex.records; const resultMap = {}; const results = []; records.forEach(({ $: item, i: idx }) => { if (isDefined(item)) { let expResults = evaluate(expression, item, idx); if (expResults.length) { if (!resultMap[idx]) { resultMap[idx] = { idx, item, matches: [] }; results.push(resultMap[idx]); } expResults.forEach(({ matches }) => { resultMap[idx].matches.push(...matches); }); } } }); return results; } _searchObjectList(query) { const searcher = createSearcher(query, this.options); const { keys, records } = this._myIndex; const results = []; records.forEach(({ $: item, i: idx }) => { if (!isDefined(item)) { return; } let matches = []; keys.forEach((key, keyIndex) => { matches.push( ...this._findMatches({ key, value: item[keyIndex], searcher }) ); }); if (matches.length) { results.push({ idx, item, matches }); } }); return results; } _findMatches({ key, value, searcher }) { if (!isDefined(value)) { return []; } let matches = []; if (isArray(value)) { value.forEach(({ v: text, i: idx, n: norm2 }) => { if (!isDefined(text)) { return; } const { isMatch, score, indices } = searcher.searchIn(text); if (isMatch) { matches.push({ score, key, value: text, idx, norm: norm2, indices }); } }); } else { const { v: text, n: norm2 } = value; const { isMatch, score, indices } = searcher.searchIn(text); if (isMatch) { matches.push({ score, key, value: text, norm: norm2, indices }); } } return matches; } }; Fuse.version = "7.0.0"; Fuse.createIndex = createIndex; Fuse.parseIndex = parseIndex; Fuse.config = Config; { Fuse.parseQuery = parse; } { register(ExtendedSearch); } // src/Core/Emotes/EmoteDatastore.ts var logger10 = new Logger(); var { log: log9, logNow, info: info8, error: error10 } = logger10.destruct(); var EmoteDatastore = class { emoteMap = /* @__PURE__ */ new Map(); emoteIdMap = /* @__PURE__ */ new Map(); emoteNameMap = /* @__PURE__ */ new Map(); emoteEmoteSetMap = /* @__PURE__ */ new Map(); emoteSetMap = /* @__PURE__ */ new Map(); favoriteEmotesDocumentsMap = /* @__PURE__ */ new Map(); emoteSets = []; emoteUsage = /* @__PURE__ */ new Map(); favoriteEmoteDocuments = []; // Map of pending emote usage changes to be synced to database hasPendingChanges = false; pendingEmoteUsageChanges = {}; pendingFavoriteEmoteChanges = {}; fuse = new Fuse([], { includeScore: true, shouldSort: false, // includeMatches: true, // isCaseSensitive: true, findAllMatches: true, threshold: 0.35, keys: [["name"], ["parts"]] }); rootContext; session; channelId; providerOverrideOrder = [2 /* SEVENTV */, 1 /* KICK */]; constructor(rootContext, session) { this.rootContext = rootContext; this.session = session; this.channelId = session.channelData.channelId; setInterval(() => { this.storeDatabase(); }, 3 * 1e3); session.eventBus.subscribe("ntv.session.destroy", () => { this.emoteNameMap.clear(); this.emoteUsage.clear(); this.emoteMap.clear(); }); } async loadDatabase() { info8("CORE", "EMOT:STORE", "Reading out emotes data from database.."); const { database } = this.rootContext; const { eventBus } = this.session; database.emoteUsages.getRecords(NTV_PLATFORM, this.channelId).then((usageRecords) => { if (usageRecords.length) { for (const record of usageRecords) { this.emoteUsage.set(record.emoteHid, record.count); } } }).then(() => eventBus.publish("ntv.datastore.emotes.usage.loaded")).catch((err) => error10("Failed to load emote usage data from database.", err.message)); database.favoriteEmotes.getRecords(NTV_PLATFORM).then((favoriteEmotes) => { if (!favoriteEmotes.length) return; for (const favoriteEmote of favoriteEmotes) { this.favoriteEmotesDocumentsMap.set(favoriteEmote.emoteHid, favoriteEmote); this.favoriteEmoteDocuments.push(favoriteEmote); } this.favoriteEmoteDocuments.sort((a, b) => a.orderIndex - b.orderIndex); log9("CORE", "EMOT:STORE", `Loaded ${favoriteEmotes.length} favorite emotes from database`); }).then(() => eventBus.publish("ntv.datastore.emotes.favorites.loaded")).catch((err) => error10("Failed to load favorite emote data from database.", err.message)); } storeDatabase() { if (!this.hasPendingChanges) return; const { database } = this.rootContext; const platformId = NTV_PLATFORM; const pendingEmoteUsageChanges = structuredClone(this.pendingEmoteUsageChanges); this.pendingEmoteUsageChanges = {}; const pendingFavoriteEmoteChanges = structuredClone(this.pendingFavoriteEmoteChanges); this.pendingFavoriteEmoteChanges = {}; const emoteUsagePuts = []; const emoteUsageDeletes = []; if (!isEmpty(pendingEmoteUsageChanges)) info8( "CORE", "EMOT:STORE", `Syncing ${Object.keys(pendingEmoteUsageChanges).length} emote usage changes to database..` ); for (const emoteHid in pendingEmoteUsageChanges) { const action = pendingEmoteUsageChanges[emoteHid]; if (action === "changed") { const emoteUsages = this.emoteUsage.get(emoteHid) || 0; emoteUsagePuts.push({ platformId, channelId: this.channelId, emoteHid, count: emoteUsages }); } else if (action === "removed") { emoteUsageDeletes.push([platformId, this.channelId, emoteHid]); } } const favoriteEmotePuts = []; const favoriteEmoteReorders = []; const favoriteEmoteDeletes = []; if (!isEmpty(pendingFavoriteEmoteChanges)) info8( "CORE", "EMOT:STORE", `Syncing ${Object.keys(pendingFavoriteEmoteChanges).length} favorite emote changes to database..` ); for (const emoteHid in pendingFavoriteEmoteChanges) { const action = pendingFavoriteEmoteChanges[emoteHid]; if (action === "added") { const favoriteEmote = this.favoriteEmotesDocumentsMap.get(emoteHid); if (!favoriteEmote) { error10("Unable to add favorite emote to database, emote not found", emoteHid); continue; } favoriteEmotePuts.push(favoriteEmote); } else if (action === "reordered") { const favoriteEmote = this.favoriteEmotesDocumentsMap.get(emoteHid); if (!favoriteEmote) { error10("Unable to reorder favorite emote to database, emote not found", emoteHid); continue; } favoriteEmoteReorders.push({ platformId, emoteHid, orderIndex: favoriteEmote.orderIndex }); } else if (action === "removed") { favoriteEmoteDeletes.push({ platformId, emoteHid }); } else { error10("Unknown favorite emote database action", action); } } if (emoteUsagePuts.length) database.emoteUsages.bulkPutRecords(emoteUsagePuts); if (emoteUsageDeletes.length) database.emoteUsages.bulkDeleteRecords(emoteUsageDeletes); if (favoriteEmotePuts.length) database.favoriteEmotes.bulkPutRecords(favoriteEmotePuts); if (favoriteEmoteReorders.length) database.favoriteEmotes.bulkOrderRecords(favoriteEmoteReorders); if (favoriteEmoteDeletes.length) database.favoriteEmotes.bulkDeleteRecordsByHid(favoriteEmoteDeletes); log9("CORE", "EMOT:STORE", "Synced emote data changes to database"); this.hasPendingChanges = false; } registerEmoteSet(emoteSet) { if (this.emoteSetMap.has(emoteSet.id)) { return; } this.emoteSetMap.set(emoteSet.id, emoteSet); this.emoteSets.push(emoteSet); let updatedEmoteSets = []; for (let i = emoteSet.emotes.length - 1; i >= 0; i--) { const emote = emoteSet.emotes[i]; const hasUpdatedEmoteSet = this.registerEmote(emote, emoteSet); if (hasUpdatedEmoteSet && !updatedEmoteSets.includes(emoteSet)) { updatedEmoteSets.push(emoteSet); } } this.session.eventBus.publish("ntv.datastore.emoteset.added", emoteSet); if (updatedEmoteSets.length > 0) { for (const updatedEmoteSet of updatedEmoteSets) { this.session.eventBus.publish("ntv.datastore.emoteset.updated", updatedEmoteSet); } } } registerEmote(emote, emoteSet) { if (!emote.hid || !emote.id || typeof emote.id !== "string" || !emote.name || void 0 === emote.provider) { return error10("CORE", "EMOT:STORE", "Invalid emote data", emote); } let hasUpdatedEmoteSet = false; this.emoteIdMap.set(emote.id, emote); const storedEmote = this.emoteNameMap.get(emote.name); const storedEmoteSet = this.emoteEmoteSetMap.get(emote.hid); if (storedEmote && storedEmoteSet) { const isHigherProviderOrder = this.providerOverrideOrder.indexOf(emoteSet.provider) > this.providerOverrideOrder.indexOf(storedEmote.provider); if (isHigherProviderOrder && storedEmoteSet.isGlobalSet || isHigherProviderOrder && storedEmoteSet.isEmoji || isHigherProviderOrder && emoteSet.isCurrentChannel && storedEmoteSet.isCurrentChannel || isHigherProviderOrder && (emoteSet.isCurrentChannel || emoteSet.isOtherChannel) && storedEmoteSet.isOtherChannel || !isHigherProviderOrder && emoteSet.isCurrentChannel && !storedEmoteSet.isCurrentChannel || !isHigherProviderOrder && emoteSet.isOtherChannel && storedEmoteSet.isGlobalSet) { log9( "CORE", "EMOT:STORE", `Registered ${emote.provider === 1 /* KICK */ ? "Kick" : "7TV"} ${emoteSet.isGlobalSet ? "global" : "channel"} emote ${emote.name} override for loaded ${storedEmote.provider === 1 /* KICK */ ? "Kick" : "7TV"} ${storedEmoteSet.isGlobalSet ? "global" : "channel"} emote` ); storedEmoteSet.emotes.splice(storedEmoteSet.emotes.indexOf(storedEmote), 1); this.fuse.remove((indexedEmote) => indexedEmote.name === emote.name); if (!hasUpdatedEmoteSet) hasUpdatedEmoteSet = true; this.emoteMap.set(emote.hid, emote); this.emoteNameMap.set(emote.name, emote); this.emoteEmoteSetMap.set(emote.hid, emoteSet); this.fuse.add(emote); } else { emoteSet.emotes.splice(emoteSet.emotes.indexOf(emote), 1); log9("CORE", "EMOT:STORE", "Skipped overridden emote", emote.name); } } else { this.emoteMap.set(emote.hid, emote); this.emoteNameMap.set(emote.name, emote); this.emoteEmoteSetMap.set(emote.hid, emoteSet); this.fuse.add(emote); } return hasUpdatedEmoteSet; } addEmoteToEmoteSetById(emote, emoteSetId) { if (this.emoteIdMap.get(emote.id)) return; const emoteSet = this.emoteSetMap.get(emoteSetId); if (!emoteSet) { return error10("CORE", "EMOT:STORE", "Unable to add emote to emote set, emote set not found", emoteSetId); } if (emoteSet.emotes.find((e) => e.id === emote.id)) { return error10( "CORE", "EMOT:STORE", "Unable to add emote to emote set, emote already exists in emote set", emote.name ); } emoteSet.emotes.push(emote); this.registerEmote(emote, emoteSet); this.session.eventBus.publish("ntv.datastore.emoteset.updated", emoteSet); } removeEmoteFromEmoteSetById(emoteId, emoteSetId) { const emote = this.emoteIdMap.get(emoteId); if (!emote) return; const emoteSet = this.emoteSetMap.get(emoteSetId); if (emoteSet) { const index = emoteSet.emotes.findIndex((e) => e.id === emoteId); if (index !== -1) { emoteSet.emotes.splice(index, 1); } else { error10( "CORE", "EMOT:STORE", "Unable to remove emote from emote set, emote not found in emote set", emote.name ); } } else error10("CORE", "EMOT:STORE", "Unable to remove emote from emote set, emote set not found", emoteSetId); this.emoteMap.delete(emote.hid); this.emoteIdMap.delete(emoteId); this.emoteNameMap.delete(emote.name); this.emoteEmoteSetMap.delete(emote.hid); this.fuse.remove((indexedEmote) => indexedEmote.name === emote.name); this.session.eventBus.publish("ntv.datastore.emoteset.updated", emoteSet); return emote; } getEmote(emoteHid) { return this.emoteMap.get(emoteHid); } getEmoteByName(emoteName) { return this.emoteNameMap.get(emoteName); } getAllEmotes() { return Array.from(this.emoteMap.values()); } getEmoteHidByName(emoteName) { return this.emoteNameMap.get(emoteName)?.hid; } getEmoteNameByHid(hid) { return this.emoteMap.get(hid)?.name; } getEmoteNameById(id) { return this.emoteIdMap.get(id)?.name; } getEmoteById(id) { return this.emoteIdMap.get(id); } getEmoteUsageCount(emoteHid) { return this.emoteUsage.get(emoteHid) || 0; } getEmoteSetByEmoteHid(emoteHid) { return this.emoteEmoteSetMap.get(emoteHid); } getFavoriteEmoteDocument(emoteHid) { return this.favoriteEmotesDocumentsMap.get(emoteHid); } isEmoteFavorited(emoteHid) { return this.favoriteEmotesDocumentsMap.has(emoteHid); } addEmoteToFavorites(emoteHid) { const emote = this.emoteMap.get(emoteHid); if (!emote) return error10("Unable to favorite emote, emote not found", emoteHid); const favoriteEmote = { platformId: NTV_PLATFORM, channelId: this.channelId, emoteHid, orderIndex: this.favoriteEmoteDocuments.length, emote }; this.favoriteEmotesDocumentsMap.set(emoteHid, favoriteEmote); this.favoriteEmoteDocuments.push(favoriteEmote); this.pendingFavoriteEmoteChanges[emoteHid] = "added"; this.hasPendingChanges = true; this.session.eventBus.publish("ntv.datastore.emotes.favorites.changed", { added: emoteHid }); } getFavoriteEmoteOrderIndex(emoteHid) { const favoriteEmote = this.favoriteEmotesDocumentsMap.get(emoteHid); if (!favoriteEmote) return error10("Unable to get favorite emote order index, emote not found", emoteHid); return favoriteEmote.orderIndex; } updateFavoriteEmoteOrderIndex(emoteHid, orderIndex) { const favoriteEmote = this.favoriteEmotesDocumentsMap.get(emoteHid); if (!favoriteEmote) return error10("Unable to reorder favorite emote, emote not found", emoteHid); const oldIndex = this.favoriteEmoteDocuments.indexOf(favoriteEmote); this.favoriteEmoteDocuments.splice(oldIndex, 1); let newIndex = this.favoriteEmoteDocuments.findIndex((emote) => emote.orderIndex >= orderIndex); if (newIndex === -1) newIndex = this.favoriteEmoteDocuments.length; this.favoriteEmoteDocuments.splice(newIndex, 0, favoriteEmote); for (let i = Math.min(oldIndex, newIndex); i < this.favoriteEmoteDocuments.length; i++) { const emote = this.favoriteEmoteDocuments[i]; emote.orderIndex = i; if (this.pendingFavoriteEmoteChanges[emote.emoteHid] === "added" || this.pendingFavoriteEmoteChanges[emote.emoteHid] === "removed") continue; this.pendingFavoriteEmoteChanges[emote.emoteHid] = "reordered"; } this.hasPendingChanges = true; this.session.eventBus.publish("ntv.datastore.emotes.favorites.changed", { reordered: emoteHid }); } removeEmoteFromFavorites(emoteHid) { const favoriteEmote = this.favoriteEmotesDocumentsMap.get(emoteHid); if (!favoriteEmote) return error10("Unable to unfavorite emote, emote not found", emoteHid); this.favoriteEmotesDocumentsMap.delete(emoteHid); this.favoriteEmoteDocuments.splice(this.favoriteEmoteDocuments.indexOf(favoriteEmote), 1); this.pendingFavoriteEmoteChanges[emoteHid] = "removed"; this.hasPendingChanges = true; this.session.eventBus.publish("ntv.datastore.emotes.favorites.changed", { removed: emoteHid }); } registerEmoteEngagement(emoteHid) { if (!this.emoteUsage.has(emoteHid)) { this.emoteUsage.set(emoteHid, 0); } this.emoteUsage.set(emoteHid, (this.emoteUsage.get(emoteHid) || 0) + 1); this.pendingEmoteUsageChanges[emoteHid] = "changed"; this.hasPendingChanges = true; this.session.eventBus.publish("ntv.datastore.emotes.usage.changed", { emoteHid }); } removeEmoteUsage(emoteHid) { this.emoteUsage.delete(emoteHid); this.pendingEmoteUsageChanges[emoteHid] = "removed"; this.hasPendingChanges = true; this.session.eventBus.publish("ntv.datastore.emotes.usage.changed", { emoteHid }); } searchEmotesWithWeightedHistory(searchVal) { return this.fuse.search(searchVal).sort((a, b) => { const aHistory = (this.emoteUsage.get(a.item.hid) || 0) + 1; const bHistory = (this.emoteUsage.get(b.item.hid) || 0) + 1; const aTotalScore = a.score - 1 - 1 / bHistory; const bTotalScore = b.score - 1 - 1 / aHistory; if (aTotalScore < bTotalScore) return -1; if (aTotalScore > bTotalScore) return 1; return 0; }); } searchEmotes(search2, biasCurrentChannel = true, biasSubscribedChannels = true) { return this.fuse.search(search2).sort((a, b) => { const aItem = a.item; const bItem = b.item; if (aItem.name.toLowerCase() === search2.toLowerCase()) { return -1; } else if (bItem.name.toLowerCase() === search2.toLowerCase()) { return 1; } const perfectMatchWeight = 1; const scoreWeight = 1; const partsWeight = 0.1; const nameLengthWeight = 0.04; const subscribedChannelWeight = 0.15; const currentChannelWeight = 0.1; let aPartsLength = aItem.parts.length; if (aPartsLength) aPartsLength -= 2; let bPartsLength = bItem.parts.length; if (bPartsLength) bPartsLength -= 2; let relevancyDelta = (a.score - b.score) * scoreWeight; relevancyDelta += (aPartsLength - bPartsLength) * partsWeight; relevancyDelta += (aItem.name.length - bItem.name.length) * nameLengthWeight; const aEmoteSet = this.emoteEmoteSetMap.get(aItem.hid); const bEmoteSet = this.emoteEmoteSetMap.get(bItem.hid); if (biasSubscribedChannels) { const aIsSubscribedChannelEmote = aEmoteSet.isSubscribed; const bIsSubscribedChannelEmote = bEmoteSet.isSubscribed; if (aIsSubscribedChannelEmote && !bIsSubscribedChannelEmote) { relevancyDelta += -1 * subscribedChannelWeight; } else if (!aIsSubscribedChannelEmote && bIsSubscribedChannelEmote) { relevancyDelta += 1 * subscribedChannelWeight; } } if (biasCurrentChannel) { const aIsCurrentChannel = aEmoteSet.isCurrentChannel; const bIsCurrentChannel = bEmoteSet.isCurrentChannel; if (aIsCurrentChannel && !bIsCurrentChannel) { relevancyDelta += -1 * currentChannelWeight; } else if (!aIsCurrentChannel && bIsCurrentChannel) { relevancyDelta += 1 * currentChannelWeight; } } return relevancyDelta; }); } contextfulSearch(search2) { } }; // src/Core/Emotes/EmotesManager.ts var logger11 = new Logger(); var { log: log10, info: info9, error: error11 } = logger11.destruct(); var emoteMatcherRegex = /\[emote:([0-9]+):(?:[^\]]+)?\]|([^\[\]\s]+)/g; var EmotesManager = class { providers = /* @__PURE__ */ new Map(); loaded = false; rootContext; session; datastore; constructor(rootContext, session) { this.rootContext = rootContext; this.session = session; this.datastore = new EmoteDatastore(rootContext, session); } initialize() { this.datastore.loadDatabase().then(() => { }).catch((err) => error11("CORE", "EMOT:MGR", "Failed to load emote data from database.", err.message)); } registerProvider(providerConstructor) { const provider = new providerConstructor(this.rootContext.settingsManager); this.providers.set(provider.id, provider); } /** * @param channelData The channel data object containing channel and user information. * @param providerOverrideOrder The index of emote providers in the array determines their override order incase of emote conflicts. */ async loadProviderEmotes(channelData) { const { datastore, providers } = this; const { eventBus } = this.session; info9("CORE", "EMOT:MGR", "Indexing emote providers.."); const fetchEmoteProviderPromises = []; providers.forEach((provider) => { const providerPromise = provider.fetchEmotes(channelData); fetchEmoteProviderPromises.push(providerPromise); providerPromise.then((emoteSets) => { if (!emoteSets) return; for (const emoteSet of emoteSets) datastore.registerEmoteSet(emoteSet); }).catch((err) => { this.session.userInterface?.toastError(`Failed to fetch emotes from provider ${provider.name}`); error11("CORE", "EMOT:MGR", "Failed to fetch emotes from provider", provider.id, err.message); }); }); Promise.allSettled(fetchEmoteProviderPromises).then((results) => { let allProvidersLoadedSuccessfully = true; for (const [, provider] of providers) { if (provider.status !== "loaded" /* LOADED */ && provider.status !== "no_emotes" /* NO_EMOTES */) { allProvidersLoadedSuccessfully = false; this.session.userInterface?.toastError( `Failed to fetch emotes from ${provider.name} emote provider` ); } } this.loaded = true; eventBus.publish("ntv.providers.loaded", allProvidersLoadedSuccessfully); }); } hasLoadedProviders() { return this.loaded; } addEmoteToEmoteSetById(emote, emoteSetId) { return this.datastore.addEmoteToEmoteSetById(emote, emoteSetId); } removeEmoteFromEmoteSetById(emoteId, emoteSetId) { return this.datastore.removeEmoteFromEmoteSetById(emoteId, emoteSetId); } getEmote(emoteHid) { return this.datastore.getEmote("" + emoteHid); } getEmoteByName(emoteName) { return this.datastore.getEmoteByName(emoteName); } getAllEmotes() { return this.datastore.getAllEmotes(); } getEmoteHidByName(emoteName) { return this.datastore.getEmoteHidByName(emoteName); } getEmoteNameByHid(hid) { return this.datastore.getEmoteNameByHid(hid); } getEmoteNameById(id) { return this.datastore.getEmoteNameById(id); } getEmoteById(id) { return this.datastore.getEmoteById(id); } getEmoteSetByEmoteHid(emoteHid) { return this.datastore.getEmoteSetByEmoteHid(emoteHid); } getEmoteSets() { return this.datastore.emoteSets; } getFavoriteEmoteDocuments() { return this.datastore.favoriteEmoteDocuments; } getFavoriteEmoteDocument(emoteHid) { return this.datastore.getFavoriteEmoteDocument(emoteHid); } getMenuEnabledEmoteSets() { return this.datastore.emoteSets.filter((set) => set.enabledInMenu); } getEmoteUsageCounts() { return this.datastore.emoteUsage; } getEmoteUsageCount(emoteHid) { return this.datastore.getEmoteUsageCount(emoteHid); } getProvider(id) { return this.providers.get(id); } getRenderableEmote(emote, classes = "", srcSetWidthDescriptor) { const provider = this.providers.get(emote.provider); if (!provider) return error11("CORE", "EMOT:MGR", "Provider not found for emote", emote); return provider.getRenderableEmote(emote, classes, srcSetWidthDescriptor); } getRenderableEmoteByHid(emoteHid, classes = "", srcSetWidthDescriptor) { const emote = this.getEmote(emoteHid); if (!emote) return error11("CORE", "EMOT:MGR", "Emote not found"); const provider = this.providers.get(emote.provider); return provider.getRenderableEmote(emote, classes, srcSetWidthDescriptor); } getEmoteEmbeddable(emoteHid, spacingBefore = false) { const emote = this.getEmote(emoteHid); if (!emote) return error11("CORE", "EMOT:MGR", "Emote not found"); const provider = this.providers.get(emote.provider); if (spacingBefore && emote.spacing) { return " " + provider.getEmbeddableEmote(emote); } else { return provider.getEmbeddableEmote(emote); } } addEmoteToFavorites(emoteHid) { this.datastore.addEmoteToFavorites(emoteHid); } getFavoriteEmoteOrderIndex(emoteHid) { return this.datastore.getFavoriteEmoteOrderIndex(emoteHid); } updateFavoriteEmoteOrderIndex(emoteHid, orderIndex) { this.datastore.updateFavoriteEmoteOrderIndex(emoteHid, orderIndex); } removeEmoteFromFavorites(emoteHid) { this.datastore.removeEmoteFromFavorites(emoteHid); } isEmoteFavorited(emoteHid) { return this.datastore.isEmoteFavorited(emoteHid); } isEmoteMenuEnabled(emoteHid) { const emoteSet = this.datastore.getEmoteSetByEmoteHid(emoteHid); if (!emoteSet) return error11("CORE", "EMOT:MGR", "Emote set not found for emote", emoteHid); return emoteSet.enabledInMenu; } registerEmoteEngagement(emoteHid) { this.datastore.registerEmoteEngagement(emoteHid); } removeEmoteUsage(emoteHid) { this.datastore.removeEmoteUsage(emoteHid); } parseEmoteText(text, resultArray = []) { if (text.endsWith(U_TAG_NTV_AFFIX)) { text = text.slice(0, (1 + U_TAG_NTV_AFFIX.length) * -1); } text = text.trim(); if (!text.length) return []; const unprocessed = []; const emojiEntries = (0, import_parser.parse)(text); if (emojiEntries.length) { let lastIndex = 0; const totalEmojis = emojiEntries.length; for (let i = 0; i < totalEmojis; i++) { const emojiData = emojiEntries[i]; const stringStart = text.slice(lastIndex, emojiData.indices[0]).trim(); if (stringStart.length) unprocessed.push(stringStart); unprocessed.push({ type: "emoji", url: emojiData.url, alt: emojiData.text }); lastIndex = emojiData.indices[1]; } if (lastIndex < text.length) { const stringEnd = text.slice(lastIndex).trim(); if (stringEnd.length) unprocessed.push(stringEnd); } } else { unprocessed.push(text); } for (const part of unprocessed) { if (typeof part === "string") { resultArray.push(...this.parseEmoteTextPart(part)); } else { resultArray.push(part); } } return resultArray; } /** * Assumes input is never an empty string. */ parseEmoteTextPart(partString) { const result = []; let match, lastMatchedIndex = 0; while ((match = emoteMatcherRegex.exec(partString)) !== null) { const [matchedText, kickEmoteFormatMatch, maybeTextEmote] = match; if (kickEmoteFormatMatch) { if (lastMatchedIndex < emoteMatcherRegex.lastIndex) { const text = partString.slice(lastMatchedIndex, match.index).trim(); if (text.length) result.push(text); lastMatchedIndex = emoteMatcherRegex.lastIndex; } const emote = this.getEmoteById(kickEmoteFormatMatch); if (emote) { result.push({ type: "emote", emote }); } else result.push(matchedText); } else if (maybeTextEmote) { const emote = this.getEmoteByName(maybeTextEmote); if (emote) { if (lastMatchedIndex < emoteMatcherRegex.lastIndex) { const text = partString.slice(lastMatchedIndex, match.index).trim(); if (text.length) result.push(text); lastMatchedIndex = emoteMatcherRegex.lastIndex; } result.push({ type: "emote", emote }); } } } if (result.length === 0 && lastMatchedIndex !== 0) { result.push(partString.trim()); } else if (lastMatchedIndex < partString.length) { const text = partString.slice(lastMatchedIndex).trim(); if (text.length) result.push(text); } return result; } searchEmotes(search2, limit = 0) { const { settingsManager } = this.rootContext; const channelId = this.session.channelData.channelId; const biasCurrentChannel = settingsManager.getSetting( channelId, "chat.behavior.search_bias_subscribed_channels" ); const biasSubscribedChannels = settingsManager.getSetting( channelId, "chat.behavior.search_bias_current_channels" ); const results = this.datastore.searchEmotes(search2, biasCurrentChannel, biasSubscribedChannels); if (limit) return results.slice(0, limit); return results; } contextfulSearch(search2) { this.datastore.contextfulSearch(search2); } }; // src/Core/Users/UsersDatastore.ts var logger12 = new Logger(); var { log: log11, info: info10, error: error12 } = logger12.destruct(); var UsersDatastore = class { constructor(rootContext, session) { this.rootContext = rootContext; this.session = session; this.session.eventBus.subscribe("ntv.session.destroy", () => { this.users.length = 0; this.usersIdMap.clear(); this.usersLowerCaseNameMap.clear(); }); } usersLowerCaseNameMap = /* @__PURE__ */ new Map(); usersIdMap = /* @__PURE__ */ new Map(); users = []; usersCount = 0; mutedUsersMap = /* @__PURE__ */ new Map(); maxUsers = 5e4; fuse = new Fuse([], { includeScore: true, shouldSort: true, includeMatches: true, // isCaseSensitive: true, findAllMatches: true, threshold: 0.4, keys: [["name"]] }); async loadDatabase() { info10("CORE", "USER:STORE", "Reading out user data from database.."); const { database } = this.rootContext; const { eventBus, channelData } = this.session; const channelId = channelData.channelId; database.mutedUsers.getRecords(NTV_PLATFORM).then((mutedUserRecords) => { if (mutedUserRecords.length) { for (const record of mutedUserRecords) { const user = this.registerUser(record.userId, record.userName); if (!user) { error12("CORE", "USER:STORE", "Failed to register muted user:", record.userId); continue; } user.muted = true; this.mutedUsersMap.set(record.userId, user); } log11( "CORE", "USER:STORE", `Loaded ${mutedUserRecords.length} muted users from database.`, this.mutedUsersMap ); } }).then(() => eventBus.publish("ntv.datastore.users.muted.loaded")).catch((err) => error12("Failed to load muted users data from database.", err.message)); } hasUser(id) { return this.usersIdMap.has(id); } hasMutedUser(id) { const user = this.usersIdMap.get(id); if (!user) return false; return user.muted ?? false; } registerUser(id, name) { typeof id === "string" || error12("CORE", "USER:STORE", "Invalid user id:", id); if (this.usersIdMap.has(id)) return; if (this.usersCount >= this.maxUsers) { error12( "CORE", "USER:STORE", `UsersDatastore: Max users of ${this.maxUsers} reached. Ignoring new user registration.` ); return; } const user = { id, name }; this.usersLowerCaseNameMap.set(name.toLowerCase(), user); this.usersIdMap.set(id, user); this.users.push(user); this.fuse.add(user); this.usersCount++; return user; } getUserById(id) { return this.usersIdMap.get(id + ""); } getUserByName(name) { return this.usersLowerCaseNameMap.get(name.toLowerCase()); } searchUsers(searchVal) { return this.fuse.search(searchVal); } muteUserById(userId, channelId) { const user = this.usersIdMap.get(userId + ""); if (!user) return; const { database } = this.rootContext; database.mutedUsers.putRecord({ platformId: NTV_PLATFORM, channelId, userId: user.id, userName: user.name }); user.muted = true; this.session.eventBus.publish("ntv.user.muted", user); } unmuteUserById(userId) { const user = this.usersIdMap.get(userId + ""); if (!user) return; const { database } = this.rootContext; database.mutedUsers.deleteRecord(NTV_PLATFORM, userId); user.muted = false; this.session.eventBus.publish("ntv.user.unmuted", user); } }; // src/Core/Users/UsersManager.ts var logger13 = new Logger(); var { log: log12, info: info11, error: error13 } = logger13.destruct(); var UsersManager = class { datastore; constructor(rootContext, session) { this.datastore = new UsersDatastore(rootContext, session); session.eventBus.subscribe( "ntv.channel.loaded.channel_data", () => { this.datastore.loadDatabase().then(() => { }).catch((err) => error13("CORE", "USER:MGR", "Failed to load users data from database.", err.message)); }, true, true ); } hasSeenUser(id) { return this.datastore.hasUser(id); } hasMutedUser(id) { return this.datastore.hasMutedUser(id); } registerUser(id, name) { this.datastore.registerUser(id, name); } getUserById(id) { return this.datastore.getUserById(id); } getUserByName(name) { return this.datastore.getUserByName(name); } searchUsers(searchVal, limit = 20) { return this.datastore.searchUsers(searchVal).slice(0, limit); } muteUserById(userId, channelId) { this.datastore.muteUserById(userId, channelId); } unmuteUserById(userId) { this.datastore.unmuteUserById(userId); } }; // src/Core/Common/DTO.ts var DTO = class { topic; data; constructor(topic, data) { this.topic = topic; this.data = data; } setter(key, value) { throw new Error("Data transfer objects are immutable, setter not allowed."); } }; // src/Core/Common/Publisher.ts var logger14 = new Logger(); var { log: log13, error: error14 } = logger14.destruct(); var Publisher = class { listeners = /* @__PURE__ */ new Map(); onceListeners = /* @__PURE__ */ new Map(); firedEvents = /* @__PURE__ */ new Map(); type; constructor(type) { this.type = type; } subscribe(event, callback, triggerOnExistingEvent = false, once = false) { assertArgument(event, "string"); assertArgument(callback, "function"); if (once) { if (once && triggerOnExistingEvent && this.firedEvents.has(event)) { callback(this.firedEvents.get(event).data); return; } if (!this.onceListeners.has(event)) { this.onceListeners.set(event, []); } this.onceListeners.get(event).push(callback); } else { if (!this.listeners.has(event)) { this.listeners.set(event, []); } this.listeners.get(event).push(callback); if (triggerOnExistingEvent && this.firedEvents.has(event)) { callback(this.firedEvents.get(event).data); } } } // Fires callback immediately and only when all passed events have been fired subscribeAllOnce(events, callback) { assertArray(events); assertArgument(callback, "function"); const eventsFired = []; for (const event of events) { if (this.firedEvents.has(event)) { eventsFired.push(event); } } if (eventsFired.length === events.length) { const data = events.map((event) => this.firedEvents.get(event).data); callback(data); return; } const eventListener = (data) => { eventsFired.push(null); if (eventsFired.length === events.length) { const firedEventsData = events.map((event) => this.firedEvents.get(event).data); callback(firedEventsData); } }; const remainingEvents = events.filter((event) => !eventsFired.includes(event)); for (const event of remainingEvents) { this.subscribe(event, eventListener, true, true); } } unsubscribe(event, callback) { assertArgument(event, "string"); assertArgument(callback, "function"); if (!this.listeners.has(event)) { return; } const listeners = this.listeners.get(event); const index = listeners.indexOf(callback); if (index === -1) { return; } listeners.splice(index, 1); } publish(topic, data, suppressLog) { if (!topic) return error14("EVENTS", this.type, "Invalid event topic, discarding event.."); const dto = new DTO(topic, data); this.firedEvents.set(dto.topic, dto); if (!suppressLog) log13("EVENTS", this.type, dto.topic); if (this.onceListeners.has(dto.topic)) { const listeners = this.onceListeners.get(dto.topic); for (let i = 0; i < listeners.length; i++) { const listener = listeners[i]; listener(dto.data); listeners.splice(i, 1); i--; } } if (this.listeners.has(dto.topic)) { const listeners = this.listeners.get(dto.topic); for (const listener of listeners) { listener(dto.data); } } } hasFiredEvent(event) { return this.firedEvents.has(event); } destroy() { this.listeners.clear(); this.firedEvents.clear(); } }; // src/Database/Models/FavoriteEmotesModel.ts var favoriteEmotesSchema = "&[platformId+channelId+emoteHid], platformId, channelId, emoteHid, orderIndex"; var FavoriteEmotesModel = class { db; constructor(db) { this.db = db; } async getRecords(platformId, channelId) { const query = channelId ? { platformId, channelId } : { platformId }; return this.db.favoriteEmotes.where(query).toArray(); } async modifyRecordOrderIndex(platformId, emoteHid, orderIndex) { return this.db.favoriteEmotes.where({ platformId, emoteHid }).modify({ orderIndex }); } async deleteRecordByHid(platformId, emoteHid) { return this.db.favoriteEmotes.where({ platformId, emoteHid }).delete(); } async bulkPutRecords(documents) { return this.db.favoriteEmotes.bulkPut(documents); } async bulkOrderRecords(records) { return Promise.all( records.map((record) => this.modifyRecordOrderIndex(record.platformId, record.emoteHid, record.orderIndex)) ); } async bulkDeleteRecords(records) { return this.db.favoriteEmotes.bulkDelete(records); } async bulkDeleteRecordsByHid(records) { return Promise.all(records.map((record) => this.deleteRecordByHid(record.platformId, record.emoteHid))); } }; // src/Database/Models/EmoteUsagesModel.ts var emoteUsagesSchema = "&[platformId+channelId+emoteHid], platformId, channelId, emoteHid"; var EmoteUsagesModel = class { db; constructor(db) { this.db = db; } async getRecords(platformId, channelId) { return this.db.emoteUsages.where({ platformId, channelId }).toArray(); } async updateRecord(platformId, channelId, emoteHid, count) { return this.db.emoteUsages.where({ platformId, channelId, emoteHid }).modify({ count }); } async deleteRecord(platformId, channelId, emoteHid) { return this.db.emoteUsages.delete([platformId, channelId, emoteHid]); } async deleteRecordByHid(platformId, emoteHid) { return this.db.emoteUsages.where({ platformId, emoteHid }).delete(); } async bulkPutRecords(documents) { return this.db.emoteUsages.bulkPut(documents); } async bulkDeleteRecords(records) { return this.db.emoteUsages.bulkDelete(records); } async bulkDeleteRecordsByHid(records) { return Promise.all(records.map((record) => this.deleteRecordByHid(record.platformId, record.emoteHid))); } }; // src/Database/Models/MutedUsersModel.ts var mutedUsersSchema = "&[platformId+userId], platformId, userId, userName, channelId, timestamp"; var MutedUsersModel = class { db; constructor(db) { this.db = db; } async getRecords(platformId, channelId) { if (channelId) return this.db.mutedUsers.where({ platformId, channelId }).toArray(); else return this.db.mutedUsers.where({ platformId }).toArray(); } async putRecord(document2) { return this.db.mutedUsers.put({ platformId: document2.platformId, channelId: document2.channelId, userId: document2.userId, userName: document2.userName, timestamp: Date.now() }); } async deleteRecord(platformId, userId) { return this.db.mutedUsers.delete([platformId, userId]); } async bulkPutRecords(documents) { return this.db.mutedUsers.bulkPut(documents); } async bulkDeleteRecords(records) { return this.db.mutedUsers.bulkDelete(records); } }; // node_modules/dexie/import-wrapper.mjs var import_dexie = __toESM(require_dexie(), 1); var DexieSymbol = Symbol.for("Dexie"); var Dexie = globalThis[DexieSymbol] || (globalThis[DexieSymbol] = import_dexie.default); if (import_dexie.default.semVer !== Dexie.semVer) { throw new Error(`Two different versions of Dexie loaded in the same app: ${import_dexie.default.semVer} and ${Dexie.semVer}`); } var { liveQuery, mergeRanges, rangesOverlap, RangeSet, cmp, Entity, PropModSymbol, PropModification, replacePrefix, add, remove } = Dexie; var import_wrapper_default = Dexie; // src/Database/Models/SettingsModel.ts var settingsSchema = "&id, platformId, channelId, key"; var SettingsModel = class { db; constructor(db) { this.db = db; } async getRecords() { return this.db.settings.toArray(); } async getRecord(id) { return this.db.settings.get(id); } async putRecord(setting) { return this.db.settings.put(setting); } async updateRecord(id, updates) { return this.db.settings.update(id, updates); } async deleteRecord(id) { return this.db.settings.delete(id); } }; // src/Database/DatabaseAbstract.ts var DatabaseAbstract = class { ready = false; async checkCompatibility() { return new Promise((resolve, reject) => { if (this.ready) return resolve(void 0); this.idb.open().then(async () => { this.ready = true; resolve(void 0); }).catch((err) => { if (err.name === "InvalidStateError") { reject("Firefox private mode not supported."); } else { reject(err); } }); }); } async getTableCount(tableName) { return this.idb.table(tableName).count(); } }; // src/Database/Database.ts var Database = class extends DatabaseAbstract { idb; dbName = "NipahTV"; settings; emoteUsages; favoriteEmotes; mutedUsers; constructor(SWDexie) { super(); this.idb = SWDexie ? new SWDexie(this.dbName) : new import_wrapper_default(this.dbName); this.idb.version(2).stores({ settings: "&id", emoteUsage: "&[channelId+emoteHid]", emoteHistory: null }).upgrade(async (tx) => { const emoteHistoryRecords = await tx.table("emoteHistory").toArray(); return tx.table("emoteUsages").bulkPut( emoteHistoryRecords.map((record) => { return { channelId: record.channelId, emoteHid: record.emoteHid, count: record.timestamps.length }; }) ); }); this.idb.version(3).stores({ emoteUsage: null, emoteUsages: emoteUsagesSchema, favoriteEmotes: favoriteEmotesSchema }).upgrade(async (tx) => { return tx.table("emoteUsage").toArray().then(async (records) => { return tx.table("emoteUsages").bulkAdd( records.map((record) => { return { platformId: "kick", channelId: "" + record.channelId, emoteHid: record.emoteHid, count: record.count }; }) ); }); }); this.idb.version(4).stores({ settings: settingsSchema }).upgrade((tx) => { return tx.table("settings").toCollection().modify((record) => { record.platformId = "global"; record.channelId = "shared"; record.key = record.id.replace("shared.", ""); record.id = `global.shared.${record.key}`; }); }); this.idb.version(5).upgrade(async (tx) => { return tx.table("favoriteEmotes").toArray().then(async (records) => { const updates = records.map((record, index) => { return tx.table("favoriteEmotes").update([record.platformId, record.channelId, record.emoteHid], { orderIndex: records.length - index }); }); return Promise.all(updates); }); }); this.idb.version(6).stores({ mutedUsers: mutedUsersSchema }); this.settings = new SettingsModel(this.idb); this.emoteUsages = new EmoteUsagesModel(this.idb); this.favoriteEmotes = new FavoriteEmotesModel(this.idb); this.mutedUsers = new MutedUsersModel(this.idb); } }; // src/Core/Chat/Components/QuickEmotesHolderComponent.ts var logger15 = new Logger(); var { log: log14, info: info12, error: error15 } = logger15.destruct(); var QuickEmotesHolderComponent = class extends AbstractComponent { constructor(rootContext, session, placeholder) { super(); this.rootContext = rootContext; this.session = session; this.placeholder = placeholder; } element; favoritesEl; commonlyUsedEl; isDraggingEmote = false; dragHandleEmoteEl = null; dragEmoteNewIndex = null; lastDraggedEmoteEl = null; favoriteEmoteElHIDMap = /* @__PURE__ */ new Map(); render() { const channelId = this.session.channelData.channelId; const oldEls = document.getElementsByClassName("ntv__quick-emotes-holder"); for (const el of oldEls) el.remove(); const showUnavailableEmotes = this.rootContext.settingsManager.getSetting( channelId, "quick_emote_holder.show_non_cross_channel_favorites" ); const rows = this.rootContext.settingsManager.getSetting(channelId, "quick_emote_holder.rows") || 2; this.element = parseHTML( `<div class="ntv__quick-emotes-holder" data-rows="${rows}"><div class="ntv__quick-emotes-holder__favorites ${showUnavailableEmotes && "ntv__quick-emotes-holder__favorites--show-unavailable" || ""}"></div><div class="ntv__quick-emotes-holder__spacer">|</div><div class="ntv__quick-emotes-holder__commonly-used"></div></div>`, true ); this.favoritesEl = this.element.querySelector(".ntv__quick-emotes-holder__favorites"); this.commonlyUsedEl = this.element.querySelector(".ntv__quick-emotes-holder__commonly-used"); this.placeholder.replaceWith(this.element); } attachEventHandlers() { const { eventBus } = this.session; const { eventBus: rootEventBus } = this.rootContext; let mouseDownTimeout = null; let skipClickEvent = false; this.element?.addEventListener("click", (evt) => { if (mouseDownTimeout) clearTimeout(mouseDownTimeout); if (skipClickEvent) { skipClickEvent = false; return; } const targetEl = evt.target; const emoteBoxEl = targetEl.classList.contains("ntv__emote-box") && targetEl || targetEl.parentElement.classList.contains("ntv__emote-box") && targetEl.parentElement || null; if (!emoteBoxEl) { return error15("CORE", "UI", "Invalid emote box element"); } if (emoteBoxEl.classList.contains("ntv__emote-box--unavailable") || emoteBoxEl.classList.contains("ntv__emote-box--locked")) return; const emoteHid = emoteBoxEl.firstElementChild?.getAttribute("data-emote-hid"); if (!emoteHid) return error15("CORE", "UI", "Invalid emote hid"); this.handleEmoteClick(emoteHid, !!evt.ctrlKey); }); this.favoritesEl?.addEventListener( "mousedown", (evt) => { const targetEl = evt.target; const emoteBoxEl = targetEl.classList.contains("ntv__emote-box") && targetEl || targetEl.parentElement.classList.contains("ntv__emote-box") && targetEl.parentElement || null; if (emoteBoxEl) { if (mouseDownTimeout) clearTimeout(mouseDownTimeout); const emoteHid = emoteBoxEl.firstElementChild?.getAttribute("data-emote-hid"); if (!emoteHid) return error15("CORE", "UI", "Unable to start dragging emote, invalid emote hid"); mouseDownTimeout = setTimeout(() => { if (!emoteBoxEl.isConnected) return; this.startDragFavoriteEmote(evt, emoteBoxEl); }, 500); emoteBoxEl.addEventListener( "mouseleave", () => { if (mouseDownTimeout) clearTimeout(mouseDownTimeout); }, { once: true, passive: true } ); window.addEventListener( "mouseup", () => { if (mouseDownTimeout) clearTimeout(mouseDownTimeout); if (this.isDraggingEmote) { this.stopDragFavoriteEmote(emoteBoxEl, emoteHid); skipClickEvent = true; } }, { once: true, passive: true } ); } }, { passive: true } ); eventBus.subscribeAllOnce( ["ntv.datastore.emotes.favorites.loaded", "ntv.datastore.emotes.usage.loaded"], () => { eventBus.subscribe( "ntv.datastore.emoteset.added", (emoteSet) => { this.renderFavoriteEmotes(emoteSet); this.renderCommonlyUsedEmotes(); }, true ); } ); eventBus.subscribe( "ntv.datastore.emotes.favorites.changed", (data) => { if (data.reordered) this.reorderFavoriteEmote(data.reordered); else { this.renderFavoriteEmotes(); this.renderCommonlyUsedEmotes(); } } ); eventBus.subscribe("ntv.datastore.emotes.usage.changed", ({ emoteHid }) => { this.reorderCommonlyUsedEmote(emoteHid); }); rootEventBus.subscribe( "ntv.settings.change.quick_emote_holder.rows", ({ value, prevValue }) => { this.element?.setAttribute("data-rows", value || "0"); } ); rootEventBus.subscribe( "ntv.settings.change.quick_emote_holder.show_non_cross_channel_favorites", () => this.renderFavoriteEmotes() ); rootEventBus.subscribe( "ntv.settings.change.quick_emote_holder.show_favorites", () => this.renderFavoriteEmotes() ); rootEventBus.subscribe( "ntv.settings.change.quick_emote_holder.show_recently_used", this.renderCommonlyUsedEmotes.bind(this) ); rootEventBus.subscribe( // TODO rename to show_unavailable_emotes, do same for setting under > emote menu "ntv.settings.change.quick_emote_holder.show_non_cross_channel_favorites", ({ value }) => { this.favoritesEl.classList.toggle("ntv__quick-emotes-holder__favorites--show-unavailable", value); } ); } handleEmoteClick(emoteHid, sendImmediately = false) { assertArgDefined(emoteHid); const { eventBus, emotesManager, channelData } = this.session; const emote = emotesManager.getEmote(emoteHid); if (!emote) return error15("CORE", "UI", "Invalid emote"); const channelId = channelData.channelId; if (this.rootContext.settingsManager.getSetting(channelId, "chat.quick_emote_holder.send_immediately")) { sendImmediately = true; } eventBus.publish("ntv.ui.emote.click", { emoteHid, sendImmediately }); } startDragFavoriteEmote(event, emoteBoxEl) { log14("CORE", "UI", "Starting emote drag mode.."); this.isDraggingEmote = true; this.element.classList.add("ntv__quick-emotes-holder--dragging-emote"); const dragHandleEmoteEl = this.dragHandleEmoteEl = emoteBoxEl.cloneNode(true); dragHandleEmoteEl.classList.add("ntv__emote-box--dragging"); document.body.appendChild(dragHandleEmoteEl); dragHandleEmoteEl.style.left = `${event.clientX}px`; dragHandleEmoteEl.style.top = `${event.clientY}px`; const mouseMoveCb = (evt) => { if (!this.isDraggingEmote) return window.removeEventListener("mousemove", mouseMoveCb); dragHandleEmoteEl.style.left = `${evt.clientX}px`; dragHandleEmoteEl.style.top = `${evt.clientY}px`; const favoriteEmoteEls = Array.from(this.favoritesEl.children); const hoveredEmoteEl = favoriteEmoteEls.find((el) => { const rect = el.getBoundingClientRect(); return evt.clientX > rect.left && evt.clientX < rect.right && evt.clientY > rect.top && evt.clientY < rect.bottom; }); if (hoveredEmoteEl && hoveredEmoteEl !== emoteBoxEl) { const emoteIndex = favoriteEmoteEls.indexOf(emoteBoxEl); const hoveredEmoteIndex = favoriteEmoteEls.indexOf(hoveredEmoteEl); const hoveredEmoteHid = this.favoriteEmoteElHIDMap.get(hoveredEmoteEl); if (!hoveredEmoteHid) return error15("CORE", "UI", "Invalid favorite emote hid while dragging emote.."); const hoveredEmoteOrderIndex = this.session.emotesManager.getFavoriteEmoteOrderIndex(hoveredEmoteHid); if (void 0 === hoveredEmoteOrderIndex) return error15("CORE", "UI", "Invalid favorite emote order index.."); if (hoveredEmoteIndex > emoteIndex) { hoveredEmoteEl.after(emoteBoxEl); this.dragEmoteNewIndex = hoveredEmoteOrderIndex + 1; } else { hoveredEmoteEl.before(emoteBoxEl); this.dragEmoteNewIndex = hoveredEmoteOrderIndex; } } }; window.addEventListener("mousemove", mouseMoveCb); } stopDragFavoriteEmote(emoteBoxEl, emoteHid) { log14("CORE", "UI", "Stopped emote drag mode"); this.element.classList.remove("ntv__quick-emotes-holder--dragging-emote"); emoteBoxEl?.classList.remove("ntv__emote-box--dragging"); this.isDraggingEmote = false; this.dragHandleEmoteEl?.remove(); this.dragHandleEmoteEl = null; this.lastDraggedEmoteEl = emoteHid; if (this.dragEmoteNewIndex !== null) { this.session.emotesManager.updateFavoriteEmoteOrderIndex(emoteHid, this.dragEmoteNewIndex); this.dragEmoteNewIndex = null; } } renderFavoriteEmotes(emoteSet) { const { settingsManager } = this.rootContext; const { emotesManager, channelData } = this.session; const channelId = channelData.channelId; const unsortedFavoriteEmoteDocuments = emotesManager.getFavoriteEmoteDocuments(); if (emoteSet && !unsortedFavoriteEmoteDocuments.some((doc) => emoteSet.emotes.find((emote) => emote.hid === doc.emote.hid))) { return; } while (this.favoritesEl.firstChild) this.favoritesEl.firstChild.remove(); this.favoriteEmoteElHIDMap = /* @__PURE__ */ new Map(); if (!settingsManager.getSetting(channelId, "quick_emote_holder.show_favorites")) return; const favoriteEmoteDocuments = unsortedFavoriteEmoteDocuments.sort((a, b) => a.orderIndex - b.orderIndex); for (const favoriteEmoteDoc of favoriteEmoteDocuments) { const emote = emotesManager.getEmote(favoriteEmoteDoc.emote.hid); const maybeFavoriteEmote = emote || favoriteEmoteDoc.emote; const emoteSet2 = emotesManager.getEmoteSetByEmoteHid(maybeFavoriteEmote.hid); let emoteBoxClasses = emote ? "" : " ntv__emote-box--unavailable"; if (!emoteSet2?.isSubscribed && maybeFavoriteEmote?.isSubscribersOnly) emoteBoxClasses += " ntv__emote-box--locked"; const emoteBoxEl = parseHTML( `<div class="ntv__emote-box ntv__emote-box--favorite${emoteBoxClasses}" size="${maybeFavoriteEmote.size}">${emotesManager.getRenderableEmote( maybeFavoriteEmote, maybeFavoriteEmote.isZeroWidth && "ntv__emote--zero-width" || "" )}</div>`, true ); this.favoriteEmoteElHIDMap.set(emoteBoxEl, maybeFavoriteEmote.hid); this.favoritesEl.append(emoteBoxEl); } } reorderFavoriteEmote(emoteHid) { const { settingsManager } = this.rootContext; const { emotesManager, channelData } = this.session; const channelId = channelData.channelId; if (!settingsManager.getSetting(channelId, "quick_emote_holder.show_favorites")) return; if (this.lastDraggedEmoteEl === emoteHid) { this.lastDraggedEmoteEl = null; log14("CORE", "UI", "Prevented reordering of dragged emote"); return; } const favoriteEmotes = [...emotesManager.getFavoriteEmoteDocuments()].sort( (a, b) => a.orderIndex - b.orderIndex ); const emoteIndex = favoriteEmotes.findIndex(({ emoteHid: hid }) => hid === emoteHid); if (emoteIndex === -1) { log14("CORE", "UI", "Unable to reorder favorited emote because it does not exist:", emoteHid); return; } const emoteEl = this.favoritesEl.querySelector(`[data-emote-hid="${emoteHid}"]`); if (!emoteEl) { error15("CORE", "UI", "Unable to reorder favorited emote, emote does not exist.."); return; } const emoteBoxEl = emoteEl.parentElement; if (!emoteBoxEl?.classList.contains("ntv__emote-box")) { return error15("CORE", "UI", "Invalid emote box element"); } emoteBoxEl.remove(); const insertBeforeEl = this.favoritesEl.children[emoteIndex]; if (insertBeforeEl) { insertBeforeEl.before(emoteBoxEl); } else { this.favoritesEl.appendChild(emoteBoxEl); } } renderCommonlyUsedEmotes() { const { settingsManager } = this.rootContext; const { emotesManager, channelData } = this.session; const emoteUsageCounts = [...emotesManager.getEmoteUsageCounts()].sort((a, b) => b[1] - a[1]); while (this.commonlyUsedEl.firstChild) this.commonlyUsedEl.firstChild.remove(); if (!settingsManager.getSetting(channelData.channelId, "quick_emote_holder.show_recently_used")) return; const favoriteEmoteDocuments = emotesManager.getFavoriteEmoteDocuments(); for (const { emoteHid } of favoriteEmoteDocuments) { const index = emoteUsageCounts.findIndex(([hid]) => hid === emoteHid); if (index !== -1) { emoteUsageCounts.splice(index, 1); } } for (const [emoteHid] of emoteUsageCounts) { const emoteSet = emotesManager.getEmoteSetByEmoteHid(emoteHid); const emote = emotesManager.getEmote(emoteHid); if (!emoteSet || !emote) { if (emotesManager.hasLoadedProviders()) { error15("CORE", "UI", "Unable to render commonly used emote, unkown emote hid:", emoteHid); } continue; } const isSubscribed = emoteSet.isSubscribed; const isMenuEnabled = emoteSet.enabledInMenu; if (!isMenuEnabled || !isSubscribed && emote.isSubscribersOnly) return; const emoteRender = emotesManager.getRenderableEmote( emote, emote.isZeroWidth && "ntv__emote--zero-width" || "" ); if (!emoteRender) continue; const emoteBoxEl = document.createElement("div"); emoteBoxEl.className = "ntv__emote-box"; emoteBoxEl.setAttribute("size", "" + emote.size); emoteBoxEl.setAttribute("data-emote-hid", emoteHid); emoteBoxEl.appendChild(parseHTML(emoteRender)); this.commonlyUsedEl.appendChild(emoteBoxEl); } } /** * Move the emote to the correct position in the emote holder, append if new emote. * @param emoteHid * @returns */ reorderCommonlyUsedEmote(emoteHid) { const { emotesManager } = this.session; const emoteEl = this.commonlyUsedEl.querySelector(`[data-emote-hid="${emoteHid}"]`); if (emoteEl) emoteEl.remove(); const isFavoritedEmote = emotesManager.getFavoriteEmoteDocument(emoteHid); if (isFavoritedEmote) return; const emoteUsageCounts = [...emotesManager.getEmoteUsageCounts()].sort((a, b) => b[1] - a[1]); const emoteIndex = emoteUsageCounts.findIndex(([hid]) => hid === emoteHid); if (emoteIndex === -1) { log14( "CORE", "UI", "Skipped emote not found in the emote usage counts, probably stale emote that has been cleaned up from database." ); return; } if (!emoteEl) { const emoteSet = emotesManager.getEmoteSetByEmoteHid(emoteHid); const emote = emotesManager.getEmote(emoteHid); if (!emoteSet || !emote) return error15("CORE", "UI", "Unable to render commonly used emote:", emoteHid); const isSubscribed = emoteSet.isSubscribed; const isMenuEnabled = emoteSet.enabledInMenu; if (!isMenuEnabled || !isSubscribed && emote.isSubscribersOnly) return; const emoteHTML = emotesManager.getRenderableEmote( emote, emote.isZeroWidth && "ntv__emote--zero-width" || "" ); if (!emoteHTML) return error15("CORE", "UI", "Unable to render commonly used emote:", emoteHid); const emoteBoxEl = document.createElement("div"); emoteBoxEl.className = "ntv__emote-box"; emoteBoxEl.setAttribute("size", "" + emote.size); emoteBoxEl.setAttribute("data-emote-hid", emoteHid); emoteBoxEl.appendChild(parseHTML(emoteHTML)); this.commonlyUsedEl.appendChild(emoteBoxEl); return; } const insertBeforeEl = this.commonlyUsedEl.children[emoteIndex]; if (insertBeforeEl) { insertBeforeEl.before(emoteEl); } else { this.commonlyUsedEl.appendChild(emoteEl); } } destroy() { this.element?.remove(); } }; // src/Core/Chat/Components/EmoteMenuButtonComponent.ts var logger16 = new Logger(); var { log: log15, info: info13, error: error16 } = logger16.destruct(); var EmoteMenuButtonComponent = class extends AbstractComponent { constructor(rootContext, session, placeholder) { super(); this.rootContext = rootContext; this.session = session; this.placeholder = placeholder; } // Not private because of reloadUIhackInterval element; footerLogoBtnEl; render() { Array.from(document.getElementsByClassName("ntv__emote-menu-button")).forEach((element) => { element.remove(); }); const basePath = NTV_RESOURCE_ROOT + "assets/img/btn"; const file = this.getFile(); this.element = parseHTML( cleanupHTML(` <div class="ntv__emote-menu-button"> <img class="${file.className}" src="${NTV_RESOURCE_ROOT + file.path}" draggable="false" alt="NTV"> </div> `), true ); this.footerLogoBtnEl = this.element.querySelector("img"); this.placeholder.replaceWith(this.element); } attachEventHandlers() { const { eventBus: rootEventBus } = this.rootContext; const { eventBus } = this.session; rootEventBus.subscribe("ntv.settings.change.chat.emote_menu.appearance.button_style", () => { if (!this.footerLogoBtnEl) return error16("CORE", "UI", "Footer logo button not found, unable to set logo src"); const file = this.getFile(); this.footerLogoBtnEl.setAttribute("src", NTV_RESOURCE_ROOT + file.path); this.footerLogoBtnEl.className = file.className; }); this.footerLogoBtnEl?.addEventListener("click", () => { if (!this.session.channelData.me.isLoggedIn) { this.session.userInterface?.toastError(`Please log in first to use NipahTV.`); error16("CORE", "UI", "User is not logged in, cannot open emote menu"); } eventBus.publish("ntv.ui.footer.click"); }); } getFile() { const channelId = this.session.channelData.channelId; const buttonStyle = this.rootContext.settingsManager.getSetting( channelId, "chat.emote_menu.appearance.button_style" ); let file = "Nipah"; switch (buttonStyle) { case "nipah": return { path: "assets/img/btn/Nipah.png", className: `ntv__emote-menu-button--${buttonStyle}` }; case "nipahtv": return { path: "assets/img/btn/NipahTV.png", className: `ntv__emote-menu-button--${buttonStyle}` }; case "ntv": return { path: "assets/img/btn/NTV.png", className: `ntv__emote-menu-button--${buttonStyle}` }; case "logo": return { path: "assets/img/NTV_Logo.svg", className: `ntv__emote-menu-button--${buttonStyle}` }; case "ntv_3d": return { path: "assets/img/btn/NTV_3D.png", className: `ntv__emote-menu-button--${buttonStyle}` }; case "ntv_3d_rgb": return { path: "assets/img/btn/NTV_3D_RGB.png", className: `ntv__emote-menu-button--${buttonStyle}` }; case "ntv_3d_shadow": return { path: "assets/img/btn/NTV_3D_RGB_Shadow.png", className: `ntv__emote-menu-button--${buttonStyle}` }; case "ntv_3d_shadow_beveled": default: return { path: "assets/img/btn/NTV_3D_RGB_Shadow_Beveled.png", className: `ntv__emote-menu-button--${buttonStyle}` }; } } destroy() { this.element?.remove(); } }; // src/Core/Chat/Components/EmoteMenuComponent.ts var logger17 = new Logger(); var { log: log16, info: info14, error: error17 } = logger17.destruct(); var EmoteMenuComponent = class extends AbstractComponent { toggleStates = {}; isShowing = false; activePanel = "emotes"; rootContext; session; parentContainer; panels = {}; containerEl; searchInputEl; scrollableEl; settingsBtnEl; sidebarSetsEl; favoritesEmoteSetEl; tooltipEl; tooltipTimeout = null; tooltipTargetEl = null; tooltipPointerMoveHandler; emoteSetEls = /* @__PURE__ */ new Map(); emoteSetSidebarEls = /* @__PURE__ */ new Map(); scrollableObserver; closeModalClickListenerHandle; scrollableHeight = 0; isDraggingEmote = false; dragHandleEmoteEl = null; dragEmoteNewIndex = null; lastDraggedEmoteEl = null; favoriteEmoteElHIDMap = /* @__PURE__ */ new Map(); constructor(rootContext, session, container) { super(); this.rootContext = rootContext; this.session = session; this.parentContainer = container; } render() { const { settingsManager } = this.rootContext; const channelId = this.session.channelData.channelId; const showSearchBox = settingsManager.getSetting(channelId, "chat.emote_menu.search_box"); const hasUpdateAvailable = settingsManager.getGlobalSetting("app.update_available"); const showSidebar = true; const highlightSettingsBtn = !!hasUpdateAvailable; Array.from(document.getElementsByClassName("ntv__emote-menu")).forEach((element) => { element.remove(); }); this.containerEl = parseHTML( cleanupHTML(` <div class="ntv__emote-menu" style="display: none"> <div class="ntv__emote-menu__header"> <div class="ntv__emote-menu__search ${showSearchBox ? "" : "ntv__hidden"}"> <div class="ntv__emote-menu__search__icon"> <svg width="15" height="15" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><path d="M11.3733 5.68667C11.3733 6.94156 10.966 8.10077 10.2797 9.04125L13.741 12.5052C14.0827 12.8469 14.0827 13.4019 13.741 13.7437C13.3992 14.0854 12.8442 14.0854 12.5025 13.7437L9.04125 10.2797C8.10077 10.9687 6.94156 11.3733 5.68667 11.3733C2.54533 11.3733 0 8.828 0 5.68667C0 2.54533 2.54533 0 5.68667 0C8.828 0 11.3733 2.54533 11.3733 5.68667ZM5.68667 9.62359C7.86018 9.62359 9.62359 7.86018 9.62359 5.68667C9.62359 3.51316 7.86018 1.74974 5.68667 1.74974C3.51316 1.74974 1.74974 3.51316 1.74974 5.68667C1.74974 7.86018 3.51316 9.62359 5.68667 9.62359Z"></path></svg> </div> <input type="text" tabindex="0" placeholder="Search emote.."> </div> </div> <div class="ntv__emote-menu__body"> <div class="ntv__emote-menu__scrollable"> <div class="ntv__emote-menu__panel__emotes"></div> <div class="ntv__emote-menu__panel__search" display="none"></div> </div> <div class="ntv__emote-menu__sidebar ${showSidebar ? "" : "ntv__hidden"}"> <div class="ntv__emote-menu__sidebar__sets"></div> <div class="ntv__emote-menu__sidebar__extra"> <a href="#" class="ntv__emote-menu__sidebar-btn ntv__chatroom-link" target="_blank" alt="Pop-out chatroom"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M22 3h7v7m-1.5-5.5L20 12m-3-7H8a3 3 0 0 0-3 3v16a3 3 0 0 0 3 3h16a3 3 0 0 0 3-3v-9" /> </svg> </a> <div class="ntv__emote-menu__sidebar-btn ntv__emote-menu__sidebar-btn--settings ${highlightSettingsBtn ? "ntv__emote-menu__sidebar-btn--highlighted" : ""}"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path fill="currentColor" d="M12 15.5A3.5 3.5 0 0 1 8.5 12A3.5 3.5 0 0 1 12 8.5a3.5 3.5 0 0 1 3.5 3.5a3.5 3.5 0 0 1-3.5 3.5m7.43-2.53c.04-.32.07-.64.07-.97c0-.33-.03-.66-.07-1l2.11-1.63c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.31-.61-.22l-2.49 1c-.52-.39-1.06-.73-1.69-.98l-.37-2.65A.506.506 0 0 0 14 2h-4c-.25 0-.46.18-.5.42l-.37 2.65c-.63.25-1.17.59-1.69.98l-2.49-1c-.22-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64L4.57 11c-.04.34-.07.67-.07 1c0 .33.03.65.07.97l-2.11 1.66c-.19.15-.25.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1.01c.52.4 1.06.74 1.69.99l.37 2.65c.04.24.25.42.5.42h4c.25 0 .46-.18.5-.42l.37-2.65c.63-.26 1.17-.59 1.69-.99l2.49 1.01c.22.08.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64z" /> </svg> </div> <a href="https://github.com/Xzensi/NipahTV/issues" class="ntv__emote-menu__sidebar-btn" target="_blank" alt="Report bug"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"> <path fill="currentColor" d="M304 280h416c4.4 0 8-3.6 8-8c0-40-8.8-76.7-25.9-108.1c-17.2-31.5-42.5-56.8-74-74C596.7 72.8 560 64 520 64h-16c-40 0-76.7 8.8-108.1 25.9c-31.5 17.2-56.8 42.5-74 74C304.8 195.3 296 232 296 272c0 4.4 3.6 8 8 8" /><path fill="currentColor" d="M940 512H792V412c76.8 0 139-62.2 139-139c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8c0 34.8-28.2 63-63 63H232c-34.8 0-63-28.2-63-63c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8c0 76.8 62.2 139 139 139v100H84c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h148v96c0 6.5.2 13 .7 19.3C164.1 728.6 116 796.7 116 876c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8c0-44.2 23.9-82.9 59.6-103.7c6 17.2 13.6 33.6 22.7 49c24.3 41.5 59 76.2 100.5 100.5c28.9 16.9 61 28.8 95.3 34.5c4.4 0 8-3.6 8-8V484c0-4.4 3.6-8 8-8h60c4.4 0 8 3.6 8 8v464.2c0 4.4 3.6 8 8 8c34.3-5.7 66.4-17.6 95.3-34.5c41.5-24.3 76.2-59 100.5-100.5c9.1-15.5 16.7-31.9 22.7-49C812.1 793.1 836 831.8 836 876c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8c0-79.3-48.1-147.4-116.7-176.7c.4-6.4.7-12.8.7-19.3v-96h148c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8" /> </svg> </a> </div> </div> </div> </div> `), true ); this.containerEl.querySelector(".ntv__chatroom-link").setAttribute("href", `/popout/${this.session.channelData.channelName}/chat`); this.searchInputEl = this.containerEl.querySelector(".ntv__emote-menu__search input"); this.scrollableEl = this.containerEl.querySelector(".ntv__emote-menu__scrollable"); this.settingsBtnEl = this.containerEl.querySelector(".ntv__emote-menu__sidebar-btn--settings"); this.sidebarSetsEl = this.containerEl.querySelector(".ntv__emote-menu__sidebar__sets"); this.panels.emotes = this.containerEl.querySelector(".ntv__emote-menu__panel__emotes"); this.panels.search = this.containerEl.querySelector(".ntv__emote-menu__panel__search"); this.renderFavoriteEmoteSet(); document.body.appendChild(this.containerEl); const parentContainerPosition = this.parentContainer.getBoundingClientRect(); this.containerEl.style.right = window.innerWidth - parentContainerPosition.left + "px"; this.containerEl.style.bottom = window.innerHeight - parentContainerPosition.top + 10 + "px"; this.scrollableObserver = new IntersectionObserver( (entries, observer) => { entries.forEach((entry) => { const emoteSetId = entry.target.getAttribute("data-id"); const sidebarIcon = this.emoteSetSidebarEls.get(emoteSetId); if (!sidebarIcon) return error17("CORE", "UI", "Invalid emote set sidebar element"); sidebarIcon.style.backgroundColor = `rgba(255, 255, 255, ${entry.intersectionRect.height / this.scrollableHeight / 7})`; }); }, { root: this.scrollableEl, rootMargin: "0px", threshold: (() => { let thresholds = []; let numSteps = 100; for (let i = 1; i <= numSteps; i++) { let ratio = i / numSteps; thresholds.push(ratio); } thresholds.push(0); return thresholds; })() } ); const emoteSets = this.session.emotesManager.getEmoteSets(); for (const emoteSet of emoteSets) { this.addEmoteSet(emoteSet); } } attachEventHandlers() { const { eventBus: rootEventBus, settingsManager } = this.rootContext; const { eventBus, emotesManager, channelData } = this.session; const channelId = channelData.channelId; let mouseDownTimeout = null; let skipClickEvent = false; this.scrollableEl?.addEventListener("click", (evt) => { const isHoldingCtrl = evt.ctrlKey; const targetEl = evt.target; const emoteBoxEl = targetEl.classList.contains("ntv__emote-box") && targetEl || targetEl.parentElement.classList.contains("ntv__emote-box") && targetEl.parentElement || null; if (!emoteBoxEl) return; const emoteHid = emoteBoxEl.firstElementChild?.getAttribute("data-emote-hid"); if (!emoteHid) return error17("CORE", "UI", "Invalid emote hid"); if (mouseDownTimeout) clearTimeout(mouseDownTimeout); if (skipClickEvent) { skipClickEvent = false; return; } this.handleEmoteClick(emoteBoxEl, emoteHid, isHoldingCtrl); }); this.favoritesEmoteSetEl?.addEventListener( "mousedown", (evt) => { const targetEl = evt.target; const emoteBoxEl = targetEl.classList.contains("ntv__emote-box") && targetEl || targetEl.parentElement.classList.contains("ntv__emote-box") && targetEl.parentElement || null; if (emoteBoxEl) { if (mouseDownTimeout) clearTimeout(mouseDownTimeout); const emoteHid = emoteBoxEl.firstElementChild?.getAttribute("data-emote-hid"); if (!emoteHid) return error17("CORE", "UI", "Unable to start dragging emote, invalid emote hid"); mouseDownTimeout = setTimeout(() => { if (!emoteBoxEl.isConnected) return; this.startDragFavoriteEmote(evt, emoteBoxEl); }, 500); emoteBoxEl.addEventListener( "mouseleave", () => { if (mouseDownTimeout) clearTimeout(mouseDownTimeout); }, { once: true, passive: true } ); window.addEventListener( "mouseup", () => { if (mouseDownTimeout) clearTimeout(mouseDownTimeout); if (this.isDraggingEmote) { this.stopDragFavoriteEmote(emoteBoxEl, emoteHid); skipClickEvent = true; } }, { once: true, passive: true } ); } }, { passive: true } ); this.scrollableEl?.addEventListener("pointerover", (evt) => { const targetEl = evt.target; const emoteBoxEl = targetEl.classList.contains("ntv__emote-box") && targetEl || targetEl.parentElement.classList.contains("ntv__emote-box") && targetEl.parentElement || null; if (!emoteBoxEl || emoteBoxEl === this.tooltipTargetEl) return; if (this.tooltipEl) { this.tooltipEl.remove(); this.tooltipEl = void 0; } if (this.tooltipPointerMoveHandler) { document.removeEventListener("pointermove", this.tooltipPointerMoveHandler); this.tooltipPointerMoveHandler = void 0; } if (this.tooltipTimeout) { window.clearTimeout(this.tooltipTimeout); this.tooltipTimeout = null; } this.tooltipTargetEl = emoteBoxEl; const emoteHid = emoteBoxEl.firstElementChild?.getAttribute("data-emote-hid"); if (!emoteHid) return; const emote = emotesManager.getEmote(emoteHid); if (!emote) return; const imageInTooltop = settingsManager.getSetting(channelId, "chat.tooltips.images"); const tooltipEl = parseHTML( cleanupHTML( `<div class="ntv__emote-tooltip ${imageInTooltop ? "ntv__emote-tooltip--has-image" : ""}"> ${imageInTooltop && emotesManager.getRenderableEmote(emote, "ntv__emote", true) || ""} <span class="ntv__emote-tooltip__title">${emote.name}</span> </div>` ), true ); if (emote.isZeroWidth) { const span = document.createElement("span"); span.className = "ntv__emote-tooltip__zero-width"; span.textContent = "Zero Width"; tooltipEl.appendChild(span); } if (emotesManager.isEmoteFavorited(emote.hid)) { const span = document.createElement("span"); span.className = "ntv__emote-tooltip__favorited"; span.textContent = "Favorited"; tooltipEl.appendChild(span); } this.tooltipEl = tooltipEl; document.body.appendChild(tooltipEl); const rect = emoteBoxEl.getBoundingClientRect(); tooltipEl.style.left = rect.left + rect.width / 2 + "px"; tooltipEl.style.top = rect.top + "px"; this.tooltipPointerMoveHandler = (moveEvt) => { const elem = document.elementFromPoint(moveEvt.clientX, moveEvt.clientY); if (!elem) return; if (elem === emoteBoxEl || emoteBoxEl.contains(elem) || elem === tooltipEl || tooltipEl.contains(elem)) { return; } if (this.tooltipEl) this.tooltipEl.remove(); this.tooltipEl = void 0; this.tooltipTargetEl = null; if (this.tooltipPointerMoveHandler) { document.removeEventListener("pointermove", this.tooltipPointerMoveHandler); this.tooltipPointerMoveHandler = void 0; } if (this.tooltipTimeout) { window.clearTimeout(this.tooltipTimeout); this.tooltipTimeout = null; } }; document.addEventListener("pointermove", this.tooltipPointerMoveHandler); this.tooltipTimeout = window.setTimeout(() => { if (this.tooltipEl) this.tooltipEl.remove(); this.tooltipEl = void 0; this.tooltipTargetEl = null; if (this.tooltipPointerMoveHandler) { document.removeEventListener("pointermove", this.tooltipPointerMoveHandler); this.tooltipPointerMoveHandler = void 0; } this.tooltipTimeout = null; }, 5e3); }); this.searchInputEl?.addEventListener("input", this.handleSearchInput.bind(this)); this.sidebarSetsEl?.addEventListener("click", (evt) => { const target = evt.target; const scrollableEl = this.scrollableEl; if (!scrollableEl) return; const emoteSetId = target.querySelector("img, svg")?.getAttribute("data-id"); const emoteSetEl = this.containerEl?.querySelector( `.ntv__emote-set[data-id="${emoteSetId}"]` ); if (!emoteSetEl) return error17("CORE", "UI", "Invalid emote set element"); const headerHeight = emoteSetEl.querySelector(".ntv__emote-set__header")?.clientHeight || 0; scrollableEl.scrollTo({ top: emoteSetEl.offsetTop - headerHeight, behavior: "smooth" }); }); this.panels.emotes?.addEventListener("click", (evt) => { const target = evt.target; if (!target.classList.contains("ntv__chevron")) return; const emoteSet = target.closest(".ntv__emote-set"); if (!emoteSet) return; const emoteSetBody = emoteSet.querySelector(".ntv__emote-set__emotes"); if (!emoteSetBody) return; emoteSet.classList.toggle("ntv__emote-set--collapsed"); }); this.settingsBtnEl?.addEventListener("click", () => { this.rootContext.eventBus.publish("ntv.ui.settings.toggle_show"); }); eventBus.subscribe( "ntv.datastore.emotes.favorites.loaded", () => { eventBus.subscribe("ntv.datastore.emoteset.added", this.addEmoteSet.bind(this), true); eventBus.subscribe("ntv.datastore.emoteset.added", this.renderFavoriteEmotes.bind(this), true); eventBus.subscribe("ntv.datastore.emoteset.updated", this.updateEmoteSet.bind(this), true); }, true, true ); eventBus.subscribe( "ntv.datastore.emotes.favorites.changed", (data) => { if (this.lastDraggedEmoteEl === data.reordered) { this.lastDraggedEmoteEl = null; return; } const emoteHid = data.added || data.reordered || data.removed; if (!emoteHid) return error17("CORE", "UI", "Invalid emote hid in favorites changed event", data); const emoteSet = emotesManager.getEmoteSetByEmoteHid(emoteHid); if (!emoteSet) return error17("CORE", "UI", "Invalid emote set in favorites changed event", data); this.updateEmoteSet(emoteSet); } ); eventBus.subscribe("ntv.ui.footer.click", this.toggleShow.bind(this)); rootEventBus.subscribe( "ntv.settings.change.emote_menu.show_unavailable_favorites", ({ value }) => { this.favoritesEmoteSetEl?.querySelector(".ntv__emote-set__emotes")?.classList.toggle("ntv__emote-set--show-unavailable", value); } ); document.addEventListener("keydown", (evt) => { if (evt.key === "Escape") this.toggleShow(false); }); if (settingsManager.getSetting(channelId, "chat.emote_menu.open_ctrl_spacebar")) { document.addEventListener("keydown", (evt) => { if (evt.ctrlKey && evt.key === " ") { evt.preventDefault(); this.toggleShow(); } }); } if (settingsManager.getSetting(channelId, "chat.emote_menu.open_ctrl_e")) { document.addEventListener("keydown", (evt) => { if (evt.ctrlKey && evt.key === "e") { evt.preventDefault(); this.toggleShow(); } }); } } handleSearchInput(evt) { if (!(evt.target instanceof HTMLInputElement)) return; if (this.tooltipEl) this.tooltipEl.remove(); const { settingsManager } = this.rootContext; const { emotesManager, channelData } = this.session; const channelId = channelData.channelId; const searchVal = evt.target.value; if (searchVal.length) { this.switchPanel("search"); } else { this.switchPanel("emotes"); } const emotesResult = emotesManager.searchEmotes(searchVal.substring(0, 20)); log16("CORE", "UI", `Searching for emotes, found ${emotesResult.length} matches"`); while (this.panels.search?.firstChild) { this.panels.search.removeChild(this.panels.search.firstChild); } const hideSubscribersEmotes = settingsManager.getSetting(channelId, "chat.emotes.hide_subscriber_emotes"); let maxResults = 75; for (const emoteResult of emotesResult) { const emote = emoteResult.item; if (maxResults-- <= 0) break; const emoteSet = emotesManager.getEmoteSetByEmoteHid(emote.hid); if (!emoteSet) { error17("CORE", "UI", "Emote set not found for emote", emote.name); continue; } if (emote.isSubscribersOnly && !emoteSet.isSubscribed) { if (hideSubscribersEmotes) continue; this.panels.search?.append( parseHTML( `<div class="ntv__emote-box ntv__emote-box--locked">${emotesManager.getRenderableEmote( emote, "ntv__emote" )}</div>` ) ); } else { this.panels.search?.append( parseHTML( `<div class="ntv__emote-box">${emotesManager.getRenderableEmote(emote, "ntv__emote")}</div>` ) ); } } } switchPanel(panel) { if (this.activePanel === panel) return; if (this.activePanel === "search") { if (this.panels.search) this.panels.search.style.display = "none"; } else if (this.activePanel === "emotes") { if (this.panels.emotes) this.panels.emotes.style.display = "none"; } if (panel === "search") { if (this.panels.search) this.panels.search.style.display = ""; } else if (panel === "emotes") { if (this.panels.emotes) this.panels.emotes.style.display = ""; } this.activePanel = panel; } renderFavoriteEmoteSet() { const { sidebarSetsEl, scrollableEl } = this; const channelId = this.session.channelData.channelId; const emotesPanelEl = this.panels.emotes; if (!emotesPanelEl || !sidebarSetsEl || !scrollableEl) return error17("CORE", "UI", "Invalid emote menu elements"); const { settingsManager } = this.rootContext; if (!settingsManager.getSetting(channelId, "emote_menu.show_favorites")) return; const sidebarFavoritesBtn = parseHTML( `<div class="ntv__emote-menu__sidebar-btn"><svg data-id="favorites" xmlns="http://www.w3.org/2000/svg" width="1.5em" height="1.5em" viewBox="0 0 24 24"><path fill="currentColor" d="m19 23.3l-.6-.5c-2-1.9-3.4-3.1-3.4-4.6c0-1.2 1-2.2 2.2-2.2c.7 0 1.4.3 1.8.8c.4-.5 1.1-.8 1.8-.8c1.2 0 2.2.9 2.2 2.2c0 1.5-1.4 2.7-3.4 4.6zM17 4v6l-2-2l-2 2V4H9v16h4.08c.12.72.37 1.39.72 2H7c-1.05 0-2-.95-2-2v-1H3v-2h2v-4H3v-2h2V7H3V5h2V4a2 2 0 0 1 2-2h12c1.05 0 2 .95 2 2v9.34c-.63-.22-1.3-.34-2-.34V4zM5 19h2v-2H5zm0-6h2v-2H5zm0-6h2V5H5z" /></svg></div`, true ); sidebarSetsEl.appendChild(sidebarFavoritesBtn); this.emoteSetSidebarEls.set("favorites", sidebarFavoritesBtn); const showUnavailableEmotes = settingsManager.getSetting(channelId, "emote_menu.show_unavailable_favorites"); this.favoritesEmoteSetEl = parseHTML( cleanupHTML( `<div class="ntv__emote-set" data-id="favorites"> <div class="ntv__emote-set__header"> <svg xmlns="http://www.w3.org/2000/svg" width="1.5em" height="1.5em" viewBox="0 0 24 24"><path fill="currentColor" d="m19 23.3l-.6-.5c-2-1.9-3.4-3.1-3.4-4.6c0-1.2 1-2.2 2.2-2.2c.7 0 1.4.3 1.8.8c.4-.5 1.1-.8 1.8-.8c1.2 0 2.2.9 2.2 2.2c0 1.5-1.4 2.7-3.4 4.6zM17 4v6l-2-2l-2 2V4H9v16h4.08c.12.72.37 1.39.72 2H7c-1.05 0-2-.95-2-2v-1H3v-2h2v-4H3v-2h2V7H3V5h2V4a2 2 0 0 1 2-2h12c1.05 0 2 .95 2 2v9.34c-.63-.22-1.3-.34-2-.34V4zM5 19h2v-2H5zm0-6h2v-2H5zm0-6h2V5H5z" /></svg> <span>Favorites</span> <div class="ntv__chevron"> <svg width="1em" height="0.6666em" viewBox="0 0 9 6" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M0.221974 4.46565L3.93498 0.251908C4.0157 0.160305 4.10314 0.0955723 4.19731 0.0577097C4.29148 0.0192364 4.39238 5.49454e-08 4.5 5.3662e-08C4.60762 5.23786e-08 4.70852 0.0192364 4.80269 0.0577097C4.89686 0.0955723 4.9843 0.160305 5.06502 0.251908L8.77803 4.46565C8.92601 4.63359 9 4.84733 9 5.10687C9 5.36641 8.92601 5.58015 8.77803 5.74809C8.63005 5.91603 8.4417 6 8.213 6C7.98431 6 7.79596 5.91603 7.64798 5.74809L4.5 2.17557L1.35202 5.74809C1.20404 5.91603 1.0157 6 0.786996 6C0.558296 6 0.369956 5.91603 0.221974 5.74809C0.0739918 5.58015 6.39938e-08 5.36641 6.08988e-08 5.10687C5.78038e-08 4.84733 0.0739918 4.63359 0.221974 4.46565Z"></path></svg> </div> </div> <div class="ntv__emote-set__emotes ${showUnavailableEmotes && "ntv__emote-set--show-unavailable" || ""}"></div> </div>` ), true ); emotesPanelEl.append(this.favoritesEmoteSetEl); } renderFavoriteEmotes(emoteSet) { const { settingsManager } = this.rootContext; const channelId = this.session.channelData.channelId; if (!settingsManager.getSetting(channelId, "emote_menu.show_favorites")) return; if (!this.favoritesEmoteSetEl) return error17("CORE", "UI", "Invalid favorites emote set element"); const { emotesManager } = this.session; const favoriteEmoteDocuments = emotesManager.getFavoriteEmoteDocuments(); if (emoteSet && !favoriteEmoteDocuments.some((doc) => emoteSet.emotes.find((emote) => emote.hid === doc.emote.hid))) { return; } log16("CORE", "UI", "Rendering favorite emote set in emote menu.."); const emotesEl = this.favoritesEmoteSetEl.getElementsByClassName("ntv__emote-set__emotes")[0]; while (emotesEl.firstChild) emotesEl.removeChild(emotesEl.firstChild); this.favoriteEmoteElHIDMap = /* @__PURE__ */ new Map(); for (const favoriteEmoteDoc of favoriteEmoteDocuments) { const emote = emotesManager.getEmote(favoriteEmoteDoc.emote.hid); const maybeFavoriteEmote = emote || favoriteEmoteDoc.emote; const emoteSet2 = emotesManager.getEmoteSetByEmoteHid(maybeFavoriteEmote.hid); let emoteBoxClasses = emote ? "" : " ntv__emote-box--unavailable"; emoteBoxClasses += !emoteSet2?.isSubscribed && emote?.isSubscribersOnly ? "ntv__emote-box--locked" : ""; const emoteBoxEl = parseHTML( `<div class="ntv__emote-box ${emoteBoxClasses}" size="${maybeFavoriteEmote.size}">${emotesManager.getRenderableEmote( maybeFavoriteEmote, maybeFavoriteEmote.isZeroWidth && "ntv__emote--zero-width" || "" )}</div>`, true ); this.favoriteEmoteElHIDMap.set(emoteBoxEl, maybeFavoriteEmote.hid); emotesEl.append(emoteBoxEl); } } addEmoteSet(emoteSet) { log16("CORE", "UI", `Adding emote set "${emoteSet.name}" to emote menu..`); const { sidebarSetsEl, scrollableEl, rootContext } = this; const { emotesManager, channelData } = this.session; const channelId = channelData.channelId; const emotesPanelEl = this.panels.emotes; if (!emotesPanelEl || !sidebarSetsEl || !scrollableEl) return error17("CORE", "UI", "Invalid emote menu elements"); if (this.emoteSetEls.has(emoteSet.id)) { error17("CORE", "UI", `Emote set "${emoteSet.name}" already exists, removing it before re-adding..`); this.emoteSetEls.get(emoteSet.id)?.remove(); this.emoteSetEls.delete(emoteSet.id); this.emoteSetSidebarEls.get(emoteSet.id)?.remove(); this.emoteSetSidebarEls.delete(emoteSet.id); } const emoteSets = emotesManager.getMenuEnabledEmoteSets(); if (!emoteSets.find((set) => set.id === emoteSet.id)) { log16("CORE", "UI", `Emote set "${emoteSet.name}" is not enabled in the emote menu, skipping..`); return; } const hideSubscribersEmotes = rootContext.settingsManager.getSetting( channelId, "chat.emotes.hide_subscriber_emotes" ); const sortedEmotes = emoteSet.emotes.sort((a, b) => a.width - b.width); const sidebarIconEl = parseHTML( `<div class="ntv__emote-menu__sidebar-btn"><img data-id="${emoteSet.id}" src="${emoteSet.icon}"></div`, true ); const emoteSetEl = parseHTML( cleanupHTML( `<div class="ntv__emote-set" data-id="${emoteSet.id}"> <div class="ntv__emote-set__header"> <img src="${emoteSet.icon}"> <span>${emoteSet.name}</span> <div class="ntv__chevron"> <svg width="1em" height="0.6666em" viewBox="0 0 9 6" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M0.221974 4.46565L3.93498 0.251908C4.0157 0.160305 4.10314 0.0955723 4.19731 0.0577097C4.29148 0.0192364 4.39238 5.49454e-08 4.5 5.3662e-08C4.60762 5.23786e-08 4.70852 0.0192364 4.80269 0.0577097C4.89686 0.0955723 4.9843 0.160305 5.06502 0.251908L8.77803 4.46565C8.92601 4.63359 9 4.84733 9 5.10687C9 5.36641 8.92601 5.58015 8.77803 5.74809C8.63005 5.91603 8.4417 6 8.213 6C7.98431 6 7.79596 5.91603 7.64798 5.74809L4.5 2.17557L1.35202 5.74809C1.20404 5.91603 1.0157 6 0.786996 6C0.558296 6 0.369956 5.91603 0.221974 5.74809C0.0739918 5.58015 6.39938e-08 5.36641 6.08988e-08 5.10687C5.78038e-08 4.84733 0.0739918 4.63359 0.221974 4.46565Z"></path></svg> </div> </div> </div>` ), true ); const emoteSetEmotesEl = document.createElement("div"); emoteSetEmotesEl.className = "ntv__emote-set__emotes"; for (const emote of sortedEmotes) { let emoteBoxClasses = emote.isZeroWidth && "ntv__emote-box--zero-width" || ""; if (emotesManager.isEmoteFavorited(emote.hid)) { emoteBoxClasses += " ntv__emote-box--favorited"; } if (emote.isSubscribersOnly && !emoteSet.isSubscribed) { if (hideSubscribersEmotes) continue; emoteBoxClasses += " ntv__emote-box--locked"; } emoteSetEmotesEl.append( parseHTML( `<div class="ntv__emote-box ${emoteBoxClasses}" size="${emote.size}">${emotesManager.getRenderableEmote(emote, "ntv__emote-set__emote ntv__emote")}</div>` ) ); } this.emoteSetSidebarEls.set(emoteSet.id, sidebarIconEl); emoteSetEl.append(emoteSetEmotesEl); this.emoteSetEls.set(emoteSet.id, emoteSetEl); const orderedEmoteSets = Array.from(emoteSets).sort((a, b) => a.orderIndex - b.orderIndex); for (const oEmoteSet of orderedEmoteSets) { if (!this.emoteSetEls.has(oEmoteSet.id)) continue; emotesPanelEl.append(this.emoteSetEls.get(oEmoteSet.id)); sidebarSetsEl.appendChild(this.emoteSetSidebarEls.get(oEmoteSet.id)); } this.scrollableObserver.observe(emoteSetEl); } updateEmoteSet(emoteSet) { log16("CORE", "UI", `Updating emote set "${emoteSet.name}" in emote menu..`); if (this.emoteSetEls.has(emoteSet.id)) { this.emoteSetEls.get(emoteSet.id)?.remove(); this.emoteSetEls.delete(emoteSet.id); this.emoteSetSidebarEls.get(emoteSet.id)?.remove(); this.emoteSetSidebarEls.delete(emoteSet.id); this.addEmoteSet(emoteSet); this.renderFavoriteEmotes(emoteSet); } else { this.addEmoteSet(emoteSet); } } handleEmoteClick(emoteBoxEl, emoteHid, isHoldingCtrl) { const { settingsManager } = this.rootContext; const { emotesManager, eventBus, channelData } = this.session; const channelId = channelData.channelId; const isUnavailable = emoteBoxEl.classList.contains("ntv__emote-box--unavailable"); const isLocked = emoteBoxEl.classList.contains("ntv__emote-box--locked"); if (isHoldingCtrl) { const isFavorited = emotesManager.isEmoteFavorited(emoteHid); if (isFavorited) emotesManager.removeEmoteFromFavorites(emoteHid); else if (!isLocked && !isUnavailable) emotesManager.addEmoteToFavorites(emoteHid); } else { if (isUnavailable || isLocked) return; const emote = emotesManager.getEmote(emoteHid); if (!emote) return error17("CORE", "UI", "Emote not found"); eventBus.publish("ntv.ui.emote.click", { emoteHid }); const closeOnClick = settingsManager.getSetting(channelId, "chat.emote_menu.close_on_click"); if (closeOnClick) this.toggleShow(false); } } handleOutsideModalClick(evt) { if (!this.containerEl) return; const containerEl = this.containerEl; const withinComposedPath = evt.composedPath().includes(containerEl); if (!withinComposedPath) this.toggleShow(false); } startDragFavoriteEmote(event, emoteBoxEl) { if (!this.favoritesEmoteSetEl) return error17("CORE", "UI", "Unable to drag emote, favorites emote set does not exist.."); log16("CORE", "UI", "Starting emote drag mode.."); this.isDraggingEmote = true; this.favoritesEmoteSetEl.classList.add("ntv__emote-set--dragging-emote"); const favoriteEmotesSetBodyEl = this.favoritesEmoteSetEl.querySelector(".ntv__emote-set__emotes"); const dragHandleEmoteEl = this.dragHandleEmoteEl = emoteBoxEl.cloneNode(true); dragHandleEmoteEl.classList.add("ntv__emote-box--dragging"); document.body.appendChild(dragHandleEmoteEl); dragHandleEmoteEl.style.left = `${event.clientX}px`; dragHandleEmoteEl.style.top = `${event.clientY}px`; const mouseMoveCb = (evt) => { if (!this.isDraggingEmote) return window.removeEventListener("mousemove", mouseMoveCb); dragHandleEmoteEl.style.left = `${evt.clientX}px`; dragHandleEmoteEl.style.top = `${evt.clientY}px`; const favoriteEmoteEls = Array.from(favoriteEmotesSetBodyEl.children); const hoveredEmoteEl = favoriteEmoteEls.find((el) => { const rect = el.getBoundingClientRect(); return evt.clientX > rect.left && evt.clientX < rect.right && evt.clientY > rect.top && evt.clientY < rect.bottom; }); if (hoveredEmoteEl && hoveredEmoteEl !== emoteBoxEl) { const emoteIndex = favoriteEmoteEls.indexOf(emoteBoxEl); const hoveredEmoteIndex = favoriteEmoteEls.indexOf(hoveredEmoteEl); const hoveredEmoteHid = this.favoriteEmoteElHIDMap.get(hoveredEmoteEl); if (!hoveredEmoteHid) return error17("CORE", "UI", "Invalid favorite emote hid while dragging emote.."); const hoveredEmoteOrderIndex = this.session.emotesManager.getFavoriteEmoteOrderIndex(hoveredEmoteHid); if (void 0 === hoveredEmoteOrderIndex) return error17("CORE", "UI", "Invalid favorite emote order index.."); if (hoveredEmoteIndex > emoteIndex) { hoveredEmoteEl.after(emoteBoxEl); this.dragEmoteNewIndex = hoveredEmoteOrderIndex + 1; } else { hoveredEmoteEl.before(emoteBoxEl); this.dragEmoteNewIndex = hoveredEmoteOrderIndex; } } }; window.addEventListener("mousemove", mouseMoveCb); } stopDragFavoriteEmote(emoteBoxEl, emoteHid) { if (!this.favoritesEmoteSetEl) return error17("CORE", "UI", "Unable to stop dragging emote, favorites emote set does not exist.."); log16("CORE", "UI", "Stopped emote drag mode"); this.favoritesEmoteSetEl.classList.remove("ntv__emote-set--dragging-emote"); emoteBoxEl?.classList.remove("ntv__emote-box--dragging"); this.isDraggingEmote = false; this.dragHandleEmoteEl?.remove(); this.dragHandleEmoteEl = null; this.lastDraggedEmoteEl = emoteHid; if (this.dragEmoteNewIndex !== null) { this.session.emotesManager.updateFavoriteEmoteOrderIndex(emoteHid, this.dragEmoteNewIndex); this.dragEmoteNewIndex = null; } } toggleShow(bool) { if (bool === this.isShowing) return; this.isShowing = !this.isShowing; const { searchInputEl } = this; if (this.isShowing) { setTimeout(() => { if (searchInputEl) searchInputEl.focus(); this.closeModalClickListenerHandle = this.handleOutsideModalClick.bind(this); window.addEventListener("click", this.closeModalClickListenerHandle); }); } else { window.removeEventListener("click", this.closeModalClickListenerHandle); } if (this.containerEl) { if (this.isShowing) { const parentContainerPosition = this.parentContainer.getBoundingClientRect(); this.containerEl.style.right = window.innerWidth - parentContainerPosition.left + 10 + "px"; this.containerEl.style.bottom = window.innerHeight - parentContainerPosition.top + 10 + "px"; } this.containerEl.style.display = this.isShowing ? "" : "none"; } this.scrollableHeight = this.scrollableEl?.clientHeight || 0; } destroy() { if (this.tooltipEl) { this.tooltipEl.remove(); this.tooltipEl = void 0; } if (this.tooltipPointerMoveHandler) { document.removeEventListener("pointermove", this.tooltipPointerMoveHandler); this.tooltipPointerMoveHandler = void 0; } if (this.tooltipTimeout) { window.clearTimeout(this.tooltipTimeout); this.tooltipTimeout = null; } this.containerEl?.remove(); } }; // src/Core/Chat/Components/ReplyMessageComponent.ts var logger18 = new Logger(); var { log: log17, info: info15, error: error18 } = logger18.destruct(); var ReplyMessageComponent = class extends AbstractComponent { element; containerEl; eventTarget = new EventTarget(); constructor(containerEl, messageNodes) { super(); this.containerEl = containerEl; this.element = parseHTML( cleanupHTML(` <div class="ntv__reply-message"> <div class="ntv__reply-message__header"> <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 32 32"> <path fill="currentColor" d="m12.281 5.281l-8 8l-.687.719l.687.719l8 8l1.438-1.438L7.438 15H21c2.773 0 5 2.227 5 5s-2.227 5-5 5v2c3.855 0 7-3.145 7-7s-3.145-7-7-7H7.437l6.282-6.281z" /> </svg> <span>Replying to:</span> <svg class="ntv__reply-message__close-btn ntv__icon-button" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 50 50"> <path fill="currentColor" d="m37.304 11.282l1.414 1.414l-26.022 26.02l-1.414-1.413z" /> <path fill="currentColor" d="m12.696 11.282l26.022 26.02l-1.414 1.415l-26.022-26.02z" /> </svg> </div> <div class="ntv__reply-message__content"> </div> </div> `), true ); const contentEl = this.element.querySelector(".ntv__reply-message__content"); for (const messageNode of messageNodes) { contentEl.append(messageNode.cloneNode(true)); } } render() { this.containerEl.append(this.element); } // Method to attach event handlers attachEventHandlers() { const closeBtn = this.element.querySelector(".ntv__reply-message__close-btn"); closeBtn.addEventListener("click", () => { this.element.remove(); this.eventTarget.dispatchEvent(new Event("close")); }); } addEventListener(event, callback) { this.eventTarget.addEventListener(event, callback); } destroy() { log17("CORE", "UI", "Destroying reply message component..", this.element); this.element.remove(); } }; // src/Core/Common/PriorityEventTarget.ts var notAllowed = function() { throw new Error("PreventDefault cannot be called because the event was set as passive."); }; var stopPropagation = function() { this._stopPropagation(); this.stoppedPropagation = true; }; var stopImmediatePropagation = function() { this._stopImmediatePropagation(); this.stoppedImmediatePropagation = true; }; var PriorityEventTarget = class { events = /* @__PURE__ */ new Map(); /** * Adds a priority event listener for the specified event type at the specified priority. It will be called in the order of priority. * @param type * @param priority * @param listener * @param options */ addEventListener(type, priority, listener, options) { if (!this.events.has(type)) { this.events.set(type, []); } const priorities = this.events.get(type); if (!priorities[priority]) priorities[priority] = []; const listeners = priorities[priority]; if (options) listeners.push([listener, options]); else listeners.push([listener]); if (options && options.signal) { options.signal.addEventListener("abort", () => { this.removeEventListener(type, priority, listener, options); }); } } removeEventListener(type, priority, listener, options) { if (this.events.has(type)) { const priorities = this.events.get(type); const listeners = priorities[priority]; if (!listeners) return; for (let i = 0; i < listeners.length; i++) { let listenerItem = listeners[i][0]; let optionsItem = listeners[i][1]; if (listenerItem === listener && optionsItem === options) { listeners.splice(i, 1); i--; } } } } dispatchEvent(event) { ; event._stopPropagation = event.stopPropagation; event.stopPropagation = stopPropagation; event._stopImmediatePropagation = event.stopImmediatePropagation; event.stopImmediatePropagation = stopImmediatePropagation; const type = event.type; if (this.events.has(type)) { const priorities = this.events.get(type); for (const key in priorities) { const listeners = priorities[key]; for (let i = 0; i < listeners.length; i++) { const listener = listeners[i][0]; const options = listeners[i][1]; if (options) { if (options.once) { listeners.splice(i, 1); i--; } if (options.passive) { event.preventDefault = notAllowed; } } listener(event); if (event.stoppedImmediatePropagation) { return; } } if (event.stoppedPropagation) { return; } } } } }; // src/Core/UI/Components/TimerComponent.ts var TimerComponent = class extends AbstractComponent { remainingTime; paused = false; interval; event = new EventTarget(); element; constructor(duration, description) { super(); this.remainingTime = parseInt(duration) * (duration.includes("s") ? 1 : duration.includes("m") ? 60 : 3600); this.element = parseHTML( cleanupHTML(` <div class="ntv__timer"> <div class="ntv__timer__body"> <div class="ntv__timer__duration">${this.formatTime(this.remainingTime)}</div> <div class="ntv__timer__description">${description || ""}</div> </div> <div class="ntv__timer__buttons"> <button class="ntv__timer__pause ntv__icon-button"> <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 20 20"> <path fill="currentColor" d="M5 4h3v12H5zm7 0h3v12h-3z" /> </svg> <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 16 16"> <path fill="currentColor" d="M10.804 8L5 4.633v6.734zm.792-.696a.802.802 0 0 1 0 1.392l-6.363 3.692C4.713 12.69 4 12.345 4 11.692V4.308c0-.653.713-.998 1.233-.696z" /> </svg> </button> <button class="ntv__timer__remove ntv__icon-button"> <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 50 50"> <path fill="currentColor" d="m37.304 11.282l1.414 1.414l-26.022 26.02l-1.414-1.413z" /> <path fill="currentColor" d="m12.696 11.282l26.022 26.02l-1.414 1.415l-26.022-26.02z" /> </svg> </button> </div> </div> `), true ); } render() { } attachEventHandlers() { const pauseButton = this.element.querySelector(".ntv__timer__pause"); const removeButton = this.element.querySelector(".ntv__timer__remove"); pauseButton.addEventListener("click", () => { if (this.paused) { this.paused = false; pauseButton.classList.remove("ntv__timer__pause--paused"); this.startTimer(); this.event.dispatchEvent(new CustomEvent("unpaused")); } else { this.paused = true; pauseButton.classList.add("ntv__timer__pause--paused"); if (this.interval) { clearInterval(this.interval); delete this.interval; } this.event.dispatchEvent(new CustomEvent("paused")); } }); removeButton.addEventListener("click", () => { this.event.dispatchEvent(new CustomEvent("destroy")); this.element.remove(); }); this.startTimer(); } startTimer() { const durationEl = this.element.querySelector(".ntv__timer__duration"); this.interval = setInterval(() => { this.remainingTime--; durationEl.textContent = this.formatTime(this.remainingTime); if (this.remainingTime <= 0) { durationEl?.classList.add("ntv__timer__duration--expired"); } }, 1e3); } formatTime(time) { const sign = time < 0 ? "-" : ""; time = Math.abs(time); const hours = Math.floor(time / 3600); const minutes = Math.floor(time % 3600 / 60); const seconds = time % 60; return `${sign}${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`; } }; // src/Core/Input/MessagesHistory.ts var MessagesHistory = class { messages; cursorIndex; maxMessages; constructor() { this.messages = []; this.cursorIndex = -1; this.maxMessages = 50; } addMessage(message) { if (message === "") return; if (this.messages[0] === message) return; this.messages.unshift(message); if (this.messages.length > this.maxMessages) { this.messages.pop(); } } canMoveCursor(direction) { if (direction === 1) { return this.cursorIndex < this.messages.length - 1; } else if (direction === -1) { return this.cursorIndex > 0; } } moveCursor(direction) { this.cursorIndex += direction; if (this.cursorIndex < 0) { this.cursorIndex = 0; } else if (this.cursorIndex >= this.messages.length) { this.cursorIndex = this.messages.length - 1; } } moveCursorUp() { if (this.cursorIndex < this.messages.length - 1) { this.cursorIndex++; } } moveCursorDown() { if (this.cursorIndex > 0) { this.cursorIndex--; } } isCursorAtStart() { return this.cursorIndex === -1; } getMessage() { return this.messages[this.cursorIndex]; } resetCursor() { this.cursorIndex = -1; } }; // src/Core/UI/Components/SteppedInputSliderComponent.ts var SteppedInputSliderComponent = class extends AbstractComponent { value; labels; steps; event = new EventTarget(); element; constructor(labels, steps, value) { super(); this.labels = labels; this.steps = steps; const defaultIndex = typeof value !== "undefined" && steps.indexOf(value) || 0; this.element = parseHTML( cleanupHTML(` <div class="ntv__stepped-input-slider"> <input type="range" min="0" max="${this.steps.length - 1}" step="1" value="${defaultIndex}"> <div>${this.labels[defaultIndex]}</div> </div> `), true ); this.value = value || steps[0]; } render() { } attachEventHandlers() { if (!this.element) return; const input = this.element.querySelector("input"); const label = this.element.querySelector("div"); input.addEventListener("input", () => { label.textContent = this.labels[parseInt(input.value)]; this.value = this.steps[parseInt(input.value)] || this.steps[0]; this.event.dispatchEvent(new Event("change")); }); } addEventListener(event, callback) { this.event.addEventListener(event, callback); } getValue() { return this.value; } }; // src/Core/Users/UserInfoModal.ts var logger19 = new Logger(); var { log: log18, info: info16, error: error19 } = logger19.destruct(); var UserInfoModal = class extends AbstractModal { rootContext; session; toaster; username; userInfo; userChannelInfo; badgesEl; messagesHistoryEl; actionGiftEl; actionFollowEl; actionMuteEl; actionReportEl; timeoutPageEl; statusPageEl; modActionButtonBanEl; modActionButtonTimeoutEl; modActionButtonVIPEl; modActionButtonModEl; modLogsMessagesEl; modLogsPageEl; timeoutSliderComponent; messagesHistoryCursor = 0; isLoadingMessages = false; giftSubButtonEnabled = false; constructor(rootContext, session, { toaster }, username, coordinates) { const modalWidth = 340; const modalHeight = modalWidth * 1.618; if (coordinates) { const screenWidth = window.innerWidth; if (screenWidth < modalWidth) coordinates.x = 0; else if (screenWidth - coordinates.x < modalWidth) coordinates.x = screenWidth - modalWidth; else if (coordinates.x < 0) coordinates.x = 0; const screenHeight = window.innerHeight; if (screenHeight < modalHeight) coordinates.y = 0; else if (coordinates.y < 0) coordinates.y = 0; else if (coordinates.y > screenHeight - modalHeight) coordinates.y = screenHeight - modalHeight; } const geometry = { width: modalWidth + "px", position: coordinates ? "coordinates" : "chat-top", coords: coordinates }; super("user-info", geometry); this.rootContext = rootContext; this.session = session; this.toaster = toaster; this.username = username; } init() { super.init(); return this; } async render() { super.render(); const { channelData, usersManager, badgeProvider } = this.session; const isModerator = channelData.me.isSuperAdmin || channelData.me.isModerator || channelData.me.isBroadcaster; await this.updateUserInfo(); const userInfo = this.userInfo || { id: "", slug: "error", username: "Error", createdAt: null, isFollowing: false, profilePic: "", bannerImg: "" }; const userChannelInfo = this.userChannelInfo || { id: "", username: "Error", slug: "error", channel: "Error", badges: [], followingSince: null, isChannelOwner: false, isModerator: false, isStaff: false }; const today = +new Date((/* @__PURE__ */ new Date()).toLocaleDateString()); let formattedAccountDate; if (userInfo.createdAt) { const createdDate = userInfo.createdAt.toLocaleDateString(); const createdDateUnix = +new Date(createdDate); if (+createdDateUnix === today) formattedAccountDate = "Today"; else if (+createdDateUnix === today - 24 * 60 * 60 * 1e3) formattedAccountDate = "Yesterday"; else formattedAccountDate = formatRelativeTime(userInfo.createdAt); } let formattedJoinDate; if (userChannelInfo.followingSince) { const joinedDate = userChannelInfo.followingSince.toLocaleDateString(); const joinedDateUnix = +new Date(joinedDate); if (+joinedDateUnix === today) formattedJoinDate = "Today"; else if (+joinedDateUnix === today - 24 * 60 * 60 * 1e3) formattedJoinDate = "Yesterday"; else formattedJoinDate = formatRelativeTime(userChannelInfo.followingSince); } const element = parseHTML( cleanupHTML(` <div class="ntv__user-info-modal__header" ${userInfo.bannerImg ? `style="--background: url('${userInfo.bannerImg}')"` : ""}> <div class="ntv__user-info-modal__header__actions"> </div> <div class="ntv__user-info-modal__header__banner"> <div class="ntv__user-info-modal__header__banner__img"><img src="${userInfo.profilePic}"></div> <h4><a href="/${userChannelInfo.slug}" target="_blank">${userInfo.username}</a></h4> <div class="ntv__user-info-modal__header__banner__dates"> ${formattedAccountDate ? `<span> <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0.5 0 24 21"> <g fill="none" stroke="currentColor" stroke-width="1.5"> <path d="M12 10H18C19.1046 10 20 10.8954 20 12V21H12" /> <path d="M12 21H4V12C4 10.8954 4.89543 10 6 10H12" /> <path stroke-linecap="round" stroke-linejoin="round" d="M12 10V8" /> <path d="M4 16H5C7 16 8.5 14 8.5 14C8.5 14 10 16 12 16C14 16 15.5 14 15.5 14C15.5 14 17 16 19 16H20" /> </g> <path fill="currentColor" d="M14 4C14 5.10457 13.1046 6 12 6C10.8954 6 10 5.10457 10 4C10 2.89543 12 0 12 0C12 0 14 2.89543 14 4Z" /> </svg> Account created: ${formattedAccountDate}</span>` : ""} ${`<span> <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 32 32"> <path fill="currentColor" d="M32 14h-4v-4h-2v4h-4v2h4v4h2v-4h4zM12 4a5 5 0 1 1-5 5a5 5 0 0 1 5-5m0-2a7 7 0 1 0 7 7a7 7 0 0 0-7-7m10 28h-2v-5a5 5 0 0 0-5-5H9a5 5 0 0 0-5 5v5H2v-5a7 7 0 0 1 7-7h6a7 7 0 0 1 7 7z" /> </svg> Following since: ${formattedJoinDate ? formattedJoinDate : "-"}</span>`} </div> </div> </div> <div class="ntv__user-info-modal__badges">${userChannelInfo.badges.length ? "Badges: " : ""}${userChannelInfo.badges.map(badgeProvider.getBadge.bind(badgeProvider)).join("")}</div> <div class="ntv__user-info-modal__actions"> <button class="ntv__button ntv__user-info-modal__follow">${userInfo.isFollowing ? "Unfollow" : "Follow"}</button> <button class="ntv__button ntv__user-info-modal__mute">${// TODO change to user ID after replacing chat system usersManager.hasMutedUser(userInfo.username) ? "Unmute" : "Mute"}</button> <!--<button class="ntv__button ntv__user-info-modal__Report">Report</button>--> </div> <div class="ntv__user-info-modal__mod-actions"></div> <div class="ntv__user-info-modal__timeout-page"></div> <div class="ntv__user-info-modal__status-page"></div> <div class="ntv__user-info-modal__mod-logs"></div> <div class="ntv__user-info-modal__mod-logs-page"></div> `) ); this.badgesEl = element.querySelector(".ntv__user-info-modal__badges"); this.actionFollowEl = element.querySelector( ".ntv__user-info-modal__actions .ntv__user-info-modal__follow" ); this.actionMuteEl = element.querySelector( ".ntv__user-info-modal__actions .ntv__user-info-modal__mute" ); if (isModerator) { this.actionReportEl = element.querySelector( ".ntv__user-info-modal__actions .ntv__button:nth-child(3)" ); this.timeoutPageEl = element.querySelector(".ntv__user-info-modal__timeout-page"); this.statusPageEl = element.querySelector(".ntv__user-info-modal__status-page"); this.modActionButtonBanEl = parseHTML( cleanupHTML(` <button class="ntv__icon-button" alt="Ban ${userInfo.username}" ${userChannelInfo.banned ? "active" : ""}> <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"> <path fill="currentColor" d="M12 2c5.5 0 10 4.5 10 10s-4.5 10-10 10S2 17.5 2 12S6.5 2 12 2m0 2c-1.9 0-3.6.6-4.9 1.7l11.2 11.2c1-1.4 1.7-3.1 1.7-4.9c0-4.4-3.6-8-8-8m4.9 14.3L5.7 7.1C4.6 8.4 4 10.1 4 12c0 4.4 3.6 8 8 8c1.9 0 3.6-.6 4.9-1.7" /> </svg> </button> `), true ); this.modActionButtonTimeoutEl = parseHTML( cleanupHTML(` <button class="ntv__icon-button" alt="Timeout ${userInfo.username}"> <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"> <g fill="none"> <path d="M24 0v24H0V0zM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035c-.01-.004-.019-.001-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427c-.002-.01-.009-.017-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093c.012.004.023 0 .029-.008l.004-.014l-.034-.614c-.003-.012-.01-.02-.02-.022m-.715.002a.023.023 0 0 0-.027.006l-.006.014l-.034.614c0 .012.007.02.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z" /> <path fill="currentColor" d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12S6.477 2 12 2m0 2a8 8 0 1 0 0 16a8 8 0 0 0 0-16m0 2a1 1 0 0 1 .993.883L13 7v4.586l2.707 2.707a1 1 0 0 1-1.32 1.497l-.094-.083l-3-3a1 1 0 0 1-.284-.576L11 12V7a1 1 0 0 1 1-1" /> </g> </svg> </button> `), true ); this.modActionButtonVIPEl = parseHTML( cleanupHTML(` <button class="ntv__icon-button" alt="VIP ${userInfo.username}" ${this.isUserVIP() ? "active" : ""}> <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"> <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 5h18M3 19h18M4 9l2 6h1l2-6m3 0v6m4 0V9h2a2 2 0 1 1 0 4h-2" /> </svg> </button> `), true ); this.modActionButtonModEl = parseHTML( cleanupHTML(` <button class="ntv__icon-button" alt="Mod ${userInfo.username}" ${this.isUserPrivileged() ? "active" : ""}> <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"> <path fill="currentColor" d="M12 22q-3.475-.875-5.738-3.988T4 11.1V5l8-3l8 3v5.675q-.475-.2-.975-.363T18 10.076V6.4l-6-2.25L6 6.4v4.7q0 1.175.313 2.35t.875 2.238T8.55 17.65t1.775 1.5q.275.8.725 1.525t1.025 1.3q-.025 0-.037.013T12 22m5 0q-2.075 0-3.537-1.463T12 17t1.463-3.537T17 12t3.538 1.463T22 17t-1.463 3.538T17 22m-.5-2h1v-2.5H20v-1h-2.5V14h-1v2.5H14v1h2.5z" /> </svg> </button> `), true ); this.updateModStatusPage(); const modActionsEl = element.querySelector(".ntv__user-info-modal__mod-actions"); modActionsEl.append( this.modActionButtonBanEl, this.modActionButtonTimeoutEl, this.modActionButtonVIPEl, this.modActionButtonModEl ); this.modLogsPageEl = element.querySelector(".ntv__user-info-modal__mod-logs-page"); this.modLogsMessagesEl = parseHTML(`<button>Messages</button>`, true); const modLogsEl = element.querySelector(".ntv__user-info-modal__mod-logs"); modLogsEl.appendChild(this.modLogsMessagesEl); } this.modalBodyEl.appendChild(element); this.updateGiftSubButton(); } attachEventHandlers() { super.attachEventHandlers(); this.actionFollowEl?.addEventListener("click", this.clickFollowHandler.bind(this)); this.actionMuteEl?.addEventListener("click", this.clickMuteHandler.bind(this)); this.actionReportEl?.addEventListener("click", () => { log18("CORE", "UI", "Report button clicked"); }); this.modActionButtonBanEl?.addEventListener("click", this.clickBanHandler.bind(this)); this.modActionButtonTimeoutEl?.addEventListener("click", this.clickTimeoutHandler.bind(this)); this.modActionButtonVIPEl?.addEventListener("click", this.clickVIPHandler.bind(this)); this.modActionButtonModEl?.addEventListener("click", this.clickModHandler.bind(this)); this.modLogsMessagesEl?.addEventListener("click", this.clickMessagesHistoryHandler.bind(this)); } async clickGiftHandler() { this.eventTarget.dispatchEvent(new Event("gift_sub_click")); } async clickFollowHandler() { const { networkInterface } = this.session; const { userInfo } = this; if (!userInfo) return; this.actionFollowEl.classList.add("ntv__button--disabled"); if (userInfo.isFollowing) { try { await networkInterface.unfollowUser(userInfo.slug); userInfo.isFollowing = false; this.actionFollowEl.textContent = "Follow"; } catch (err) { if (err.errors && err.errors.length > 0) { this.toaster.addToast("Failed to follow user: " + err.errors.join(" "), 6e3, "error"); } else if (err.message) { this.toaster.addToast("Failed to follow user: " + err.message, 6e3, "error"); } else { this.toaster.addToast("Failed to follow user, reason unknown", 6e3, "error"); } } } else { try { await networkInterface.followUser(userInfo.slug); userInfo.isFollowing = true; this.actionFollowEl.textContent = "Unfollow"; } catch (err) { if (err.errors && err.errors.length > 0) { this.toaster.addToast("Failed to unfollow user: " + err.errors.join(" "), 6e3, "error"); } else if (err.message) { this.toaster.addToast("Failed to unfollow user: " + err.message, 6e3, "error"); } else { this.toaster.addToast("Failed to unfollow user, reason unknown", 6e3, "error"); } } } this.actionFollowEl.classList.remove("ntv__button--disabled"); } async clickMuteHandler() { const { userInfo } = this; if (!userInfo) return; const { id, username } = userInfo; const { usersManager, channelData } = this.session; const channelId = channelData.channelId; const user = usersManager.getUserById(username); if (!user) return; if (user.muted) { log18("CORE", "UI", "Unmuting user:", username); usersManager.unmuteUserById(user.id); this.actionMuteEl.textContent = "Mute"; } else { log18("CORE", "UI", "Muting user:", username); usersManager.muteUserById(user.id, channelId); this.actionMuteEl.textContent = "Unmute"; } } async clickTimeoutHandler() { const { timeoutPageEl } = this; if (!timeoutPageEl) return; while (timeoutPageEl.firstChild) timeoutPageEl.firstChild.remove(); if (this.timeoutSliderComponent) { delete this.timeoutSliderComponent; return; } const timeoutWrapperEl = parseHTML( cleanupHTML(` <div class="ntv__user-info-modal__timeout-page__wrapper"> <div></div> <button class="ntv__button">></button> <textarea placeholder="Reason" rows="1" capture-focus></textarea> </div>`), true ); timeoutPageEl.appendChild(timeoutWrapperEl); const rangeWrapperEl = timeoutWrapperEl.querySelector( ".ntv__user-info-modal__timeout-page__wrapper div" ); this.timeoutSliderComponent = new SteppedInputSliderComponent( ["5 minutes", "15 minutes", "1 hour", "1 day", "1 week"], [5, 15, 60, 60 * 24, 60 * 24 * 7] ).init(); rangeWrapperEl.appendChild(this.timeoutSliderComponent.element); const buttonEl = timeoutWrapperEl.querySelector("button"); buttonEl.addEventListener("click", async () => { if (!this.timeoutSliderComponent) return; const duration = this.timeoutSliderComponent.getValue(); const reason = timeoutWrapperEl.querySelector("textarea").value; timeoutPageEl.setAttribute("disabled", ""); try { await this.session.networkInterface.executeCommand("timeout", this.session.channelData.channelName, [ this.username, duration, reason ]); await this.updateUserInfo(); } catch (err) { if (err.errors && err.errors.length > 0) { this.toaster.addToast("Failed to timeout user: " + err.errors.join(" "), 6e3, "error"); } else if (err.message) { this.toaster.addToast("Failed to timeout user: " + err.message, 6e3, "error"); } else { this.toaster.addToast("Failed to timeout user, reason unknown", 6e3, "error"); } timeoutPageEl.removeAttribute("disabled"); return; } this.modActionButtonBanEl.setAttribute("active", ""); while (timeoutPageEl.firstChild) timeoutPageEl.firstChild.remove(); timeoutPageEl.removeAttribute("disabled"); delete this.timeoutSliderComponent; this.updateModStatusPage(); log18("CORE", "UI", `Successfully timed out user: ${this.username} for ${duration} minutes`); }); } async clickVIPHandler() { const { networkInterface } = this.session; const { userInfo, userChannelInfo } = this; if (!userInfo || !userChannelInfo) return; const { channelData } = this.session; if (!channelData.me.isBroadcaster && !channelData.me.isSuperAdmin) { this.toaster.addToast("You do not have permission to perform this action.", 6e3, "error"); return; } this.modActionButtonVIPEl.classList.add("ntv__icon-button--disabled"); if (this.isUserVIP()) { log18("CORE", "UI", `Attempting to remove VIP status from user: ${userInfo.username}..`); try { await this.session.networkInterface.executeCommand("unvip", this.session.channelData.channelName, [ userInfo.username ]); log18("CORE", "UI", "Successfully removed VIP status from user:", userInfo.username); } catch (err) { if (err.errors && err.errors.length > 0) { this.toaster.addToast( "Failed to remove VIP status from user: " + err.errors.join(" "), 6e3, "error" ); } else if (err.message) { this.toaster.addToast("Failed to remove VIP status from user: " + err.message, 6e3, "error"); } else { this.toaster.addToast("Failed to remove VIP status from user, reason unknown", 6e3, "error"); } this.modActionButtonVIPEl.classList.remove("ntv__icon-button--disabled"); return; } this.removeUserVIPStatus(); this.modActionButtonVIPEl?.removeAttribute("active"); } else { log18("CORE", "UI", `Attempting to give VIP status to user: ${userInfo.username}..`); try { await this.session.networkInterface.executeCommand("vip", this.session.channelData.channelName, [ userInfo.username ]); log18("CORE", "UI", "Successfully gave VIP status to user:", userInfo.username); } catch (err) { if (err.errors && err.errors.length > 0) { this.toaster.addToast("Failed to give VIP status to user: " + err.errors.join(" "), 6e3, "error"); } else if (err.message) { this.toaster.addToast("Failed to give VIP status to user: " + err.message, 6e3, "error"); } else { this.toaster.addToast("Failed to give VIP status to user, reason unknown", 6e3, "error"); } this.modActionButtonVIPEl.classList.remove("ntv__icon-button--disabled"); return; } this.modActionButtonVIPEl?.setAttribute("active", ""); await this.updateUserInfo(); } this.updateUserBadges(); this.modActionButtonVIPEl.classList.remove("ntv__icon-button--disabled"); } async clickModHandler() { const { networkInterface } = this.session; const { userInfo, userChannelInfo } = this; if (!userInfo || !userChannelInfo) return; const { channelData } = this.session; if (!channelData.me.isBroadcaster && !channelData.me.isSuperAdmin) { this.toaster.addToast("You do not have permission to perform this action.", 6e3, "error"); return; } this.modActionButtonModEl.classList.add("ntv__icon-button--disabled"); if (this.isUserPrivileged()) { log18("CORE", "UI", `Attempting to remove mod status from user: ${userInfo.username}..`); try { await this.session.networkInterface.executeCommand("unmod", this.session.channelData.channelName, [ userInfo.username ]); log18("CORE", "UI", "Successfully removed mod status from user:", userInfo.username); } catch (err) { if (err.errors && err.errors.length > 0) { this.toaster.addToast( "Failed to remove mod status from user: " + err.errors.join(" "), 6e3, "error" ); } else if (err.message) { this.toaster.addToast("Failed to remove mod status from user: " + err.message, 6e3, "error"); } else { this.toaster.addToast("Failed to remove mod status from user, reason unknown", 6e3, "error"); } this.modActionButtonModEl.classList.remove("ntv__icon-button--disabled"); return; } this.removeUserModStatus(); this.modActionButtonModEl?.removeAttribute("active"); } else { log18("CORE", "UI", `Attempting to give mod status to user: ${userInfo.username}..`); try { await this.session.networkInterface.executeCommand("mod", this.session.channelData.channelName, [ userInfo.username ]); log18("CORE", "UI", "Successfully gave mod status to user:", userInfo.username); } catch (err) { if (err.errors && err.errors.length > 0) { this.toaster.addToast("Failed to give mod status to user: " + err.errors.join(" "), 6e3, "error"); } else if (err.message) { this.toaster.addToast("Failed to give mod status to user: " + err.message, 6e3, "error"); } else { this.toaster.addToast("Failed to give mod status to user, reason unknown", 6e3, "error"); } this.modActionButtonModEl.classList.remove("ntv__icon-button--disabled"); return; } this.modActionButtonModEl?.setAttribute("active", ""); await this.updateUserInfo(); } this.updateUserBadges(); this.modActionButtonModEl.classList.remove("ntv__icon-button--disabled"); } async clickBanHandler() { if (this.modActionButtonBanEl.classList.contains("ntv__icon-button--disabled")) return; this.modActionButtonBanEl.classList.add("ntv__icon-button--disabled"); const { networkInterface } = this.session; const { userInfo, userChannelInfo } = this; if (!userInfo || !userChannelInfo) return; if (userChannelInfo.banned) { log18("CORE", "UI", `Attempting to unban user: ${userInfo.username}..`); try { await this.session.networkInterface.executeCommand("unban", this.session.channelData.channelName, [ userInfo.username ]); log18("CORE", "UI", "Successfully unbanned user:", userInfo.username); } catch (err) { if (err.errors && err.errors.length > 0) { this.toaster.addToast("Failed to unban user: " + err.errors.join(" "), 6e3, "error"); } else if (err.message) { this.toaster.addToast("Failed to unban user: " + err.message, 6e3, "error"); } else { this.toaster.addToast("Failed to unban user, reason unknown", 6e3, "error"); } this.modActionButtonBanEl.classList.remove("ntv__icon-button--disabled"); return; } delete userChannelInfo.banned; this.modActionButtonBanEl.removeAttribute("active"); } else { log18("CORE", "UI", `Attempting to ban user: ${userInfo.username}..`); try { await this.session.networkInterface.executeCommand("ban", this.session.channelData.channelName, [ userInfo.username ]); log18("CORE", "UI", "Successfully banned user:", userInfo.username); } catch (err) { if (err.errors && err.errors.length > 0) { this.toaster.addToast("Failed to ban user: " + err.errors.join(" "), 6e3, "error"); } else if (err.message) { this.toaster.addToast("Failed to ban user: " + err.message, 6e3, "error"); } else { this.toaster.addToast("Failed to ban user, reason unknown", 6e3, "error"); } this.modActionButtonBanEl.classList.remove("ntv__icon-button--disabled"); return; } this.modActionButtonBanEl.setAttribute("active", ""); await this.updateUserInfo(); } this.updateModStatusPage(); this.modActionButtonBanEl.classList.remove("ntv__icon-button--disabled"); } async clickMessagesHistoryHandler() { const { userInfo, modLogsPageEl } = this; if (!userInfo || !modLogsPageEl) return; if (modLogsPageEl.querySelector(".ntv__user-info-modal__mod-logs-page__messages[loading]")) return; while (modLogsPageEl.firstChild) modLogsPageEl.firstChild.remove(); this.messagesHistoryCursor = 0; const messagesHistoryEl = this.messagesHistoryEl = parseHTML( `<div class="ntv__user-info-modal__mod-logs-page__messages" loading></div>`, true ); modLogsPageEl.appendChild(messagesHistoryEl); log18("CORE", "UI", `Fetching user messages of ${userInfo.username}..`); await this.loadMoreMessagesHistory(); let autoLoadCount = 0; const MAX_AUTO_LOADS = 5; while (this.messagesHistoryCursor !== null && messagesHistoryEl.scrollHeight <= messagesHistoryEl.clientHeight && !this.isLoadingMessages && autoLoadCount < MAX_AUTO_LOADS) { log18( "CORE", "UI", `Content too short (scrollHeight: ${messagesHistoryEl.scrollHeight}, clientHeight: ${messagesHistoryEl.clientHeight}), auto-loading more messages for ${userInfo.username}...` ); const previousScrollHeight = messagesHistoryEl.scrollHeight; await this.loadMoreMessagesHistory(); autoLoadCount++; if (this.messagesHistoryCursor === null || messagesHistoryEl.scrollHeight === previousScrollHeight) { log18( "CORE", "UI", `Auto-load break: cursor is ${this.messagesHistoryCursor}, scrollHeight changed from ${previousScrollHeight} to ${messagesHistoryEl.scrollHeight}` ); break; } } if (autoLoadCount >= MAX_AUTO_LOADS && this.messagesHistoryCursor !== null && messagesHistoryEl.scrollHeight <= messagesHistoryEl.clientHeight) { log18( "CORE", "UI", `Max auto-loads (${MAX_AUTO_LOADS}) reached for ${userInfo.username}, but content may still be too short.` ); } if (messagesHistoryEl.scrollHeight > messagesHistoryEl.clientHeight) { messagesHistoryEl.scrollTop = messagesHistoryEl.scrollHeight - messagesHistoryEl.clientHeight; } else { messagesHistoryEl.scrollTop = 0; } messagesHistoryEl.removeAttribute("loading"); messagesHistoryEl.addEventListener("scroll", this.messagesScrollHandler.bind(this)); } async loadMoreMessagesHistory() { const { networkInterface, emotesManager, userInterface, channelData } = this.session; const { userInfo, modLogsPageEl, messagesHistoryEl } = this; if (!userInfo || !modLogsPageEl || !messagesHistoryEl) return; const cursor = this.messagesHistoryCursor; if (typeof cursor !== "number") return; if (this.isLoadingMessages) return; this.isLoadingMessages = true; let res; try { res = await networkInterface.getUserMessages(channelData.channelId, userInfo.id, cursor); } catch (err) { if (err.errors && err.errors.length > 0) { this.toaster.addToast("Failed to load user message history: " + err.errors.join(" "), 6e3, "error"); } else if (err.message) { this.toaster.addToast("Failed to load user message history: " + err.message, 6e3, "error"); } else { this.toaster.addToast("Failed to load user message history, reason unknown", 6e3, "error"); } messagesHistoryEl.removeAttribute("loading"); return; } this.messagesHistoryCursor = res.cursor ? +res.cursor : null; let entriesHTML = "", lastDate, dateCursor; for (const message of res.messages) { const d = new Date(message.createdAt); const time = ("" + d.getHours()).padStart(2, "0") + ":" + ("" + d.getMinutes()).padStart(2, "0"); const dateString = d.getUTCFullYear() + "" + d.getUTCMonth() + d.getUTCDay(); if (lastDate && dateString !== dateCursor) { const formattedDate = lastDate.toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric" }); dateCursor = dateString; lastDate = d; entriesHTML += `<div class="ntv__chat-message-separator ntv__chat-message-separator--date"><div></div><span>${formattedDate}</span><div></div></div>`; } else if (!lastDate) { lastDate = d; dateCursor = dateString; } entriesHTML += `<div class="ntv__chat-message" unrendered> <span class="ntv__chat-message__identity"> <span class="ntv__chat-message__timestamp">${time}</span> <span class="ntv__chat-message__badges"></span> <span class="ntv__chat-message__username" style="color:${message.sender.color}">${message.sender.username}</span> <span class="ntv__chat-message__separator">: </span> </span> <span class="ntv__chat-message__part">${message.content}</span> </div>`; } if (!this.messagesHistoryCursor && lastDate) { const formattedDate = lastDate.toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric" }); entriesHTML += `<div class="ntv__chat-message-separator ntv__chat-message-separator--date"><div></div><span>${formattedDate}</span><div></div></div><span class="ntv__chat-message-separator ntv__chat-message-separator--start">Start of user's messages</span>`; } messagesHistoryEl.append(parseHTML(cleanupHTML(entriesHTML))); messagesHistoryEl.querySelectorAll(".ntv__chat-message[unrendered]").forEach((messageEl) => { messageEl.querySelectorAll(".ntv__chat-message__part").forEach((messagePartEl) => { const parsedMessageParts = emotesManager.parseEmoteText(messagePartEl.textContent || ""); const nodes = userInterface.renderMessageParts(parsedMessageParts); messagePartEl.after(...nodes); messagePartEl.remove(); }); messageEl.removeAttribute("unrendered"); }); this.isLoadingMessages = false; messagesHistoryEl.removeAttribute("loading"); } async messagesScrollHandler(event) { const target = event.currentTarget; if (target.scrollTop < 30 && this.messagesHistoryCursor !== null && !this.isLoadingMessages) { await this.loadMoreMessagesHistory(); await this.loadMoreMessagesHistory(); } } enableGiftSubButton() { this.giftSubButtonEnabled = true; this.updateGiftSubButton(); } updateGiftSubButton() { if (!this.giftSubButtonEnabled) return; if (this.isUserSubscribed()) { if (!this.actionGiftEl) return; this.actionGiftEl.remove(); delete this.actionGiftEl; } else { if (this.actionGiftEl) return; const actionsEl = this.modalBodyEl.querySelector(".ntv__user-info-modal__actions"); if (!actionsEl) return; this.actionGiftEl = parseHTML( `<button class="ntv__button ntv__user-info-modal__gift">Gift a sub</button>`, true ); actionsEl.prepend(this.actionGiftEl); this.actionGiftEl.addEventListener("click", this.clickGiftHandler.bind(this)); } } isUserSubscribed() { return !!this.userChannelInfo?.badges.find((badge) => badge.type === "subscriber"); } // TODO move this to dedicated class with methods isUserVIP() { return !!this.userChannelInfo?.badges.find((badge) => badge.type === "vip"); } // TODO move this to dedicated class with methods isUserPrivileged() { return this.userChannelInfo?.isChannelOwner || this.userChannelInfo?.isModerator || this.userChannelInfo?.isStaff; } // TODO move this to dedicated class with methods removeUserVIPStatus() { if (!this.userChannelInfo) return; this.userChannelInfo.badges = this.userChannelInfo.badges.filter((badge) => badge.type !== "vip"); } // TODO move this to dedicated class with methods removeUserModStatus() { if (!this.userChannelInfo) return; this.userChannelInfo.isModerator = false; this.userChannelInfo.badges = this.userChannelInfo.badges.filter((badge) => badge.type !== "moderator"); } async updateUserInfo() { const { networkInterface } = this.session; const { channelData } = this.session; try { delete this.userInfo; delete this.userChannelInfo; this.userChannelInfo = await networkInterface.getUserChannelInfo(channelData.channelName, this.username); this.userInfo = await networkInterface.getUserInfo(this.userChannelInfo.slug); this.updateGiftSubButton(); } catch (err) { if (err.errors && err.errors.length > 0) { this.toaster.addToast("Failed to get user info: " + err.errors.join(" "), 6e3, "error"); } else if (err.message) { this.toaster.addToast("Failed to get user info: " + err.message, 6e3, "error"); } else { this.toaster.addToast("Failed to get user info, reason unknown", 6e3, "error"); } } } updateUserBadges() { const { badgeProvider } = this.session; const { badgesEl, userChannelInfo } = this; if (!badgesEl || !userChannelInfo) return; badgesEl.innerHTML = userChannelInfo.badges.length ? "Badges: " + userChannelInfo.badges.map(badgeProvider.getBadge.bind(badgeProvider)).join("") : ""; } updateModStatusPage() { const { userChannelInfo, statusPageEl } = this; if (!userChannelInfo || !statusPageEl) return; if (userChannelInfo.banned) { statusPageEl.innerHTML = cleanupHTML(` <div class="ntv__user-info-modal__status-page__banned"> <span><b>Banned</b></span> <span>Reason: ${userChannelInfo.banned.reason}</span> <span>Expires: ${userChannelInfo.banned.expiresAt ? formatRelativeTime(userChannelInfo.banned.expiresAt) : "Not set"}</span> </div> `); } else { while (statusPageEl.firstChild) statusPageEl.firstChild.remove(); } } }; // src/Core/UI/Modals/PollModal.ts var PollModal = class extends AbstractModal { rootContext; session; toaster; pollQuestionEl; pollOptionsEls; durationSliderComponent; displayDurationSliderComponent; createButtonEl; cancelButtonEl; constructor(rootContext, session, { toaster }) { const geometry = { width: "340px", position: "center" }; super("poll", geometry); this.rootContext = rootContext; this.session = session; this.toaster = toaster; } init() { super.init(); return this; } async render() { super.render(); const element = parseHTML( cleanupHTML(` <h3 class="ntv__poll-modal__title">Create a new Poll</h3> <span class="ntv__poll-modal__subtitle">Question:</span> <textarea rows="1" class="ntv__input ntv__poll-modal__q-input" placeholder="Poll question" capture-focus></textarea> <span class="ntv__poll-modal__subtitle">Options (minimum 2):</span> <input type="text" class="ntv__input ntv__poll-modal__o-input" placeholder="Option 1" capture-focus> <input type="text" class="ntv__input ntv__poll-modal__o-input" placeholder="Option 2" capture-focus> <input type="text" class="ntv__input ntv__poll-modal__o-input" placeholder="Option 3" capture-focus disabled> <input type="text" class="ntv__input ntv__poll-modal__o-input" placeholder="Option 4" capture-focus disabled> <input type="text" class="ntv__input ntv__poll-modal__o-input" placeholder="Option 5" capture-focus disabled> <input type="text" class="ntv__input ntv__poll-modal__o-input" placeholder="Option 6" capture-focus disabled> <span class="ntv__poll-modal__subtitle">Duration</span> <div class="ntv__poll-modal__duration"></div> <span class="ntv__poll-modal__subtitle">Result displayed for</span> <div class="ntv__poll-modal__display-duration"></div> <div class="ntv__poll-modal__footer"> <button class="ntv__button ntv__button--regular ntv__poll-modal__close-btn">Cancel</button> <button class="ntv__button ntv__poll-modal__create-btn">Create</button> </div>`) ); this.pollQuestionEl = element.querySelector(".ntv__poll-modal__q-input"); this.pollOptionsEls = element.querySelectorAll(".ntv__poll-modal__o-input"); const durationWrapper = element.querySelector(".ntv__poll-modal__duration"); this.durationSliderComponent = new SteppedInputSliderComponent( ["30 seconds", "1 minute", "2 minutes", "3 minutes", "4 minutes", "5 minutes"], [30, 60, 120, 180, 240, 300] ).init(); durationWrapper.appendChild(this.durationSliderComponent.element); const displayDurationWrapper = element.querySelector(".ntv__poll-modal__display-duration"); this.displayDurationSliderComponent = new SteppedInputSliderComponent( ["30 seconds", "1 minute", "2 minutes", "3 minutes", "4 minutes", "5 minutes"], [30, 60, 120, 180, 240, 300] ).init(); displayDurationWrapper.appendChild(this.displayDurationSliderComponent.element); this.createButtonEl = element.querySelector(".ntv__poll-modal__create-btn"); this.cancelButtonEl = element.querySelector(".ntv__poll-modal__close-btn"); this.modalBodyEl.appendChild(element); } attachEventHandlers() { super.attachEventHandlers(); for (let i = 1; i < this.pollOptionsEls.length; i++) { this.pollOptionsEls[i].addEventListener("input", () => { const currentOptionEl = this.pollOptionsEls[i]; const nextOptionEl = this.pollOptionsEls[i + 1]; if (nextOptionEl) { nextOptionEl.disabled = !currentOptionEl.value.trim(); } }); } this.createButtonEl.addEventListener("click", async () => { const question = this.pollQuestionEl.value.trim(); const options = Array.from(this.pollOptionsEls).map((el) => el.value.trim()).filter((option) => !!option); const duration = this.durationSliderComponent.getValue(); const displayDuration = this.displayDurationSliderComponent.getValue(); if (!question) { this.toaster.addToast("Please enter a question", 6e3, "error"); return; } if (options.length < 2) { this.toaster.addToast("Please enter at least 2 options", 6e3, "error"); return; } if (options.some((option) => !option)) { this.toaster.addToast("Please fill in all options", 6e3, "error"); return; } const channelName = this.session.channelData.channelName; this.session.networkInterface.createPoll(channelName, question, options, duration, displayDuration); this.destroy(); }); this.cancelButtonEl.addEventListener("click", async () => { this.destroy(); }); } }; // src/Core/Common/Toaster.ts var Toaster = class { toasts = []; addToast(message, duration, type = "info") { const toastEl = parseHTML( `<div class="ntv__toast ntv__toast--${type} ntv__toast--top-right" aria-live="polite">${message}</div>`, true ); const timeout = Date.now() + duration; const toast = { message, type, timeout, element: toastEl }; this.toasts.push(toast); document.body.appendChild(toastEl); setTimeout(() => { const index = this.toasts.indexOf(toast); if (index !== -1) { this.toasts[index].element.remove(); this.toasts.splice(index, 1); } }, duration); this.moveToasts(); } moveToasts() { const spacing = 20; let y = 20; const toasts = this.toasts.toReversed(); for (const toast of toasts) { toast.element.style.top = `${y}px`; y += toast.element.clientHeight + spacing; } } }; // src/Core/UI/Caret.ts var logger20 = new Logger(); var { log: log19, info: info17, error: error20 } = logger20.destruct(); var Caret = class { static moveCaretTo(container, offset) { const selection = window.getSelection(); if (!selection || !selection.rangeCount) return; const range = document.createRange(); range.setStart(container, offset); selection.removeAllRanges(); selection.addRange(range); } static collapseToEndOfNode(node) { const selection = window.getSelection(); if (!selection) return error20("CORE", "UI", "Unable to get selection, cannot collapse to end of node", node); const range = document.createRange(); if (node instanceof Text) { const offset = node.textContent ? node.textContent.length : 0; range.setStart(node, offset); } else { range.setStartAfter(node); } selection.removeAllRanges(); selection.addRange(range); } static hasNonWhitespaceCharacterBeforeCaret() { const selection = window.getSelection(); if (!selection || !selection.rangeCount) return false; const range = selection.anchorNode ? selection.getRangeAt(0) : null; if (!range) return false; let textContent, offset; const caretIsInTextNode = range.startContainer.nodeType === Node.TEXT_NODE; if (caretIsInTextNode) { textContent = range.startContainer.textContent; offset = range.startOffset - 1; } else { const childNode = range.startContainer.childNodes[range.startOffset - 1]; if (!childNode) return false; if (childNode.nodeType === Node.TEXT_NODE) { textContent = childNode.textContent || ""; offset = textContent.length - 1; } else { return false; } } if (!textContent) return false; const leadingChar = textContent[offset]; return leadingChar !== " " && leadingChar !== "\uFEFF"; } static hasNonWhitespaceCharacterAfterCaret() { const selection = window.getSelection(); if (!selection) return false; const range = selection.anchorNode ? selection.getRangeAt(0) : null; if (!range) return false; let textContent, offset; const caretIsInTextNode = range.startContainer.nodeType === Node.TEXT_NODE; if (caretIsInTextNode) { textContent = range.startContainer.textContent; offset = range.startOffset; } else { const childNode = range.startContainer.childNodes[range.startOffset]; if (!childNode) return false; if (childNode.nodeType === Node.TEXT_NODE) { textContent = childNode.textContent || ""; offset = textContent.length - 1; } else { return false; } } if (!textContent) return false; const trailingChar = textContent[offset]; return trailingChar !== " " && trailingChar !== "\uFEFF"; } // Checks if the caret is at the start of a node static isCaretAtStartOfNode(node) { const selection = window.getSelection(); if (!selection || !selection.rangeCount || !selection.isCollapsed) return false; if (!node.childNodes.length) return true; const { focusNode, focusOffset } = selection; if (focusNode === node && focusOffset === 0) return true; if (focusNode?.parentElement?.classList.contains("ntv__input-component") && !focusNode?.previousSibling && focusOffset === 0) { return true; } else if (focusNode instanceof Text) { return focusNode === node.firstChild && focusOffset === 0; } else { return false; } } static isCaretAtEndOfNode(node) { const selection = window.getSelection(); if (!selection || !selection.rangeCount || !selection.isCollapsed) return false; if (!node.childNodes.length) return true; const { focusNode, focusOffset } = selection; if (focusNode === node && focusOffset === node.childNodes.length) return true; if (focusNode?.parentElement?.classList.contains("ntv__input-component") && !focusNode?.nextSibling && focusOffset === 1) { return true; } else if (focusNode instanceof Text) { return focusNode === node.lastChild && focusOffset === focusNode.textContent?.length; } else { return false; } } static getWordBeforeCaret() { const selection = window.getSelection(); if (!selection || !selection.rangeCount) { return { word: null, start: 0, end: 0, startOffset: 0, node: null }; } const range = selection.getRangeAt(0); if (range.startContainer.nodeType !== Node.TEXT_NODE) { const textNode = range.startContainer.childNodes[range.startOffset - 1]; if (textNode && textNode.nodeType === Node.TEXT_NODE) { const text2 = textNode.textContent || ""; const startOffset = text2.lastIndexOf(" ") + 1; const word2 = text2.slice(startOffset); if (word2) { return { word: word2, start: startOffset, end: text2.length, startOffset: text2.length, node: textNode }; } } return { word: null, start: 0, end: 0, startOffset: 0, node: textNode }; } const text = range.startContainer.textContent || ""; const offset = range.startOffset; let start = offset; while (start > 0 && text[start - 1] !== " ") start--; let end = offset; while (end < text.length && text[end] !== " ") end++; const word = text.slice(start, end); if (word === "") { return { word: null, start: 0, end: 0, startOffset: 0, node: null }; } return { word, start, end, startOffset: offset, node: range.startContainer }; } static getCaretStartOffset() { const selection = window.getSelection(); if (!selection || !selection.rangeCount) return 0; const range = selection.getRangeAt(0); return range.startOffset; } static insertNodeAtCaret(range, node) { if (!node.nodeType || node.nodeType !== Node.ELEMENT_NODE && node.nodeType !== Node.TEXT_NODE) { return error20("CORE", "UI", "Invalid node type", node); } if (range.startContainer.nodeType === Node.TEXT_NODE) { range.insertNode(node); range.startContainer?.parentElement?.normalize(); } else { if (range.startOffset - 1 === -1) { ; range.startContainer.prepend(node); return; } const childNode = range.startContainer.childNodes[range.startOffset - 1]; if (!childNode) { range.startContainer.appendChild(node); return; } childNode.after(node); } } // Replace text at start to end with replacement. // Start and end are the indices of the text node // Replacement can be a string or an element node. static replaceTextInRange(container, start, end, replacement) { if (container.nodeType !== Node.TEXT_NODE) { error20("CORE", "UI", "Invalid container node type", container); return 0; } const text = container.textContent || ""; const halfText = text.slice(0, start) + replacement; container.textContent = halfText + text.slice(end); return halfText.length; } static replaceTextWithElementInRange(container, start, end, replacement) { const text = container.textContent || ""; const before = text.slice(0, start); const after = text.slice(end); container.textContent = before; container.after(replacement, document.createTextNode(after)); } }; // src/Core/Common/Clipboard.ts var logger21 = new Logger(); var { log: log20, info: info18, error: error21 } = logger21.destruct(); function flattenNestedElement(node) { const result = []; function traverse(node2) { if (node2.nodeType === Node.TEXT_NODE) { result.push(node2); } else if (node2.nodeType === Node.ELEMENT_NODE && node2.nodeName === "IMG") { result.push(node2); } else { for (var i = 0; i < node2.childNodes.length; i++) { traverse(node2.childNodes[i]); } } } traverse(node); return result; } var Clipboard2 = class { domParser = new DOMParser(); handleCopyEvent(event) { const selection = document.getSelection(); if (!selection || !selection.rangeCount) return error21("CORE", "UI", "Selection is null"); event.preventDefault(); const fragment = document.createDocumentFragment(); const nodeList = []; for (let i = 0; i < selection.rangeCount; i++) { fragment.append(selection.getRangeAt(i).cloneContents()); } const walker = document.createTreeWalker( fragment, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, (node) => node.nodeType === Node.TEXT_NODE && node.textContent?.trim() || node?.tagName === "IMG" ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP ); let currentNode = walker.currentNode; while (currentNode) { nodeList.push(currentNode); currentNode = walker.nextNode(); } const copyString = nodeList.map((node) => { if (node instanceof Text) { return node.textContent?.trim(); } else if (node instanceof HTMLElement && node.dataset.emoteName) { return node.dataset.emoteName || "UNSET_EMOTE_NAME"; } else if (node instanceof HTMLElement && node.tagName === "IMG" && node.hasAttribute("alt")) { return node.getAttribute("alt"); } }).filter((text) => typeof text === "string" && text.length > 0).join(" ").replaceAll(CHAR_ZWSP, ""); event.clipboardData?.setData("text/plain", copyString); log20("CORE", "UI", `Copied: "${copyString}"`); } handleCutEvent(event) { const selection = document.getSelection(); if (!selection || !selection.rangeCount) return; const range = selection.getRangeAt(0); if (!range) return; const commonAncestorContainer = range.commonAncestorContainer; if (!(commonAncestorContainer instanceof HTMLElement) && !commonAncestorContainer.isContentEditable && !commonAncestorContainer.parentElement.isContentEditable) { return; } event.preventDefault(); this.handleCopyEvent(event); selection.deleteFromDocument(); } paste(text) { const selection = window.getSelection(); if (!selection || !selection.rangeCount) return; selection.deleteFromDocument(); selection.getRangeAt(0).insertNode(document.createTextNode(text)); selection.collapseToEnd(); } pasteHTML(html) { const nodes = Array.from(this.domParser.parseFromString(html, "text/html").body.childNodes); const selection = window.getSelection(); if (!selection || !selection.rangeCount) return; selection.deleteFromDocument(); const range = selection.getRangeAt(0); for (const node of nodes) { Caret.insertNodeAtCaret(range, node); } const lastNode = nodes[nodes.length - 1]; if (lastNode) { if (lastNode.nodeType === Node.TEXT_NODE) { selection.collapse(lastNode, lastNode.length); selection.collapseToEnd(); } else if (lastNode.nodeType === Node.ELEMENT_NODE) { selection.collapse(lastNode, lastNode.childNodes.length); } } } parsePastedMessage(evt) { const clipboardData = evt.clipboardData || window.clipboardData; if (!clipboardData) return []; const html = clipboardData.getData("text/html"); if (html) { const doc = this.domParser.parseFromString(html.replaceAll(CHAR_ZWSP, ""), "text/html"); const childNodes = doc.body.childNodes; if (childNodes.length === 0) { return []; } let startFragmentComment = null, endFragmentComment = null; for (let i = 0; i < childNodes.length; i++) { const node = childNodes[i]; if (node.nodeType === Node.COMMENT_NODE) { if (node.textContent === "StartFragment") { startFragmentComment = i; } else if (node.textContent === "EndFragment") { endFragmentComment = i; } if (startFragmentComment && endFragmentComment) { break; } } } if (startFragmentComment === null || endFragmentComment === null) { error21("CORE", "UI", "Failed to find fragment markers, clipboard data seems to be corrupted."); return []; } const pastedNodes = Array.from(childNodes).slice(startFragmentComment + 1, endFragmentComment); const flattenedNodes = pastedNodes.map(flattenNestedElement).flat(); const parsedNodes = []; for (const node of flattenedNodes) { if (node.nodeType === Node.TEXT_NODE && node.textContent) { parsedNodes.push(node.textContent); } else if (node.nodeType === Node.ELEMENT_NODE && node.nodeName === "IMG") { const emoteName = node.dataset.emoteName; if (emoteName) { parsedNodes.push(emoteName); } } } if (parsedNodes.length) return parsedNodes; return []; } else { const text = clipboardData.getData("text/plain"); if (!text) return []; return [text.replaceAll(CHAR_ZWSP, "")]; } } }; // src/Core/UI/AbstractUserInterface.ts var logger22 = new Logger(); var { log: log21, info: info19, error: error22 } = logger22.destruct(); var AbstractUserInterface = class { rootContext; session; inputController = null; clipboard = new Clipboard2(); toaster = new Toaster(); messageHistory = new MessagesHistory(); submitButtonPriorityEventTarget = new PriorityEventTarget(); celebrationData; replyMessageData; replyMessageComponent; maxMessageLength = 500; /** * @param {EventBus} eventBus * @param {object} deps */ constructor(rootContext, session) { this.rootContext = rootContext; this.session = session; } getInputController() { return this.inputController; } loadInterface() { const { eventBus } = this.session; eventBus.subscribe("ntv.ui.show_modal.user_info", (data) => { assertArgDefined(data.username); this.showUserInfoModal(data.username); }); eventBus.subscribe("ntv.ui.show_modal.poll", () => { new PollModal(this.rootContext, this.session, { toaster: this.toaster }).init(); }); document.addEventListener("mouseover", (evt) => { const target = evt.target; const tooltip = target?.getAttribute("ntv-tooltip"); if (!tooltip) return; const rect = target.getBoundingClientRect(); const left = rect.left + rect.width / 2; const top = rect.top; const tooltipEl = parseHTML( `<div class="ntv__tooltip" style="top: ${top}px; left: ${left}px;">${tooltip}</div>`, true ); document.body.appendChild(tooltipEl); target.addEventListener( "mouseleave", () => { tooltipEl.remove(); }, { once: true, passive: true } ); }); eventBus.subscribe("ntv.ui.timers.add", this.addTimer.bind(this)); } toastSuccess(message) { this.toaster.addToast(message.replaceAll("<", "<"), 6e3, "success"); } toastError(message) { this.toaster.addToast(message.replaceAll("<", "<"), 6e3, "error"); } renderMessageParts(parsedMessageParts) { const result = []; let prevPart = null; for (let index = 0; index < parsedMessageParts.length; index++) { const part = parsedMessageParts[index]; if (typeof part === "string") { result.push(this.createPlainTextMessagePartNode(part)); } else if (part instanceof Node) { const newContentNode = document.createElement("span"); newContentNode.classList.add("ntv__chat-message__part"); newContentNode.appendChild(part); result.push(newContentNode); } else if (part.type === "emote") { const prevPartWasEmote = prevPart && typeof prevPart !== "string" && !(prevPart instanceof Node) && prevPart.type === "emote"; if (prevPartWasEmote && part.emote.isZeroWidth) { const prevElement = result[result.length - 1]; this.insertZeroWidthEmotePart(part.emote, prevElement); } else { result.push(this.createEmoteMessagePartElement(part.emote)); } } else if (part.type === "emoji") { const spanEl = document.createElement("span"); spanEl.className = "ntv__chat-message__part"; const emojiNode = document.createElement("img"); emojiNode.className = "ntv__inline-emoji"; emojiNode.src = part.url; emojiNode.alt = part.alt; spanEl.appendChild(emojiNode); result.push(spanEl); } else { error22("CORE", "UI", "Unknown message part type", part); } prevPart = part; } return result; } createEmoteMessagePartElement(emote) { const spanEl = document.createElement("span"); spanEl.className = "ntv__chat-message__part"; spanEl.setAttribute("contenteditable", "false"); const emoteBoxEl = document.createElement("div"); emoteBoxEl.className = "ntv__inline-emote-box"; spanEl.appendChild(emoteBoxEl); const emoteRender = this.session.emotesManager.getRenderableEmote(emote); if (!emoteRender) { error22( "CORE", "UI", "Failed to create emote message part element, emote render not found.", emote, emote.isZeroWidth && "ntv__emote--zero-width" || "" ); return spanEl; } emoteBoxEl.appendChild(parseHTML(emoteRender)); return spanEl; } insertZeroWidthEmotePart(emote, messagePartEl) { const emoteRender = this.session.emotesManager.getRenderableEmote(emote, "ntv__emote--zero-width"); if (!emoteRender) { error22("CORE", "UI", "Failed to insert zero width emote part, emote render not found.", emote); return; } const emoteBoxEl = messagePartEl.firstElementChild; if (!emoteBoxEl) return error22("CORE", "UI", "Failed to insert zero width emote part, target does not have child element."); emoteBoxEl.appendChild(parseHTML(emoteRender)); } createPlainTextMessagePartNode(textContent) { if (textContent === " ") { error22("CORE", "UI", "Attempted to create a text node with a single space character."); return document.createTextNode(" "); } const newNode = document.createElement("span"); newNode.append(document.createTextNode(textContent)); newNode.className = "ntv__chat-message__part"; return newNode; } changeInputStatus(status, reason) { if (!this.inputController) return error22("CORE", "UI", "Input controller not loaded yet."); const contentEditableEditor = this.inputController.contentEditableEditor; if (status === "enabled") { contentEditableEditor.enableInput(); contentEditableEditor.setPlaceholder(reason || "Send message.."); } else if (status === "disabled") { contentEditableEditor.clearInput(); contentEditableEditor.setPlaceholder(reason || "Chat is disabled"); contentEditableEditor.disableInput(); } } loadInputStatusBehaviour() { if (!this.inputController) return error22("CORE", "UI", "Input controller not loaded yet. Cannot load input status behaviour."); const chatroomData = this.session.channelData.chatroom; const channelMeData = this.session.channelData.me; if (!chatroomData) return error22("CORE", "UI", "Chatroom data is missing from channelData"); if (!channelMeData) return error22("CORE", "UI", "Channel me data is missing from channelData"); const updateInputStatus = () => { const chatroomData2 = this.session.channelData.chatroom; const channelMeData2 = this.session.channelData.me; const isPrivileged2 = channelMeData2.isSuperAdmin || channelMeData2.isBroadcaster || channelMeData2.isModerator; let inputChanged = false; if (!chatroomData2) return error22("CORE", "UI", "Chatroom data is missing from channelData"); if (!isPrivileged2 && channelMeData2.isBanned) { log21("CORE", "UI", "You got banned from chat"); if (channelMeData2.isBanned.permanent) { this.changeInputStatus("disabled", `You are banned from chat.`); } else { const expiresAt = new Date(channelMeData2.isBanned.expiresAt).getTime(); const duration = Math.ceil((expiresAt - Date.now()) / 1e3 / 60); this.changeInputStatus( "disabled", `You are banned from chat for ${duration || "unknown"} minute(s).` ); } inputChanged = true; } if (!inputChanged && chatroomData2.subscribersMode?.enabled && (!channelMeData2.isSubscribed || isPrivileged2)) { log21("CORE", "UI", "Subscribers only mode enabled"); this.changeInputStatus( isPrivileged2 || channelMeData2.isSubscribed ? "enabled" : "disabled", "Subscribers only" ); inputChanged = true; } if (!inputChanged && chatroomData2.followersMode?.enabled && (!channelMeData2.isFollowing || isPrivileged2)) { log21("CORE", "UI", "Followers only mode enabled"); this.changeInputStatus( isPrivileged2 || channelMeData2.isFollowing ? "enabled" : "disabled", "Followers only" ); inputChanged = true; } if (!inputChanged && chatroomData2.followersMode?.enabled && channelMeData2.isFollowing) { const followingSince = new Date(channelMeData2.followingSince); const minDuration = (chatroomData2.followersMode.min_duration || 0) * 60; const now = /* @__PURE__ */ new Date(); const timeElapsed = (now.getTime() - followingSince.getTime()) / 1e3 << 0; let remainingTime = minDuration - timeElapsed; if (remainingTime > 0) { const hours = remainingTime / 3600 << 0; remainingTime -= hours * 3600; const minutes = remainingTime / 60 << 0; const hoursString = hours > 0 ? `${hours} hour${hours > 1 ? "s" : ""}` : ""; const minutesString = minutes > 0 ? `${minutes} minute${minutes > 1 ? "s" : ""}` : ""; const secondsString = remainingTime % 60 > 0 ? `${remainingTime % 60} second${remainingTime % 60 > 1 ? "s" : ""}` : ""; const formattedRemainingTime = hoursString ? `${hoursString} and ${minutesString || "0 minutes"}`.trim() : minutesString ? `${minutesString} and ${secondsString || "0 seconds"}`.trim() : `${secondsString}`; this.changeInputStatus( isPrivileged2 ? "enabled" : "disabled", `Followers only, please wait for ${formattedRemainingTime} before you can chat.` ); inputChanged = true; } } if (!inputChanged && chatroomData2.emotesMode?.enabled) { log21("CORE", "UI", "Emotes only mode enabled"); this.changeInputStatus("enabled", "Emotes only"); } if (!inputChanged) { log21("CORE", "UI", "Normal chat input restored"); this.changeInputStatus("enabled", "Send message.."); } }; const isPrivileged = channelMeData.isSuperAdmin || channelMeData.isBroadcaster || channelMeData.isModerator; if (chatroomData.followersMode?.enabled && chatroomData.followersMode?.min_duration && !isPrivileged) { const followingSince = new Date(channelMeData.followingSince); const minDuration = (chatroomData.followersMode?.min_duration || 0) * 60; const now = /* @__PURE__ */ new Date(); const timeElapsed = (now.getTime() - followingSince.getTime()) / 1e3 << 0; const remainingTime = minDuration - timeElapsed; if (remainingTime > 0) { let intervalHandle = setInterval(updateInputStatus, 1e3); setTimeout(() => { clearInterval(intervalHandle); updateInputStatus(); }, remainingTime * 1e3); } } this.session.eventBus.subscribe("ntv.channel.chatroom.me.banned", (data) => { const channelMeData2 = this.session.channelData.me; const isPrivileged2 = channelMeData2.isSuperAdmin || channelMeData2.isBroadcaster || channelMeData2.isModerator; if (data.permanent) { this.changeInputStatus(isPrivileged2 ? "enabled" : "disabled", `You are banned from chat.`); } else { const expiresAt = new Date(data.expiresAt).getTime(); const now = Date.now(); const duration = Math.ceil((expiresAt - now) / 1e3 / 60); this.changeInputStatus( isPrivileged2 ? "enabled" : "disabled", `You are banned from chat for ${duration || "unknown"} minute(s).` ); } }); this.session.eventBus.subscribe("ntv.channel.chatroom.me.unbanned", (data) => { updateInputStatus(); }); updateInputStatus(); this.session.eventBus.subscribe("ntv.channel.chatroom.updated", updateInputStatus); this.session.eventBus.subscribe("ntv.channel.chatroom.me.unbanned", updateInputStatus); } showUserInfoModal(username, position) { log21("CORE", "UI", "Loading user info modal.."); return new UserInfoModal( this.rootContext, this.session, { toaster: this.toaster }, username, position ).init(); } addTimer({ duration, description }) { log21("CORE", "UI", "Adding timer..", duration, description); const timersContainer = this.elm.timersContainer; if (!timersContainer) return error22("CORE", "UI", "Unable to add timet, UI container does not exist yet."); const timer = new TimerComponent(duration, description).init(); timersContainer.appendChild(timer.element); } // Submits input to chat submitInput(suppressEngagementEvent, dontClearInput) { const { eventBus, inputExecutionStrategyRegister } = this.session; const contentEditableEditor = this.inputController?.contentEditableEditor; if (!contentEditableEditor) return error22("CORE", "UI", "Unable to submit input, the input controller is not loaded yet."); if (contentEditableEditor.getCharacterCount() > this.maxMessageLength - 14) { error22("CORE", "UI", "Message is too long to send."); return this.toastError("Message is too long to send."); } const messageContent = contentEditableEditor.getMessageContent(); if (!messageContent.length) return log21("CORE", "UI", "No message content to send."); eventBus.publish("ntv.ui.submit_input", { suppressEngagementEvent }); if (this.celebrationData) { const celebrationId = this.celebrationData.id; } if (this.replyMessageData && !this.celebrationData) { const { chatEntryId, chatEntryContentString, chatEntryUserId, chatEntryUsername } = this.replyMessageData; inputExecutionStrategyRegister.routeInput( contentEditableEditor, { input: messageContent, isReply: true, replyRefs: { messageId: chatEntryId, messageContent: chatEntryContentString, senderId: chatEntryUserId, senderUsername: chatEntryUsername } }, dontClearInput ).then((successMessage) => { if (successMessage) { if (typeof successMessage !== "string") throw new Error("Success message returned by input execution strategy is not a string."); this.toastSuccess(successMessage); } eventBus.publish("ntv.ui.submitted_input", { suppressEngagementEvent }); }).catch((err) => { if (err && err.message) { error22("CORE", "UI", err.message); this.toastError(err.message); } else { error22("CORE", "UI", "Failed to reply to message. Reason unknown."); this.toastError("Failed to reply to message. Reason unknown."); } }); this.destroyReplyMessageContext(); } else { inputExecutionStrategyRegister.routeInput( contentEditableEditor, { input: messageContent, isReply: false, celebrationRefs: this.celebrationData }, dontClearInput ).then((successMessage) => { const celebrationData = this.celebrationData; if (celebrationData) { const celebrations = this.session.channelData.me.celebrations; if (celebrations) { this.session.channelData.me.celebrations = celebrations.filter( (c) => c.id !== celebrationData.id ); } delete this.celebrationData; } if (successMessage) { if (typeof successMessage !== "string") throw new Error("Success message returned by input execution strategy is not a string."); this.toastSuccess(successMessage); } eventBus.publish("ntv.ui.submitted_input", { suppressEngagementEvent }); }).catch((err) => { if (err && err.message) { error22("CORE", "UI", err.message); this.toastError(err.message); } else { error22("CORE", "UI", "Failed to send message. Reason unknown."); this.toastError("Failed to send message. Reason unknown."); } }); } } sendEmoteToChat(emoteHid) { const { emotesManager, inputExecutionStrategyRegister } = this.session; const contentEditableEditor = this.inputController?.contentEditableEditor; if (!contentEditableEditor) return error22("CORE", "UI", "Unable to send emote to chat, input controller is not loaded yet."); const emoteEmbedding = emotesManager.getEmoteEmbeddable(emoteHid); if (!emoteEmbedding) return error22("CORE", "UI", "Failed to send emote to chat, emote embedding not found."); inputExecutionStrategyRegister.routeInput(contentEditableEditor, { input: emoteEmbedding, isReply: false }).catch((err) => { if (err) { error22("CORE", "UI", "Failed to send emote because:", err); this.toastError("Failed to send emote because: " + err); } else { error22("CORE", "UI", "Failed to send emote to chat. Reason unknown."); this.toastError("Failed to send emote to chat. Reason unknown."); } }); } replyMessage(messageNodes, chatEntryId, chatEntryContent, chatEntrySenderId, chatEntrySenderUsername) { log21( "CORE", "UI", `Replying to message ${chatEntryId} of user ${chatEntrySenderUsername} with ID ${chatEntrySenderId}..` ); if (!this.inputController) return error22("CORE", "UI", "Input controller not loaded for reply behaviour"); if (!this.elm.replyMessageWrapper) return error22("CORE", "UI", "Unable to load reply message, reply message wrapper not found"); if (this.replyMessageData) this.destroyReplyMessageContext(); this.replyMessageData = { chatEntryId, chatEntryContentString: chatEntryContent, chatEntryUsername: chatEntrySenderUsername, chatEntryUserId: chatEntrySenderId }; this.replyMessageComponent = new ReplyMessageComponent(this.elm.replyMessageWrapper, messageNodes).init(); this.replyMessageComponent.addEventListener("close", () => { this.destroyReplyMessageContext(); }); if (this.inputController) this.inputController.contentEditableEditor.focusInput(); } isReplyingToMessage() { return !!this.replyMessageComponent; } destroyReplyMessageContext() { this.replyMessageComponent?.destroy(); this.elm.replyMessageWrapper?.remove(); delete this.replyMessageComponent; delete this.replyMessageData; } isContentEditableEditorDestroyed() { const contentEditableEditor = this.inputController?.contentEditableEditor; if (!contentEditableEditor) return false; return !isElementInDOM(contentEditableEditor.getInputNode()); } }; // src/Core/Common/DOMEventManager.ts var DOMEventManager = class { listeners = []; addEventListener(element, type, listener, options) { element.addEventListener(type, listener, options); this.listeners.push({ element, type, listener, options }); } removeAllEventListeners() { for (const { element, type, listener, options } of this.listeners) { element.removeEventListener(type, listener, options); } this.listeners = []; } }; // src/Core/Input/Completion/Strategies/AbstractInputCompletionStrategy.ts var AbstractInputCompletionStrategy = class { constructor(rootContext, session, contentEditableEditor, navListWindowManager) { this.rootContext = rootContext; this.session = session; this.contentEditableEditor = contentEditableEditor; this.navListWindowManager = navListWindowManager; } navWindow; allowInlineStrategyDelegation = false; clearNavWindow() { if (!this.navWindow) return; this.navWindow.clearEntries(); } isClickInsideNavWindow(node) { return this.navWindow?.containsNode(node) || false; } isShowingNavWindow() { return !!this.navWindow; } reset() { if (this.navWindow) { this.navWindow = void 0; this.navListWindowManager.destroyNavWindow(this.id); } } handleBlockingKeyDownEvent(event) { } handleKeyDownEvent(event) { } handleKeyUpEvent(event) { } handleClickEvent(event, clickIsInInput) { } }; // src/Core/Input/Completion/Strategies/ColonEmoteCompletionStrategy.ts var logger23 = new Logger(); var { log: log22, info: info20, error: error23 } = logger23.destruct(); var ColonEmoteCompletionStrategy = class extends AbstractInputCompletionStrategy { constructor(rootContext, session, contentEditableEditor, navListWindowManager) { super(rootContext, session, contentEditableEditor, navListWindowManager); this.rootContext = rootContext; this.session = session; this.contentEditableEditor = contentEditableEditor; this.navListWindowManager = navListWindowManager; } id = "colon_emotes"; isFullLineStrategy = false; start = 0; end = 0; node = null; word = null; emoteComponent = null; hasNavigated = false; shouldUseStrategy(event, contentEditableEditor) { const word = Caret.getWordBeforeCaret().word; return word !== null && word[0] === ":" || event instanceof KeyboardEvent && event.key === ":"; } maybeCreateNavWindow() { if (this.navWindow) return; this.navWindow = this.navListWindowManager.createNavWindow(this.id); this.navWindow.addEventListener("entry-click", (event) => { this.renderInlineCompletion(); }); } getRelevantEmotes(searchString) { if (searchString.length) { const emotesManager = this.session.emotesManager; return this.session.emotesManager.searchEmotes(searchString.substring(0, 20), 20).filter((fuseResult) => { const emote = fuseResult.item; const emoteSet = emotesManager.getEmoteSetByEmoteHid(emote.hid); if (!emoteSet) return false; return emoteSet.enabledInMenu && (!emote.isSubscribersOnly || emote.isSubscribersOnly && emoteSet.isSubscribed); }); } else { return []; } } updateCompletionEntries({ word, start, end, node }, event) { const { contentEditableEditor } = this; const isInputEmpty = contentEditableEditor.isInputEmpty(); this.start = start; this.end = end; this.word = word; this.node = node; let searchString = ""; if (word) { searchString = word.slice(1); } else if (!isInputEmpty) { return true; } this.clearNavWindow(); this.maybeCreateNavWindow(); const navWindow = this.navWindow; const relevantEmotes = this.getRelevantEmotes(searchString); if (!relevantEmotes.length) { if (!searchString) { navWindow.addEntry( { name: "none", description: "Type an emote name to see suggestions", type: "info" }, parseHTML( `<li class="not_found_entry"><div>Type an emote name to see suggestions</div></li>`, true ) ); } else { navWindow.addEntry( { name: "none", description: "Emote not found", type: "info" }, parseHTML(`<li class="not_found_entry"><div>Emote not found</div></li>`, true) ); } return false; } const { emotesManager } = this.session; const emoteNames = relevantEmotes.map((result) => result.item.name); const emoteHids = relevantEmotes.map((result) => emotesManager.getEmoteHidByName(result.item.name)); for (let i = 0; i < emoteNames.length; i++) { const emoteName = emoteNames[i]; const emoteHid = emoteHids[i]; const emoteRender = emotesManager.getRenderableEmoteByHid(emoteHid, "ntv__emote"); navWindow.addEntry( { emoteHid }, parseHTML( `<li data-emote-hid="${emoteHid}">${emoteRender}<span>${emoteName}</span></li>`, true ) ); } if (event?.key === ":") { const perfectEmoteMatch = relevantEmotes.find((emote) => emote.item.name === searchString); if (perfectEmoteMatch) { const perfectEmoteMatchIndex = relevantEmotes.indexOf(perfectEmoteMatch); if (perfectEmoteMatchIndex !== -1) { navWindow.setSelectedIndex(perfectEmoteMatchIndex); event.preventDefault(); event.stopPropagation(); this.renderInlineCompletion(); return true; } } } navWindow.setSelectedIndex(0); } moveSelectorUp() { if (!this.navWindow) return error23("CORE", "EMCOMPS", "No tab completion window to move selector up"); if (this.hasNavigated) this.navWindow.moveSelectorUp(); else this.navWindow.setSelectedIndex(0); this.renderInlineCompletion(); this.hasNavigated = true; } moveSelectorDown() { if (!this.navWindow) return error23("CORE", "EMCOMPS", "No tab completion window to move selector down"); if (this.hasNavigated) this.navWindow.moveSelectorDown(); else this.navWindow.setSelectedIndex(this.navWindow.getEntriesCount() - 1); this.renderInlineCompletion(); this.hasNavigated = true; } renderInlineCompletion() { if (!this.navWindow) return error23("CORE", "EMCOMPS", "No tab completion window to render inline completion"); const selectedEntry = this.navWindow.getSelectedEntry(); if (!selectedEntry) return error23("CORE", "EMCOMPS", "No selected entry to render completion"); const { emoteHid } = selectedEntry; if (!emoteHid) return error23("CORE", "EMCOMPS", "No emote hid to render inline emote"); if (this.emoteComponent) { this.contentEditableEditor.replaceEmote(this.emoteComponent, emoteHid); } else { if (!this.node) return error23("CORE", "EMCOMPS", "Invalid node to restore original text"); const range = document.createRange(); range.setStart(this.node, this.start); range.setEnd(this.node, this.end); range.deleteContents(); const selection = window.getSelection(); if (selection) { selection.removeAllRanges(); selection.addRange(range); } this.contentEditableEditor.normalize(); this.emoteComponent = this.contentEditableEditor.insertEmote(emoteHid); } } restoreOriginalText() { if (this.word) { if (!this.emoteComponent) return error23("CORE", "EMCOMPS", "Invalid embed node to restore original text"); this.contentEditableEditor.replaceEmoteWithText(this.emoteComponent, this.word); } } handleKeyDownEvent(event) { if (this.navWindow) { switch (event.key) { case "Tab": event.preventDefault(); if (this.navWindow.getEntriesCount() === 1) { this.renderInlineCompletion(); return true; } if (event.shiftKey) { this.moveSelectorDown(); } else { this.moveSelectorUp(); } return false; case "ArrowUp": event.preventDefault(); this.moveSelectorUp(); return false; case "ArrowDown": event.preventDefault(); this.moveSelectorDown(); return false; case "ArrowLeft": case "ArrowRight": return false; case "Enter": event.preventDefault(); event.stopPropagation(); this.renderInlineCompletion(); return true; } } switch (event.key) { case " ": return true; case "Backspace": case "Delete": return false; case "Escape": this.restoreOriginalText(); return true; case "Control": case "Shift": return false; } const wordBeforeCaretResult = Caret.getWordBeforeCaret(); const { word, start, startOffset, node } = wordBeforeCaretResult; if (word && startOffset <= start) return true; return this.updateCompletionEntries(wordBeforeCaretResult, event); } handleKeyUpEvent(event) { switch (event.key) { case "Tab": case "ArrowUp": case "ArrowDown": case "Control": return false; } const wordBeforeCaretResult = Caret.getWordBeforeCaret(); const { word, start, startOffset } = wordBeforeCaretResult; if (!word || word[0] !== ":" || // If caret is at start of word startOffset <= start) return true; return this.updateCompletionEntries(wordBeforeCaretResult); } handleClickEvent(event, clickIsInInput) { if (clickIsInInput) { const wordBeforeCaretResult = Caret.getWordBeforeCaret(); const { word } = wordBeforeCaretResult; if (!word || word[0] !== ":") return true; const stopStrategy = this.updateCompletionEntries(wordBeforeCaretResult); this.hasNavigated = true; return stopStrategy; } return true; } reset() { super.reset(); this.start = 0; this.end = 0; this.node = null; this.word = null; this.emoteComponent = null; this.hasNavigated = false; } }; // src/Core/Input/Completion/Strategies/CommandCompletionStrategy.ts var logger24 = new Logger(); var { log: log23, info: info21, error: error24 } = logger24.destruct(); var CommandCompletionStrategy = class extends AbstractInputCompletionStrategy { // private handleEventInKeyUp = false constructor(rootContext, session, contentEditableEditor, navListWindowManager) { super(rootContext, session, contentEditableEditor, navListWindowManager); this.rootContext = rootContext; this.session = session; this.contentEditableEditor = contentEditableEditor; this.navListWindowManager = navListWindowManager; } id = "commands"; isFullLineStrategy = true; allowInlineStrategyDelegation = true; shouldUseStrategy(event, contentEditableEditor) { const firstChar = contentEditableEditor.getFirstCharacter(); return firstChar === "/" || event instanceof KeyboardEvent && event.key === "/" && contentEditableEditor.isInputEmpty(); } maybeCreateNavWindow() { if (this.navWindow) return; this.navWindow = this.navListWindowManager.createNavWindow(this.id); this.navWindow.addEventListener("entry-click", (event) => { this.renderInlineCompletion(); }); } getAvailableCommands() { const channelData = this.session.channelData; const is_broadcaster = channelData?.me?.isBroadcaster || false; const is_moderator = channelData?.me?.isModerator || false; let res = KICK_COMMANDS.filter((commandEntry) => { if (commandEntry.minAllowedRole === "broadcaster") return is_broadcaster; if (commandEntry.minAllowedRole === "moderator") return is_moderator || is_broadcaster; return true; }); return res.filter((commandEntry) => { if (!commandEntry.alias) return true; const aliasCommandEntry = res.find((n) => n.name === commandEntry.alias); if (!aliasCommandEntry) return false; return true; }); } getRelevantCommands(availableCommands, commandName, hasSpace) { if (hasSpace) { const foundEntry = availableCommands.find((commandEntry) => commandEntry.name === commandName); if (foundEntry) return [foundEntry]; return []; } else { if (!commandName) return availableCommands; return availableCommands.filter((commandEntry) => commandEntry.name.startsWith(commandName)); } } getAliasedCommand(commandsList, commandName) { const commandEntry = commandsList.find((n) => n.name === commandName); return commandEntry; } updateCompletionEntries(commandName, inputString) { this.clearNavWindow(); this.maybeCreateNavWindow(); const navWindow = this.navWindow; const availableCommands = this.getAvailableCommands(); const relevantCommands = this.getRelevantCommands( availableCommands, commandName, inputString.indexOf(" ") !== -1 ); if (!relevantCommands.length) { navWindow.addEntry( { name: "none", description: "Command not found" }, parseHTML(`<li class="not_found_entry"><div>Command not found</div></li>`, true) ); return; } for (const relevantCommand of relevantCommands) { const entryEl = parseHTML(`<li><div></div><span class="subscript"></span></li>`, true); entryEl.childNodes[1].textContent = relevantCommand.description; let aliasedCommandEntry; if (relevantCommand.alias) { aliasedCommandEntry = availableCommands.find((n) => n.name === relevantCommand.alias); if (!aliasedCommandEntry) continue; } if (relevantCommands.length === 1) { let args; if (aliasedCommandEntry) { args = aliasedCommandEntry.params?.split(" ") || []; } else { args = relevantCommand.params?.split(" ") || []; } const commandName2 = relevantCommand.name; const commandOrAliasEntry = aliasedCommandEntry || relevantCommand; const inputParts = inputString.split(" "); const inputArgs = inputParts.slice(1); const commandEl = document.createElement("span"); commandEl.textContent = "/" + commandName2; entryEl.childNodes[0].appendChild(commandEl); for (let i = 0; i < args.length; i++) { const argEl = document.createElement("span"); const arg = args[i]; const inputArg = inputArgs[i] || ""; const argValidator = commandOrAliasEntry?.argValidators?.[arg]; if (argValidator) { const argIsInvalid = argValidator(inputArg); if (argIsInvalid) { argEl.style.color = "red"; } else { argEl.style.color = "green"; } } argEl.textContent = " " + arg; entryEl.childNodes[0].appendChild(argEl); } } else { const commandEl = document.createElement("span"); if (aliasedCommandEntry) { commandEl.textContent = "/" + relevantCommand.name + " " + (aliasedCommandEntry.params || ""); } else { commandEl.textContent = "/" + relevantCommand.name + " " + (relevantCommand.params || ""); } entryEl.childNodes[0].appendChild(commandEl); } navWindow.addEntry(relevantCommand, entryEl); navWindow.setSelectedIndex(0); } } renderInlineCompletion() { if (!this.navWindow) return error24("CORE", "COMCOMS", "Tab completion window does not exist yet"); const selectedEntry = this.navWindow.getSelectedEntry(); if (!selectedEntry) return error24("CORE", "COMCOMS", "No selected entry to render completion"); const { name } = selectedEntry; this.contentEditableEditor.setInputContent("/" + name); } moveSelectorUp() { if (!this.navWindow) return error24("CORE", "COMCOMS", "No tab completion window to move selector up"); this.navWindow.moveSelectorUp(); this.renderInlineCompletion(); } moveSelectorDown() { if (!this.navWindow) return error24("CORE", "COMCOMS", "No tab completion window to move selector down"); this.navWindow.moveSelectorDown(); this.renderInlineCompletion(); } handleBlockingKeyDownEvent(event) { log23("CORE", "COMCOMS", "CommandCompletionStrategy.handleBlockingKeyDownEvent", event.key); } handleKeyDownEvent(event) { const { contentEditableEditor } = this; switch (event.key) { case "ArrowUp": if (!this.navWindow) return false; if (this.navWindow.getEntriesCount() <= 1) return false; event.preventDefault(); this.moveSelectorUp(); return false; case "ArrowDown": if (!this.navWindow) return false; if (this.navWindow.getEntriesCount() <= 1) return false; event.preventDefault(); this.moveSelectorDown(); return false; case "Tab": if (!this.navWindow) return false; if (event.shiftKey) { this.moveSelectorDown(); } else { this.moveSelectorUp(); } event.preventDefault(); return false; case "Backspace": case "Delete": case "Shift": case "Control": return false; } let inputContent = contentEditableEditor.getMessageContent(); if (inputContent[0] !== "/" && !(!inputContent && event.key === "/")) return true; const commandName = inputContent.substring(1).split(" ")[0]; return this.updateCompletionEntries(commandName, inputContent); } handleKeyUpEvent(event) { const { contentEditableEditor } = this; switch (event.key) { case "ArrowUp": case "ArrowDown": case "Tab": case "Shift": case "Control": return false; } const inputContent = contentEditableEditor.getMessageContent(); const commandName = inputContent.substring(1).split(" ")[0]; if (inputContent[0] !== "/") return true; return this.updateCompletionEntries(commandName, inputContent); } handleClickEvent(event, clickIsInInput) { if (!clickIsInInput && !this.isClickInsideNavWindow(event.target)) { return true; } } reset() { super.reset(); } }; // src/Core/Input/Completion/Strategies/MentionCompletionStrategy.ts var logger25 = new Logger(); var { log: log24, info: info22, error: error25 } = logger25.destruct(); var MentionCompletionStrategy = class extends AbstractInputCompletionStrategy { constructor(rootContext, session, contentEditableEditor, navListWindowManager) { super(rootContext, session, contentEditableEditor, navListWindowManager); this.rootContext = rootContext; this.session = session; this.contentEditableEditor = contentEditableEditor; this.navListWindowManager = navListWindowManager; } id = "mentions"; isFullLineStrategy = false; start = 0; end = 0; node = null; word = null; hasNavigated = false; shouldUseStrategy(event, contentEditableEditor) { const word = Caret.getWordBeforeCaret().word; return word !== null && word[0] === "@" || event instanceof KeyboardEvent && event.key === "@"; } maybeCreateNavWindow() { if (this.navWindow) return; this.navWindow = this.navListWindowManager.createNavWindow(this.id); this.navWindow.addEventListener("entry-click", (event) => { this.renderInlineCompletion(); }); } getRelevantUsers(searchString) { return this.session.usersManager.searchUsers(searchString.substring(0, 20), 20); } updateCompletionEntries({ word, start, end, node }, event) { const { contentEditableEditor } = this; const isInputEmpty = contentEditableEditor.isInputEmpty(); this.start = start; this.end = end; this.word = word; this.node = node; let searchString = ""; if (word) { searchString = word.slice(1); } else if (!isInputEmpty) { return true; } const relevantUsers = searchString.length ? this.getRelevantUsers(searchString) : []; if (relevantUsers.length === 1 && searchString === relevantUsers[0].item.name) { return true; } this.clearNavWindow(); this.maybeCreateNavWindow(); const navWindow = this.navWindow; if (!relevantUsers.length) { if (!searchString) { navWindow.addEntry( { name: "none", description: "Type an username to see suggestions", type: "info" }, parseHTML( `<li class="not_found_entry"><div>Type an username to see suggestions</div></li>`, true ) ); } else { navWindow.addEntry( { name: "none", description: "User not seen in chat yet", type: "info" }, parseHTML( `<li class="not_found_entry"><div>User not seen in chat yet</div></li>`, true ) ); } return false; } const userNames = relevantUsers.map((result) => result.item.name); const userIds = relevantUsers.map((result) => result.item.id); for (let i = 0; i < userNames.length; i++) { const userName = userNames[i]; const userId = userIds[i]; navWindow.addEntry( { userId, userName }, parseHTML(`<li data-user-id="${userId}"><span>@${userName}</span></li>`, true) ); } navWindow.setSelectedIndex(0); } moveSelectorUp() { if (!this.navWindow) return error25("CORE", "MENCOMST", "No tab completion window to move selector up"); if (this.hasNavigated) this.navWindow.moveSelectorUp(); else this.navWindow.setSelectedIndex(0); this.renderInlineCompletion(); this.hasNavigated = true; } moveSelectorDown() { if (!this.navWindow) return error25("CORE", "MENCOMST", "No tab completion window to move selector down"); if (this.hasNavigated) this.navWindow.moveSelectorDown(); else this.navWindow.setSelectedIndex(this.navWindow.getEntriesCount() - 1); this.renderInlineCompletion(); this.hasNavigated = true; } renderInlineCompletion() { if (!this.navWindow) return error25("CORE", "MENCOMST", "Tab completion window does not exist yet"); if (!this.node) return error25("CORE", "MENCOMST", "Invalid node to render inline user mention"); const entry = this.navWindow.getSelectedEntry(); if (!entry) return error25("CORE", "MENCOMST", "No selected entry to render inline user mention"); const { userId, userName } = entry; const userMention = `@${userName}`; this.end = Caret.replaceTextInRange(this.node, this.start, this.end, userMention); this.word = userName; Caret.moveCaretTo(this.node, this.end); this.contentEditableEditor.processInputContent(true); } handleKeyDownEvent(event) { if (this.navWindow) { switch (event.key) { case "Tab": event.preventDefault(); log24( "CORE", "MENCOMST", "Tab key pressed in mention completion strategy", this.navWindow.getEntriesCount() ); if (this.navWindow.getEntriesCount() === 1) { this.renderInlineCompletion(); this.contentEditableEditor.insertText(" "); return true; } if (event.shiftKey) { this.moveSelectorDown(); } else { this.moveSelectorUp(); } return false; case "ArrowUp": event.preventDefault(); this.moveSelectorUp(); return false; case "ArrowDown": event.preventDefault(); this.moveSelectorDown(); return false; case "ArrowLeft": case "ArrowRight": return false; case "Enter": event.preventDefault(); event.stopPropagation(); this.renderInlineCompletion(); this.contentEditableEditor.insertText(" "); return true; } } switch (event.key) { case " ": case "Escape": return true; case "Backspace": case "Delete": return false; case "Control": case "Shift": return false; } const wordBeforeCaretResult = Caret.getWordBeforeCaret(); const { word, start, startOffset, node } = wordBeforeCaretResult; if (word && startOffset <= start) return true; return this.updateCompletionEntries(wordBeforeCaretResult, event); } handleKeyUpEvent(event) { switch (event.key) { case "Tab": case "ArrowUp": case "ArrowDown": case "Control": return false; } const wordBeforeCaretResult = Caret.getWordBeforeCaret(); const { word, start, startOffset } = wordBeforeCaretResult; if (!word || word[0] !== "@" || // If caret is at start of word startOffset <= start) return true; return this.updateCompletionEntries(wordBeforeCaretResult); } handleClickEvent(event, clickIsInInput) { if (clickIsInInput) { const wordBeforeCaretResult = Caret.getWordBeforeCaret(); const { word } = wordBeforeCaretResult; if (!word || word[0] !== "@") return true; const stopStrategy = this.updateCompletionEntries(wordBeforeCaretResult); this.hasNavigated = true; return stopStrategy; } if (!this.isClickInsideNavWindow(event.target)) { return true; } } reset() { super.reset(); this.start = 0; this.end = 0; this.node = null; this.word = null; this.hasNavigated = false; } }; // src/Core/UI/Components/NavigatableListWindowComponent.ts var NavigatableListWindowComponent = class extends AbstractComponent { entries = []; entriesMap = /* @__PURE__ */ new Map(); selectedIndex = 0; container; element; listEl; classes; clickCallback; eventTarget = new EventTarget(); constructor(container, classes = "") { super(); this.container = container; this.classes = classes; this.clickCallback = this.clickHandler.bind(this); this.element = parseHTML( `<div class="ntv__nav-window ${this.classes}"><ul class="ntv__nav-window__list"></ul></div>`, true ); this.listEl = this.element.querySelector("ul"); } render() { this.container.appendChild(this.element); } attachEventHandlers() { this.element.addEventListener("click", this.clickCallback); } addEventListener(type, listener) { this.eventTarget.addEventListener(type, listener); } clickHandler(e) { let targetEntry = e.target; while (targetEntry.parentElement !== this.listEl && targetEntry.parentElement !== null) { targetEntry = targetEntry.parentElement; } const entry = this.entriesMap.get(targetEntry); if (entry) { this.setSelectedIndex(this.entries.indexOf(entry)); this.eventTarget.dispatchEvent(new CustomEvent("entry-click", { detail: entry })); } } containsNode(node) { return this.element.contains(node); } getEntriesCount() { return this.entries.filter((entry) => entry["type"] !== "info").length; } addEntry(data, element) { this.entries.push(data); this.entriesMap.set(element, data); this.listEl.appendChild(element); } addEntries(entries) { entries.forEach((entry) => { const element = entry.element; this.addEntry(entry, element); }); } setEntries(entries) { this.entries = []; this.entriesMap.clear(); while (this.listEl.firstChild) this.listEl.firstChild.remove(); entries.forEach((el) => { this.addEntry({}, el); }); } clearEntries() { this.selectedIndex = 0; this.entries = []; this.entriesMap.clear(); while (this.listEl.firstChild) this.listEl.firstChild.remove(); } getSelectedEntry() { const entry = this.entries[this.selectedIndex]; if (entry["type"] === "info") return null; return entry; } setSelectedIndex(index) { this.selectedIndex = index; this.listEl.querySelectorAll("li.selected").forEach((el) => el.classList.remove("selected")); const selectedEl = this.listEl.children[this.selectedIndex]; selectedEl.classList.add("selected"); this.scrollToSelected(); } show() { this.element.style.display = "block"; } hide() { this.element.style.display = "none"; } // Scroll selected element into middle of the list which has max height set and is scrollable scrollToSelected() { const selectedEl = this.listEl.children[this.selectedIndex]; const listHeight = this.listEl.clientHeight; const selectedHeight = selectedEl.clientHeight; const win = selectedEl.ownerDocument.defaultView; const offsetTop = selectedEl.getBoundingClientRect().top + win.scrollY; const offsetParent = selectedEl.offsetParent; const offsetParentTop = offsetParent ? offsetParent.getBoundingClientRect().top : 0; const relativeTop = offsetTop - offsetParentTop; const selectedCenter = relativeTop + selectedHeight / 2; const middleOfList = listHeight / 2; const scroll = selectedCenter - middleOfList + this.listEl.scrollTop; this.listEl.scrollTop = scroll; } moveSelectorUp() { this.listEl.children[this.selectedIndex].classList.remove("selected"); if (this.selectedIndex < this.entries.length - 1) { this.selectedIndex++; } else { this.selectedIndex = 0; } this.listEl.children[this.selectedIndex].classList.add("selected"); this.scrollToSelected(); } moveSelectorDown() { this.listEl.children[this.selectedIndex].classList.remove("selected"); if (this.selectedIndex > 0) { this.selectedIndex--; } else { this.selectedIndex = this.entries.length - 1; } this.listEl.children[this.selectedIndex].classList.add("selected"); this.scrollToSelected(); } destroy() { this.element.removeEventListener("click", this.clickCallback); this.element.remove(); delete this.element; delete this.listEl; } }; // src/Core/Common/NavigatableListWindowManager.ts var NavigatableListWindowManager = class { constructor(containerEl) { this.containerEl = containerEl; } navWindows = /* @__PURE__ */ new Map(); createNavWindow(id) { if (this.navWindows.has(id)) { throw new Error(`Navigatable list window with ID ${id} already exists.`); } const navWindow = new NavigatableListWindowComponent(this.containerEl, `ntv__${id}-window`); navWindow.init(); this.navWindows.set(id, navWindow); return navWindow; } getNavWindow(id) { return this.navWindows.get(id); } hasNavListWindows() { return this.navWindows.size > 0; } destroyNavWindow(id) { const navWindow = this.navWindows.get(id); if (navWindow) { navWindow.destroy(); this.navWindows.delete(id); } } destroyAllWindows() { this.navWindows.forEach((navWindow) => navWindow.destroy()); this.navWindows.clear(); } }; // src/Core/Input/Completion/InputCompletionStrategyManager.ts var InputCompletionStrategyManager = class { constructor(session, inputCompletionStrategyRegister, contentEditableEditor, navWindowManagerContainerEl) { this.inputCompletionStrategyRegister = inputCompletionStrategyRegister; this.contentEditableEditor = contentEditableEditor; this.navListWindowManager = new NavigatableListWindowManager(navWindowManagerContainerEl); this.clickEventHandler = this.handleClickEvent.bind(this); document.addEventListener("click", this.clickEventHandler); session.eventBus.subscribe("ntv.input_controller.empty_input", () => { this.resetStrategies(); }); } navListWindowManager; // Full line strategy that will be used for full line completions (e.g. /commands) // Always takes precedence over inline strategy fullLineStrategy = null; // Inline strategy that will only be used for inline completions (e.g. @mentions, emotes) // Full line strategy can delegate to this strategy if it wants to inlineStrategy = null; clickEventHandler; handleClickEvent(event) { let clickIsInInput = this.contentEditableEditor.isClickEventInInput(event); if (clickIsInInput) { if (!this.fullLineStrategy) this.fullLineStrategy = this.inputCompletionStrategyRegister.findApplicableFullLineStrategy( event, this.contentEditableEditor ) || null; if (!this.fullLineStrategy && !this.inlineStrategy) this.inlineStrategy = this.inputCompletionStrategyRegister.findApplicableInlineStrategy( event, this.contentEditableEditor ) || null; } if (this.fullLineStrategy) { if (this.fullLineStrategy.allowInlineStrategyDelegation && this.inlineStrategy) { const stopUsingInlineStrategy = this.inlineStrategy.handleClickEvent(event, clickIsInInput); if (stopUsingInlineStrategy) this.resetInlineStrategy(); return; } const stopUsingFullLineStrategy = this.fullLineStrategy.handleClickEvent(event, clickIsInInput); if (stopUsingFullLineStrategy) this.resetStrategies(); } else if (this.inlineStrategy) { const stopUsingInlineStrategy = this.inlineStrategy.handleClickEvent(event, clickIsInInput); if (stopUsingInlineStrategy) this.resetInlineStrategy(); } } handleBlockingKeyDownEvent(event) { const { contentEditableEditor } = this; if (this.inlineStrategy) { const shouldUseStrategy = this.inlineStrategy.shouldUseStrategy(event, contentEditableEditor); if (shouldUseStrategy) { const stopUsingInlineStrategy = this.inlineStrategy.handleBlockingKeyDownEvent(event); if (stopUsingInlineStrategy) { this.resetInlineStrategy(); } } else { this.resetInlineStrategy(); } } if (this.fullLineStrategy) { const shouldUseStrategy = this.fullLineStrategy.shouldUseStrategy(event, contentEditableEditor); if (shouldUseStrategy) { const stopUsingFullLineStrategy = this.fullLineStrategy.handleBlockingKeyDownEvent(event); if (stopUsingFullLineStrategy) { this.resetStrategies(); } } else { this.resetStrategies(); } } } handleKeyDownEvent(event) { const { inputCompletionStrategyRegister, contentEditableEditor } = this; this.fullLineStrategy = this.fullLineStrategy || inputCompletionStrategyRegister.findApplicableFullLineStrategy(event, contentEditableEditor) || null; if (this.fullLineStrategy) { if (this.fullLineStrategy.allowInlineStrategyDelegation) { this.inlineStrategy = this.inlineStrategy || inputCompletionStrategyRegister.findApplicableInlineStrategy(event, contentEditableEditor) || null; if (this.inlineStrategy) { const stopUsingInlineStrategy = this.inlineStrategy.handleKeyDownEvent(event); if (stopUsingInlineStrategy) { this.resetInlineStrategy(); } else { return; } } } else if (this.inlineStrategy) { this.resetInlineStrategy(); } const stopUsingFullLineStrategy = this.fullLineStrategy.handleKeyDownEvent(event); if (stopUsingFullLineStrategy) { this.resetStrategies(); } return; } this.inlineStrategy = this.inlineStrategy || inputCompletionStrategyRegister.findApplicableInlineStrategy(event, contentEditableEditor) || null; if (this.inlineStrategy) { const stopUsingInlineStrategy = this.inlineStrategy.handleKeyDownEvent(event); if (stopUsingInlineStrategy) { this.resetStrategies(); } } } handleKeyUpEvent(event) { if (this.fullLineStrategy) { if (this.fullLineStrategy.allowInlineStrategyDelegation && this.inlineStrategy) { const stopUsingInlineStrategy = this.inlineStrategy.handleKeyUpEvent(event); if (stopUsingInlineStrategy) { this.resetInlineStrategy(); } else { return; } } const stopUsingFullLineStrategy = this.fullLineStrategy.handleKeyUpEvent(event); if (stopUsingFullLineStrategy) { this.resetStrategies(); return; } } if (this.inlineStrategy) { const stopUsingInlineStrategy = this.inlineStrategy.handleKeyUpEvent(event); if (stopUsingInlineStrategy) { this.resetInlineStrategy(); } } if (!this.inlineStrategy && (event.key === "ArrowLeft" || event.key === "ArrowRight" || event.key === "ArrowUp" || event.key === "ArrowDown" || event.key === ":" || event.key === "@")) { this.inlineStrategy = this.inputCompletionStrategyRegister.findApplicableInlineStrategy(event, this.contentEditableEditor) || null; if (this.inlineStrategy) { const stopUsingInlineStrategy = this.inlineStrategy.handleKeyUpEvent(event); if (stopUsingInlineStrategy) { this.resetInlineStrategy(); } } } } hasNavListWindows() { return this.navListWindowManager.hasNavListWindows(); } resetStrategies() { if (this.fullLineStrategy) { this.fullLineStrategy.reset(); this.fullLineStrategy = null; } this.resetInlineStrategy(); } resetInlineStrategy() { if (this.inlineStrategy) { this.inlineStrategy.reset(); this.inlineStrategy = null; } } destroy() { document.removeEventListener("click", this.clickEventHandler); this.navListWindowManager.destroyAllWindows(); this.resetStrategies(); } }; // src/Core/Input/Completion/Strategies/EmoteCompletionStrategy.ts var logger26 = new Logger(); var { log: log25, info: info23, error: error26 } = logger26.destruct(); var EmoteCompletionStrategy = class extends AbstractInputCompletionStrategy { constructor(rootContext, session, contentEditableEditor, navListWindowManager) { super(rootContext, session, contentEditableEditor, navListWindowManager); this.rootContext = rootContext; this.session = session; this.contentEditableEditor = contentEditableEditor; this.navListWindowManager = navListWindowManager; } id = "emotes"; isFullLineStrategy = false; start = 0; end = 0; node = null; word = null; emoteComponent = null; shouldUseStrategy(event, contentEditableEditor) { const word = Caret.getWordBeforeCaret().word; const codepoint = word ? word.codePointAt(0) || 0 : 0; return event instanceof KeyboardEvent && event.key === "Tab" && word !== null && // BMP codepoints for latin special characters !(codepoint >= 33 && codepoint <= 47); } maybeCreateNavWindow() { if (this.navWindow) return; this.navWindow = this.navListWindowManager.createNavWindow(this.id); this.navWindow.addEventListener("entry-click", (event) => { this.renderInlineCompletion(); }); } getRelevantEmotes(searchString) { const emotesManager = this.session.emotesManager; return this.session.emotesManager.searchEmotes(searchString.substring(0, 20), 20).filter((fuseResult) => { const emote = fuseResult.item; const emoteSet = emotesManager.getEmoteSetByEmoteHid(emote.hid); if (!emoteSet) return false; return emoteSet.enabledInMenu && (!emote.isSubscribersOnly || emote.isSubscribersOnly && emoteSet.isSubscribed); }); } updateCompletionEntries() { const { word, start, end, startOffset, node } = Caret.getWordBeforeCaret(); if (!word) return true; this.word = word; this.start = start; this.end = end; this.node = node; const relevantEmotes = this.getRelevantEmotes(word); if (!relevantEmotes.length) return true; this.clearNavWindow(); this.maybeCreateNavWindow(); const { emotesManager } = this.session; const navWindow = this.navWindow; const emoteNames = relevantEmotes.map((result) => result.item.name); const emoteHids = relevantEmotes.map((result) => emotesManager.getEmoteHidByName(result.item.name)); for (let i = 0; i < emoteNames.length; i++) { const emoteName = emoteNames[i]; const emoteHid = emoteHids[i]; const emoteRender = emotesManager.getRenderableEmoteByHid(emoteHid, "ntv__emote"); navWindow.addEntry( { emoteHid }, parseHTML( `<li data-emote-hid="${emoteHid}">${emoteRender}<span>${emoteName}</span></li>`, true ) ); } navWindow.setSelectedIndex(0); this.renderInlineCompletion(); } moveSelectorUp() { if (!this.navWindow) return error26("CORE", "EMCOMST", "No tab completion window to move selector up"); this.navWindow.moveSelectorUp(); this.renderInlineCompletion(); } moveSelectorDown() { if (!this.navWindow) return error26("CORE", "EMCOMST", "No tab completion window to move selector down"); this.navWindow.moveSelectorDown(); this.renderInlineCompletion(); } renderInlineCompletion() { if (!this.navWindow) return error26("CORE", "EMCOMST", "Tab completion window does not exist yet"); const selectedEntry = this.navWindow.getSelectedEntry(); if (!selectedEntry) return error26("CORE", "EMCOMST", "No selected entry to render completion"); const { emoteHid } = selectedEntry; if (!emoteHid) return error26("CORE", "EMCOMST", "No emote hid to render inline emote"); if (this.emoteComponent) { this.contentEditableEditor.replaceEmote(this.emoteComponent, emoteHid); } else { if (!this.node) return error26("CORE", "EMCOMST", "Invalid node to restore original text"); const range = document.createRange(); range.setStart(this.node, this.start); range.setEnd(this.node, this.end); range.deleteContents(); const selection = window.getSelection(); if (selection) { selection.removeAllRanges(); selection.addRange(range); } this.contentEditableEditor.normalize(); this.emoteComponent = this.contentEditableEditor.insertEmote(emoteHid); } } restoreOriginalText() { if (this.word) { if (!this.emoteComponent) return error26("CORE", "EMCOMST", "Invalid embed node to restore original text"); this.contentEditableEditor.replaceEmoteWithText(this.emoteComponent, this.word); } } handleKeyDownEvent(event) { if (this.navWindow) { switch (event.key) { case "Tab": event.preventDefault(); if (event.shiftKey) { this.moveSelectorDown(); } else { this.moveSelectorUp(); } return false; case "ArrowUp": event.preventDefault(); this.moveSelectorUp(); return false; case "ArrowDown": event.preventDefault(); this.moveSelectorDown(); return false; } } switch (event.key) { case "Enter": case " ": event.preventDefault(); event.stopPropagation(); return true; case "Backspace": event.preventDefault(); event.stopImmediatePropagation(); this.restoreOriginalText(); return true; case "ArrowRight": case "ArrowLeft": return true; case "Shift": case "Control": return false; case "Escape": this.restoreOriginalText(); return true; } event.preventDefault(); return this.updateCompletionEntries(); } handleClickEvent(event, clickIsInInput) { return true; } reset() { super.reset(); this.start = 0; this.end = 0; this.node = null; this.word = null; this.emoteComponent = null; } }; // src/Core/Input/ContentEditableEditor.ts var logger27 = new Logger(); var { log: log26, info: info24, error: error27 } = logger27.destruct(); var ContentEditableEditor = class { rootContext; session; messageHistory; clipboard; inputNode; eventTarget = new PriorityEventTarget(); processInputContentDebounce; inputEmpty = true; isInputEnabled = true; characterCount = 0; messageContent = ""; emotesInMessage = /* @__PURE__ */ new Set(); hasMouseDown = false; hasUnprocessedContentChanges = false; constructor(rootContext, session, { messageHistory, clipboard }, contentEditableEl) { this.rootContext = rootContext; this.session = session; this.messageHistory = messageHistory; this.clipboard = clipboard; this.inputNode = contentEditableEl; this.processInputContentDebounce = debounce(this.processInputContent.bind(this), 25); } destroy() { if (this.inputNode) this.inputNode.remove(); } getInputNode() { return this.inputNode; } getCharacterCount() { return this.characterCount; } getFirstCharacter() { const firstChild = this.inputNode.firstChild; if (firstChild instanceof Text) return firstChild.data[0]; return null; } getMessageContent() { this.processInputContent(); return this.messageContent; } getInputHTML() { return this.inputNode.innerHTML; } getEmotesInMessage() { return this.emotesInMessage; } setPlaceholder(placeholder) { placeholder ? this.inputNode.setAttribute("placeholder", placeholder) : this.inputNode.removeAttribute("placeholder"); } isInputEmpty() { return this.inputEmpty; } focusInput() { this.inputNode.focus(); } clearInput() { while (this.inputNode.firstChild) this.inputNode.removeChild(this.inputNode.firstChild); this.hasUnprocessedContentChanges = true; this.updateEmptyInputContent(); this.processInputContent(); } enableInput() { this.inputNode.setAttribute("contenteditable", "true"); this.inputNode.parentElement?.classList.remove("ntv__message-input__wrapper--locked"); this.isInputEnabled = true; } disableInput() { this.inputNode.setAttribute("contenteditable", "false"); this.inputNode.parentElement?.classList.add("ntv__message-input__wrapper--locked"); this.isInputEnabled = false; } isEnabled() { return this.isInputEnabled; } isClickEventInInput(event) { return event.target === this.inputNode || this.inputNode.contains(event.target); } addEventListener(type, priority, listener, options) { this.eventTarget.addEventListener(type, priority, listener, options); } forwardEvent(event) { this.eventTarget.dispatchEvent(event); } attachEventListeners() { const { emotesManager } = this.session; const { inputNode, clipboard } = this; document.addEventListener("selectionchange", (evt) => { const activeElement = document.activeElement; if (activeElement !== inputNode) return; this.adjustSelection(); }); inputNode.addEventListener("paste", (event) => { event.preventDefault(); const messageParts = clipboard.parsePastedMessage(event); if (!messageParts.length) return; for (let i = 0; i < messageParts.length; i++) { messageParts[i] = messageParts[i].replace(/[\u{E0001}-\u{E007F}]/gu, ""); } const newNodes = []; for (let i = 0; i < messageParts.length; i++) { const tokens = messageParts[i].split(" "); for (let j = 0; j < tokens.length; j++) { const token = tokens[j]; const emoteHid = emotesManager.getEmoteHidByName(token); if (emoteHid) { const emoteEmbed = this.createEmoteComponent(emoteHid); if (!emoteEmbed) { error27("CORE", "EDITOR", "Failed to create emote component for", token); continue; } newNodes.push(emoteEmbed); } else if (i === 0 && j === 0) { newNodes.push(document.createTextNode(token)); } else { if (newNodes[newNodes.length - 1] instanceof Text) { newNodes[newNodes.length - 1].textContent += " " + token; } else { newNodes.push(document.createTextNode(token)); } } } } this.insertNodes(newNodes); this.processInputContent(); this.updateEmptyInputContent(); }); inputNode.addEventListener("copy", (evt) => { evt.preventDefault(); this.clipboard.handleCopyEvent(evt); }); inputNode.addEventListener("cut", (evt) => { this.clipboard.handleCutEvent(evt); this.hasUnprocessedContentChanges = true; }); this.eventTarget.addEventListener("keydown", 10, this.handleKeydown.bind(this)); inputNode.addEventListener("keydown", this.eventTarget.dispatchEvent.bind(this.eventTarget)); this.eventTarget.addEventListener("keyup", 10, this.handleKeyUp.bind(this)); inputNode.addEventListener("keyup", this.eventTarget.dispatchEvent.bind(this.eventTarget)); inputNode.addEventListener("mousedown", this.handleMouseDown.bind(this)); inputNode.addEventListener("mouseup", this.handleMouseUp.bind(this)); inputNode.addEventListener("input", (_evt) => { const evt = _evt; const isInsertCompositionTextEvent = evt.inputType === "insertCompositionText"; if (isInsertCompositionTextEvent && !evt.data) return; this.hasUnprocessedContentChanges = true; this.updateEmptyInputContent(); this.processInputContentDebounce(); }); inputNode.addEventListener("compositionstart", (event) => { this.adjustSelectionForceOutOfComponent(); }); } handleKeydown(event) { if (event.key === "ArrowLeft" || event.key === "ArrowRight") { event.stopPropagation(); } if (event.ctrlKey && (event.key === "ArrowLeft" || event.key === "ArrowRight")) { return this.handleCtrlArrowKeyDown(event); } if (event.ctrlKey && (event.key === " " || event.key === "e")) { return; } if (!this.isInputEnabled) { event.preventDefault(); return; } switch (event.key) { case "Backspace": this.deleteBackwards(event); break; case "Delete": this.deleteForwards(event); break; case "Enter": event.preventDefault(); event.stopImmediatePropagation(); if (!this.inputEmpty) { this.session.eventBus.publish("ntv.input_controller.submit", { dontClearInput: event.ctrlKey }); } break; case "Tab": event.preventDefault(); break; case " ": event.preventDefault(); event.stopPropagation(); this.handleSpaceKey(event); break; default: if (eventKeyIsLetterDigitPuncSpaceChar(event)) { event.preventDefault(); event.stopPropagation(); this.insertText(event.key); } } } handleMouseDown(event) { this.hasMouseDown = true; } handleMouseUp(event) { this.hasMouseDown = false; } handleKeyUp(event) { const { inputNode } = this; if (!this.isInputEnabled) { event.preventDefault(); return; } if (inputNode.children.length === 1 && inputNode.children[0].tagName === "BR") { inputNode.children[0].remove(); } if (event.key === "Backspace" || event.key === "Delete") { this.normalizeComponents(); } const isNotEmpty = inputNode.childNodes.length && inputNode.childNodes[0]?.tagName !== "BR"; if (this.hasUnprocessedContentChanges) { if (isNotEmpty) { this.processInputContentDebounce(); } else { this.processInputContent(); } } this.updateEmptyInputContent(); } handleSpaceKey(event) { const { inputNode } = this; if (!this.isInputEnabled) { event.preventDefault(); return; } const selection = document.getSelection(); if (!selection || !selection.rangeCount) return; const { focusNode } = selection; if (focusNode?.parentElement?.classList.contains("ntv__input-component")) { event.preventDefault(); this.hasUnprocessedContentChanges = true; return this.insertText(" "); } const { word, start, end, node } = Caret.getWordBeforeCaret(); if (!word) { event.preventDefault(); return this.insertText(" "); } const emoteHid = this.session.emotesManager.getEmoteHidByName(word); if (!emoteHid) { event.preventDefault(); return this.insertText(" "); } const textContent = node.textContent; if (!textContent) { event.preventDefault(); return this.insertText(" "); } node.textContent = textContent.slice(0, start) + textContent.slice(end); inputNode.normalize(); selection?.setPosition(node, start); this.insertEmote(emoteHid); event.preventDefault(); this.hasUnprocessedContentChanges = true; } handleCtrlArrowKeyDown(event) { event.preventDefault(); const selection = document.getSelection(); if (!selection || !selection.rangeCount) return; const { focusNode, focusOffset } = selection; const { inputNode } = this; const direction = event.key === "ArrowRight"; const isFocusInComponent = selection.focusNode?.parentElement?.classList.contains("ntv__input-component"); if (isFocusInComponent) { const component = focusNode.parentElement; const isRightSideOfComp = !focusNode.nextSibling; if (!isRightSideOfComp && direction || isRightSideOfComp && !direction) { event.shiftKey ? selection.modify("extend", direction ? "forward" : "backward", "character") : selection.modify("move", direction ? "forward" : "backward", "character"); } else if (isRightSideOfComp && direction) { if (component.nextSibling instanceof Text) { event.shiftKey ? selection.extend(component.nextSibling, component.nextSibling.textContent?.length || 0) : selection.setPosition(component.nextSibling, component.nextSibling.textContent?.length || 0); } else if (component.nextSibling instanceof HTMLElement && component.nextSibling.classList.contains("ntv__input-component")) { event.shiftKey ? selection.extend(component.nextSibling.childNodes[2], 1) : selection.setPosition(component.nextSibling.childNodes[2], 1); } } else if (!isRightSideOfComp && !direction) { if (component.previousSibling instanceof Text) { event.shiftKey ? selection.extend(component.previousSibling, 0) : selection.setPosition(component.previousSibling, 0); } else if (component.previousSibling instanceof HTMLElement && component.previousSibling.classList.contains("ntv__input-component")) { event.shiftKey ? selection.extend(component.previousSibling.childNodes[0], 0) : selection.setPosition(component.previousSibling.childNodes[0], 0); } } } else if (focusNode instanceof Text) { if (direction) { if (focusOffset === focusNode.textContent?.length) { event.shiftKey ? selection.modify("extend", "forward", "character") : selection.modify("move", "forward", "character"); } else { let nextSpaceIndex = focusNode.textContent?.indexOf(" ", focusOffset + 1); if (nextSpaceIndex === -1) nextSpaceIndex = focusNode.textContent?.length; event.shiftKey ? selection.extend(focusNode, nextSpaceIndex || 0) : selection.setPosition(focusNode, nextSpaceIndex || 0); } } else { if (focusOffset === 0) { event.shiftKey ? selection.modify("extend", "backward", "character") : selection.modify("move", "backward", "character"); } else { let prevSpaceIndex = focusNode.textContent?.lastIndexOf(" ", focusOffset - 1); if (prevSpaceIndex === -1) prevSpaceIndex = 0; event.shiftKey ? selection.extend(focusNode, prevSpaceIndex) : selection.setPosition(focusNode, prevSpaceIndex); } } } else if (direction && inputNode.childNodes[focusOffset] instanceof Text) { const nodeAtOffset = inputNode.childNodes[focusOffset]; const firstSpaceIndexInTextnode = nodeAtOffset.textContent?.indexOf(" ") || 0; event.shiftKey ? selection.extend(nodeAtOffset, firstSpaceIndexInTextnode) : selection.setPosition(nodeAtOffset, firstSpaceIndexInTextnode); } else if (!direction && inputNode.childNodes[focusOffset - 1] instanceof Text) { const nodeAtOffset = inputNode.childNodes[focusOffset - 1]; let firstSpaceIndexInTextnode = nodeAtOffset.textContent?.lastIndexOf(" ") || -1; if (firstSpaceIndexInTextnode === -1) firstSpaceIndexInTextnode = 0; else firstSpaceIndexInTextnode += 1; event.shiftKey ? selection.extend(nodeAtOffset, firstSpaceIndexInTextnode) : selection.setPosition(nodeAtOffset, firstSpaceIndexInTextnode); } else { if (direction && inputNode.childNodes[focusOffset]) { event.shiftKey ? selection.extend(inputNode, focusOffset + 1) : selection.setPosition(inputNode, focusOffset + 1); } else if (!direction && inputNode.childNodes[focusOffset - 1]) { event.shiftKey ? selection.extend(inputNode, focusOffset - 1) : selection.setPosition(inputNode, focusOffset - 1); } } } normalize() { this.inputNode.normalize(); } normalizeComponents() { const { inputNode } = this; const components = inputNode.querySelectorAll(".ntv__input-component"); for (let i = 0; i < components.length; i++) { const component = components[i]; if (!component.childNodes[1] || component.childNodes[1].className !== "ntv__input-component__body") { log26("CORE", "EDITOR", "!! Cleaning up empty component", component); component.remove(); } } } createEmoteComponent(emoteHID) { const emotesManager = this.session.emotesManager; const emote = emotesManager.getEmote(emoteHID); if (!emote) return error27("CORE", "EDITOR", "Emote not found for HID", emoteHID); const emoteHTML = emotesManager.getRenderableEmote(emote, emote.isZeroWidth && "ntv__emote--zero-width" || ""); if (!emoteHTML) return error27("CORE", "EDITOR", "Failed to get renderable emote for HID", emoteHID); const component = document.createElement("span"); component.className = "ntv__input-component"; component.appendChild(document.createTextNode(CHAR_ZWSP)); const componentBody = document.createElement("span"); componentBody.className = "ntv__input-component__body"; componentBody.setAttribute("contenteditable", "false"); const inlineEmoteBox = document.createElement("span"); inlineEmoteBox.className = "ntv__inline-emote-box"; inlineEmoteBox.setAttribute("data-emote-hid", emoteHID); inlineEmoteBox.appendChild(parseHTML(emoteHTML, true)); componentBody.appendChild(inlineEmoteBox); component.appendChild(componentBody); component.appendChild(document.createTextNode(CHAR_ZWSP)); return component; } updateEmptyInputContent() { const inputNode = this.inputNode; const isNotEmpty = !!inputNode.childNodes.length && inputNode.childNodes[0]?.tagName !== "BR"; if (this.inputEmpty === !isNotEmpty) return; this.inputEmpty = !isNotEmpty; this.eventTarget.dispatchEvent(new CustomEvent("is_empty", { detail: { isEmpty: !isNotEmpty } })); if (!isNotEmpty) this.session.eventBus.publish("ntv.input_controller.empty_input"); } setInputContent(content) { this.inputNode.innerHTML = content; this.hasUnprocessedContentChanges = true; this.processInputContent(); this.updateEmptyInputContent(); } /** * @param force Force processing of input content in case input content was changed through direct DOM manipulation. */ processInputContent(force = false) { if (!this.hasUnprocessedContentChanges && !force) return; const { eventBus, emotesManager } = this.session; const { inputNode } = this; const buffer = []; let bufferString = ""; let emotesInMessage = this.emotesInMessage; emotesInMessage.clear(); for (const node of inputNode.childNodes) { if (node.nodeType === Node.TEXT_NODE) { bufferString += node.textContent; } else if (node.nodeType === Node.ELEMENT_NODE) { const componentBody = node.childNodes[1]; if (!componentBody) { error27("CORE", "EDITOR", "Invalid component node", node); continue; } const emoteBox = componentBody.childNodes[0]; if (emoteBox) { const emoteHid = emoteBox.dataset.emoteHid; if (emoteHid) { if (bufferString) buffer.push(bufferString.trim()); bufferString = ""; emotesInMessage.add(emoteHid); buffer.push(emotesManager.getEmoteEmbeddable(emoteHid)); } else { error27("CORE", "EDITOR", "Invalid emote node, missing HID", emoteBox); } } else { error27("CORE", "EDITOR", "Invalid component node", componentBody.childNodes); } } } if (bufferString) buffer.push(bufferString.trim()); this.messageContent = buffer.join(" "); this.emotesInMessage = emotesInMessage; this.characterCount = this.messageContent.length; this.hasUnprocessedContentChanges = false; eventBus.publish("ntv.input_controller.character_count", { value: this.characterCount }); } deleteBackwards(event) { const { inputNode } = this; const selection = document.getSelection(); if (!selection || !selection.rangeCount) return error27("CORE", "EDITOR", "No ranges found in selection"); const { focusNode, focusOffset } = selection; if (focusNode === inputNode && focusOffset === 0) { event.preventDefault(); return; } let range = selection.getRangeAt(0); if (range.startContainer.parentElement?.classList.contains("ntv__input-component")) { this.adjustSelectionForceOutOfComponent(selection); range = selection.getRangeAt(0); } const { startContainer, endContainer, startOffset } = range; const isStartContainerTheInputNode = startContainer === inputNode; if (!isStartContainerTheInputNode && startContainer.parentElement !== inputNode) { return; } if (endContainer !== inputNode && endContainer.parentElement !== inputNode) { return; } const isStartInComponent = startContainer instanceof Element && startContainer.classList.contains("ntv__input-component"); const prevSibling = startContainer.previousSibling; let rangeIncludesComponent = false; if (isStartInComponent) { range.setStartBefore(startContainer); rangeIncludesComponent = true; } else if (startContainer instanceof Text && startOffset === 0 && prevSibling instanceof Element) { range.setStartBefore(prevSibling); rangeIncludesComponent = true; } else if (isStartContainerTheInputNode && inputNode.childNodes[startOffset - 1] instanceof Element) { if (range.collapsed) range.setStartBefore(inputNode.childNodes[startOffset - 1]); rangeIncludesComponent = true; } if (rangeIncludesComponent) { event.preventDefault(); range.deleteContents(); selection.removeAllRanges(); selection.addRange(range); inputNode.normalize(); } this.hasUnprocessedContentChanges = true; } deleteForwards(event) { const { inputNode } = this; const selection = document.getSelection(); if (!selection || !selection.rangeCount) return error27("CORE", "EDITOR", "No ranges found in selection"); let range = selection.getRangeAt(0); this.adjustSelectionForceOutOfComponent(selection); range = selection.getRangeAt(0); const { startContainer, endContainer, collapsed, startOffset, endOffset } = range; const isEndContainerTheInputNode = endContainer === inputNode; if (!isEndContainerTheInputNode && endContainer.parentElement !== inputNode) { return; } if (startContainer !== inputNode && startContainer.parentElement !== inputNode) { return; } const isEndInComponent = endContainer instanceof Element && endContainer.classList.contains("ntv__input-component"); const nextSibling = endContainer.nextSibling; let rangeIncludesComponent = false; if (isEndInComponent) { range.setEndAfter(endContainer); rangeIncludesComponent = true; } else if (endContainer instanceof Text && endOffset === endContainer.length && nextSibling instanceof Element) { range.setEndAfter(nextSibling); rangeIncludesComponent = true; } else if (isEndContainerTheInputNode && inputNode.childNodes[endOffset] instanceof Element) { if (range.collapsed) range.setEndAfter(inputNode.childNodes[endOffset]); rangeIncludesComponent = true; } if (rangeIncludesComponent) { event.preventDefault(); range.deleteContents(); selection.removeAllRanges(); selection.addRange(range); inputNode.normalize(); } this.hasUnprocessedContentChanges = true; } /** * Adjusts the selection to ensure that the selection focus and anchor are never * inbetween a component's body and it's adjecent zero-width space text nodes. */ adjustSelection() { const selection = document.getSelection(); if (!selection || !selection.rangeCount) return; const { inputNode } = this; if (selection.isCollapsed) { const { startContainer, startOffset } = selection.getRangeAt(0); if (!startContainer.parentElement?.classList.contains("ntv__input-component")) return; const nextSibling = startContainer.nextSibling; const prevSibling = startContainer.previousSibling; if (!nextSibling && startOffset === 0) { const prevZWSP = prevSibling?.previousSibling; if (prevZWSP) selection.collapse(prevZWSP, 0); } else if (startOffset === 1) { const nextZWSP = nextSibling?.nextSibling; if (nextZWSP) selection.collapse(nextZWSP, 1); } } else { const { focusNode, focusOffset, anchorNode, anchorOffset } = selection; const { hasMouseDown } = this; const isFocusInComponent = focusNode?.parentElement?.classList.contains("ntv__input-component"); const isAnchorInComponent = anchorNode?.parentElement?.classList.contains("ntv__input-component"); let adjustedFocusOffset = null, adjustedAnchorOffset = null; if (isFocusInComponent) { const componentIndex = Array.from(inputNode.childNodes).indexOf(focusNode?.parentElement); if (focusNode?.nextSibling) { if (hasMouseDown) { adjustedFocusOffset = componentIndex; } else { if (focusOffset === 0) { adjustedFocusOffset = componentIndex; } else { adjustedFocusOffset = componentIndex + 1; } } } else { if (hasMouseDown) { adjustedFocusOffset = componentIndex + 1; } else { if (focusOffset === 0) { adjustedFocusOffset = componentIndex; } else { adjustedFocusOffset = componentIndex + 1; } } } } if (isAnchorInComponent) { const componentIndex = Array.from(inputNode.childNodes).indexOf( anchorNode?.parentElement ); if (anchorNode?.nextSibling) { if (anchorOffset === 0) { adjustedAnchorOffset = componentIndex; } else { adjustedAnchorOffset = componentIndex + 1; } } else { if (anchorOffset === 0) { adjustedAnchorOffset = componentIndex; } else { adjustedAnchorOffset = componentIndex + 1; } } } if (adjustedFocusOffset !== null && adjustedAnchorOffset !== null) { selection.setBaseAndExtent(inputNode, adjustedAnchorOffset, inputNode, adjustedFocusOffset); } else if (adjustedFocusOffset !== null) { selection.extend(inputNode, adjustedFocusOffset); } } } adjustSelectionForceOutOfComponent(selection) { selection = selection || window.getSelection(); if (!selection || !selection.rangeCount) return; const { inputNode } = this; const { focusNode, focusOffset } = selection; const componentNode = focusNode?.parentElement; if (!componentNode || !componentNode.classList.contains("ntv__input-component")) { return; } const range = selection.getRangeAt(0); const { startContainer } = range; const nextSibling = startContainer.nextSibling; if (selection.isCollapsed) { if (nextSibling) { if (componentNode.previousSibling instanceof Text) { selection.collapse(componentNode.previousSibling, componentNode.previousSibling.length); } else { const emptyTextNode = document.createTextNode(""); componentNode.before(emptyTextNode); selection.collapse(emptyTextNode, 0); } } else { if (componentNode.nextSibling instanceof Text) { selection.collapse(componentNode.nextSibling, 0); } else { const emptyTextNode = new Text(""); inputNode.appendChild(emptyTextNode); selection.collapse(emptyTextNode, 0); } } } else { error27( "CORE", "EDITOR", "Unadjusted selection focus somehow reached inside component. This should never happen." ); } } insertText(text) { const { inputNode } = this; if (!this.isInputEnabled) return; const selection = window.getSelection(); if (!selection) { inputNode.append(new Text(text)); inputNode.normalize(); this.hasUnprocessedContentChanges = true; return; } let range; if (selection.rangeCount) { const { focusNode, anchorNode } = selection; const componentNode = focusNode?.parentElement; if (focusNode && componentNode && componentNode.classList.contains("ntv__input-component")) { const componentIndex = Array.from(inputNode.childNodes).indexOf(componentNode); if (focusNode.nextSibling) { if (selection.isCollapsed) { selection.setPosition(inputNode, componentIndex); } else { selection.extend(inputNode, componentIndex); } } else { if (selection.isCollapsed) { selection.setPosition(inputNode, componentIndex + 1); } else { selection.extend(inputNode, componentIndex + 1); } } } else if (focusNode !== inputNode && focusNode?.parentElement !== inputNode || anchorNode !== inputNode && anchorNode?.parentElement !== inputNode) { inputNode.append(new Text(text)); inputNode.normalize(); this.hasUnprocessedContentChanges = true; if (inputNode.lastChild) { const range2 = document.createRange(); range2.setStartAfter(inputNode.lastChild); selection.removeAllRanges(); selection.addRange(range2); } return; } range = selection.getRangeAt(0); } else { range = new Range(); range.setStart(inputNode, inputNode.childNodes.length); } range.deleteContents(); range.insertNode(document.createTextNode(text)); range.collapse(); selection.removeAllRanges(); selection.addRange(range); this.normalizeComponents(); inputNode.normalize(); this.hasUnprocessedContentChanges = true; } insertNodes(nodes) { const selection = document.getSelection(); if (!selection) return; if (!selection.rangeCount) { for (let i = 0; i < nodes.length; i++) { this.inputNode.appendChild(nodes[i]); } Caret.collapseToEndOfNode(this.inputNode.lastChild); this.hasUnprocessedContentChanges = true; return; } const { inputNode } = this; const { focusNode, focusOffset } = selection; const componentNode = focusNode?.parentElement; if (focusNode && componentNode && componentNode.classList.contains("ntv__input-component")) { const componentIndex = Array.from(inputNode.childNodes).indexOf(componentNode); if (focusNode.nextSibling) { if (selection.isCollapsed) { selection.setPosition(inputNode, componentIndex); } else { selection.extend(inputNode, componentIndex); } } else { if (selection.isCollapsed) { selection.setPosition(inputNode, componentIndex + 1); } else { selection.extend(inputNode, componentIndex + 1); } } } let range = selection.getRangeAt(0); range.deleteContents(); for (let i = nodes.length - 1; i >= 0; i--) { range.insertNode(nodes[i]); } range.collapse(); inputNode.normalize(); this.hasUnprocessedContentChanges = true; } insertComponent(component) { const { inputNode } = this; const selection = document.getSelection(); if (!selection) { inputNode.appendChild(component); this.hasUnprocessedContentChanges = true; return error27( "CORE", "EDITOR", "Selection API is not available, please use a modern browser supports the Selection API." ); } if (!selection.rangeCount) { const range2 = new Range(); range2.setStart(inputNode, inputNode.childNodes.length); range2.insertNode(component); range2.collapse(); selection.addRange(range2); this.hasUnprocessedContentChanges = true; return; } const { focusNode, focusOffset } = selection; const componentNode = focusNode?.parentElement; if (focusNode && componentNode && componentNode.classList.contains("ntv__input-component")) { const componentIndex = Array.from(inputNode.childNodes).indexOf(componentNode); if (focusNode.nextSibling) { if (selection.isCollapsed) { selection.setPosition(inputNode, componentIndex); } else { selection.extend(inputNode, componentIndex); } } else { if (selection.isCollapsed) { selection.setPosition(inputNode, componentIndex + 1); } else { selection.extend(inputNode, componentIndex + 1); } } } let range = selection.getRangeAt(0); let { commonAncestorContainer } = range; if (commonAncestorContainer !== inputNode && commonAncestorContainer.parentElement !== inputNode) { range = new Range(); range.setStart(inputNode, inputNode.childNodes.length); commonAncestorContainer = range.commonAncestorContainer; } range.deleteContents(); this.normalizeComponents(); const { startContainer, startOffset } = range; const isFocusInInputNode = startContainer === inputNode; if (!isFocusInInputNode && startContainer.parentElement !== inputNode) { inputNode.appendChild(component); } else if (isFocusInInputNode) { if (inputNode.childNodes[startOffset]) { inputNode.insertBefore(component, inputNode.childNodes[startOffset]); } else { inputNode.appendChild(component); } } else if (startContainer instanceof Text) { range.insertNode(component); } else { return error27( "CORE", "EDITOR", "Encountered unexpected unprocessable node", component, startContainer, range ); } range.setEnd(component.childNodes[2], 1); range.collapse(); selection.removeAllRanges(); selection.addRange(range); this.hasUnprocessedContentChanges = true; inputNode.dispatchEvent(new Event("input")); } insertEmote(emoteHid) { assertArgDefined(emoteHid); const { messageHistory, eventTarget } = this; const { emotesManager } = this.session; if (!this.isInputEnabled) return null; messageHistory.resetCursor(); const emoteComponent = this.createEmoteComponent(emoteHid); if (!emoteComponent) { error27("CORE", "EDITOR", "Invalid emote embed"); return null; } this.insertComponent(emoteComponent); this.processInputContent(); this.updateEmptyInputContent(); return emoteComponent; } replaceEmote(component, emoteHid) { const { emotesManager } = this.session; const emoteHTML = emotesManager.getRenderableEmoteByHid(emoteHid); if (!emoteHTML) { error27("CORE", "EDITOR", "Invalid emote embed"); return null; } const emoteBox = component.querySelector(".ntv__inline-emote-box"); if (!emoteBox) { error27("CORE", "EDITOR", "Component does not contain emote box"); return null; } emoteBox.innerHTML = emoteHTML; emoteBox.setAttribute("data-emote-hid", emoteHid); this.hasUnprocessedContentChanges = true; this.processInputContentDebounce(); return component; } replaceEmoteWithText(component, text) { const { inputNode } = this; const textNode = document.createTextNode(text); component.replaceWith(textNode); const selection = document.getSelection(); if (!selection) return; const range = document.createRange(); range.setStart(textNode, text.length); range.setEnd(textNode, text.length); selection.removeAllRanges(); selection.addRange(range); inputNode.normalize(); this.hasUnprocessedContentChanges = true; this.updateEmptyInputContent(); this.processInputContentDebounce(); return textNode; } }; // src/Core/Input/InputController.ts var InputController = class { rootContext; session; messageHistory; inputCompletionStrategyManager; contentEditableEditor; constructor(rootContext, session, { clipboard, submitButtonPriorityEventTarget }, textFieldEl) { this.rootContext = rootContext; this.session = session; this.messageHistory = new MessagesHistory(); this.contentEditableEditor = new ContentEditableEditor( rootContext, session, { messageHistory: this.messageHistory, clipboard }, textFieldEl ); const { inputCompletionStrategyRegister, channelData } = session; const channelId = channelData.channelId; this.inputCompletionStrategyManager = new InputCompletionStrategyManager( session, inputCompletionStrategyRegister, this.contentEditableEditor, textFieldEl.parentElement?.parentElement?.parentElement ); session.inputCompletionStrategyManager = this.inputCompletionStrategyManager; if (rootContext.settingsManager.getSetting(channelId, "chat.input.completion.commands.enabled")) { inputCompletionStrategyRegister.registerStrategy( new CommandCompletionStrategy( rootContext, session, this.contentEditableEditor, this.inputCompletionStrategyManager.navListWindowManager ) ); } if (rootContext.settingsManager.getSetting(channelId, "chat.input.completion.mentions.enabled")) { inputCompletionStrategyRegister.registerStrategy( new MentionCompletionStrategy( rootContext, session, this.contentEditableEditor, this.inputCompletionStrategyManager.navListWindowManager ) ); } if (rootContext.settingsManager.getSetting(channelId, "chat.input.completion.emotes.enabled")) { inputCompletionStrategyRegister.registerStrategy( new EmoteCompletionStrategy( rootContext, session, this.contentEditableEditor, this.inputCompletionStrategyManager.navListWindowManager ) ); } if (rootContext.settingsManager.getSetting(channelId, "chat.input.completion.colon_emotes.enabled")) { inputCompletionStrategyRegister.registerStrategy( new ColonEmoteCompletionStrategy( rootContext, session, this.contentEditableEditor, this.inputCompletionStrategyManager.navListWindowManager ) ); } } initialize() { const { contentEditableEditor } = this; const { eventBus } = this.session; contentEditableEditor.attachEventListeners(); contentEditableEditor.addEventListener("keydown", 9, (event) => { if (event.key.length === 1 && !event.ctrlKey && !event.altKey && !event.shiftKey && !event.metaKey) { this.messageHistory.resetCursor(); } }); eventBus.subscribe("ntv.ui.submit_input", this.handleInputSubmit.bind(this)); } addEventListener(type, priority, listener, options) { this.contentEditableEditor.addEventListener(type, priority, listener, options); } loadInputCompletionBehaviour() { this.contentEditableEditor.addEventListener( "keydown", 8, this.inputCompletionStrategyManager.handleKeyDownEvent.bind(this.inputCompletionStrategyManager) ); this.contentEditableEditor.addEventListener( "keyup", 10, this.inputCompletionStrategyManager.handleKeyUpEvent.bind(this.inputCompletionStrategyManager) ); } loadChatHistoryBehaviour() { const { settingsManager } = this.rootContext; const { contentEditableEditor } = this; const channelId = this.session.channelData.channelId; if (!settingsManager.getSetting(channelId, "chat.input.history.enabled")) return; contentEditableEditor.addEventListener("keydown", 4, (event) => { if (this.inputCompletionStrategyManager.hasNavListWindows()) return; const textFieldEl = contentEditableEditor.getInputNode(); if (event.key === "ArrowUp" || event.key === "ArrowDown") { if (Caret.isCaretAtStartOfNode(textFieldEl) && event.key === "ArrowUp") { event.preventDefault(); if (!this.messageHistory.canMoveCursor(1)) return; const leftoverHTML = contentEditableEditor.getInputHTML(); if (this.messageHistory.isCursorAtStart() && leftoverHTML) { this.messageHistory.addMessage(leftoverHTML); this.messageHistory.moveCursor(2); } else { this.messageHistory.moveCursor(1); } contentEditableEditor.setInputContent(this.messageHistory.getMessage()); } else if (Caret.isCaretAtEndOfNode(textFieldEl) && event.key === "ArrowDown") { event.preventDefault(); if (this.messageHistory.canMoveCursor(-1)) { this.messageHistory.moveCursor(-1); contentEditableEditor.setInputContent(this.messageHistory.getMessage()); } else { if (!contentEditableEditor.isInputEmpty()) this.messageHistory.addMessage(contentEditableEditor.getInputHTML()); this.messageHistory.resetCursor(); contentEditableEditor.clearInput(); } } } }); } handleInputSubmit({ suppressEngagementEvent }) { const { emotesManager } = this.session; const { contentEditableEditor, messageHistory } = this; if (!suppressEngagementEvent) { const emotesInMessage = contentEditableEditor.getEmotesInMessage(); for (const emoteHid of emotesInMessage) { emotesManager.registerEmoteEngagement(emoteHid); } } if (!contentEditableEditor.isInputEmpty()) messageHistory.addMessage(contentEditableEditor.getInputHTML()); messageHistory.resetCursor(); } isShowingInputCompletorNavListWindow() { return this.inputCompletionStrategyManager.hasNavListWindows(); } destroy() { this.contentEditableEditor.destroy(); } }; // src/Core/UI/Components/VerticalMenuComponent.ts var logger28 = new Logger(); var { log: log27, info: info25, error: error28 } = logger28.destruct(); var VerticalMenuComponent = class extends AbstractComponent { constructor(anchorElement, options) { super(); this.anchorElement = anchorElement; this.options = options; this.element = document.createElement("div"); this.element.classList.add("ntv__vertical-menu"); for (const option of options) { const button = document.createElement("button"); button.textContent = option.label; button.dataset.value = option.value; this.element.appendChild(button); } } event = new EventTarget(); element; render() { const boundRect = this.anchorElement.getBoundingClientRect(); this.element.style.left = boundRect.right + "px"; this.element.style.top = boundRect.top + "px"; document.body.appendChild(this.element); } attachEventHandlers() { const closeMenu = (event) => { if (event.target === this.element || this.element.contains(event.target)) return; this.element.remove(); document.removeEventListener("click", closeMenu); this.event.dispatchEvent(new Event("close")); }; const buttonEls = this.element.querySelectorAll("button"); buttonEls.forEach((buttonEl) => { buttonEl.addEventListener("click", (event) => { this.element.remove(); document.removeEventListener("click", closeMenu); this.event.dispatchEvent(new CustomEvent("action", { detail: buttonEl.dataset.value })); this.event.dispatchEvent(new Event("close")); }); }); setTimeout(() => { document.addEventListener("click", closeMenu); }, 0); } addEventListener(event, callback) { this.event.addEventListener(event, callback); } }; // src/Sites/Kick/KickUserInterface.ts var logger29 = new Logger(); var { log: log28, info: info26, error: error29 } = logger29.destruct(); var KickUserInterface = class extends AbstractUserInterface { abortController = new AbortController(); domEventManager = new DOMEventManager(); chatObserver = null; footerObserver = null; deletedChatEntryObserver = null; inputComponentObserver = null; submitButtonObserver = null; replyObserver = null; pinnedMessageObserver = null; emoteMenu = null; emoteMenuButton = null; emoteMenuButtonObserver = null; quickEmotesHolder = null; quickEmotesHolderObserver = null; celebrationsContainerEl = null; clearQueuedChatMessagesInterval = null; reloadUIhackInterval = null; // Timeout handles for resetting panic counters in various MutationObservers emoteMenuButtonPanicResetTimeout = null; quickEmotesHolderPanicResetTimeout = null; inputComponentPanicResetTimeout = null; submitButtonPanicResetTimeout = null; currentEmoteTooltip = null; currentEmoteTooltipTarget = null; emoteTooltipPointerMoveHandler = null; emoteTooltipTimeout = null; elm = { chatMessagesContainer: null, replyMessageWrapper: null, submitButton: null, originalSubmitButton: null, textField: null, timersContainer: null }; stickyScroll = true; maxMessageLength = 500; queuedChatMessages = []; constructor(rootContext, session) { super(rootContext, session); } async loadInterface() { info26("KICK", "UI", "Creating user interface.."); super.loadInterface(); const { abortController } = this; const { settingsManager, eventBus: rootEventBus } = this.rootContext; const { channelData, eventBus } = this.session; const { channelId } = channelData; const abortSignal = abortController.signal; this.loadAnnouncements(); this.loadSettings(); this.loadDocumentPatches(); rootEventBus.subscribe("ntv.settings.loaded", this.loadStylingVariables.bind(this), true, true); this.loadEmoteMenuButton(); this.loadQuickEmotesHolder(); this.loadCelebrationsBehaviour(); this.loadInputBehaviour(); const footerSelector = "#channel-chatroom > div > div > .z-common:not(.absolute)"; waitForElements([`${footerSelector}`], 15e3, abortSignal).then((foundElements) => { if (this.session.isDestroyed) return; const [footerEl] = foundElements; footerEl.classList.add("kick__chat-footer"); this.footerObserver = new MutationObserver((mutations) => { if (!footerEl.classList.contains("kick__chat-footer")) { footerEl.classList.add("kick__chat-footer"); } }); this.footerObserver.observe(footerEl, { attributes: true }); const timersContainer = document.createElement("div"); timersContainer.id = "ntv__timers-container"; footerEl.append(timersContainer); this.elm.timersContainer = timersContainer; }).catch(() => { }); const chatMessagesContainerSelector = "#chatroom-messages > .no-scrollbar"; waitForElements([chatMessagesContainerSelector], 15e3, abortSignal).then((foundElements) => { if (this.session.isDestroyed) return; const [chatMessagesContainerEl] = foundElements; this.elm.chatMessagesContainer = chatMessagesContainerEl; this.applyChatContainerClasses(); this.domEventManager.addEventListener(chatMessagesContainerEl, "copy", (evt) => { this.clipboard.handleCopyEvent(evt); }); eventBus.subscribe("ntv.providers.loaded", this.loadChatMesssageRenderingBehaviour.bind(this), true); this.observeChatMessages(chatMessagesContainerEl); if (channelData.isVod) { this.loadVodBehaviour(); } else { this.observePinnedMessage().catch( (err) => error29("KICK", "UI", "Failed to observe pinned messages", err) ); this.observeChatEntriesForDeletionEvents(); } this.reloadUIhackInterval = setInterval(() => { if (!chatMessagesContainerEl.isConnected) { info26("KICK", "UI", "Chat messages container got removed. Reloading session to reinitialize UI."); this.destroy(); this.session.eventBus.publish("ntv.session.reload"); } }, 500); }).catch(() => { }); waitForElements(["#video-player", chatMessagesContainerSelector], 15e3, abortSignal).then((foundElements) => { if (this.session.isDestroyed) return; this.loadTheatreModeBehaviour(); }).catch(() => { }); eventBus.subscribe( "ntv.ui.emote.click", ({ emoteHid, sendImmediately }) => { assertArgDefined(emoteHid); if (sendImmediately && !this.isReplyingToMessage()) { this.sendEmoteToChat(emoteHid); } else { this.inputController?.contentEditableEditor.insertEmote(emoteHid); } } ); eventBus.subscribe("ntv.input_controller.submit", (data) => this.submitInput(false, data?.dontClearInput)); rootEventBus.subscribe( "ntv.settings.change.moderators.chat.show_quick_actions", ({ value, prevValue }) => { Array.from(document.getElementsByClassName("ntv__chat-message")).forEach((el) => { el.classList.toggle("ntv__chat-message--show-quick-actions", !!value); }); } ); rootEventBus.subscribe( "ntv.settings.change.chat.messages.show_timestamps", ({ value, prevValue }) => { document.querySelector(".ntv__chat-messages-container")?.classList.toggle("ntv__show-message-timestamps", !!value); } ); rootEventBus.subscribe( "ntv.settings.change.chat.behavior.smooth_scrolling", ({ value, prevValue }) => { document.querySelector(".ntv__chat-messages-container")?.classList.toggle("ntv__smooth-scrolling", !!value); } ); rootEventBus.subscribe( "ntv.settings.change.moderators.chat.show_quick_actions", ({ value, prevValue }) => { document.querySelector(".ntv__chat-messages-container")?.classList.toggle("ntv__alternating-background", !!value); } ); rootEventBus.subscribe( "ntv.settings.change.chat.messages.alternating_background", ({ value, prevValue }) => { document.querySelector(".ntv__chat-messages-container")?.classList.toggle("ntv__alternating-background", !!value); } ); rootEventBus.subscribe( "ntv.settings.change.chat.messages.seperators", ({ value, prevValue }) => { Array.from(document.getElementsByClassName("ntv__chat-message")).forEach((el) => { if (prevValue !== "none") el.classList.remove(`ntv__chat-message--seperator-${prevValue}`); if (value !== "none") el.classList.add(`ntv__chat-message--seperator-${value}`); }); } ); rootEventBus.subscribe( "ntv.settings.change.chat.messages.spacing", ({ value, prevValue }) => { Array.from(document.getElementsByClassName("ntv__chat-message")).forEach((el) => { if (value === "none" && prevValue !== "none") el.classList.remove(`ntv__chat-message--${prevValue}`); if (value !== "none" && prevValue === "none") el.classList.add(`ntv__chat-message--${value}`); }); } ); rootEventBus.subscribe( "ntv.settings.change.chat.messages.style", ({ value, prevValue }) => { Array.from(document.getElementsByClassName("ntv__chat-message")).forEach((el) => { if (prevValue !== "none") el.classList.remove(`ntv__chat-message--theme-${prevValue}`); if (value !== "none") el.classList.add(`ntv__chat-message--theme-${value}`); }); } ); eventBus.subscribe("ntv.session.destroy", this.destroy.bind(this)); eventBus.subscribe("ntv.session.ui.restore_original", this.restoreOriginalUi.bind(this)); } applyChatContainerClasses() { const settingsManager = this.rootContext.settingsManager; const channelId = this.session.channelData.channelId; const chatMessagesContainerEl = this.elm.chatMessagesContainer; if (!chatMessagesContainerEl) return error29("KICK", "UI", "Chat messages container not loaded for settings"); chatMessagesContainerEl.classList.add("ntv__chat-messages-container"); if (settingsManager.getSetting(channelId, "chat.messages.show_timestamps")) { chatMessagesContainerEl.classList.add("ntv__show-message-timestamps"); } if (settingsManager.getSetting(channelId, "chat.messages.alternating_background")) { chatMessagesContainerEl.classList.add("ntv__alternating-background"); } if (settingsManager.getSetting(channelId, "chat.behavior.smooth_scrolling")) { chatMessagesContainerEl.classList.add("ntv__smooth-scrolling"); } } // TODO move methods like this to super class. this.elm.textfield event can be in contentEditableEditor async loadEmoteMenu() { if (!this.session.channelData.me.isLoggedIn) return; if (!this.elm.textField) return error29("KICK", "UI", "Text field not loaded for emote menu"); const container = this.elm.textField.parentElement.parentElement.parentElement; this.emoteMenu = new EmoteMenuComponent(this.rootContext, this.session, container).init(); this.elm.textField.addEventListener("click", this.emoteMenu.toggleShow.bind(this.emoteMenu, false)); } async loadEmoteMenuButton() { const { abortController } = this; const abortSignal = abortController.signal; const footerSelector = "#channel-chatroom > div > div > .z-common:not(.absolute)"; const footerBottomBarSelector = `${footerSelector} > div.flex > .flex.items-center > div.ml-auto`; waitForElements([footerBottomBarSelector], 15e3, abortSignal).then((foundElements) => { if (this.session.isDestroyed) return; const [kickFooterBottomBarEl] = foundElements; if (!kickFooterBottomBarEl) return error29("KICK", "UI", "Footer submit button wrapper not found for emote menu button"); const placeholder = document.createElement("div"); kickFooterBottomBarEl.prepend(placeholder); this.emoteMenuButton = new EmoteMenuButtonComponent(this.rootContext, this.session, placeholder).init(); let panicCounter = 0; const resetPanicCounter = () => { panicCounter = 0; this.emoteMenuButtonPanicResetTimeout = null; log28("KICK", "UI", "Emote menu button panic counter reset"); }; const schedulePanicReset = () => { if (this.emoteMenuButtonPanicResetTimeout) clearTimeout(this.emoteMenuButtonPanicResetTimeout); this.emoteMenuButtonPanicResetTimeout = window.setTimeout(resetPanicCounter, 5e3); }; const observer = this.emoteMenuButtonObserver = new MutationObserver((mutations) => { const emoteMenuButtonElement = this.emoteMenuButton?.element; if (!emoteMenuButtonElement || emoteMenuButtonElement.isConnected) return; if (panicCounter >= 20) { log28("KICK", "UI", "Emote menu button panic limit reached, stopping observer"); observer.disconnect(); if (this.emoteMenuButtonPanicResetTimeout) { clearTimeout(this.emoteMenuButtonPanicResetTimeout); this.emoteMenuButtonPanicResetTimeout = null; } setTimeout(() => this.loadEmoteMenuButton(), 4e3); return; } log28("KICK", "UI", "Emote menu button got removed"); const kickFooterBottomBarEl2 = document.querySelector(footerBottomBarSelector); if (!kickFooterBottomBarEl2) return; log28("KICK", "UI", "Footer bottom bar found, reinjecting emote menu button.."); kickFooterBottomBarEl2.prepend(emoteMenuButtonElement); panicCounter++; schedulePanicReset(); }); observer.observe(kickFooterBottomBarEl.parentElement, { childList: true, subtree: true }); }).catch(() => { }); } async loadQuickEmotesHolder() { const { settingsManager, eventBus: rootEventBus } = this.rootContext; const { channelData } = this.session; const { channelId } = channelData; const { abortController } = this; const abortSignal = abortController.signal; const footerSelector = "#channel-chatroom > div > div > .z-common:not(.absolute)"; const quickEmotesHolderSelector = "#quick-emotes-holder"; const wrapperFunction = () => { waitForElements([footerSelector], 15e3, abortSignal).then((foundElements) => { if (this.session.isDestroyed) return; const [kickFooterEl] = foundElements; waitForElements([quickEmotesHolderSelector], 1e4, abortSignal).then((foundElements2) => { const [kickQuickEmotesHolderEl] = foundElements2; const quickEmotesHolderEnabled = settingsManager.getSetting( channelId, "quick_emote_holder.enabled" ); if (quickEmotesHolderEnabled) { const quickEmotesHolderPlaceholder = document.createElement("div"); kickFooterEl.prepend(quickEmotesHolderPlaceholder); kickQuickEmotesHolderEl?.style.setProperty("display", "none", "important"); this.quickEmotesHolder = new QuickEmotesHolderComponent( this.rootContext, this.session, quickEmotesHolderPlaceholder ).init(); let panicCounter = 0; const resetPanicCounter = () => { panicCounter = 0; this.quickEmotesHolderPanicResetTimeout = null; }; const schedulePanicReset = () => { if (this.quickEmotesHolderPanicResetTimeout) clearTimeout(this.quickEmotesHolderPanicResetTimeout); this.quickEmotesHolderPanicResetTimeout = window.setTimeout(resetPanicCounter, 5e3); }; const observer = this.quickEmotesHolderObserver = new MutationObserver((mutations) => { const quickEmotesHolderElement = this.quickEmotesHolder?.element; if (!quickEmotesHolderElement || quickEmotesHolderElement.isConnected) return; if (panicCounter >= 20) { log28("KICK", "UI", "Quick emote holder panic limit reached, stopping observer"); observer.disconnect(); if (this.quickEmotesHolderPanicResetTimeout) { clearTimeout(this.quickEmotesHolderPanicResetTimeout); this.quickEmotesHolderPanicResetTimeout = null; } return; } log28("KICK", "UI", "Quick emote holder got removed"); const kickFooterEl2 = document.querySelector(footerSelector); if (!kickFooterEl2) return; log28("KICK", "UI", "Footer emote holder found, reinjecting quick emote holder.."); kickFooterEl2.prepend(quickEmotesHolderElement); if (this.celebrationsContainerEl) { quickEmotesHolderElement.after(this.celebrationsContainerEl); } panicCounter++; schedulePanicReset(); }); observer.observe(kickFooterEl, { childList: true }); } }).catch(() => { }); }).catch(() => { }); }; wrapperFunction(); rootEventBus.subscribe("ntv.settings.change.quick_emote_holder.enabled", ({ value, prevValue }) => { this.quickEmotesHolder?.destroy(); if (value) { wrapperFunction(); } else { this.quickEmotesHolder = null; const kickQuickEmotesHolderEl = document.querySelector(quickEmotesHolderSelector); kickQuickEmotesHolderEl?.style.removeProperty("display"); } }); } loadAnnouncements() { const rootContext = this.rootContext; if (!rootContext) throw new Error("Root context is not initialized."); const { announcementService, eventBus: rootEventBus } = rootContext; const showAnnouncements = () => { if (window.location.pathname.split("/")[2] === "chatroom") { const newURL = `popout/${window.location.pathname.split("/")[1]}/chat`; announcementService.registerAnnouncement({ id: "old_popout_chatroom", message: ` <h2>\u26A0\uFE0F <strong>Outdated Chatroom Warning</strong> \u26A0\uFE0F</h2> <p>Uh-oh.. Looks like you found the old popout chatroom page from before the Kick 2.0 website update.</p> <p>This page uses the old Kick layout and is no longer compatible with NTV. You can find the new popout chatroom page here: <a href='//kick.com/${newURL}'>kick.com/${newURL}</a></p> ` }); if (announcementService.hasAnnouncement("old_popout_chatroom")) { setTimeout(() => { announcementService.displayAnnouncement("old_popout_chatroom"); }, 1e3); } } }; rootEventBus.subscribe( "ntv.settings.loaded", () => { document.addEventListener("DOMContentLoaded", showAnnouncements); if (document.readyState === "complete" || document.readyState === "interactive") showAnnouncements(); }, true ); } loadSettings() { const { settingsManager } = this.rootContext; const { eventBus, channelData } = this.session; const channelId = channelData.channelId; const firstMessageHighlightColor = settingsManager.getSetting(channelId, "chat.messages.highlight_color"); if (firstMessageHighlightColor) { const rgb = hex2rgb(firstMessageHighlightColor); document.documentElement.style.setProperty( "--ntv-background-highlight-accent-1", `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.8)` ); } eventBus.subscribe( "ntv.settings.change.chat.messages.highlight_color", ({ value, prevValue }) => { if (!value) return; const rgb = hex2rgb(value); document.documentElement.style.setProperty( "--ntv-background-highlight-accent-1", `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.8)` ); } ); } loadStylingVariables() { const { settingsManager, eventBus: rootEventBus } = this.rootContext; const { channelData, eventBus } = this.session; const channelId = channelData.channelId; const messageFontSize = settingsManager.getSetting(channelId, "chat.messages.font_size") || "13px"; document.documentElement.style.setProperty("--ntv-chat-message-font-size", messageFontSize); rootEventBus.subscribe("ntv.settings.change.chat.messages.font_size", ({ value }) => { if (!value) return; document.documentElement.style.setProperty("--ntv-chat-message-font-size", value); }); const messageSpacing = settingsManager.getSetting(channelId, "chat.messages.spacing") || "0"; document.documentElement.style.setProperty("--ntv-chat-message-spacing", messageSpacing); rootEventBus.subscribe("ntv.settings.change.chat.messages.spacing", ({ value }) => { if (!value) return; document.documentElement.style.setProperty("--ntv-chat-message-spacing", value); }); const emoteSize = settingsManager.getSetting(channelId, "chat.messages.emotes.size") || "28px"; document.documentElement.style.setProperty("--ntv-chat-message-emote-size", emoteSize); rootEventBus.subscribe("ntv.settings.change.chat.messages.emotes.size", ({ value }) => { if (!value) return; document.documentElement.style.setProperty("--ntv-chat-message-emote-size", value); }); const setEmoteOverlap = (settingValue) => { const overlapValue = ["0", "0.2em", "0.3em", "0.4em"][settingValue] || "0.4em"; document.documentElement.style.setProperty("--ntv-chat-message-emote-overlap", "-" + overlapValue); document.documentElement.style.setProperty("--ntv-chat-message-emote-overlap-compensation", overlapValue); }; const emoteOverlap = settingsManager.getSetting(channelId, "chat.messages.emotes.overlap") || 3; setEmoteOverlap(emoteOverlap); rootEventBus.subscribe("ntv.settings.change.chat.messages.emotes.overlap", ({ value }) => { if (value === void 0) return; setEmoteOverlap(value); }); } async loadInputBehaviour() { if (!this.session.channelData.me.isLoggedIn) return; if (this.session.channelData.isVod) return; const { abortController } = this; const abortSignal = abortController.signal; const footerSelector = "#channel-chatroom > div > div > .z-common:not(.absolute)"; const editorInputSelector = '#channel-chatroom .editor-input[contenteditable="true"]'; const foundInputElements = await waitForElements([editorInputSelector], 15e3, abortSignal).catch( () => void 0 ); if (this.session.isDestroyed) return error29("KICK", "UI", "Session destroyed before input element could be loaded"); if (!foundInputElements || !foundInputElements.length) return error29("KICK", "UI", "Input element not found"); const [kickTextFieldEl] = foundInputElements; Array.from(document.getElementsByClassName("ntv__message-input__wrapper")).forEach((el) => el.remove()); Array.from(document.getElementsByClassName("ntv__message-input")).forEach((el) => el.remove()); document.querySelectorAll(".ntv__message-input__wrapper").forEach((el) => el.remove()); document.querySelectorAll(".ntv__message-input").forEach((el) => el.remove()); const textFieldEl = this.elm.textField = document.createElement("div"); textFieldEl.id = "ntv__message-input"; textFieldEl.tabIndex = 0; textFieldEl.contentEditable = "true"; textFieldEl.spellcheck = true; textFieldEl.setAttribute("placeholder", "Send a message"); textFieldEl.setAttribute("enterkeyhint", "send"); textFieldEl.setAttribute("role", "textbox"); const textFieldWrapperEl = parseHTML( `<div class="ntv__message-input__wrapper" data-char-limit="${this.maxMessageLength}"></div>`, true ); textFieldWrapperEl.append(textFieldEl); kickTextFieldEl.parentElement.before(textFieldWrapperEl); if (document.activeElement === kickTextFieldEl) textFieldEl.focus(); const inputController = this.inputController = new InputController( this.rootContext, this.session, { clipboard: this.clipboard, submitButtonPriorityEventTarget: this.submitButtonPriorityEventTarget }, textFieldEl ); inputController.initialize(); inputController.loadInputCompletionBehaviour(); inputController.loadChatHistoryBehaviour(); this.loadSubmitButtonBehaviour(); this.loadInputStatusBehaviour(); this.loadEmoteMenu(); this.session.eventBus.subscribe("ntv.input_controller.character_count", ({ value }) => { if (value > this.maxMessageLength) { textFieldWrapperEl.setAttribute("data-char-count", value); textFieldWrapperEl.classList.add("ntv__message-input__wrapper--char-limit-reached"); textFieldWrapperEl.classList.remove("ntv__message-input__wrapper--char-limit-close"); } else if (value > this.maxMessageLength * 0.8) { textFieldWrapperEl.setAttribute("data-char-count", value); textFieldWrapperEl.classList.add("ntv__message-input__wrapper--char-limit-close"); textFieldWrapperEl.classList.remove("ntv__message-input__wrapper--char-limit-reached"); } else { textFieldWrapperEl.removeAttribute("data-char-count"); textFieldWrapperEl.classList.remove( "ntv__message-input__wrapper--char-limit-reached", "ntv__message-input__wrapper--char-limit-close" ); } }); const ignoredKeys = { ArrowUp: true, ArrowDown: true, ArrowLeft: true, ArrowRight: true, Control: true, Shift: true, Alt: true, Meta: true, Home: true, End: true, PageUp: true, PageDown: true, Insert: true, Delete: true, Tab: true, Escape: true, Enter: true, Backspace: true, CapsLock: true, ContextMenu: true, F1: true, F2: true, F3: true, F4: true, F5: true, F6: true, F7: true, F8: true, F9: true, F10: true, F11: true, F12: true, PrintScreen: true, ScrollLock: true, Pause: true, NumLock: true }; if (!this.session.channelData.isVod) { const hasStealFocus = () => this.rootContext.settingsManager.getSetting( this.session.channelData.channelId, "chat.input.steal_focus" ); this.domEventManager.addEventListener(document.body, "keydown", (evt) => { if (evt.ctrlKey || evt.altKey || evt.metaKey || ignoredKeys[evt.key] || !hasStealFocus() || inputController.isShowingInputCompletorNavListWindow() || document.activeElement?.tagName === "INPUT" || document.activeElement?.getAttribute("contenteditable") || evt.target?.hasAttribute("capture-focus") || !inputController.contentEditableEditor.isEnabled() || document.getElementById("modal-content")) { return; } textFieldEl.focus(); this.inputController?.contentEditableEditor.forwardEvent(evt); }); } let panicCounter = 0; const resetPanicCounter = () => { panicCounter = 0; this.inputComponentPanicResetTimeout = null; }; const schedulePanicReset = () => { if (this.inputComponentPanicResetTimeout) clearTimeout(this.inputComponentPanicResetTimeout); this.inputComponentPanicResetTimeout = window.setTimeout(resetPanicCounter, 5e3); }; const observer = this.inputComponentObserver = new MutationObserver((mutations) => { if (panicCounter >= 25) { log28("KICK", "UI", "Emote menu button panic limit reached, stopping observer"); observer.disconnect(); if (this.inputComponentPanicResetTimeout) { clearTimeout(this.inputComponentPanicResetTimeout); this.inputComponentPanicResetTimeout = null; } return; } if (!textFieldWrapperEl.isConnected) { log28("KICK", "UI", "Text field got removed"); const kickTextFieldEl2 = document.querySelector(editorInputSelector); if (kickTextFieldEl2) { log28("KICK", "UI", "Footer text field found, reinjecting text field.."); kickTextFieldEl2.parentElement.before(textFieldWrapperEl); panicCounter++; schedulePanicReset(); } } }); observer.observe(document.querySelector(footerSelector), { childList: true, subtree: true }); } async loadSubmitButtonBehaviour() { if (!this.session.channelData.me.isLoggedIn) return; if (this.session.channelData.isVod) return; const { abortController, inputController } = this; const abortSignal = abortController.signal; if (!inputController) return log28("KICK", "UI", "Input controller not initialized for submit button behaviour"); const footerSelector = "#channel-chatroom > div > div > .z-common:not(.absolute)"; const submitButtonSelector = "#send-message-button"; const foundInputElements = await waitForElements([submitButtonSelector], 15e3, abortSignal).catch( () => void 0 ); if (this.session.isDestroyed) return error29("KICK", "UI", "Session destroyed before input elements could be loaded"); if (!foundInputElements || !foundInputElements.length) return error29("KICK", "UI", "Submit button element not found"); const [kickSubmitButtonEl] = foundInputElements; Array.from(document.getElementsByClassName("ntv__submit-button")).forEach((element) => { element.remove(); }); const submitButtonEl = this.elm.submitButton = document.createElement("button"); submitButtonEl.classList.add("ntv__submit-button", "disabled"); submitButtonEl.textContent = "Chat"; this.elm.originalSubmitButton = kickSubmitButtonEl; kickSubmitButtonEl.before(submitButtonEl); submitButtonEl.addEventListener("click", (event) => this.submitButtonPriorityEventTarget.dispatchEvent(event)); this.submitButtonPriorityEventTarget.addEventListener("click", 10, () => this.submitInput(false)); inputController.addEventListener("is_empty", 10, (event) => { if (event.detail.isEmpty) { submitButtonEl.setAttribute("disabled", ""); submitButtonEl.classList.add("disabled"); } else { submitButtonEl.removeAttribute("disabled"); submitButtonEl.classList.remove("disabled"); } }); let panicCounter = 0; const resetPanicCounter = () => { panicCounter = 0; this.submitButtonPanicResetTimeout = null; }; const schedulePanicReset = () => { if (this.submitButtonPanicResetTimeout) clearTimeout(this.submitButtonPanicResetTimeout); this.submitButtonPanicResetTimeout = window.setTimeout(resetPanicCounter, 5e3); }; const observer = this.submitButtonObserver = new MutationObserver((mutations) => { if (panicCounter >= 25) { log28("KICK", "UI", "Emote menu button panic limit reached, stopping observer"); observer.disconnect(); if (this.submitButtonPanicResetTimeout) { clearTimeout(this.submitButtonPanicResetTimeout); this.submitButtonPanicResetTimeout = null; } return; } if (!submitButtonEl.isConnected) { log28("KICK", "UI", "Submit button got removed"); const kickSubmitButtonEl2 = document.querySelector(submitButtonSelector); if (kickSubmitButtonEl2) { log28("KICK", "UI", "Footer bottom bar found, reinjecting emote menu button.."); kickSubmitButtonEl2.before(submitButtonEl); panicCounter++; schedulePanicReset(); } } }); observer.observe(document.querySelector(footerSelector), { childList: true, subtree: true }); } loadScrollingBehaviour() { const chatMessagesContainerEl = this.elm.chatMessagesContainer; if (!chatMessagesContainerEl) return error29("KICK", "UI", "Chat messages container not loaded for scrolling behaviour"); if (this.stickyScroll) chatMessagesContainerEl.parentElement?.classList.add("ntv__sticky-scroll"); this.domEventManager.addEventListener( chatMessagesContainerEl, "scroll", (evt) => { if (!this.stickyScroll) { const target = evt.target; const isAtBottom = (target.scrollHeight || 0) - target.scrollTop <= target.clientHeight + 15; if (isAtBottom) { chatMessagesContainerEl.parentElement?.classList.add("ntv__sticky-scroll"); target.scrollTop = 99999; this.stickyScroll = true; } } }, { passive: true } ); this.domEventManager.addEventListener( chatMessagesContainerEl, "wheel", (evt) => { if (this.stickyScroll && evt.deltaY < 0) { chatMessagesContainerEl.parentElement?.classList.remove("ntv__sticky-scroll"); this.stickyScroll = false; } }, { passive: true } ); } loadDocumentPatches() { const { settingsManager, eventBus: rootEventBus } = this.rootContext; const channelId = this.session.channelData.channelId; waitForElements(["body > div[data-theatre]"], 1e4).then(([containerEl]) => { const chatPositionModeSetting = settingsManager.getSetting(channelId, "chat.position"); if (chatPositionModeSetting && chatPositionModeSetting !== "none") { containerEl.classList.add("ntv__chat-position--" + chatPositionModeSetting); } }).catch(() => { }); rootEventBus.subscribe( "ntv.settings.change.chat.position", ({ value, prevValue }) => { const containerEl = document.querySelector("body > div[data-theatre]"); if (!containerEl) return error29("KICK", "UI", "Theatre container not found"); if (prevValue && prevValue !== "none") containerEl.classList.remove("ntv__chat-position--" + prevValue); if (value && value !== "none") containerEl.classList.add("ntv__chat-position--" + value); } ); } loadTheatreModeBehaviour() { if (this.session.isDestroyed) return; const { settingsManager, eventBus: rootEventBus } = this.rootContext; const channelId = this.session.channelData.channelId; waitForElements(["body > div[data-theatre]"], 1e4).then(([containerEl]) => { const chatPositionModeSetting = settingsManager.getSetting(channelId, "chat.position"); if (chatPositionModeSetting && chatPositionModeSetting !== "none") { containerEl.classList.add("ntv__chat-position--" + chatPositionModeSetting); } const chatOverlayModeSetting = settingsManager.getSetting(channelId, "appearance.layout.overlay_chat"); if (chatOverlayModeSetting && chatOverlayModeSetting !== "none") { containerEl.classList.add("ntv__theatre-overlay__mode"); containerEl.classList.add( "ntv__theatre-overlay__mode--" + chatOverlayModeSetting.replaceAll("_", "-") ); } const chatOverlayPositionSetting = settingsManager.getSetting( channelId, "appearance.layout.overlay_chat.position" ); if (chatOverlayPositionSetting) { containerEl.classList.add( "ntv__theatre-overlay__position--" + chatOverlayPositionSetting.replaceAll("_", "-") ); } const videoAlignmentModeSetting = settingsManager.getSetting( channelId, "appearance.layout.overlay_chat.video_alignment" ); if (videoAlignmentModeSetting && videoAlignmentModeSetting !== "none") { containerEl.classList.add( "ntv__theatre-overlay__video-alignment--" + videoAlignmentModeSetting.replaceAll("_", "-") ); } }).catch(() => { }); rootEventBus.subscribe( "ntv.settings.change.appearance.layout.overlay_chat", ({ value, prevValue }) => { const containerEl = document.querySelector("body > div[data-theatre]"); if (!containerEl) return error29("KICK", "UI", "Theatre mode container not found"); if (prevValue && prevValue !== "none") { containerEl.classList.remove("ntv__theatre-overlay__mode--" + prevValue.replaceAll("_", "-")); } if (value && value !== "none") { containerEl.classList.add("ntv__theatre-overlay__mode"); containerEl.classList.add("ntv__theatre-overlay__mode--" + value.replaceAll("_", "-")); } else { containerEl.classList.remove("ntv__theatre-overlay__mode"); } } ); rootEventBus.subscribe( "ntv.settings.change.appearance.layout.overlay_chat.video_alignment", ({ value, prevValue }) => { const containerEl = document.querySelector("body > div[data-theatre]"); if (!containerEl) return error29("KICK", "UI", "Theatre container not found"); if (prevValue && prevValue !== "none") { containerEl.classList.remove( "ntv__theatre-overlay__video-alignment--" + prevValue.replaceAll("_", "-") ); } if (value && value !== "none") { containerEl.classList.add("ntv__theatre-overlay__video-alignment--" + value.replaceAll("_", "-")); } } ); rootEventBus.subscribe( "ntv.settings.change.appearance.layout.overlay_chat.position", ({ value, prevValue }) => { const containerEl = document.querySelector("body > div[data-theatre]"); if (!containerEl) return error29("KICK", "UI", "Theatre container not found"); if (prevValue && prevValue !== "none") { containerEl.classList.remove("ntv__theatre-overlay__position--" + prevValue.replaceAll("_", "-")); } if (value && value !== "none") { containerEl.classList.add("ntv__theatre-overlay__position--" + value.replaceAll("_", "-")); } } ); } loadCelebrationsBehaviour() { const { settingsManager, eventBus: rootEventBus } = this.rootContext; const { channelData } = this.session; const { abortController } = this; const abortSignal = abortController.signal; waitForElements(["#quick-emotes-holder"], 1e4, abortSignal).then((foundElements) => { const [kickQuickEmotesHolderEl] = foundElements; const celebrationsContainerEl = document.createElement("div"); celebrationsContainerEl.classList.add("ntv__celebrations"); kickQuickEmotesHolderEl.parentElement.after(celebrationsContainerEl); this.celebrationsContainerEl = celebrationsContainerEl; const celebrations = channelData.me.celebrations; if (!celebrations) return; for (const celebration of celebrations) { if (celebration.type === "subscription_renewed") { const months = celebration.metadata.totalMonths; const celebrationEl = parseHTML( cleanupHTML(` <div class="ntv__celebration ntv__celebration--subscription-renewed relative flex min-h-[60px] flex-row flex-nowrap items-center justify-between gap-2 rounded p-2 text-black [&>svg]:fill-black mb-2" style="background-color: #ff9d00"> <div class="flex h-full flex-row flex-nowrap items-center gap-2 empty:hidden shrink grow"> <svg width="32" height="32" viewBox="0 0 32 32" class="size-6 shrink-0 grow-0 fill-black" fill="white" xmlns="http://www.w3.org/2000/svg" class="size-6 shrink-0 grow-0 fill-black"><path d="M5.00215 17.5057L12.6433 13.9772C13.2297 13.7058 13.7024 13.233 13.9737 12.6464L17.5011 5.00286L21.0285 12.6464C21.2998 13.233 21.7724 13.7058 22.3589 13.9772L30 17.5057L22.3589 21.0342C21.7724 21.3056 21.2998 21.7784 21.0285 22.365L17.5011 30.0085L13.9737 22.365C13.7024 21.7784 13.2297 21.3056 12.6433 21.0342L4.9934 17.5057H5.00215Z"></path><path d="M2 7.37587L5.29104 5.86117C5.54487 5.74735 5.74618 5.54597 5.85997 5.29207L7.37419 2L8.88842 5.29207C9.0022 5.54597 9.20352 5.74735 9.45735 5.86117L12.7484 7.37587L9.45735 8.89057C9.20352 9.0044 9.0022 9.20577 8.88842 9.45968L7.37419 12.7517L5.85997 9.46844C5.74618 9.21453 5.54487 9.01315 5.29104 8.89933L2 7.38463V7.37587Z"></path></svg> <div class="relative flex h-full grow flex-col justify-center"> <span class="text-sm font-medium leading-5 absolute left-0">It's your ${months} month sub anniversary!</span> </div> </div> <div class="flex h-full flex-row flex-nowrap items-center gap-2 empty:hidden ml-auto shrink-0 grow-0"> <button class="group inline-flex gap-1.5 items-center justify-center rounded font-semibold box-border relative transition-all betterhover:active:scale-[0.98] disabled:pointer-events-none select-none whitespace-nowrap [&_svg]:size-[1em] outline-transparent outline-2 outline-offset-2 disabled:text-disabled-onSurface focus-visible:outline-outline-decorative text-white [&_svg]:fill-current focus-visible:bg-secondary-base/40 disabled:opacity-30 px-3 py-1.5 text-sm betterhover:hover:bg-surface-base bg-surface-highest" dir="ltr">Share</button> <button class="group relative box-border flex shrink-0 grow-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded font-semibold ring-0 transition-all focus-visible:outline-none active:scale-[0.95] disabled:pointer-events-none [&_svg]:size-[1em] bg-transparent focus-visible:outline-grey-300 [&_svg]:fill-current lg:data-[state=open]:bg-surface-tint data-[state=active]:bg-surface-tint disabled:text-grey-600 disabled:bg-grey-1000 size-8 text-sm leading-none betterhover:hover:bg-black/10 text-black" data-state="closed" type="button" id="radix-:r8t:" aria-haspopup="menu" aria-expanded="true" aria-controls="radix-:r8u:" data-aria-hidden="true" aria-hidden="true"><svg width="32" height="32" viewBox="0 0 32 32" fill="white" xmlns="http://www.w3.org/2000/svg"><path d="M19 4H13V10H19V4Z" fill="current"></path><path d="M19 13H13V19H19V13Z" fill="current"></path><path d="M19 22H13V28H19V22Z" fill="current"></path></svg></button> </div> </div> `), true ); const shareBtn = celebrationEl.querySelector("button"); const hamburgerBtn = celebrationEl.querySelector("button:last-of-type"); shareBtn.addEventListener("click", () => { log28("KICK", "UI", "Share celebration button clicked"); const kickFooterInputContainer = document.querySelector( "div:has(#quick-emotes-holder) ~ div:has(#chat-input-wrapper), #quick-emotes-holder ~ div:has(#chat-input-wrapper)" ); if (!kickFooterInputContainer) return error29("KICK", "UI", "Kick footer input container not found"); kickFooterInputContainer.classList.add("kick__chat-input-container--share-celebration"); this.celebrationData = { id: celebration.id }; const channelName = this.session.channelData.channelName; const textEl = parseHTML( cleanupHTML(` <span class="text-sm leading-5 font-medium" style="padding-top: 8px">Let <span class="font-bold">${channelName}</span> know you've been subbed for ${months} months</span> `), true ); kickFooterInputContainer.prepend(textEl); celebrationEl.querySelector("span").textContent = "Share sub anniversary"; celebrationEl.querySelector("& > div:last-of-type").remove(); this.session.eventBus.subscribe( "ntv.ui.submitted_input", () => { log28("KICK", "UI", "Submitted input", this.celebrationData); celebrationEl.remove(); textEl.remove(); kickFooterInputContainer.classList.remove( "kick__chat-input-container--share-celebration" ); }, false, true ); }); hamburgerBtn.addEventListener("click", () => { const menu = new VerticalMenuComponent(hamburgerBtn, [ { label: "Share later", value: "share_later" }, { label: "Skip for this month", value: "skip" } ]).init(); menu.addEventListener("action", (evt) => { const customEvent = evt; const action = customEvent.detail; log28("KICK", "UI", "Menu action", action); this.toastError( "This feature is not yet implemented. If you're willing to help do a couple tests for the sub anniversary so I can implement this feature, please reach out to us in the Discord server!" ); }); menu.addEventListener("close", (evt) => { delete this.celebrationData; }); }); celebrationsContainerEl.append(celebrationEl); } } }).catch(() => { }); } getMessageContentString(chatMessageEl) { const messageNodes = Array.from( chatMessageEl.querySelectorAll(".chat-entry .chat-message-identity + span ~ span") ); let messageContent = []; for (const messageNode of messageNodes) { if (messageNode.textContent) messageContent.push(messageNode.textContent); else if (messageNode.querySelector("img")) { const emoteName = messageNode.querySelector("img")?.getAttribute("data-emote-name"); if (emoteName) messageContent.push(emoteName); } } return messageContent.join(" "); } loadChatMesssageRenderingBehaviour() { const tps = 60; const queue = this.queuedChatMessages; const renderChatMessagesLoop = () => { const queueLength = queue.length; if (queueLength) { if (queueLength > 150) { log28("KICK", "UI", "Chat message queue is too large, discarding overhead..", queueLength); queue.splice(queueLength - 1 - 150); } let messageChunkSize = 10; if (queueLength > 100) { messageChunkSize = 3; } else if (queueLength > 50) { messageChunkSize = 6; } for (let i = queue.length - 1; i >= 0; i--) { const msgEl = queue[i]; if (!isElementInDOM(msgEl)) { queue.splice(0, i); break; } } const queueSlice = queue.splice(0, messageChunkSize); for (const msgEl of queueSlice) { this.renderChatMessage(msgEl); } } setTimeout(() => requestAnimationFrame(renderChatMessagesLoop), 1e3 / tps); }; renderChatMessagesLoop(); this.clearQueuedChatMessagesInterval = setInterval(() => { const queue2 = this.queuedChatMessages; if (queue2.length > 150) { log28("KICK", "UI", "Chat message queue is too large, discarding overhead..", queue2.length); queue2.splice(queue2.length - 1 - 150); } }, 4e3); this.addExistingMessagesToQueue(); } addExistingMessagesToQueue() { const enableChatRendering = this.rootContext.settingsManager.getSetting( this.session.channelData.channelId, "chat.behavior.enable_chat_rendering" ); if (!enableChatRendering) return; const chatMessageEls = Array.from(this.elm.chatMessagesContainer?.children || []); if (chatMessageEls.length) { for (const chatMessageEl of chatMessageEls) { this.prepareMessageForRendering(chatMessageEl); this.queuedChatMessages.push(chatMessageEl); } } } observeChatMessages(chatMessagesContainerEl) { const { settingsManager, eventBus: rootEventBus } = this.rootContext; const channelId = this.session.channelData.channelId; const emoteAddedCb = ({ actor, emote }) => { this.addEmoteEventChatMessage("emote_added", emote, actor.name); }; const emoteRemovedCb = ({ actor, emote }) => { this.addEmoteEventChatMessage("emote_removed", emote, actor.name); }; const chatEmoteUpdateMessagesSetting = settingsManager.getSetting( channelId, "chat.messages.emote_updates.enabled" ); if (chatEmoteUpdateMessagesSetting) { this.session.eventBus.subscribe("ntv.channel.moderation.emote_added", emoteAddedCb); this.session.eventBus.subscribe("ntv.channel.moderation.emote_removed", emoteRemovedCb); } rootEventBus.subscribe( "ntv.settings.change.chat.messages.emote_updates.enabled", ({ value, prevValue }) => { if (value && value !== "none") { this.session.eventBus.subscribe("ntv.channel.moderation.emote_added", emoteAddedCb); this.session.eventBus.subscribe("ntv.channel.moderation.emote_removed", emoteRemovedCb); } else { this.session.eventBus.unsubscribe("ntv.channel.moderation.emote_added", emoteAddedCb); this.session.eventBus.unsubscribe("ntv.channel.moderation.emote_removed", emoteRemovedCb); } } ); const scrollToBottom = () => chatMessagesContainerEl.scrollTop = 99999; const loadObserver = () => { const observer = this.chatObserver = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.addedNodes.length) { for (const messageNode of mutation.addedNodes) { if (messageNode instanceof HTMLElement) { this.prepareMessageForRendering(messageNode); this.queuedChatMessages.push(messageNode); } } } }); }); observer.observe(chatMessagesContainerEl, { childList: true }); }; const enableChatRendering = settingsManager.getSetting(channelId, "chat.behavior.enable_chat_rendering"); if (enableChatRendering) { this.session.eventBus.subscribe("ntv.providers.loaded", () => loadObserver(), true); } this.rootContext.eventBus.subscribe( "ntv.settings.change.chat.behavior.enable_chat_rendering", ({ value }) => { if (this.chatObserver) { this.chatObserver.disconnect(); this.chatObserver = null; } if (value) loadObserver(); } ); const showTooltipImage = this.rootContext.settingsManager.getSetting(channelId, "chat.tooltips.images"); const removeEmoteTooltip = () => { if (this.currentEmoteTooltip) { this.currentEmoteTooltip.remove(); this.currentEmoteTooltip = null; this.currentEmoteTooltipTarget = null; } if (this.emoteTooltipPointerMoveHandler) { document.removeEventListener("pointermove", this.emoteTooltipPointerMoveHandler); this.emoteTooltipPointerMoveHandler = null; } if (this.emoteTooltipTimeout) { window.clearTimeout(this.emoteTooltipTimeout); this.emoteTooltipTimeout = null; } }; this.domEventManager.addEventListener( chatMessagesContainerEl, "pointerover", (evt) => { if (this.currentEmoteTooltip) { removeEmoteTooltip(); } const target = evt.target; if (target.tagName !== "IMG" || !target?.parentElement?.classList.contains("ntv__inline-emote-box")) return; const emoteName = target.getAttribute("data-emote-name"); if (!emoteName) return; const tooltipEl = parseHTML( `<div class="ntv__emote-tooltip" data-emote-for="${emoteName}"><span class="ntv__emote-tooltip__title">${emoteName}</span></div>`, true ); const emote = this.session.emotesManager.getEmoteByName(emoteName); if (emote && emote.isZeroWidth) { const span = document.createElement("span"); span.className = "ntv__emote-tooltip__zero-width"; span.textContent = "Zero Width"; tooltipEl.appendChild(span); } if (showTooltipImage) { if (emote) { const imageNode = parseHTML( this.session.emotesManager.getRenderableEmote(emote, "", true), true ); imageNode.className = "ntv__emote"; tooltipEl.prepend(imageNode); } else { const imgEl = target.cloneNode(true); imgEl.className = "ntv__emote"; imgEl.setAttribute("loading", "lazy"); imgEl.setAttribute("decoding", "async"); tooltipEl.prepend(imgEl); } } const rect = target.getBoundingClientRect(); tooltipEl.style.left = `${rect.x + rect.width / 2}px`; tooltipEl.style.top = `${rect.y}px`; document.body.append(tooltipEl); this.currentEmoteTooltip = tooltipEl; this.currentEmoteTooltipTarget = target; this.emoteTooltipPointerMoveHandler = (moveEvt) => { const elem = document.elementFromPoint(moveEvt.clientX, moveEvt.clientY); if (!elem) return; if (elem === target || target.contains(elem) || elem === tooltipEl || tooltipEl.contains(elem)) { return; } removeEmoteTooltip(); }; document.addEventListener("pointermove", this.emoteTooltipPointerMoveHandler); this.emoteTooltipTimeout = window.setTimeout(() => removeEmoteTooltip(), 5e3); }, { passive: true } ); this.domEventManager.addEventListener(chatMessagesContainerEl, "click", (evt) => { const target = evt.target; if (target.tagName === "IMG" && target?.parentElement?.classList.contains("ntv__inline-emote-box")) { const emoteHid = target.getAttribute("data-emote-hid"); if (!emoteHid) return; const emote = this.session.emotesManager.getEmote(emoteHid); if (emote && (!emote.isSubscribersOnly || emote.isSubscribersOnly && this.session.emotesManager.getEmoteSetByEmoteHid(emoteHid)?.isSubscribed)) this.inputController?.contentEditableEditor.insertEmote(emoteHid); } else if (target.tagName === "SPAN") { evt.stopPropagation(); if (!target.classList.contains("ntv__chat-message__username")) return; const usernameEl = target; const username = usernameEl?.getAttribute("ntv-username") ?? usernameEl.title ?? usernameEl.textContent; const rect = usernameEl.getBoundingClientRect(); const screenPosition = { x: rect.x, y: rect.y - 100 }; if (username) this.handleUserInfoModalClick(username, screenPosition); } }); } /** * We unpack the Kick chat entries to our optimized format, however to be able to detect * when a chat entry is deleted we need to observe a preserved untouched original Kick chat entry. * * TODO eventually replace this completely by a new funnel where receive message deletion events over websocket. */ observeChatEntriesForDeletionEvents() { this.deletedChatEntryObserver = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.addedNodes.length && mutation.addedNodes[0] instanceof HTMLElement) { const addedNode = mutation.addedNodes[0]; const chatMessageElement = addedNode.closest(".ntv__chat-message"); if (!chatMessageElement || chatMessageElement.classList.contains("ntv__chat-message--deleted")) return; chatMessageElement.classList.add("ntv__chat-message--deleted"); const chatMessageInnerEl = chatMessageElement.querySelector("& > .ntv__chat-message__inner"); if (addedNode.className === "line-through") { chatMessageInnerEl.append( parseHTML(`<span class="ntv__chat-message__part">(Deleted)</span>`, true) ); } else { Array.from(chatMessageElement.getElementsByClassName("ntv__chat-message__part")).forEach( (node) => node.remove() ); const deletedMessageContent = addedNode.textContent || "Deleted by a moderator"; chatMessageInnerEl.append( parseHTML(`<span class="ntv__chat-message__part">${deletedMessageContent}</span>`, true) ); } } }); }); } loadVodBehaviour() { log28("KICK", "UI", "Loading VOD behaviour.."); const chatroomParentContainerEl = document.getElementById("channel-chatroom")?.querySelector("& > .bg-surface-lower"); if (!chatroomParentContainerEl) return error29("KICK", "UI", "Chatroom container not found"); this.addExistingMessagesToQueue(); const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.addedNodes.length) { for (const node of mutation.addedNodes) { if (node instanceof HTMLElement && node.firstElementChild?.id === "chatroom-messages") { log28("KICK", "UI", "New chatroom messages container found, reloading chat UI.."); const chatroomContainerEl = node.firstElementChild.querySelector("& > .no-scrollbar"); this.elm.chatMessagesContainer = chatroomContainerEl; this.applyChatContainerClasses(); this.chatObserver?.disconnect(); this.observeChatMessages(chatroomContainerEl); } } } }); }); observer.observe(chatroomParentContainerEl, { childList: true }); } async handleUserInfoModalClick(username, screenPosition) { const userInfoModal = this.showUserInfoModal(username, screenPosition); const processKickUserProfileModal = async function(userInfoModal2, kickUserInfoModalContainerEl2) { if (userInfoModal2.isDestroyed()) { log28("KICK", "UI", "User info modal is already destroyed, cleaning up Kick modal.."); destroyKickModal(kickUserInfoModalContainerEl2); return; } userInfoModal2.addEventListener("destroy", () => { log28("KICK", "UI", "Destroying modal.."); destroyKickModal(kickUserInfoModalContainerEl2); }); kickUserInfoModalContainerEl2.style.display = "none"; kickUserInfoModalContainerEl2.style.opacity = "0"; const [giftSubButtonSvgPath] = await waitForTargetedElements( kickUserInfoModalContainerEl2, ['#user-identity button path[d^="M28.75 7.5L33.75 0H23.75L20"]'], 2e4 ); const giftSubButton = giftSubButtonSvgPath?.closest("button"); if (!giftSubButton) return; connectGiftSubButtonInModal(userInfoModal2, giftSubButton); }; const connectGiftSubButtonInModal = function(userInfoModal2, giftSubButton) { userInfoModal2.addEventListener("gift_sub_click", () => { const event = new MouseEvent("click", { bubbles: true, cancelable: true }); Object.defineProperty(event, "target", { value: giftSubButton, enumerable: true }); giftSubButton.dispatchEvent(event); }); userInfoModal2.enableGiftSubButton(); }; const destroyKickModal = function(container) { const closeBtnEl = container?.querySelector("& > button.absolute.select-none"); const event = new MouseEvent("click", { bubbles: true, cancelable: true }); Object.defineProperty(event, "target", { value: closeBtnEl, enumerable: true }); closeBtnEl?.dispatchEvent(event); }; const kickUserProfileCards = Array.from(document.querySelectorAll(".base-floating-card.user-profile")); const kickUserInfoModalContainerEl = kickUserProfileCards.find((node) => findNodeWithTextContent(node, username)); if (kickUserInfoModalContainerEl) { const usernameEl = kickUserInfoModalContainerEl.querySelector('a[rel="noreferrer"][title]'); const usernameElText = usernameEl?.textContent; if (!usernameElText || username !== usernameElText) return; processKickUserProfileModal(userInfoModal, kickUserInfoModalContainerEl); } else { let hideModalFaster = document.getElementById("user-identity"); if (hideModalFaster) { hideModalFaster.style.display = "none"; hideModalFaster.style.opacity = "0"; } const [usernameEl] = await waitForElements(['#user-identity a[rel="noreferrer"][title]'], 2e4); const usernameElText = usernameEl?.textContent; if (!usernameElText || username !== usernameElText) return; const kickUserInfoModalContainerEl2 = document.getElementById("user-identity"); if (!kickUserInfoModalContainerEl2) return error29("KICK", "UI", "Kick user profile modal container not found"); processKickUserProfileModal(userInfoModal, kickUserInfoModalContainerEl2); } } async observePinnedMessage() { const pinnedMessagesContainerSelector = "#channel-chatroom div:has(+ #chatroom-messages) > div"; const pinnedMessageContentSelector = ".\\[\\&\\>a\\:hover\\]\\:text-primary-base"; const pinnedMessageContainerEl = document.querySelector(pinnedMessagesContainerSelector); if (!pinnedMessageContainerEl) return error29("KICK", "UI", "Pinned message container not found for observation"); const renderPinnedMessageBody = (contentBodyEl) => { Array.from(document.getElementsByClassName("ntv__pinned-message__content")).forEach((node) => { node.remove(); }); this.renderPinnedMessageContent(contentBodyEl); }; this.session.eventBus.subscribe("ntv.providers.loaded", () => { const observer = this.pinnedMessageObserver = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type === "characterData") { if (mutation.target.parentElement?.classList.contains("[&>a:hover]:text-primary-base")) { const contentBodyEl = mutation.target.parentElement; renderPinnedMessageBody(contentBodyEl); return; } } else if (mutation.addedNodes.length) { for (const node of mutation.addedNodes) { if (node instanceof HTMLElement && node.classList.contains("z-absolute")) { const contentBodyEl = node.querySelector(pinnedMessageContentSelector); if (!contentBodyEl) return; renderPinnedMessageBody(contentBodyEl); return; } else if (node.parentElement?.classList.contains("[&>a:hover]:text-primary-base")) { renderPinnedMessageBody(node.parentElement); return; } } } } }); observer.observe(pinnedMessageContainerEl, { childList: true, subtree: true, characterData: true }); const pinnedMessageContent = document.querySelector( pinnedMessagesContainerSelector + " " + pinnedMessageContentSelector ); if (pinnedMessageContent) this.renderPinnedMessageContent(pinnedMessageContent); }); this.domEventManager.addEventListener(pinnedMessageContainerEl, "click", (evt) => { const target = evt.target; if (target.tagName === "IMG" && target?.parentElement?.classList.contains("ntv__inline-emote-box")) { const emoteHid = target.getAttribute("data-emote-hid"); if (emoteHid) this.inputController?.contentEditableEditor.insertEmote(emoteHid); } }); } prepareMessageForRendering(messageEl) { const settingsManager = this.rootContext.settingsManager; const channelData = this.session.channelData; const channelId = channelData.channelId; const settingStyle = settingsManager.getSetting(channelId, "chat.messages.style"); const settingSeperator = settingsManager.getSetting(channelId, "chat.messages.seperators"); const settingSpacing = settingsManager.getSetting(channelId, "chat.messages.spacing"); if (settingStyle && settingStyle !== "none") messageEl.classList.add("ntv__chat-message--theme-" + settingStyle); if (settingSeperator && settingSeperator !== "none") messageEl.classList.add(`ntv__chat-message--seperator-${settingSeperator}`); if (settingSpacing && settingSpacing !== "none") messageEl.classList.add("ntv__chat-message--" + settingSpacing); if (channelData.me.isBroadcaster || channelData.me.isModerator || channelData.me.isSuperAdmin) { const settingModeratorQuickAction = settingsManager.getSetting( channelId, "moderators.chat.show_quick_actions" ); if (settingModeratorQuickAction) { messageEl.classList.add("ntv__chat-message--show-quick-actions"); } } messageEl.classList.add("ntv__chat-message", "ntv__chat-message--unrendered"); } addEmoteEventChatMessage(type, emote, actorName) { const { emotesManager } = this.session; const messageTemplate = parseHTML( cleanupHTML(` <div class="ntv__event-message ntv__event-message--emote-update"> <div class="ntv__event-message__inner"> <span class="ntv__chat-message__part">Emote</span> <span class="ntv__chat-message__part"> <div class="ntv__inline-emote-box"> ${emotesManager.getRenderableEmote(emote, "", true)} </div> </span> <span class="ntv__chat-message__part">got ${type === "emote_added" ? "added to" : "removed from"} channel by ${actorName}</span> </div> </div> `) ); const lastProcessedMessage = this.elm.chatMessagesContainer?.querySelector( "& > .ntv__chat-message:not(.ntv__chat-message--unrendered):last-of-type" ); if (!lastProcessedMessage) { log28("KICK", "UI", "No processed chat message found to insert emote event message into"); return; } lastProcessedMessage.appendChild(messageTemplate); } renderChatMessage(messageNode) { const { settingsManager } = this.rootContext; const { emotesManager, usersManager } = this.session; const { channelData } = this.session; const channelId = channelData.channelId; if (!messageNode.children || !messageNode.firstElementChild.classList.contains("group")) { messageNode.classList.add("ntv__chat-message"); messageNode.classList.remove("ntv__chat-message--unrendered"); if (messageNode.firstElementChild?.classList.contains("items-center")) { messageNode.classList.add("ntv__chat-message--history-breaker"); } else { messageNode.classList.add("ntv__chat-message--history-breaker"); } return; } const messageObject = { username: "", createdAt: "", isReply: false, isReplyToMe: false, badges: [], content: [], style: { color: "" } }; const ntvMessageInnerEl = document.createElement("div"); const ntvIdentityWrapperEl = document.createElement("div"); ntvIdentityWrapperEl.classList.add("ntv__chat-message__identity"); let groupElementNode = messageNode.firstElementChild; if (!groupElementNode?.classList.contains("group")) groupElementNode = groupElementNode?.nextElementSibling; if (!groupElementNode?.classList.contains("group")) { messageNode.classList.remove("ntv__chat-message--unrendered"); error29("KICK", "UI", "Chat message content wrapper node not found", messageNode); return; } const betterHoverEl = groupElementNode.firstElementChild; if (!betterHoverEl) { messageNode.classList.remove("ntv__chat-message--unrendered"); error29("KICK", "UI", "Better hover element not found"); return; } const messageHasMentionedMe = betterHoverEl.classList.contains("border-green-500"); if (messageHasMentionedMe) { messageNode.classList.add("ntv__chat-message--mentioned-me"); } let isReply = false; let isReplyToMe = false; if (betterHoverEl.firstElementChild?.classList.contains("w-full")) { isReply = true; messageObject.isReply = true; if (betterHoverEl.classList.contains("border-green-500")) { isReplyToMe = true; messageObject.isReplyToMe = true; messageNode.classList.add("ntv__chat-message--reply-to-me"); } const replyMessageAttachmentEl = betterHoverEl.firstElementChild; if (!replyMessageAttachmentEl) { messageNode.classList.remove("ntv__chat-message--unrendered"); error29("KICK", "UI", "Reply message attachment element not found", messageNode); return; } const ntvMessageAttachmentEl = replyMessageAttachmentEl.cloneNode(true); ntvMessageAttachmentEl.className = "ntv__chat-message__attachment"; ntvMessageInnerEl.append(ntvMessageAttachmentEl); replyMessageAttachmentEl.style.setProperty("display", "none", "important"); const ntvReplyMessageAttachmentEl = replyMessageAttachmentEl.cloneNode(true); ntvReplyMessageAttachmentEl.classList.add("ntv__chat-message__reply-attachment"); messageNode.append(ntvReplyMessageAttachmentEl); } const messageBodyWrapper = isReply ? betterHoverEl.lastElementChild : betterHoverEl; if (!messageBodyWrapper) { messageNode.classList.remove("ntv__chat-message--unrendered"); error29("KICK", "UI", "Chat message body wrapper node not found", messageNode); return; } let ntvModBtnsWrapperEl = null; if (channelData.me.isBroadcaster || channelData.me.isModerator || channelData.me.isSuperAdmin) { const modBtnsWrapperEl = messageBodyWrapper.firstElementChild; if (modBtnsWrapperEl?.classList.contains("inline-flex")) { ntvModBtnsWrapperEl = document.createElement("div"); ntvModBtnsWrapperEl.className = "ntv__chat-message__mod-buttons"; const modDeleteBtnEl = modBtnsWrapperEl.children[0]; const modTimeoutBtnEl = modBtnsWrapperEl.children[1]; const modBanBtnEl = modBtnsWrapperEl.children[2]; const ntvModDeleteBtnEl = modDeleteBtnEl.cloneNode(true); const ntvModTimeoutBtnEl = modTimeoutBtnEl.cloneNode(true); const ntvModBanBtnEl = modBanBtnEl.cloneNode(true); ntvModBtnsWrapperEl.append(ntvModDeleteBtnEl, ntvModTimeoutBtnEl, ntvModBanBtnEl); ntvModDeleteBtnEl.addEventListener("click", () => { const mEvent = new MouseEvent("click", { bubbles: true, cancelable: true }); Object.defineProperty(mEvent, "target", { value: modDeleteBtnEl, enumerable: true }); modDeleteBtnEl.dispatchEvent(mEvent); }); ntvModTimeoutBtnEl.addEventListener("click", () => { const mEvent = new MouseEvent("click", { bubbles: true, cancelable: true }); Object.defineProperty(mEvent, "target", { value: modTimeoutBtnEl, enumerable: true }); modTimeoutBtnEl.dispatchEvent(mEvent); }); ntvModBanBtnEl.addEventListener("click", () => { const mEvent = new MouseEvent("click", { bubbles: true, cancelable: true }); Object.defineProperty(mEvent, "target", { value: modBanBtnEl, enumerable: true }); modBanBtnEl.dispatchEvent(mEvent); }); } } const contentWrapperNode = messageBodyWrapper.querySelector("span:last-of-type"); if (!contentWrapperNode) { messageNode.classList.remove("ntv__chat-message--unrendered"); error29("KICK", "UI", "Chat message content wrapper node not found", messageNode); return; } let timestampEl = messageBodyWrapper.firstElementChild; while (timestampEl && timestampEl.tagName !== "SPAN") timestampEl = timestampEl.nextElementSibling; if (!timestampEl) { messageNode.classList.remove("ntv__chat-message--unrendered"); error29("KICK", "UI", "Chat message timestamp node not found", messageNode); return; } messageObject.createdAt = timestampEl.textContent || "00:00 AM"; const ntvTimestampEl = document.createElement("span"); ntvTimestampEl.className = "ntv__chat-message__timestamp"; ntvTimestampEl.textContent = messageObject.createdAt; const identityEl = timestampEl?.nextElementSibling; if (!identityEl) { messageNode.classList.remove("ntv__chat-message--unrendered"); error29("KICK", "UI", "Chat message identity node not found", messageNode); return; } const ntvBadgesEl = document.createElement("span"); ntvBadgesEl.className = "ntv__chat-message__badges"; const badgesEl = identityEl.firstElementChild; if (badgesEl && badgesEl.tagName !== "BUTTON") { if (badgesEl.firstElementChild) ntvBadgesEl.append( ...Array.from(badgesEl.children).map((badgeWrapperEl) => { const subWrapperEl = badgeWrapperEl.firstElementChild; const svgEl = subWrapperEl?.firstElementChild; svgEl?.setAttribute("class", "ntv__badge"); subWrapperEl?.setAttribute("class", "ntv__chat-message__badge"); return subWrapperEl || badgeWrapperEl; }) ); } let usernameEl = identityEl.childNodes.length > 1 ? identityEl.children[1] : identityEl.firstElementChild; while (usernameEl && usernameEl.tagName !== "BUTTON") usernameEl = usernameEl.nextElementSibling; if (!usernameEl) { messageNode.classList.remove("ntv__chat-message--unrendered"); error29("KICK", "UI", "Chat message username node not found", messageNode); return; } const username = usernameEl.title; messageObject.username = username; messageObject.style.color = usernameEl.style.color; const ntvUsernameEl = document.createElement("span"); ntvUsernameEl.className = "ntv__chat-message__username"; ntvUsernameEl.title = username; ntvUsernameEl.setAttribute("ntv-username", username); ntvUsernameEl.textContent = usernameEl.textContent || "Unknown user"; ntvUsernameEl.style.color = usernameEl.style.color; if (!channelData.isVod && username) { if (usersManager.hasMutedUser(username)) { messageNode.classList.add("ntv__chat-message--muted"); return; } if (!usersManager.hasSeenUser(username)) { const enableFirstMessageHighlight = settingsManager.getSetting( channelId, "chat.messages.highlight_first_time" ); const highlightWhenModeratorOnly = settingsManager.getSetting( channelId, "chat.messages.highlight_first_moderator" ); if (enableFirstMessageHighlight && (!highlightWhenModeratorOnly || highlightWhenModeratorOnly && channelData.me.isModerator)) { messageNode.classList.add("ntv__chat-message--first-message"); } usersManager.registerUser(username, username); } } const separatorEl = identityEl?.nextElementSibling; if (!separatorEl || !separatorEl.hasAttribute("aria-hidden")) { messageNode.classList.remove("ntv__chat-message--unrendered"); error29("KICK", "UI", "Chat message separator node not found", separatorEl); return; } const ntvSeparatorEl = document.createElement("span"); ntvSeparatorEl.className = "ntv__chat-message__separator"; ntvSeparatorEl.textContent = ":"; if (settingsManager.getSetting(channelId, "chat.badges.show_ntv_badge")) { const lastChildNode = contentWrapperNode.lastChild; if (lastChildNode?.textContent?.endsWith(U_TAG_NTV_AFFIX)) { const badgeEl = parseHTML( this.session.badgeProvider.getBadge({ type: "nipahtv" }), true ); ntvBadgesEl.append(badgeEl); } } if (ntvModBtnsWrapperEl) ntvIdentityWrapperEl.append(ntvModBtnsWrapperEl); ntvIdentityWrapperEl.append(ntvTimestampEl, ntvBadgesEl, ntvUsernameEl, ntvSeparatorEl); const messageParts = []; for (const contentNode of contentWrapperNode.childNodes) { if (contentNode.nodeType === Node.TEXT_NODE) { emotesManager.parseEmoteText(contentNode.textContent || "", messageParts); } else if (contentNode instanceof HTMLElement && contentNode.tagName === "SPAN") { const imgEl = contentNode.firstElementChild?.firstElementChild; if (!imgEl || imgEl instanceof HTMLImageElement === false) { error29("KICK", "UI", "Emote image element not found", imgEl); continue; } const emoteId = contentNode.getAttribute("data-emote-id"); const emoteName = contentNode.getAttribute("data-emote-name"); if (!emoteId || !emoteName) { error29("KICK", "UI", "Emote ID or name not found", contentNode); continue; } let emote = emotesManager.getEmoteByName(emoteName); if (!emote) { emote = { id: emoteId, name: emoteName, isSubscribersOnly: true, provider: 1 /* KICK */ }; } messageParts.push({ type: "emote", emote }); } else { messageParts.push(contentNode.cloneNode(true)); } } const hasEmotesInMessage = messageParts.some( (part) => typeof part === "object" && part !== null && "type" in part && part.type === "emote" ); if (hasEmotesInMessage) { messageNode.classList.add("ntv__chat-message--has-emotes"); } const clipAttachmentEl = contentWrapperNode.nextElementSibling; if (clipAttachmentEl && clipAttachmentEl.nodeName === "BUTTON") { const clipPreviewBtnEl = clipAttachmentEl.cloneNode(true); clipPreviewBtnEl.addEventListener("click", (evt) => { evt.preventDefault(); evt.stopPropagation(); evt.stopImmediatePropagation(); const event = new MouseEvent("click", { bubbles: true, cancelable: true }); Object.defineProperty(event, "target", { value: clipAttachmentEl, enumerable: true }); clipAttachmentEl.dispatchEvent(event); }); messageParts.push(clipPreviewBtnEl); } messageObject.content = messageParts; groupElementNode.style.display = "none"; this.rootContext.renderMessagePipeline.process(messageObject, ntvBadgesEl, ntvUsernameEl, messageParts); ntvMessageInnerEl.className = "ntv__chat-message__inner"; ntvMessageInnerEl.append(ntvIdentityWrapperEl); ntvMessageInnerEl.append(...this.renderMessageParts(messageParts)); messageNode.append(ntvMessageInnerEl); messageNode.classList.add("ntv__chat-message"); messageNode.classList.remove("ntv__chat-message--unrendered"); let chatMessageActionsEl = groupElementNode.lastElementChild; while (chatMessageActionsEl && chatMessageActionsEl.id !== "chat-message-actions") chatMessageActionsEl = chatMessageActionsEl.previousElementSibling; if (chatMessageActionsEl) { const ntvChatMessageActionsEl = document.createElement("div"); ntvChatMessageActionsEl.className = chatMessageActionsEl.className; ntvChatMessageActionsEl.classList.add("kick__chat-message__actions"); ntvChatMessageActionsEl.classList.remove("hidden"); const replyButtonEl = chatMessageActionsEl.querySelector('[d*="M18.64 8.82996H"]')?.parentElement?.parentElement; if (replyButtonEl) replyButtonEl.classList.add("kick__reply-button"); for (const buttonEl of chatMessageActionsEl.children) { const ntvBtnEl = buttonEl.cloneNode(true); ntvBtnEl.addEventListener("click", (evt) => { evt.preventDefault(); evt.stopPropagation(); evt.stopImmediatePropagation(); if (evt.target instanceof HTMLElement && evt.target.classList.contains("kick__reply-button")) { this.handleMessageReplyBtnClick(messageNode, buttonEl); } else { const event = new MouseEvent("click", { bubbles: true, cancelable: true }); Object.defineProperty(event, "target", { writable: false, value: buttonEl }); buttonEl.dispatchEvent(event); } }); ntvChatMessageActionsEl.append(ntvBtnEl); } messageNode.append(ntvChatMessageActionsEl); } ntvUsernameEl.addEventListener("click", (evt) => { const event = new MouseEvent("click", { bubbles: true, cancelable: false }); Object.defineProperty(event, "target", { value: usernameEl, enumerable: true }); usernameEl.dispatchEvent(event); }); this.deletedChatEntryObserver?.observe(contentWrapperNode, { childList: true, attributes: false, subtree: false }); } async loadNativeKickFallbackReplyBehaviour(fallbackButtonEl) { const event = new MouseEvent("click", { bubbles: true, cancelable: true }); Object.defineProperty(event, "target", { writable: false, value: fallbackButtonEl }); fallbackButtonEl.dispatchEvent(event); const textFieldEl = this.elm.textField; const kickTextFieldEl = document.querySelector('.editor-input[contenteditable="true"]'); textFieldEl.parentElement.style.display = "none"; kickTextFieldEl?.parentElement?.style.setProperty("display", "block", "important"); const kickEmoteButtonEl = kickTextFieldEl?.parentElement?.nextElementSibling; if (kickEmoteButtonEl) kickEmoteButtonEl.style.setProperty("display", "flex", "important"); this.elm.submitButton.style.setProperty("display", "none", "important"); this.elm.originalSubmitButton.style.setProperty("display", "flex", "important"); document.querySelector(".ntv__emote-menu-button")?.style.setProperty( "display", "none", "important" ); document.querySelector(".ntv__quick-emotes-holder")?.style.setProperty( "display", "none", "important" ); await waitForElements(['.kick__chat-footer path[d*="M28 6.99204L25.008 4L16"]'], 2e3); const closeReplyButton = document.querySelector('.kick__chat-footer path[d*="M28 6.99204L25.008 4L16"]')?.closest("button"); const replyPreviewWrapperEl = closeReplyButton.closest(".flex.flex-col")?.parentElement; if (!replyPreviewWrapperEl) return error29("KICK", "UI", "Reply preview wrapper element not found"); const restoreFields = () => { kickTextFieldEl.parentElement.style.removeProperty("display"); textFieldEl.parentElement.style.removeProperty("display"); this.elm.submitButton.style.removeProperty("display"); this.elm.originalSubmitButton.style.removeProperty("display"); document.querySelector(".ntv__quick-emotes-holder")?.style.removeProperty("display"); document.querySelector(".ntv__emote-menu-button")?.style.removeProperty("display"); const kickEmoteButtonEl2 = kickTextFieldEl?.parentElement?.nextElementSibling; if (kickEmoteButtonEl2) kickEmoteButtonEl2.style.removeProperty("display"); }; if (!isElementInDOM(replyPreviewWrapperEl)) restoreFields(); else { const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.removedNodes.length) { restoreFields(); observer.disconnect(); } }); }); observer.observe(replyPreviewWrapperEl, { childList: true }); } const footerEl = document.querySelector(".kick__chat-footer"); if (!footerEl) return error29("KICK", "UI", "Footer element not found"); const textFieldParent = textFieldEl.parentElement; const intervalId = setInterval(() => { const closeReplyBtnEl = footerEl.querySelector('path[d*="M28 6.99204L25.008 4L16"]'); if (!closeReplyBtnEl) { if (textFieldParent.style.display === "none") restoreFields(); clearInterval(intervalId); } }, 400); kickTextFieldEl?.focus(); } async handleMessageReplyBtnClick(messageNode, fallbackButtonEl) { const { inputController } = this; const { channelData } = this.session; if (!channelData.me.isLoggedIn) return; const randomId = "NTV" + Math.random().toString(36).substring(2, 11); messageNode.classList.add(randomId); const messageProps = await ReactivePropsFromMain.getByClassName(randomId); if (!messageProps) return this.loadNativeKickFallbackReplyBehaviour(fallbackButtonEl); const chatEntry = messageProps.chatEntry; if (!chatEntry) return this.loadNativeKickFallbackReplyBehaviour(fallbackButtonEl); const { chat_id, content: messageContent, created_at, id: messageId, user_id, sender } = chatEntry.data; if (!sender) return this.loadNativeKickFallbackReplyBehaviour(fallbackButtonEl); const { id: senderId, slug: senderSlug, username: senderUsername } = sender; if (!senderId || !senderUsername) return this.loadNativeKickFallbackReplyBehaviour(fallbackButtonEl); if (!inputController) return error29("KICK", "UI", "Input controller not loaded for reply behaviour"); const replyMessageWrapperEl = document.createElement("div"); replyMessageWrapperEl.classList.add("ntv__reply-message__wrapper"); document.querySelector("#chat-input-wrapper")?.parentElement?.prepend(replyMessageWrapperEl); this.elm.replyMessageWrapper = replyMessageWrapperEl; const msgInnerEl = messageNode.querySelector(".ntv__chat-message__inner"); if (!msgInnerEl) { error29("KICK", "UI", "Message inner element not found", messageNode); return this.loadNativeKickFallbackReplyBehaviour(fallbackButtonEl); } this.replyMessage(Array.from(msgInnerEl.children), messageId, messageContent, senderId, senderUsername); inputController.addEventListener("keydown", 9, (event) => { if (event.key === "Escape" && (this.replyMessageData || this.replyMessageComponent)) { this.destroyReplyMessageContext(); } }); document.addEventListener("keydown", (event) => { if (event.key === "Escape" && (this.replyMessageData || this.replyMessageComponent)) { this.destroyReplyMessageContext(); } }); } applyModViewFixes() { const chatroomEl = document.getElementById("chatroom"); if (chatroomEl) { const chatroomParentEl = chatroomEl.parentElement; chatroomParentEl.style.setProperty("overflow-x", "hidden"); } } renderPinnedMessageContent(contentBodyEl) { log28("KICK", "UI", "Rendering pinned message.."); const ntvPinnedMessageBodyEl = document.createElement("div"); ntvPinnedMessageBodyEl.className = "ntv__pinned-message__content"; const emotesManager = this.session.emotesManager; const parsedEmoteParts = []; for (const childNode of Array.from(contentBodyEl.childNodes)) { if (childNode.nodeType === Node.TEXT_NODE) { emotesManager.parseEmoteText(childNode.textContent || "", parsedEmoteParts); } else if (childNode.nodeType === Node.ELEMENT_NODE) { const emoteName = childNode.getAttribute("data-emote-name"); const emoteId = childNode.getAttribute("data-emote-id"); if (!emoteId || !emoteName) { error29("KICK", "UI", "Emote ID or name not found", childNode); parsedEmoteParts.push(childNode.cloneNode(true)); continue; } let emote = emotesManager.getEmoteByName(emoteName); if (!emote) { emote = { id: emoteId, name: emoteName, isSubscribersOnly: true, provider: 1 /* KICK */ }; } parsedEmoteParts.push({ type: "emote", emote }); } else { error29("KICK", "UI", "Unknown node found for pinned message", childNode); parsedEmoteParts.push(childNode.cloneNode(true)); } } ntvPinnedMessageBodyEl.append(...this.renderMessageParts(parsedEmoteParts)); contentBodyEl.before(ntvPinnedMessageBodyEl); } insertNodesInChat(embedNodes) { if (!embedNodes.length) return error29("KICK", "UI", "No nodes to insert in chat"); const textFieldEl = this.elm.textField; if (!textFieldEl) return error29("KICK", "UI", "Text field not loaded for inserting node"); const selection = window.getSelection(); if (selection && selection.rangeCount) { const range = selection.getRangeAt(0); const caretIsInTextField = range.commonAncestorContainer === textFieldEl || range.commonAncestorContainer?.parentElement === textFieldEl; if (caretIsInTextField) { range.deleteContents(); for (const node of embedNodes) { range.insertNode(node); } selection.collapseToEnd(); } else { textFieldEl.append(...embedNodes); } } else { textFieldEl.append(...embedNodes); } textFieldEl.normalize(); textFieldEl.dispatchEvent(new Event("input")); textFieldEl.focus(); } insertNodeInChat(embedNode) { if (embedNode.nodeType !== Node.TEXT_NODE && embedNode.nodeType !== Node.ELEMENT_NODE) { return error29("KICK", "UI", "Invalid node type", embedNode); } const textFieldEl = this.elm.textField; if (!textFieldEl) return error29("KICK", "UI", "Text field not loaded for inserting node"); const selection = window.getSelection(); const range = selection?.anchorNode ? selection.getRangeAt(0) : null; if (range) { const caretIsInTextField = range.commonAncestorContainer === textFieldEl || range.commonAncestorContainer?.parentElement === textFieldEl; if (caretIsInTextField) { Caret.insertNodeAtCaret(range, embedNode); } else { textFieldEl.appendChild(embedNode); } Caret.collapseToEndOfNode(embedNode); } else { textFieldEl.appendChild(embedNode); } textFieldEl.normalize(); textFieldEl.dispatchEvent(new Event("input")); textFieldEl.focus(); } destroy() { if (this.abortController) this.abortController.abort(); if (this.chatObserver) this.chatObserver.disconnect(); if (this.footerObserver) this.footerObserver.disconnect(); if (this.deletedChatEntryObserver) this.deletedChatEntryObserver.disconnect(); if (this.replyObserver) this.replyObserver.disconnect(); if (this.pinnedMessageObserver) this.pinnedMessageObserver.disconnect(); if (this.inputController) this.inputController.destroy(); if (this.inputComponentObserver) this.inputComponentObserver.disconnect(); if (this.submitButtonObserver) this.submitButtonObserver.disconnect(); if (this.emoteMenu) this.emoteMenu.destroy(); if (this.emoteMenuButton) this.emoteMenuButton.destroy(); if (this.emoteMenuButtonObserver) this.emoteMenuButtonObserver.disconnect(); if (this.quickEmotesHolder) this.quickEmotesHolder.destroy(); if (this.quickEmotesHolderObserver) this.quickEmotesHolderObserver.disconnect(); if (this.celebrationsContainerEl) this.celebrationsContainerEl.remove(); if (this.clearQueuedChatMessagesInterval) clearInterval(this.clearQueuedChatMessagesInterval); if (this.reloadUIhackInterval) clearInterval(this.reloadUIhackInterval); if (this.emoteMenuButtonPanicResetTimeout) clearTimeout(this.emoteMenuButtonPanicResetTimeout); if (this.quickEmotesHolderPanicResetTimeout) clearTimeout(this.quickEmotesHolderPanicResetTimeout); if (this.inputComponentPanicResetTimeout) clearTimeout(this.inputComponentPanicResetTimeout); if (this.currentEmoteTooltip) { this.currentEmoteTooltip.remove(); this.currentEmoteTooltip = null; } if (this.emoteTooltipPointerMoveHandler) { document.removeEventListener("pointermove", this.emoteTooltipPointerMoveHandler); this.emoteTooltipPointerMoveHandler = null; } if (this.emoteTooltipTimeout) { window.clearTimeout(this.emoteTooltipTimeout); this.emoteTooltipTimeout = null; } this.domEventManager.removeAllEventListeners(); this.restoreOriginalUi(); } restoreOriginalUi() { Array.from(document.querySelectorAll(".ntv__chat-message, .ntv__chat-message--unrendered")).forEach((node) => { const el = node; el.querySelectorAll(".ntv__chat-message__inner").forEach((innerNode) => innerNode.remove()); el.querySelectorAll(".kick__chat-message__actions").forEach((node2) => node2.remove()); el.querySelectorAll("#chat-message-actions").forEach((node2) => { node2.parentElement?.style.removeProperty("display"); }); Array.from(el.classList).forEach((className) => { if (className.startsWith("ntv__")) el.classList.remove(className); }); }); document.querySelectorAll(".ntv__chat-messages-container").forEach((node) => node.classList.remove("ntv__chat-messages-container")); ["ntv__pinned-message__content"].forEach((className) => { Array.from(document.querySelectorAll(`.${className}`)).forEach((node) => node.remove()); }); ["ntv__emote-menu-button", "ntv__submit-button disabled", "ntv__quick-emotes-holder"].forEach((className) => { Array.from(document.querySelectorAll(`.${className}`)).forEach((node) => node.classList.remove(className)); }); } }; // src/Sites/Kick/KickNetworkInterface.ts var logger30 = new Logger(); var { log: log29, info: info27, error: error30 } = logger30.destruct(); function tryParseErrorMessage(res) { if (res.status && res.status.error && res.status.message) { return res.status.message; } if (res.errors) { let formattedMessage = res.message ? `${res.message} ` : ""; for (const [field, messages] of Object.entries(res.errors)) { if (Array.isArray(messages)) { formattedMessage += `- ${[field]}: ${messages.join("\n").replaceAll(`[${field.replaceAll("_", " ")}]`, "").replace(/\s+/g, " ").replace(/\s\./g, ".")} `; } else { formattedMessage += `- ${[field]}: ${messages} `; } } return formattedMessage.trim(); } if (typeof res.error === "string") { return res.error; } if (!res.error && !res.errors && !res.status && res.message) { return res.message; } } function handleApiError(res, defaultMessage) { let messageString = defaultMessage; if (res instanceof Error) { messageString += " Server gave as reason:\n\n" + res.message; } else { const errorMessage = tryParseErrorMessage(res); if (errorMessage) { messageString += " Server gave as reason:\n\n" + errorMessage; } else { messageString += " No error message provided."; } } throw new Error(messageString); } var KickNetworkInterface = class { constructor(session) { this.session = session; } async connect() { return Promise.resolve(); } async disconnect() { return Promise.resolve(); } getChannelName() { const pathArr = window.location.pathname.substring(1).split("/"); switch (pathArr[0]) { case "popout": return pathArr[1] || null; default: return pathArr[0] || null; } } isVOD() { const pathArr = window.location.pathname.substring(1).split("/"); return pathArr[1] === "videos" && !!pathArr[2]; } async loadMeData() { const userData = await RESTFromMainService.get("https://kick.com/api/v1/user").catch(() => { }); if (!userData) throw new Error("Failed to fetch user data"); if (!userData.streamer_channel) throw new Error('Invalid user data, missing property "streamer_channel"'); const { id, user_id, slug } = userData.streamer_channel; if (!id) throw new Error('Invalid user data, missing property "id"'); if (!user_id) throw new Error('Invalid user data, missing property "user_id"'); if (!slug) throw new Error('Invalid user data, missing property "slug"'); this.session.meData = { channelId: "" + id, userId: "" + user_id, username: userData.username, slug }; log29("KICK", "NET", "LOADED ME DATA", this.session.meData); } async loadChannelData() { const pathArr = window.location.pathname.substring(1).split("/"); const channelData = {}; if (pathArr[1] === "videos" && pathArr[2]) { info27("KICK", "NET", "VOD video detected.."); const videoId = pathArr[2]; if (!videoId) throw new Error("Failed to extract video ID from URL"); const responseChannelData = await RESTFromMainService.get(`https://kick.com/api/v1/video/${videoId}`).catch( () => { } ); if (!responseChannelData) { throw new Error("Failed to fetch VOD data"); } if (!responseChannelData.livestream) { throw new Error('Invalid VOD data, missing property "livestream"'); } const { id, user_id, slug, user } = responseChannelData.livestream.channel; if (!id) { throw new Error('Invalid VOD data, missing property "id"'); } if (!user_id) { throw new Error('Invalid VOD data, missing property "user_id"'); } if (!user) { throw new Error('Invalid VOD data, missing property "user"'); } Object.assign(channelData, { userId: "" + user_id, channelId: "" + id, channelName: user.username, isVod: true, me: { isLoggedIn: false } }); } else { if (pathArr[0] === "popout") { pathArr.shift(); } let isModView = false; if (pathArr[0] === "moderator") { pathArr.shift(); isModView = true; } let channelName2 = pathArr[0]; if (!channelName2) throw new Error("Failed to extract channel name from URL"); let isCreatorView = false; if (channelName2 === "dashboard" || channelName2 === "stream" && document.location.hostname.split(".").includes("dashboard")) { const userData = await RESTFromMainService.get("https://kick.com/api/v1/user"); if (!userData) throw new Error("Failed to fetch user data"); if (!userData.streamer_channel) throw new Error('Invalid user data, missing property "streamer_channel"'); const slug = userData.streamer_channel?.slug; if (!slug) throw new Error('Invalid user data, missing property "slug"'); channelName2 = slug; isCreatorView = true; } const responseChannelData = await RESTFromMainService.get( `https://kick.com/api/v2/channels/${channelName2}` ).catch(() => { }); if (!responseChannelData) { throw new Error("Failed to fetch channel data"); } if (!responseChannelData.id) { throw new Error('Invalid channel data, missing property "id"'); } if (!responseChannelData.user_id) { throw new Error('Invalid channel data, missing property "user_id"'); } if (!responseChannelData.chatroom?.id) { throw new Error('Invalid channel data, missing property "chatroom.id"'); } Object.assign(channelData, { userId: "" + responseChannelData.user_id, channelId: "" + responseChannelData.id, channelName: channelName2, isVod: false, isCreatorView, isModView, chatroom: { id: "" + responseChannelData.chatroom.id, emotesMode: { enabled: !!responseChannelData.chatroom.emotes_mode }, subscribersMode: { enabled: !!responseChannelData.chatroom.subscribers_mode }, followersMode: { enabled: !!responseChannelData.chatroom.followers_mode, min_duration: responseChannelData.chatroom.following_min_duration || 0 }, slowMode: { enabled: !!responseChannelData.chatroom.slow_mode, messageInterval: responseChannelData.chatroom.message_interval || 6 } }, me: { isLoggedIn: false } }); } const channelName = channelData.channelName; const responseChannelMeData = await RESTFromMainService.get( `https://kick.com/api/v2/channels/${channelName}/me` ).catch((err) => error30("KICK", "NET", err.message)); if (responseChannelMeData) { Object.assign(channelData, { me: { isLoggedIn: true, isSubscribed: !!responseChannelMeData.subscription, isFollowing: !!responseChannelMeData.is_following, isSuperAdmin: !!responseChannelMeData.is_super_admin, isBroadcaster: !!responseChannelMeData.is_broadcaster, isModerator: !!responseChannelMeData.is_moderator } }); if (responseChannelMeData.following_since) channelData.me.followingSince = responseChannelMeData.following_since; if (responseChannelMeData.banned) { channelData.me.isBanned = { bannedAt: responseChannelMeData.banned.created_at, expiresAt: responseChannelMeData.banned.expires_at, permanent: responseChannelMeData.banned.permanent, reason: responseChannelMeData.banned.reason }; } if (responseChannelMeData.celebrations && responseChannelMeData.celebrations.length) { channelData.me.celebrations = responseChannelMeData.celebrations.map( (celebration) => { return { createdAt: celebration.created_at, deferred: celebration.deferred, id: celebration.id, type: celebration.type, metadata: { provider: celebration.metadata.provider, streakMonths: celebration.metadata.streak_months, totalMonths: celebration.metadata.total_months } }; } ); } } else { info27("KICK", "NET", "User is not logged in."); } this.session.channelData = channelData; log29("KICK", "NET", "LOADED CHANNEL DATA", this.session.channelData); } async sendMessage(message, noUtag = false) { if (!this.session.channelData) throw new Error("Channel data is not loaded yet."); if (!this.session.channelData.chatroom) throw new Error("Chatroom data is not loaded yet."); if (!noUtag) message[message.length - 1] === " " || (message += " "); const chatroomId = this.session.channelData.chatroom.id; return RESTFromMainService.post("https://kick.com/api/v2/messages/send/" + chatroomId, { content: message + (noUtag ? "" : U_TAG_NTV_AFFIX), type: "message" // metadata: {} // Pinned messages break if we send metadata }).then((res) => { const parsedError = tryParseErrorMessage(res); if (parsedError) throw new Error(parsedError); }).catch((err) => { handleApiError(err, "Failed to send message."); }); } async sendReply(message, originalMessageId, originalMessageContent, originalSenderId, originalSenderUsername, noUtag = false) { if (!this.session.channelData) throw new Error("Channel data is not loaded yet."); if (!this.session.channelData.chatroom) throw new Error("Chatroom data is not loaded yet."); if (!noUtag) message[message.length - 1] === " " || (message += " "); const chatroomId = this.session.channelData.chatroom.id; return RESTFromMainService.post("https://kick.com/api/v2/messages/send/" + chatroomId, { content: message + (noUtag ? "" : U_TAG_NTV_AFFIX), type: "reply", metadata: { original_message: { id: originalMessageId // content: originalMessageContent }, original_sender: { id: +originalSenderId // username: originalSenderUsername } } }).then((res) => { const parsedError = tryParseErrorMessage(res); if (parsedError) throw new Error(parsedError); }).catch((err) => { handleApiError(err, "Failed to reply to message."); }); } async sendCelebrationAction(celebrationId, message, action) { if (!this.session.channelData) throw new Error("Channel data is not loaded yet."); const chatroomId = this.session.channelData.chatroom?.id; return RESTFromMainService.post( `https://kick.com/api/v2/chatrooms/${chatroomId}/celebrations/${celebrationId}/action`, { action: "consume", message } ).then((res) => { const parsedError = tryParseErrorMessage(res); if (parsedError) throw new Error(parsedError); }).catch((err) => { handleApiError(err, "Failed to perform celebration action."); }); } async apiCall(method, url, data, errorMessage, successMessage) { try { const res = await RESTFromMainService[method](url, data); const parsedError = tryParseErrorMessage(res); if (parsedError) throw new Error(parsedError); } catch (res) { handleApiError(res, errorMessage); } return successMessage; } async executeCommand(commandName, channelName, args) { let command = KICK_COMMANDS.find((command2) => command2.name === commandName || command2.alias === commandName); if (command?.alias) command = KICK_COMMANDS.find((n) => n.name === command.alias); if (command) { if (command.api && command.api.protocol === "http") { const { method, uri, data, errorMessage, successMessage } = command.api; return await this.apiCall( method, uri(channelName, args), data ? data(args) : null, errorMessage, successMessage ); } else { throw new Error(`Command "${commandName}" is not supported yet.`); } } else { throw new Error(`Unknown command: ${commandName}`); } } async createPoll(channelName, title, options, duration, displayDuration) { return RESTFromMainService.post(`https://kick.com/api/v2/channels/${channelName}/polls`, { title, options, duration, result_display_duration: displayDuration }); } async deletePoll(channelName) { return RESTFromMainService.delete(`https://kick.com/api/v2/channels/${channelName}/polls`); } async followUser(slug) { return RESTFromMainService.post(`https://kick.com/api/v2/channels/${slug}/follow`); } async unfollowUser(slug) { return RESTFromMainService.delete(`https://kick.com/api/v2/channels/${slug}/follow`); } async setChannelUserIdentity(channelId, userId, badges, color) { return RESTFromMainService.put(`https://kick.com/api/v2/channels/${channelId}/users/${userId}/identity`, { badges, color }); } async getUserInfo(slug) { const [res1, res2] = await Promise.allSettled([ // The reason underscores are replaced with dashes is likely because it's a slug RESTFromMainService.get(`https://kick.com/api/v2/channels/${slug}/me`), RESTFromMainService.get(`https://kick.com/api/v2/channels/${slug}`) ]); if (res1.status === "rejected" || res2.status === "rejected") { throw new Error("Failed to fetch user data"); } const userMeInfo = res1.value; const userOwnChannelInfo = res2.value; return { id: userOwnChannelInfo.user.id, slug: userOwnChannelInfo.slug, username: userOwnChannelInfo.user.username, profilePic: userOwnChannelInfo.user.profile_pic || NTV_RESOURCE_ROOT + "assets/img/kick/default-user-profile.png", bannerImg: userOwnChannelInfo?.banner_image?.url || "", createdAt: userOwnChannelInfo?.chatroom?.created_at ? new Date(userOwnChannelInfo?.chatroom?.created_at) : null, isFollowing: userMeInfo.is_following }; } async getUserChannelInfo(channelName, username) { const channelUserInfo = await RESTFromMainService.get( `https://kick.com/api/v2/channels/${channelName}/users/${username}` ); return { id: channelUserInfo.id, username: channelUserInfo.username, slug: channelUserInfo.slug, channel: channelName, badges: channelUserInfo.badges || [], followingSince: channelUserInfo.following_since ? new Date(channelUserInfo.following_since) : null, isChannelOwner: channelUserInfo.is_channel_owner, isModerator: channelUserInfo.is_moderator, isStaff: channelUserInfo.is_staff, banned: channelUserInfo.banned ? { reason: channelUserInfo.banned?.reason || "No reason provided", since: channelUserInfo.banned?.created_at ? new Date(channelUserInfo.banned?.created_at) : null, expiresAt: channelUserInfo.banned?.expires_at ? new Date(channelUserInfo.banned?.expires_at) : null, permanent: channelUserInfo.banned?.permanent || false } : void 0 }; } async getUserMessages(channelId, userId, cursor) { const res = await RESTFromMainService.get( `https://kick.com/api/v2/channels/${channelId}/users/${userId}/messages?cursor=${cursor}` ); const { data, status } = res; if (status.error) { error30("KICK", "NET", "Failed to fetch user messages", status); throw new Error("Failed to fetch user messages"); } const messages = data.messages; return { cursor: data.cursor, messages: messages.map((message) => { return { id: message.id, content: message.content, createdAt: message.created_at, sender: { id: message.sender?.id || "Unknown", username: message.sender?.username || "Unknown", badges: message.sender?.identity?.badges || [], color: message.sender?.identity?.color || "#dec859" } }; }) }; } }; // src/Sites/Kick/KickEmoteProvider.ts var logger31 = new Logger(); var { log: log30, info: info28, error: error31 } = logger31.destruct(); var KickEmoteProvider = class extends AbstractEmoteProvider { id = 1 /* KICK */; name = "Kick"; constructor(settingsManager) { super(settingsManager); } async fetchEmotes({ channelId, channelName, userId, me }) { if (!channelId) { this.status = "connection_failed" /* CONNECTION_FAILED */; throw new Error("Missing channel id for Kick provider"); } if (!channelName) { this.status = "connection_failed" /* CONNECTION_FAILED */; throw new Error("Missing channel name for Kick provider"); } const { settingsManager } = this; const isChatEnabled = !!settingsManager.getSetting(channelId, "chat.emote_providers.kick.show_emotes"); if (!isChatEnabled) { this.status = "loaded" /* LOADED */; return []; } info28("KICK", "EMOT:PROV", "Fetching emote data from Kick.."); const dataSets = await RESTFromMainService.get(`https://kick.com/emotes/${channelName}`); if (!dataSets) { this.status = "connection_failed" /* CONNECTION_FAILED */; return error31("KICK", "EMOT:PROV", "Failed to fetch Kick emotes"); } const emoteSets = []; for (const dataSet of dataSets) { const emotesMapped = dataSet.emotes.map((emote) => { const sanitizedEmoteName = emote.name.replaceAll("<", "<").replaceAll('"', """); const parts = splitEmoteName(sanitizedEmoteName, 2); return { id: "" + emote.id, hid: md5(emote.name), name: sanitizedEmoteName, isSubscribersOnly: emote.subscribers_only, provider: this.id, width: 32, size: 1, parts }; }); const emoteSetIcon = dataSet?.user?.profile_pic || "https://kick.com/favicon.ico"; const emoteSetName = dataSet.user ? `${dataSet.user.username}'s Emotes` : `${dataSet.name} Emotes`; let orderIndex = 1; if (dataSet.id === "Global") { orderIndex = 10; } else if (dataSet.id === "Emoji") { orderIndex = 15; } let isMenuEnabled = true, isGlobalSet = false, isEmoji = false; if (dataSet.id === "Global") { isGlobalSet = true; dataSet.id = "kick_global"; isMenuEnabled = !!settingsManager.getSetting(channelId, "emote_menu.emote_providers.kick.show_global"); } else if (dataSet.id === "Emoji") { isEmoji = true; dataSet.id = "kick_emoji"; isMenuEnabled = !!settingsManager.getSetting(channelId, "emote_menu.emote_providers.kick.show_emojis"); } else if ("" + dataSet.id === channelId) { isMenuEnabled = !!settingsManager.getSetting( channelId, "emote_menu.emote_providers.kick.show_current_channel" ); } else { isMenuEnabled = !!settingsManager.getSetting( channelId, "emote_menu.emote_providers.kick.show_other_channels" ); } const dataSetId = "" + dataSet.id; emoteSets.push({ provider: this.id, orderIndex, name: emoteSetName, emotes: emotesMapped, enabledInMenu: isMenuEnabled, isEmoji, isGlobalSet, isCurrentChannel: dataSetId === channelId, isOtherChannel: dataSetId !== channelId && !isGlobalSet && !isEmoji, isSubscribed: dataSetId === channelId ? !!me.isSubscribed || !!me.isBroadcaster : true, icon: emoteSetIcon, id: "kick_" + dataSetId }); } if (!emoteSets.length) { log30("KICK", "EMOT:PROV", "No emote sets found on Kick provider with current settings."); this.status = "no_emotes" /* NO_EMOTES */; return []; } if (emoteSets.length > 1) { log30("KICK", "EMOT:PROV", `Fetched ${emoteSets.length} emote sets from Kick`); } else { log30("KICK", "EMOT:PROV", `Fetched 1 emote set from Kick`); } this.status = "loaded" /* LOADED */; return emoteSets; } getRenderableEmote(emote, classes = "", srcSetWidthDescriptor) { const srcset = `https://files.kick.com/emotes/${emote.id}/fullsize 1x`; return `<img class="ntv__emote ${classes}" tabindex="0" data-emote-name="${emote.name || ""}" data-emote-hid="${emote.hid || ""}" alt="${emote.name || ""}" srcset="${srcset}" loading="lazy" decoding="async" draggable="false">`; } getRenderableEmoteById(emoteId, classes = "") { const srcset = `https://files.kick.com/emotes/${emoteId}/fullsize 1x`; return `<img class="ntv__emote ${classes}" tabindex="0" srcset="${srcset}" loading="lazy" decoding="async" draggable="false">`; } getEmbeddableEmote(emote) { return `[emote:${emote.id}:${emote.name}]`; } getEmoteSrc(emote) { return `https://files.kick.com/emotes/${emote.id}/fullsize`; } }; // src/Sites/Kick/KickBadgeProvider.ts var logger32 = new Logger(); var { log: log31, info: info29, error: error32 } = logger32.destruct(); var KickBadgeProvider = class { rootContext; channelData; subscriberBadges = []; subscriberBadgesLookupTable = /* @__PURE__ */ new Map(); highestBadgeCount = 1; hasCustomBadges = false; constructor(rootContext, channelData) { this.rootContext = rootContext; this.channelData = channelData; } async initialize() { const { channelName } = this.channelData; const channelInfo = await REST.get(`https://kick.com/api/v2/channels/${channelName}`); if (!channelInfo) return error32( "KICK", "BADGE:PROV", "Unable to fetch channel info from Kick API for badge provider initialization." ); if (!channelInfo.subscriber_badges) return error32( "KICK", "BADGE:PROV", "No subscriber badges found in channel info from Kick API for badge provider initialization." ); const subscriber_badges = channelInfo.subscriber_badges; if (!subscriber_badges.length) return; this.hasCustomBadges = true; this.highestBadgeCount = subscriber_badges[subscriber_badges.length - 1].months || 1; for (const subscriber_badge of subscriber_badges) { const badge = { html: `<img class="ntv__badge" src="${subscriber_badge?.badge_image.src}" srcset="${subscriber_badge?.badge_image.srcset}" alt="${subscriber_badge.months} months subscriber" ntv-tooltip="${subscriber_badge.months === 1 ? subscriber_badge.months + " month subscriber" : subscriber_badge.months + " months subscriber"}">`, months: subscriber_badge.months }; this.subscriberBadges.push(badge); } const thresholds = this.subscriberBadges.map((badge) => badge.months); for (let i = 0; i < this.highestBadgeCount; i++) { let j = 0; while (i > thresholds[j] && j < 100) j++; this.subscriberBadgesLookupTable.set(i, this.subscriberBadges[j]); } } getBadge(badge) { if (badge.type === "subscriber") { if (this.hasCustomBadges) { const subscriberBadge = this.subscriberBadgesLookupTable.get(badge.count || 0); if (subscriberBadge) return subscriberBadge.html; else if (badge.count || 0 > this.highestBadgeCount) { const highestBadge = this.subscriberBadges[this.subscriberBadges.length - 1]; return highestBadge.html; } } else { return this.getGlobalBadge(badge); } } return this.getGlobalBadge(badge); } getGlobalBadge(badge) { const randomId = "_" + (Math.random() * 1e7 << 0); switch (badge.type) { case "nipahtv": return `<svg class="ntv__badge" ntv-tooltip="NipahTV" width="16" height="16" viewBox="0 0 16 16" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"><path style="opacity:1;fill:#ff3e64;fill-opacity:1;fill-rule:nonzero;stroke-width:0.0213617" d="m 0.70822498,15.290987 0.001812,-7.8292114 C 0.70653032,6.965316 1.2237852,5.8717964 2.194683,5.3684396 2.7755683,5.0672828 3.2919441,4.9660023 4.2567281,4.9460193 5.1878339,4.9267313 5.6534771,5.112261 5.8207707,5.1941088 6.2800381,5.4005097 6.7575397,5.8333063 6.888905,6.0197258 l 4.116172,5.7127422 c 0.246656,0.313304 0.472304,0.515883 0.789429,0.483165 0.268048,-0.02132 0.391095,-0.244543 0.39227,-0.447937 l 0.0028,-8.1313972 -11.48244477,5.907e-4 4.061e-5,-2.93166096 14.58710216,1.9564e-4 -1.02e-4,11.93294882 c -0.0053,0.135916 -0.09041,1.086166 -0.973117,1.847756 -0.790189,0.736683 -2.136076,0.801691 -2.136076,0.801691 -0.566301,0.0081 -0.99516,0.02153 -1.315048,-0.04282 -0.53047,-0.135372 -0.7605,-0.249894 -1.1996957,-0.554391 -0.3850818,-0.284365 -0.6418055,-0.56177 -0.8582569,-0.84361 0,0 -1.0218521,-1.278603 -1.7618165,-2.355658 C 6.4828049,10.622664 5.5215787,9.3197084 5.0517693,8.7103949 4.6940804,8.212752 4.4451023,8.1044584 4.311807,8.0586264 4.1695927,8.0132966 4.0131483,8.0540122 3.8926944,8.1041229 3.7861451,8.1526458 3.6954488,8.2420316 3.6793314,8.3630461 l 7.185e-4,6.9288349 z"/></svg>`; case "broadcaster": return `<svg class="ntv__badge" ntv-tooltip="Broadcaster" x="0px" y="0px" width="16" height="16" viewBox="0 0 16 16" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g id="Badge_Chat_host"><linearGradient id="badge-host-gradient-1${randomId}" gradientUnits="userSpaceOnUse" x1="4" y1="180.5864" x2="4" y2="200.6666" gradientTransform="matrix(1 0 0 1 0 -182)"><stop offset="0" style="stop-color:#FF1CD2;"></stop><stop offset="0.99" style="stop-color:#B20DFF;"></stop></linearGradient><rect x="3.2" y="9.6" style="fill:url(#badge-host-gradient-1${randomId});" width="1.6" height="1.6"></rect><linearGradient id="badge-host-gradient-2${randomId}" gradientUnits="userSpaceOnUse" x1="8" y1="180.5864" x2="8" y2="200.6666" gradientTransform="matrix(1 0 0 1 0 -182)"><stop offset="0" style="stop-color:#FF1CD2;"></stop><stop offset="0.99" style="stop-color:#B20DFF;"></stop></linearGradient><polygon style="fill:url(#badge-host-gradient-2${randomId});" points="6.4,9.6 9.6,9.6 9.6,8 11.2,8 11.2,1.6 9.6,1.6 9.6,0 6.4,0 6.4,1.6 4.8,1.6 4.8,8 6.4,8"></polygon><linearGradient id="badge-host-gradient-3${randomId}" gradientUnits="userSpaceOnUse" x1="2.4" y1="180.5864" x2="2.4" y2="200.6666" gradientTransform="matrix(1 0 0 1 0 -182)"><stop offset="0" style="stop-color:#FF1CD2;"></stop><stop offset="0.99" style="stop-color:#B20DFF;"></stop></linearGradient><rect x="1.6" y="6.4" style="fill:url(#badge-host-gradient-3${randomId});" width="1.6" height="3.2"></rect><linearGradient id="badge-host-gradient-4${randomId}" gradientUnits="userSpaceOnUse" x1="12" y1="180.5864" x2="12" y2="200.6666" gradientTransform="matrix(1 0 0 1 0 -182)"><stop offset="0" style="stop-color:#FF1CD2;"></stop><stop offset="0.99" style="stop-color:#B20DFF;"></stop></linearGradient><rect x="11.2" y="9.6" style="fill:url(#badge-host-gradient-4${randomId});" width="1.6" height="1.6"></rect><linearGradient id="badge-host-gradient-5${randomId}" gradientUnits="userSpaceOnUse" x1="8" y1="180.5864" x2="8" y2="200.6666" gradientTransform="matrix(1 0 0 1 0 -182)"><stop offset="0" style="stop-color:#FF1CD2;"></stop><stop offset="0.99" style="stop-color:#B20DFF;"></stop></linearGradient><polygon style="fill:url(#badge-host-gradient-5${randomId});" points="4.8,12.8 6.4,12.8 6.4,14.4 4.8,14.4 4.8,16 11.2,16 11.2,14.4 9.6,14.4 9.6,12.8 11.2,12.8 11.2,11.2 4.8,11.2 "></polygon><linearGradient gradientUnits="userSpaceOnUse" x1="13.6" y1="180.5864" x2="13.6" y2="200.6666" gradientTransform="matrix(1 0 0 1 0 -182)" id="badge-host-gradient-6${randomId}"><stop offset="0" style="stop-color:#FF1CD2;"></stop><stop offset="0.99" style="stop-color:#B20DFF;"></stop></linearGradient><rect x="12.8" y="6.4" style="fill:url(#badge-host-gradient-6${randomId});" width="1.6" height="3.2"></rect></g></svg>`; case "verified": return `<svg class="ntv__badge" ntv-tooltip="Verified" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <defs><linearGradient id="badge-verified-gradient${randomId}" x1="25.333%" y1="99.375%" x2="73.541%" y2="2.917%" gradientUnits="objectBoundingBox"><stop stop-color="#1EFF00"/><stop offset="0.99" stop-color="#00FF8C"/></linearGradient></defs><path d="M14.72 7.00003V6.01336H15.64V4.12003H14.6733V3.16003H9.97332V1.2667H8.96665V0.280029H7.03332V1.2667H6.03332V3.16003H1.32665V4.12003H0.359985V6.01336H1.28665V7.00003H2.23332V9.0067H1.28665V9.99336H0.359985V11.8867H1.32665V12.8467H6.03332V14.74H7.03332V15.7267H8.96665V14.74H9.97332V12.8467H14.6733V11.8867H15.64V9.99336H14.72V9.0067H13.7733V7.00003H14.72ZM12.5 6.59336H11.44V7.66003H10.3733V8.72003H9.31332V9.7867H8.24665V10.8467L7.09332 10.9V11.8H6.02665V10.8467H5.05999V9.7867H3.99332V7.66003H6.11999V8.72003H7.18665V7.66003H8.24665V6.59336H9.31332V5.53336H10.3733V4.4667H12.5V6.59336Z" fill="url(#badge-verified-gradient${randomId})"/></svg>`; case "staff": return `<svg class="ntv__badge" ntv-tooltip="Staff" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="badge-verified-gradient${randomId}" x1="33.791%" y1="97.416%" x2="65.541%" y2="4.5%" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#1EFF00"></stop><stop offset="0.99" stop-color="#00FF8C"></stop></linearGradient></defs><path fill-rule="evenodd" clip-rule="evenodd" d="M2.07324 1.33331H6.51991V4.29331H7.99991V2.81331H9.47991V1.33331H13.9266V5.77998H12.4466V7.25998H10.9599V8.73998H12.4466V10.22H13.9266V14.6666H9.47991V13.1866H7.99991V11.7066H6.51991V14.6666H2.07324V1.33331Z" fill="url(#badge-verified-gradient${randomId})"></path></svg>`; case "global_moderator": return `<svg class="ntv__badge" ntv-tooltip="Global Moderator" version="1.1" x="0px" y="0px" viewBox="0 0 16 16" xml:space="preserve" width="16" height="16" style="enable-background:new 0 0 16 16"><g><linearGradient id="badge-global-mod-gradient${randomId}" gradientUnits="userSpaceOnUse" x1="-1.918" y1="382.5619" x2="17.782" y2="361.3552" gradientTransform="matrix(1 0 0 1 0 -364)"><stop offset="0" style="stop-color:#FCA800"></stop><stop offset="0.99" style="stop-color:#FF5100"></stop></linearGradient><path style="fill:url(#badge-global-mod-gradient${randomId})" d="M10.5,0v1.5H9V3H7.5v1.5h-6v6H0V16h5.5v-1.5h6v-6H13V7h1.5V5.5H16V0H10.5z M14.7,4.3h-1.5 v1.5h-1.5v1.5h-1.5v1.5H8.7v1.5h1.5v3h-3v-1.5H5.8v1.5H4.3v1.5h-3v-3h1.5v-1.5h1.5V8.7H2.8v-3h3v1.5h1.5V5.8h1.5V4.3h1.5V2.8h1.5 V1.3h3v3H14.7z"></path></g></svg>`; case "global_admin": return `<svg class="ntv__badge" ntv-tooltip="Global Admin" version="1.1" x="0px" y="0px" viewBox="0 0 16 16" xml:space="preserve" width="16" height="16" style="enable-background:new 0 0 16 16"><linearGradient id="badge-global-staff-gradient${randomId}" gradientUnits="userSpaceOnUse" x1="-0.03948053" y1="-180.1338" x2="15.9672" y2="-163.9405" gradientTransform="matrix(1 0 0 -1 0 -164)"><stop offset="0" style="stop-color:#FCA800"></stop><stop offset="0.99" style="stop-color:#FF5100"></stop></linearGradient><path style="fill-rule:evenodd;clip-rule:evenodd;fill:url(#badge-global-staff-gradient${randomId});" d="M1.1,0.3v15.3H15V0.3H1.1z M12.9,6.2h-1.2v1.2h-1.2v1.2h1.2v1.2h1.2v3.7H9.2v-1.2H8V11H6.8v2.4H3.1v-11h3.7v2.4H8V3.7h1.2V2.5h3.7V6.2z"></path></svg>`; case "sidekick": return `<svg class="ntv__badge" ntv-tooltip="Sidekick" version="1.1" x="0px" y="0px" viewBox="0 0 16 16" xml:space="preserve" width="16" height="16" style="enable-background:new 0 0 16 16"><linearGradient id="badge-sidekick-gradient${randomId}" gradientUnits="userSpaceOnUse" x1="9.3961" y1="-162.6272" x2="5.8428" y2="-180.3738" gradientTransform="matrix(1 0 0 -1 0 -164)"><stop offset="0" style="stop-color:#FF6A4A;"></stop><stop offset="1" style="stop-color:#C70C00;"></stop></linearGradient><path style="fill:url(#badge-sidekick-gradient${randomId});" d="M0,2.8v5.6h1.1V10h1.1v1.6h1.1v1.6h3.4v-1.6H9v1.6h3.4v-1.6h1.1V10h1.1V8.4H16V2.8h-4.6v1.6H9.1V6H6.8V4.4H4.5V2.8H0z M6.9,9.6H3.4V8H2.3V4.8h1.1v1.6h2.3V8h1.1v1.6H6.9z M13.7,8h-1.1v1.6H9.2V8h1.1V6.4h2.3V4.8h1.1C13.7,4.8,13.7,8,13.7,8z"></path></svg>`; case "moderator": return `<svg class="ntv__badge" ntv-tooltip="Moderator" version="1.1" x="0px" y="0px" viewBox="0 0 16 16" xml:space="preserve" width="16" height="16" style="enable-background:new 0 0 16 16"><path style="fill: rgb(0, 199, 255);" d="M11.7,1.3v1.5h-1.5v1.5 H8.7v1.5H7.3v1.5H5.8V5.8h-3v3h1.5v1.5H2.8v1.5H1.3v3h3v-1.5h1.5v-1.5h1.5v1.5h3v-3H8.7V8.7h1.5V7.3h1.5V5.8h1.5V4.3h1.5v-3C14.7,1.3,11.7,1.3,11.7,1.3z"></path></svg>`; case "vip": return `<svg class="ntv__badge" ntv-tooltip="VIP" version="1.1" x="0px" y="0px" viewBox="0 0 16 16" xml:space="preserve" width="16" height="16"><linearGradient id="badge-vip-gradient${randomId}" gradientUnits="userSpaceOnUse" x1="8" y1="-163.4867" x2="8" y2="-181.56" gradientTransform="matrix(1 0 0 -1 0 -164)"><stop offset="0" style="stop-color: rgb(255, 201, 0);"></stop><stop offset="0.99" style="stop-color: rgb(255, 149, 0);"></stop></linearGradient><path d="M13.9,2.4v1.1h-1.2v2.3 h-1.1v1.1h-1.1V4.6H9.3V1.3H6.7v3.3H5.6v2.3H4.4V5.8H3.3V3.5H2.1V2.4H0v12.3h16V2.4H13.9z" style="fill: url(#badge-vip-gradient${randomId});"></path></svg>`; case "og": return `<svg class="ntv__badge" ntv-tooltip="OG" version="1.1" x="0px" y="0px" viewBox="0 0 16 16" xml:space="preserve" width="16" height="16"><g><linearGradient id="badge-og-gradient-1${randomId}" gradientUnits="userSpaceOnUse" x1="12.2" y1="-180" x2="12.2" y2="-165.2556" gradientTransform="matrix(1 0 0 -1 0 -164)"><stop offset="0" style="stop-color:#00FFF2;"></stop><stop offset="0.99" style="stop-color:#006399;"></stop></linearGradient><path style="fill:url(#badge-og-gradient-1${randomId});" d="M16,16H9.2v-0.8H8.4v-8h0.8V6.4H16v3.2h-4.5v4.8H13v-1.6h-0.8v-1.6H16V16z"></path><linearGradient id="badge-og-gradient-2${randomId}" gradientUnits="userSpaceOnUse" x1="3.7636" y1="-164.265" x2="4.0623" y2="-179.9352" gradientTransform="matrix(1 0 0 -1 0 -164)"><stop offset="0" style="stop-color:#00FFF2;"></stop><stop offset="0.99" style="stop-color:#006399;"></stop></linearGradient><path style="fill:url(#badge-og-gradient-2${randomId});" d="M6.8,8.8v0.8h-6V8.8H0v-8h0.8V0h6.1v0.8 h0.8v8H6.8z M4.5,6.4V1.6H3v4.8H4.5z"></path><path style="fill:#00FFF2;" d="M6.8,15.2V16h-6v-0.8H0V8.8h0.8V8h6.1v0.8h0.8v6.4C7.7,15.2,6.8,15.2,6.8,15.2z M4.5,14.4V9.6H3v4.8 C3,14.4,4.5,14.4,4.5,14.4z"></path><path style="fill:#00FFF2;" d="M16,8H9.2V7.2H8.4V0.8h0.8V0H16v1.6h-4.5v4.8H13V4.8h-0.8V3.2H16V8z"></path></g></svg>`; case "founder": return `<svg class="ntv__badge" ntv-tooltip="Founder" version="1.1" x="0px" y="0px" viewBox="0 0 16 16" xml:space="preserve" width="16" height="16"><linearGradient id="badge-founder-gradient${randomId}" gradientUnits="userSpaceOnUse" x1="7.874" y1="20.2333" x2="8.1274" y2="-0.3467" gradientTransform="matrix(1 0 0 -1 0 18)"><stop offset="0" style="stop-color: rgb(255, 201, 0);"></stop><stop offset="0.99" style="stop-color: rgb(255, 149, 0);"></stop></linearGradient><path d="M14.6,4V2.7h-1.3V1.4H12V0H4v1.4H2.7v1.3H1.3V4H0v8h1.3v1.3h1.4v1.3H4V16h8v-1.4h1.3v-1.3h1.3V12H16V4H14.6z M9.9,12.9H6.7V6.4H4.5 V5.2h1V4.1h1v-1h3.4V12.9z" style="fill-rule: evenodd; clip-rule: evenodd; fill: url(#badge-founder-gradient${randomId});"></path></svg>`; case "subscriber": return `<svg class="ntv__badge" ntv-tooltip="${badge.count ? badge.count === 1 ? badge.count + " month subscriber" : badge.count + " months subscriber" : ""} months" version="1.1" x="0px" y="0px" viewBox="0 0 16 16" xml:space="preserve" width="16" height="16"><g><linearGradient id="badge-subscriber-gradient-1${randomId}" gradientUnits="userSpaceOnUse" x1="-2.386" y1="-151.2764" x2="42.2073" y2="-240.4697" gradientTransform="matrix(1 0 0 -1 0 -164)"><stop offset="0" style="stop-color:#E1FF00;"></stop><stop offset="0.99" style="stop-color:#2AA300;"></stop></linearGradient><path style="fill:url(#badge-subscriber-gradient-1${randomId});" d="M14.8,7.3V6.1h-2.4V4.9H11V3.7H9.9V1.2H8.7V0H7.3v1.2H6.1v2.5H5v1.2H3.7v1.3H1.2v1.2H0v1.4 h1.2V10h2.4v1.3H5v1.2h1.2V15h1.2v1h1.3v-1.2h1.2v-2.5H11v-1.2h1.3V9.9h2.4V8.7H16V7.3H14.8z"></path><linearGradient id="badge-subscriber-gradient-2${randomId}" gradientUnits="userSpaceOnUse" x1="-5.3836" y1="-158.3055" x2="14.9276" y2="-189.0962" gradientTransform="matrix(1 0 0 -1 0 -164)"><stop offset="0" style="stop-color:#E1FF00;"></stop><stop offset="0.99" style="stop-color:#2AA300;"></stop></linearGradient><path style="fill:url(#badge-subscriber-gradient-2${randomId});" d="M7.3,7.3v7.5H6.1v-2.5H5v-1.2H3.7V9.9H1.2 V8.7H0V7.3H7.3z"></path><linearGradient id="badge-subscriber-gradient-3${randomId}" gradientUnits="userSpaceOnUse" x1="3.65" y1="-160.7004" x2="3.65" y2="-184.1244" gradientTransform="matrix(1 0 0 -1 0 -164)"><stop offset="0" style="stop-color:#E1FF00;"></stop><stop offset="0.99" style="stop-color:#2AA300;"></stop></linearGradient><path style="fill:url(#badge-subscriber-gradient-3${randomId});" d="M7.3,7.3v7.5H6.1v-2.5H5v-1.2H3.7V9.9H1.2 V8.7H0V7.3H7.3z"></path><linearGradient id="badge-subscriber-gradient-4${randomId}" gradientUnits="userSpaceOnUse" x1="22.9659" y1="-167.65" x2="-5.3142" y2="-167.65" gradientTransform="matrix(1 0 0 -1 0 -164)"><stop offset="0" style="stop-color:#E1FF00;"></stop><stop offset="0.99" style="stop-color:#2AA300;"></stop></linearGradient><path style="fill:url(#badge-subscriber-gradient-4${randomId});" d="M8.7,0v7.3H1.2V6.1h2.4V4.9H5V3.7h1.2V1.2 h1.2V0H8.7z"></path><linearGradient id="badge-subscriber-gradient-5${randomId}" gradientUnits="userSpaceOnUse" x1="12.35" y1="-187.6089" x2="12.35" y2="-161.5965" gradientTransform="matrix(1 0 0 -1 0 -164)"><stop offset="0" style="stop-color:#E1FF00;"></stop><stop offset="0.99" style="stop-color:#2AA300;"></stop></linearGradient><path style="fill:url(#badge-subscriber-gradient-5${randomId});" d="M8.7,8.7V1.2h1.2v2.5H11v1.2h1.3v1.3h2.4 v1.2H16v1.4L8.7,8.7L8.7,8.7z"></path><linearGradient id="badge-subscriber-gradient-6${randomId}" gradientUnits="userSpaceOnUse" x1="-6.5494" y1="-176.35" x2="21.3285" y2="-176.35" gradientTransform="matrix(1 0 0 -1 0 -164)"><stop offset="0" style="stop-color:#E1FF00;"></stop><stop offset="0.99" style="stop-color:#2AA300;"></stop></linearGradient><path style="fill:url(#badge-subscriber-gradient-6${randomId});" d="M7.3,16V8.7h7.4v1.2h-2.4v1.3H11v1.2H9.9 v2.5H8.7V16H7.3z"></path><linearGradient id="badge-subscriber-gradient-7${randomId}" gradientUnits="userSpaceOnUse" x1="6.72" y1="-169.44" x2="12.2267" y2="-180.4533" gradientTransform="matrix(1 0 0 -1 0 -164)"><stop offset="0" style="stop-color:#E1FF00;"></stop><stop offset="0.99" style="stop-color:#2AA300;"></stop></linearGradient><path style="fill:url(#badge-subscriber-gradient-7${randomId});" d="M8.7,7.3H7.3v1.4h1.3L8.7,7.3L8.7,7.3z"></path></g></svg>`; case "sub_gifter": const count = badge.count || 1; if (count < 25) { return `<svg class="ntv__badge" ntv-tooltip="Gifted ${badge.count} subs" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#clip0_301_17810)"><path d="M7.99999 9.14999V6.62499L0.484985 3.35999V6.34499L1.15499 6.63499V12.73L7.99999 15.995V9.14999Z" fill="#0269D4"></path><path d="M8.00003 10.735V9.61501L1.15503 6.63501V7.70501L8.00003 10.735Z" fill="#0269D4"></path><path d="M15.515 3.355V6.345L14.85 6.64V12.73L12.705 13.755L11.185 14.48L8.00499 15.995V6.715L4.81999 5.295H4.81499L3.29499 4.61L0.484985 3.355L3.66999 1.935L3.67999 1.93L5.09499 1.3L8.00499 0L10.905 1.3L12.32 1.925L12.33 1.935L15.515 3.355Z" fill="#04D0FF"></path><path d="M14.845 6.63501V7.70501L8 10.735V9.61501L14.845 6.63501Z" fill="#0269D4"></path></g><defs><clipPath id="clip0_301_17810"><rect width="16" height="16" fill="white"></rect></clipPath></defs></svg>`; } else if (count >= 25) { return `<svg class="ntv__badge" ntv-tooltip="Gifted ${badge.count} subs" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#clip0_301_17815)"><path d="M8.02501 9.14999V6.62499L0.51001 3.35999V6.34499L1.17501 6.63499V12.73L8.02501 15.995V9.14999Z" fill="#7B1BAB"></path><path d="M8.02505 10.735V9.61501L1.17505 6.63501V7.70501L8.02505 10.735Z" fill="#7B1BAB"></path><path d="M15.535 3.355V6.345L14.87 6.64V12.73L12.725 13.755L11.21 14.48L8.02501 15.995V6.715L4.84001 5.295H4.83501L3.32001 4.61L0.51001 3.355L3.69001 1.935L3.70501 1.93L5.11501 1.3L8.02501 0L10.93 1.3L12.34 1.925L12.355 1.935L15.535 3.355Z" fill="#A947D3"></path><path d="M14.87 6.63501V7.70501L8.02502 10.735V9.61501L14.87 6.63501Z" fill="#7B1BAB"></path></g><defs><clipPath id="clip0_301_17815"><rect width="16" height="16" fill="white"></rect></clipPath></defs></svg>`; } else if (count >= 50) { return `<svg class="ntv__badge" ntv-tooltip="Gifted ${badge.count} subs" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#clip0_301_17820)"><path d="M7.99999 9.14999V6.62499L0.484985 3.35999V6.34499L1.14999 6.63999V12.73L7.99999 16V9.14999Z" fill="#CF0038"></path><path d="M8.00002 10.74V9.61501L1.15002 6.64001V7.71001L8.00002 10.74Z" fill="#CF0038"></path><path d="M15.515 3.355V6.345L14.85 6.64V12.73L12.705 13.755L11.185 14.48L8.00499 15.995V6.715L4.81999 5.295H4.81499L3.29499 4.61L0.484985 3.355L3.66999 1.935L3.67999 1.93L5.09499 1.3L8.00499 0L10.905 1.3L12.32 1.925L12.33 1.935L15.515 3.355Z" fill="#FA4E78"></path><path d="M14.85 6.64001V7.71001L8 10.74V9.61501L14.85 6.64001Z" fill="#CF0038"></path></g><defs><clipPath id="clip0_301_17820"><rect width="16" height="16" fill="white"></rect></clipPath></defs></svg>`; } else if (count >= 100) { return `<svg class="ntv__badge" ntv-tooltip="Gifted ${badge.count} subs" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#clip0_301_17825)"><path d="M7.99999 9.14999V6.62499L0.484985 3.35999V6.34499L1.14999 6.63999V12.73L7.99999 16V9.14999Z" fill="#FF5008"></path><path d="M8.00002 10.74V9.61501L1.15002 6.64001V7.71001L8.00002 10.74Z" fill="#FF5008"></path><path d="M15.515 3.355V6.345L14.85 6.64V12.73L12.705 13.755L11.185 14.48L8.00499 15.995V6.715L4.81999 5.295H4.81499L3.29499 4.61L0.484985 3.355L3.66999 1.935L3.67999 1.93L5.09499 1.3L8.00499 0L10.905 1.3L12.32 1.925L12.33 1.935L15.515 3.355Z" fill="#FFC800"></path><path d="M14.85 6.64001V7.71001L8 10.74V9.61501L14.85 6.64001Z" fill="#FF5008"></path></g><defs><clipPath id="clip0_301_17825"><rect width="16" height="16" fill="white"></rect></clipPath></defs></svg>`; } else if (count >= 200) { return `<svg class="ntv__badge" ntv-tooltip="Gifted ${badge.count} subs" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#clip0_301_17830)"><path d="M7.99999 9.14999V6.62499L0.484985 3.35999V6.34499L1.14999 6.63999V12.73L7.99999 16V9.14999Z" fill="#2FA604"></path><path d="M8.00002 10.74V9.61501L1.15002 6.64001V7.71001L8.00002 10.74Z" fill="#2FA604"></path><path d="M15.515 3.355V6.345L14.85 6.64V12.73L12.705 13.755L11.185 14.48L8.00499 15.995V6.715L4.81999 5.295H4.81499L3.29499 4.61L0.484985 3.355L3.66999 1.935L3.67999 1.93L5.09499 1.3L8.00499 0L10.905 1.3L12.32 1.925L12.33 1.935L15.515 3.355Z" fill="#53F918"></path><path d="M14.85 6.64001V7.71001L8 10.74V9.61501L14.85 6.64001Z" fill="#2FA604"></path></g><defs><clipPath id="clip0_301_17830"><rect width="16" height="16" fill="white"></rect></clipPath></defs></svg>`; } } } }; // src/Extensions/SevenTV/SevenTVEmoteProvider.ts var logger33 = new Logger(); var { log: log32, info: info30, error: error33 } = logger33.destruct(); var SevenTVEmoteProvider = class _SevenTVEmoteProvider extends AbstractEmoteProvider { static id = 2 /* SEVENTV */; id = 2 /* SEVENTV */; name = "7TV"; constructor(settingsManager) { super(settingsManager); } async fetchEmotes({ userId, channelId }) { info30("EXT:STV", "EMOT:PROV", "Fetching emote data from SevenTV.."); this.status = "loading" /* LOADING */; if (!userId) { this.status = "connection_failed" /* CONNECTION_FAILED */; throw new Error("Missing Kick user id for SevenTV provider."); } const isChatEnabled = !!this.settingsManager.getSetting(channelId, "chat.emote_providers.7tv.show_emotes"); if (!isChatEnabled) { this.status = "loaded" /* LOADED */; return; } const [globalData, userData] = await Promise.all([ REST.get(`https://7tv.io/v3/emote-sets/global`).catch((err) => { error33("EXT:STV", "EMOT:PROV", "Failed to fetch SevenTV global emotes:", err); }), REST.get(`https://7tv.io/v3/users/KICK/${userId}`).catch((err) => { error33("EXT:STV", "EMOT:PROV", "Failed to fetch SevenTV user emotes:", err); }) ]); if (!globalData) { this.status = "connection_failed" /* CONNECTION_FAILED */; return error33("EXT:STV", "EMOT:PROV", "Failed to fetch SevenTV global emotes."); } const globalEmoteSet = this.unpackGlobalEmotes(channelId, globalData || {}); const userEmoteSet = this.unpackUserEmotes(channelId, userData || {}); if (!globalEmoteSet) { this.status = "connection_failed" /* CONNECTION_FAILED */; return error33("EXT:STV", "EMOT:PROV", "Failed to unpack global emotes from SevenTV provider."); } if (userEmoteSet) { const plural = globalEmoteSet.length + userEmoteSet.length > 1 ? "sets" : "set"; log32( "EXT:STV", "EMOT:PROV", `Fetched ${globalEmoteSet.length + userEmoteSet.length} emote ${plural} from SevenTV.` ); } else { log32("EXT:STV", "EMOT:PROV", `Fetched ${globalEmoteSet.length} global emote set from SevenTV.`); } this.status = "loaded" /* LOADED */; return userEmoteSet && [...globalEmoteSet, ...userEmoteSet] || [...globalEmoteSet]; } unpackGlobalEmotes(channelId, globalData) { if (!globalData.emotes || !globalData.emotes?.length) { error33("EXT:STV", "EMOT:PROV", "No global emotes found for SevenTV provider"); return; } let emotesMapped = globalData.emotes.map((emote) => { if (!emote.data?.host?.files || !emote.data.host.files.length) { error33("EXT:STV", "EMOT:PROV", "Emote has no files:", emote); return; } const parts = splitEmoteName(emote.name, 2); const file = emote.data.host.files[0]; let size; switch (true) { case file.width > 74: size = 4; break; case file.width > 53: size = 3; break; case file.width > 32: size = 2; break; default: size = 1; } return { id: "" + emote.id, hid: md5(emote.name), name: emote.name, provider: this.id, isZeroWidth: (emote.flags & 1) !== 0, spacing: true, width: file.width, size, parts }; }); emotesMapped = emotesMapped.filter(Boolean); const isMenuEnabled = !!this.settingsManager.getSetting(channelId, "emote_menu.emote_providers.7tv.show_global"); return [ { provider: this.id, orderIndex: 9, name: globalData.name, emotes: emotesMapped, enabledInMenu: isMenuEnabled, isEmoji: false, isGlobalSet: true, isCurrentChannel: false, isOtherChannel: false, isSubscribed: false, icon: globalData.owner?.avatar_url || "https://7tv.app/favicon.svg", id: "7tv_global" } ]; } unpackUserEmotes(channelId, userData) { if (!userData.emote_set || !userData.emote_set?.emotes?.length) { log32("EXT:STV", "EMOT:PROV", "No user emotes found for SevenTV provider"); return; } let emotesMapped = userData.emote_set.emotes.map(_SevenTVEmoteProvider.unpackUserEmote); emotesMapped = emotesMapped.filter(Boolean); const isMenuEnabled = !!this.settingsManager.getSetting( channelId, "emote_menu.emote_providers.7tv.show_current_channel" ); return [ { provider: this.id, orderIndex: 8, name: userData.emote_set.name, emotes: emotesMapped, enabledInMenu: isMenuEnabled, isEmoji: false, isGlobalSet: false, isCurrentChannel: true, isOtherChannel: false, isSubscribed: false, icon: userData.emote_set?.user?.avatar_url || "https://7tv.app/favicon.svg", id: "7tv_" + userData.emote_set.id } ]; } static unpackUserEmote(emoteData) { if (!emoteData.data?.host?.files || !emoteData.data.host.files.length) { error33("EXT:STV", "EMOT:PROV", "Emote has no files:", emoteData); return; } const file = emoteData.data.host.files[0]; const size = file.width / 24 + 0.5 << 0; const sanitizedEmoteName = emoteData.name.replaceAll("<", "<").replaceAll('"', """); const parts = splitEmoteName(sanitizedEmoteName, 2); return { id: "" + emoteData.id, hid: md5(emoteData.name), name: sanitizedEmoteName, provider: _SevenTVEmoteProvider.id, isZeroWidth: (emoteData.flags & 1) !== 0, spacing: true, width: file.width, size, parts }; } getRenderableEmote(emote, classes = "", srcSetWidthDescriptor) { const ext = NTV_SUPPORTS_AVIF && NTV_BROWSER !== 5 /* SAFARI */ && "avif" || "webp"; let srcSet; if (srcSetWidthDescriptor) { srcSet = `https://cdn.7tv.app/emote/${emote.id}/1x.${ext} 32w 32h, https://cdn.7tv.app/emote/${emote.id}/2x.${ext} 64w 64h, https://cdn.7tv.app/emote/${emote.id}/3x.${ext} 96w 96h, https://cdn.7tv.app/emote/${emote.id}/4x.${ext} 128w 128h`; } else { srcSet = `https://cdn.7tv.app/emote/${emote.id}/1x.${ext} 1x, https://cdn.7tv.app/emote/${emote.id}/2x.${ext} 2x, https://cdn.7tv.app/emote/${emote.id}/3x.${ext} 3x, https://cdn.7tv.app/emote/${emote.id}/4x.${ext} 4x`; } return `<img class="ntv__emote ${classes}" tabindex="0" data-emote-name="${emote.name || ""}" data-emote-hid="${emote.hid || ""}" alt="${emote.name || ""}" srcset="${srcSet}" loading="lazy" decoding="async" draggable="false">`; } getEmbeddableEmote(emote) { return emote.name; } getEmoteSrc(emote) { return `https://cdn.7tv.app/emote/${emote.id}/4x.avif`; } }; // src/Extensions/SevenTV/Database/SevenTVDatabase.ts var SevenTVDatabase = class extends DatabaseAbstract { idb; dbName = "NTV_Ext_SevenTV"; constructor(SWDexie) { super(); this.idb = SWDexie ? new SWDexie(this.dbName) : new import_wrapper_default(this.dbName); this.idb.version(1).stores({}); } }; // src/Extensions/SevenTV/SevenTVGraphQL.ts function getUserCosmeticDataByConnection(platformId, userId) { return REST.post("https://7tv.io/v3/gql", { query: `query GetUsersByConnection($platform: ConnectionPlatform!, $user_id: String!) { userByConnection(platform: $platform, id: $user_id) { id display_name style { badge_id color paint_id paint { angle color function gradients { angle at canvas_repeat canvas_size function image_url repeat shape stops { center_at at color } } id image_url kind name repeat shadows { y_offset x_offset radius color } shape text { weight variant transform stroke { width color } shadows { color radius x_offset y_offset } } stops { color at center_at } } badge { id kind name tag tooltip host { url files { size height width format } } } } avatar_url cosmetics { id kind selected } username } }`, variables: { platform: platformId, user_id: userId } }).then((res) => res.data); } function getUserEmoteSetConnectionsDataByConnection(platformId, userId) { return REST.post("https://7tv.io/v3/gql", { query: `query GetUsersByConnection($platform: ConnectionPlatform!, $user_id: String!) { userByConnection(platform: $platform, id: $user_id) { id emote_sets(entitled: true) { flags id name tags owner_id } connections { platform emote_set_id } } }`, variables: { platform: platformId, user_id: userId } }).then((res) => res.data?.userByConnection); } // src/Extensions/SevenTV/SevenTVEventAPI.ts var logger34 = new Logger(); var { log: log33, info: info31, error: error34 } = logger34.destruct(); function createRoom(channelId, stvChannelUserId, stvUserId, emoteSetId) { return stvUserId && { presenceTimestamp: 0, channelId, stvChannelUserId, stvUserId, emoteSetId } || { presenceTimestamp: 0, channelId, stvChannelUserId }; } var SevenTVEventAPI = class _SevenTVEventAPI { constructor(rootContext, datastore) { this.rootContext = rootContext; this.datastore = datastore; } static CONNECTION_TIMEOUT = 15e3; // 15s static MAX_RECONNECT_DELAY = 1e4; // 10s static MAX_RECONNECT_ATTEMPTS = 360 * 2; // 2 hours (360 * 2 * MAX_RECONNECT_DELAY) static PRESENCE_THROTTLE_INTERVAL = 1e4; // 10s connectionTimeoutId; socket = null; msgBuffer = []; roomBuffer = []; connectionState = 0 /* DISCONNECTED */; reconnectAttempts = 0; shouldResume = false; shouldReconnect = true; heartbeatInterval = 3e4; heartbeatTimeoutId = null; connectionId = null; rooms = []; eventTarget = new EventTarget(); scopedEventTarget = new EventTarget(); // stvListenerWrappers maps: channelId -> eventType -> originalListener -> wrapper stvListenerWrappers = /* @__PURE__ */ new Map(); addEventListener(type, listener) { this.eventTarget.addEventListener(type, listener); } addEventListenerByStvUser(room, type, listener) { if (!room.stvChannelUserId) { throw new Error("Cannot add EventAPI listener by STV user without STV channel user ID"); } const wrapper = (event) => { const detail = event.detail; if (detail && (!detail.object?.user || detail.object?.user?.id === room.stvChannelUserId)) { if (typeof listener === "function") { ; listener(event); } else { ; listener.handleEvent(event); } } }; let typeMap = this.stvListenerWrappers.get(room.channelId); if (!typeMap) { typeMap = /* @__PURE__ */ new Map(); this.stvListenerWrappers.set(room.channelId, typeMap); } let listenerMap = typeMap.get(type); if (!listenerMap) { listenerMap = /* @__PURE__ */ new Map(); typeMap.set(type, listenerMap); } listenerMap.set(listener, wrapper); this.scopedEventTarget.addEventListener(type, wrapper); } removeEventlistener(type, listener) { this.eventTarget.removeEventListener(type, listener); } removeEventListenerByStvUser(room) { const typeMap = this.stvListenerWrappers.get(room.channelId); if (!typeMap) return; for (const [type, listenerMap] of typeMap.entries()) { for (const wrapper of listenerMap.values()) { this.scopedEventTarget.removeEventListener(type, wrapper); } } this.stvListenerWrappers.delete(room.channelId); } /** * Connection State Flow: * * Initial Connection: * DISCONNECTED -> connect() -> CONNECTING -> onopen -> CONNECTED * | | * | | * error connection timeout * | | * v v * DISCONNECTED <- cleanupSocket() * | * v * scheduleReconnect(backoff) * | * v * connect() * * Heartbeat Timeout: * CONNECTED -> heartbeat timeout -> cleanupSocket() -> DISCONNECTED -> scheduleReconnect() * * Manual Disconnect: * ANY_STATE -> disconnect() -> cleanupSocket() -> DISCONNECTED * * Server Requested Reconnect: * CONNECTED -> RECONNECT event -> cleanupSocket() -> DISCONNECTED -> connect() */ connect() { if (this.connectionState !== 0 /* DISCONNECTED */) return; this.cleanupSocket(); this.connectionState = 1 /* CONNECTING */; this.shouldReconnect = true; const url = new URL("wss://events.7tv.io/v3"); url.searchParams.append("app", "NipahTV"); url.searchParams.append("version", NTV_APP_VERSION); try { this.socket = new WebSocket(url); this.connectionTimeoutId = setTimeout(() => { if (this.connectionState === 1 /* CONNECTING */) { error34("EXT:STV", "EVENTAPI", "Connection attempt timed out"); this.cleanupSocket(); this.scheduleReconnect(true); } }, _SevenTVEventAPI.CONNECTION_TIMEOUT); this.socket.onopen = () => { clearTimeout(this.connectionTimeoutId); this.connectionState = 2 /* CONNECTED */; this.reconnectAttempts = 0; log33("EXT:STV", "EVENTAPI", "EventAPI Connected!"); }; this.socket.onclose = (event) => { const wasConnecting = this.connectionState === 1 /* CONNECTING */; clearTimeout(this.connectionTimeoutId); this.cleanupSocket(); if (!this.shouldReconnect) return log33("EXT:STV", "EVENTAPI", "EventAPI Disconnected, not reconnecting.."); if (this.reconnectAttempts >= _SevenTVEventAPI.MAX_RECONNECT_ATTEMPTS) { error34("EXT:STV", "EVENTAPI", "Max reconnection attempts reached"); return; } this.scheduleReconnect(wasConnecting); }; this.socket.onmessage = (event) => { if (this.connectionState !== 2 /* CONNECTED */ || !this.socket) { log33("EXT:STV", "EVENTAPI", "Dropping message - socket not ready"); return; } let payload; try { payload = JSON.parse(event.data); } catch (err) { error34("EXT:STV", "EVENTAPI", "EventAPI[HELLO] Failed to parse message:", event); return; } const { d: data, op } = payload; switch (op) { case 0 /* DISPATCH */: this.onDispatchEvent(data); break; case 1 /* HELLO */: this.onHelloEvent(data); break; case 2 /* HEARTBEAT */: this.onHeartBeatEvent(data); break; case 4 /* RECONNECT */: this.onReconnectEvent(data); break; case 5 /* ACK */: this.onAckEvent(data); break; case 6 /* ERROR */: this.onErrorEvent(data); break; case 7 /* END_OF_STREAM */: this.onEndOfStreamEvent(data); break; default: error34("EXT:STV", "EVENTAPI", "EventAPI[MESSAGE] Unknown opcode:", payload.op); break; } }; } catch (err) { clearTimeout(this.connectionTimeoutId); this.cleanupSocket(); this.scheduleReconnect(true); } } disconnect() { this.shouldReconnect = false; this.unsubscribeRooms(); this.cleanupSocket(); } resume() { if (!this.socket || this.connectionState !== 2 /* CONNECTED */) { return error34("EXT:STV", "EVENTAPI", "EventAPI[RESUME] Socket is not connected!"); } if (!this.connectionId) return error34("EXT:STV", "EVENTAPI", "EventAPI[RESUME] No connection id to resume!"); this.emit({ op: 34 /* RESUME */, d: { session_id: this.connectionId } }); log33("EXT:STV", "EVENTAPI", `EventAPI[RESUME] Sent resume connection <${this.connectionId}> request...`); } scheduleReconnect(useBackoff) { if (this.connectionState !== 0 /* DISCONNECTED */) return; const jitter = (Math.min(this.reconnectAttempts, 1) * 800 + Math.min(this.reconnectAttempts ** 2 * 100, 1200)) * Math.random(); const delay = useBackoff ? Math.min(this.reconnectAttempts ** 2 * 500 + jitter, _SevenTVEventAPI.MAX_RECONNECT_DELAY) : 0; this.reconnectAttempts++; log33( "EXT:STV", "EVENTAPI", `EventAPI Attempting reconnect ${this.reconnectAttempts}/${_SevenTVEventAPI.MAX_RECONNECT_ATTEMPTS} in ${(delay / 10 << 0) / 100}s` ); setTimeout(() => { if (this.connectionState === 0 /* DISCONNECTED */) { this.connect(); } }, delay); } doHeartbeat() { if (this.heartbeatTimeoutId) { clearTimeout(this.heartbeatTimeoutId); this.heartbeatTimeoutId = null; } if (this.connectionState === 2 /* CONNECTED */ && this.socket) { this.heartbeatTimeoutId = setTimeout(() => { error34("EXT:STV", "EVENTAPI", "Heartbeat timed out"); this.cleanupSocket(); this.scheduleReconnect(true); }, this.heartbeatInterval); } } cleanupSocket() { if (this.heartbeatTimeoutId) { clearTimeout(this.heartbeatTimeoutId); this.heartbeatTimeoutId = null; } if (this.socket) { try { this.socket.close(); } catch (e) { } this.socket = null; } this.connectionState = 0 /* DISCONNECTED */; this.connectionId = null; } registerRoom(channelId, stvChannelUserId, stvUserId, emoteSetId) { log33( "EXT:STV", "EVENTAPI", `Registering room <${channelId}@${stvChannelUserId || "no channel"}> with user <${stvUserId || "no user"}>` ); if (this.rooms.some((room2) => room2.channelId === channelId)) return error34("EXT:STV", "EVENTAPI", "EventAPI Room is already registered!"); const room = createRoom(channelId, stvChannelUserId, stvUserId, emoteSetId); if (this.connectionState !== 2 /* CONNECTED */) { this.roomBuffer.push(room); return room; } this.rooms.push(room); this.subscribeRoom(room); if (stvUserId) this.sendPresence(room, true, true); return room; } removeRoom(channelId) { log33("EXT:STV", "EVENTAPI", `Removing room <${channelId}>`); const index = this.rooms.findIndex((room2) => room2.channelId === channelId); if (index === -1) return error34("EXT:STV", "EVENTAPI", `Unable to find room to remove <${channelId}>`); const room = this.rooms[index]; this.rooms.splice(index, 1); this.unsubscribeRoom(room); } subscribeRooms() { for (const room of this.rooms) this.subscribeRoom(room); } subscribeRoom(room) { const channelId = room.channelId; if (room.emoteSetId) { this.subscribe("emote_set.update", { object_id: room.emoteSetId }); this.subscribe("user.*", { object_id: room.emoteSetId }); } const platformId = getStvPlatformId(); const condition = { ctx: "channel", platform: platformId, id: channelId }; this.subscribe("user.*", condition); this.subscribe("emote.*", condition); this.subscribe("cosmetic.*", condition); this.subscribe("emote_set.*", condition); this.subscribe("entitlement.*", condition); } unsubscribeRoom(room) { const channelId = room.channelId; if (room.emoteSetId) { this.unsubscribe("emote_set.update", { object_id: room.emoteSetId }); this.unsubscribe("user.*", { object_id: room.emoteSetId }); } const platformId = getStvPlatformId(); const condition = { ctx: "channel", platform: platformId, id: channelId }; this.unsubscribe("user.*", condition); this.unsubscribe("emote.*", condition); this.unsubscribe("cosmetic.*", condition); this.unsubscribe("emote_set.*", condition); this.unsubscribe("entitlement.*", condition); } unsubscribeRooms() { for (const room of this.rooms) this.unsubscribeRoom(room); } sendPresences() { for (const room of this.rooms) { if (room.stvUserId) this.sendPresence(room); } } sendPresence(room, self2 = false, force = false) { if (this.connectionState !== 2 /* CONNECTED */) return; const { channelId, stvUserId } = room; if (!stvUserId) return error34("EXT:STV", "EVENTAPI", "No user ID provided for presence update"); if (!force) { const now = Date.now(); if (room.presenceTimestamp > now - _SevenTVEventAPI.PRESENCE_THROTTLE_INTERVAL) return; room.presenceTimestamp = now; } REST.post(`https://7tv.io/v3/users/${stvUserId}/presences`, { kind: 1, passive: self2, session_id: self2 ? this.connectionId : void 0, data: { platform: getStvPlatformId(), ctx: "channel", id: channelId } }).catch((err) => { error34("EXT:STV", "EVENTAPI", "Failed to send presence:", err); }); return true; } subscribe(topic, condition) { this.emit({ op: 35 /* SUBSCRIBE */, d: { type: topic, condition } }); } unsubscribe(topic, condition) { if (!this.socket || this.connectionState !== 2 /* CONNECTED */) return; this.emit({ op: 36 /* UNSUBSCRIBE */, d: { type: topic, condition } }); } emit(payload) { if (!this.socket || this.connectionState !== 2 /* CONNECTED */) { this.msgBuffer.push(payload); return; } this.socket.send(JSON.stringify(payload)); } onHelloEvent(event) { log33( "EXT:STV", "EVENTAPI", `[HELLO] <${event.session_id}> Heartbeat: ${event.heartbeat_interval}ms Population: ${event.instance.population}` ); this.reconnectAttempts = 0; this.heartbeatInterval = event.heartbeat_interval + 5e3; this.doHeartbeat(); if (this.shouldResume) this.resume(); else this.subscribeRooms(); this.connectionId = event.session_id; this.shouldResume = false; if (this.roomBuffer.length) { for (const { channelId, stvChannelUserId, stvUserId } of this.roomBuffer) { this.registerRoom(channelId, stvChannelUserId); } this.roomBuffer = []; } if (!this.socket) return error34("EXT:STV", "EVENTAPI", "[HELLO] Socket is not connected!"); if (this.msgBuffer.length) { for (const payload of this.msgBuffer) { this.socket.send(JSON.stringify(payload)); } this.msgBuffer = []; } this.sendPresences(); } onHeartBeatEvent(event) { this.doHeartbeat(); } onAckEvent(event) { const command = event.command; switch (command) { case "RESUME": const { success, dispatches_replayed, subscriptions_restored } = event.data; if (success) { log33( "EXT:STV", "EVENTAPI", "[ACK] Resumed connection successfully..", `[dispatchesReplayed=${dispatches_replayed} subscriptionsRestored=${subscriptions_restored}]` ); } else { log33("EXT:STV", "EVENTAPI", "[ACK] Failed to resume connection.."); this.shouldResume = false; this.subscribeRooms(); } break; case "IDENTIFY": log33("EXT:STV", "EVENTAPI", "[ACK] Identified.."); break; case "SUBSCRIBE": log33( "EXT:STV", "EVENTAPI", `[ACK] Subscribed to <${event.data?.type}> at <${event.data?.condition?.id || event.data?.condition?.object_id}>` ); break; case "UNSUBSCRIBE": log33( "EXT:STV", "EVENTAPI", `[ACK] Unsubscribed to <${event.data?.type}> at <${event.data?.condition?.id || event.data?.condition?.object_id}>` ); break; case "SIGNAL": log33("EXT:STV", "EVENTAPI", "[ACK] Signaled.."); break; case "BRIDGE": log33("EXT:STV", "EVENTAPI", "[ACK] Bridged.."); break; default: error34("EXT:STV", "EVENTAPI", "[ACK] Unknown command:", command); break; } } onReconnectEvent(event) { log33("EXT:STV", "EVENTAPI", "[RECONNECT]", event); this.scheduleReconnect(true); } onErrorEvent(event) { error34("EXT:STV", "EVENTAPI", "[ERROR]", event); } onEndOfStreamEvent(event) { if ([ 4e3 /* SERVER_ERROR */, 4012 /* RECONNECT */, 4006 /* RESTART */, 4007 /* MAINTENANCE */, 4008 /* TIMEOUT */ ].includes(event.code)) { log33("EXT:STV", "EVENTAPI", "[END_OF_STREAM] Reconnecting due to:", event); this.shouldReconnect = true; this.scheduleReconnect(true); } else { error34("EXT:STV", "EVENTAPI", "[END_OF_STREAM] Unexpected end of stream:", event); this.shouldReconnect = false; } } onDispatchEvent(event) { log33("EXT:STV", "EVENTAPI", `[DISPATCH] <${event.type}>`, event); switch (event.type) { case "system.announcement" /* SYSTEM_ANNOUNCEMENT */: break; case "emote_set.update" /* EMOTE_SET_UPDATED */: const emoteSetUpdate = event.body; if (emoteSetUpdate.pushed) { this.scopedEventTarget.dispatchEvent( new CustomEvent("emotes_added", { detail: emoteSetUpdate }) ); } if (emoteSetUpdate.pulled) { this.scopedEventTarget.dispatchEvent( new CustomEvent("emotes_removed", { detail: emoteSetUpdate }) ); } break; case "entitlement.create" /* ENTITLEMENT_CREATED */: const entitlement = event.body.object; this.datastore.createEntitlement(entitlement); if (entitlement.kind === "PAINT") { this.eventTarget.dispatchEvent(new CustomEvent("paint_entitled", { detail: entitlement.user })); } break; case "entitlement.delete" /* ENTITLEMENT_DELETED */: this.datastore.deleteEntitlement(event.body.object); break; case "entitlement.reset" /* ENTITLEMENT_RESET */: this.datastore.resetEntitlements(event.body.id); break; case "cosmetic.create" /* COSMETIC_CREATED */: const object = event.body.object; this.datastore.createCosmetic(object); if (object.kind === "PAINT") { this.eventTarget.dispatchEvent(new CustomEvent("paint_created", { detail: object.data })); } break; } } }; // src/Extensions/Extension.ts var Extension = class { constructor(rootContext, sessions) { this.rootContext = rootContext; this.sessions = sessions; } }; // src/Extensions/SevenTV/SevenTVDatastore.ts var logger35 = new Logger(); var { log: log34, info: info32, error: error35 } = logger35.destruct(); var SevenTVDatastore = class { constructor(database) { this.database = database; } users = /* @__PURE__ */ new Map(); usersByName = /* @__PURE__ */ new Map(); entitlements = /* @__PURE__ */ new Map(); hashedEntitlements = /* @__PURE__ */ new Map(); cosmetics = /* @__PURE__ */ new Map(); createEntitlement(entitlement) { const user = entitlement.user; if (!user) return error35("EXT:STV", "STORE", "No user provided for entitlement"); if (!this.users.has(user.id)) { this.users.set(user.id, user); this.usersByName.set(user.display_name, user); } const entitlements = this.entitlements.get(user.id); if (entitlements) entitlements.push(entitlement); else this.entitlements.set(user.id, [entitlement]); this.hashedEntitlements.set(user.id + "_" + entitlement.kind, entitlement); } deleteEntitlement(entitlement) { const user = entitlement.user; if (!user) return error35("EXT:STV", "STORE", "No user provided for entitlement"); const storedEntitlement = this.hashedEntitlements.get(user.id + "_" + entitlement.kind); if (storedEntitlement) { this.hashedEntitlements.delete(user.id + "_" + entitlement.kind); const entitlements = this.entitlements.get(user.id); if (entitlements) { const index = entitlements.findIndex((e) => e.id === entitlement.id); if (index !== -1) { entitlements.splice(index, 1); } } } } resetEntitlements(userId) { this.entitlements.delete(userId); } createCosmetic(cosmetic) { this.cosmetics.set(cosmetic.id, cosmetic); } getUserByName(name) { return this.usersByName.get(name); } getUserPaint(userId) { const entitlement = this.hashedEntitlements.get(userId + "_PAINT"); return entitlement && this.cosmetics.get(entitlement.ref_id)?.data; } getUserBadge(userId) { const entitlement = this.hashedEntitlements.get(userId + "_BADGE"); return entitlement && this.cosmetics.get(entitlement.ref_id)?.data; } }; // src/Extensions/SevenTV/SevenTVPaintStyleGenerator.ts var SevenTVPaintStyleGenerator = class _SevenTVPaintStyleGenerator { static convertColorToHex(color) { return `#${(color >>> 0).toString(16).padStart(8, "0")}`; } static createColorStop(stop) { const color = _SevenTVPaintStyleGenerator.convertColorToHex(stop.color); return `${color} ${stop.at * 100}%`; } static createLinearGradient(paint) { if (!paint.stops?.length) return ""; const gradientStops = paint.stops.map(_SevenTVPaintStyleGenerator.createColorStop); const prefix = paint.repeat ? "repeating-" : ""; return `background-image: ${prefix}linear-gradient(${paint.angle}deg, ${gradientStops.join(", ")});`; } static createRadialGradient(paint) { if (!paint.stops?.length) return ""; const gradientStops = paint.stops.map(_SevenTVPaintStyleGenerator.createColorStop); const prefix = paint.repeat ? "repeating-" : ""; return `background-image: ${prefix}radial-gradient(circle, ${gradientStops.join( ", " )}); background-size: 100% auto;`; } static createImageBackground(imageUrl) { return `background-image: url('${imageUrl}'); background-size: 100% auto;`; } static createShadowEffects(shadows) { if (!shadows?.length) return "filter: '';"; const dropShadows = shadows.map((shadow) => { const color = _SevenTVPaintStyleGenerator.convertColorToHex(shadow.color); return `drop-shadow(${color} ${shadow.x_offset}px ${shadow.y_offset}px ${shadow.radius}px)`; }); return `filter: ${dropShadows.join(" ")};`; } static generateCSSRules(paint, shadows) { const rules = []; switch (paint.function) { case "LINEAR_GRADIENT": rules.push(_SevenTVPaintStyleGenerator.createLinearGradient(paint)); break; case "RADIAL_GRADIENT": rules.push(_SevenTVPaintStyleGenerator.createRadialGradient(paint)); break; case "URL": rules.push(_SevenTVPaintStyleGenerator.createImageBackground(paint.image_url)); break; } if (shadows) rules.push(_SevenTVPaintStyleGenerator.createShadowEffects(paint.shadows ?? [])); return rules.filter(Boolean).join(""); } }; // src/Extensions/SevenTV/index.ts var logger36 = new Logger(); var { log: log35, info: info33, error: error36 } = logger36.destruct(); var SevenTV; ((SevenTV2) => { let EmoteLifecycle; ((EmoteLifecycle2) => { EmoteLifecycle2[EmoteLifecycle2["DELETED"] = 0] = "DELETED"; EmoteLifecycle2[EmoteLifecycle2["PENDING"] = 1] = "PENDING"; EmoteLifecycle2[EmoteLifecycle2["PROCESSING"] = 2] = "PROCESSING"; EmoteLifecycle2[EmoteLifecycle2["DISABLED"] = 3] = "DISABLED"; EmoteLifecycle2[EmoteLifecycle2["LIVE"] = 4] = "LIVE"; EmoteLifecycle2[EmoteLifecycle2["FAILED"] = 5] = "FAILED"; })(EmoteLifecycle = SevenTV2.EmoteLifecycle || (SevenTV2.EmoteLifecycle = {})); let EmoteFlags; ((EmoteFlags2) => { EmoteFlags2[EmoteFlags2["PRIVATE"] = 1] = "PRIVATE"; EmoteFlags2[EmoteFlags2["AUTHENTIC"] = 2] = "AUTHENTIC"; EmoteFlags2[EmoteFlags2["ZERO_WIDTH"] = 256] = "ZERO_WIDTH"; })(EmoteFlags = SevenTV2.EmoteFlags || (SevenTV2.EmoteFlags = {})); let EmoteSetFlags; ((EmoteSetFlags2) => { })(EmoteSetFlags = SevenTV2.EmoteSetFlags || (SevenTV2.EmoteSetFlags = {})); let ObjectKind; ((ObjectKind2) => { ObjectKind2[ObjectKind2["USER"] = 1] = "USER"; ObjectKind2[ObjectKind2["EMOTE"] = 2] = "EMOTE"; ObjectKind2[ObjectKind2["EMOTE_SET"] = 3] = "EMOTE_SET"; ObjectKind2[ObjectKind2["ROLE"] = 4] = "ROLE"; ObjectKind2[ObjectKind2["ENTITLEMENT"] = 5] = "ENTITLEMENT"; ObjectKind2[ObjectKind2["BAN"] = 6] = "BAN"; ObjectKind2[ObjectKind2["MESSAGE"] = 7] = "MESSAGE"; ObjectKind2[ObjectKind2["REPORT"] = 8] = "REPORT"; ObjectKind2[ObjectKind2["PRESENCE"] = 9] = "PRESENCE"; ObjectKind2[ObjectKind2["COSMETIC"] = 10] = "COSMETIC"; })(ObjectKind = SevenTV2.ObjectKind || (SevenTV2.ObjectKind = {})); })(SevenTV || (SevenTV = {})); function getStvPlatformId() { switch (NTV_PLATFORM) { case "twitch" /* TWITCH */: return "TWITCH"; case "kick" /* KICK */: return "KICK"; case "youtube" /* YOUTUBE */: return "YOUTUBE"; } error36("EXT:STV", "MAIN", "Unsupported platform:", NTV_PLATFORM); return "UNKNOWN"; } var SevenTVExtension = class extends Extension { name = "7TV"; version = "1.0.0"; description = "7TV extension for emote support"; database; datastore; sessionCreateCb; sessionDestroyCb; renderMessageMiddleware; paintSheet = null; eventAPI = null; cachedStvMeUser = null; // private cachedStvChannelUser: Pick<SevenTV.User, 'id' | 'emote_sets' | 'connections'> | null = null constructor(rootContext, sessions) { super(rootContext, sessions); this.sessionCreateCb = this.onSessionCreate.bind(this); this.sessionDestroyCb = this.onSessionDestroy.bind(this); } async init() { this.database = true ? DatabaseProxyFactory.create("NTV_Ext_SevenTV", new SevenTVDatabase()) : DatabaseProxyFactory.create("NTV_Ext_SevenTV"); return this.loadDatabase().then(async () => { return this.rootContext.settingsManager.loadSettings(); }).then(() => { this.datastore = new SevenTVDatastore(this.database); this.hookRenderMessagePipeline(this.datastore); }); } static getExtensionDatabase() { return new SevenTVDatabase(import_wrapper_default); } loadDatabase() { return new Promise((resolve, reject) => { if (!this.database) return reject("Database is not initialized"); this.database.checkCompatibility().then(() => { log35("EXT:STV", "INIT", "SevenTV database passed compatibility check."); resolve(void 0); }).catch((err) => { error36("EXT:STV", "INIT", "Failed to open SevenTV database because:", err); reject(); }); }); } onEnable() { info33("EXT:STV", "INIT", "Enabling extension:", this.name, this.version); const { eventBus: rootEventBus, settingsManager } = this.rootContext; this.init().then(async () => { this.eventAPI = new SevenTVEventAPI(this.rootContext, this.datastore); this.eventAPI.connect(); this.eventAPI.addEventListener( "paint_created", ((event) => { this.handlePaintCreated(event); }).bind(this) ); this.eventAPI.addEventListener( "paint_entitled", ((event) => { this.handlePaintEntitled(event); }).bind(this) ); if (rootEventBus.hasFiredEvent("ntv.session.create")) this.sessions.forEach(this.onSessionCreate.bind(this)); rootEventBus.subscribe("ntv.session.create", this.sessionCreateCb); rootEventBus.subscribe("ntv.session.destroy", this.sessionDestroyCb); }).catch((err) => { error36("EXT:STV", "INIT", "Failed to initialize SevenTV extension", err); }); } onDisable() { info33("EXT:STV", "MAIN", "Disabling extension:", this.name, this.version); const { eventBus: rootEventBus } = this.rootContext; if (this.eventAPI) { this.eventAPI.disconnect(); this.eventAPI = null; } this.unhookRenderMessagePipeline(); rootEventBus.unsubscribe("ntv.session.create", this.sessionCreateCb); rootEventBus.unsubscribe("ntv.session.destroy", this.sessionDestroyCb); } async onSessionCreate(session) { const { datastore } = this; const { eventBus, emotesManager } = session; const { settingsManager } = this.rootContext; if (!session.channelData) return error36("EXT:STV", "MAIN", `Skipping session without channel data, you're probably not in a channel..`); const { channelId, userId: channelUserId } = session.channelData; const platformMeUserId = session.meData.userId; this.registerEmoteProvider(session); if (!datastore) return error36("EXT:STV", "MAIN", "Datastore is not initialized, cannot add session:", session); if (!this.eventAPI) return error36("EXT:STV", "MAIN", "Event API is not initialized, cannot add session:", session); const STV_ID_NULL = "00000000000000000000000000"; const platformId = getStvPlatformId(); let promises = []; if (!this.cachedStvMeUser) { promises.push( getUserCosmeticDataByConnection(platformId, platformMeUserId).then((res) => res?.userByConnection ?? { id: STV_ID_NULL }).then((user) => { if (user.id === STV_ID_NULL) info33( "EXT:STV", "MAIN", "SevenTV failed to get user, looks like you don't have a 7TV account.." ); return this.cachedStvMeUser = user; }).then((user) => { if (user.id === STV_ID_NULL) return; queueMicrotask(() => { const paint = user.style?.paint; if (paint) { datastore.createEntitlement({ id: STV_ID_NULL, kind: "PAINT", ref_id: paint.id, user }); datastore.createCosmetic({ id: paint.id, kind: "PAINT", data: paint }); this.handlePaintCreated(new CustomEvent("paint_created", { detail: paint })); } }); }).catch((err) => this.cachedStvMeUser = { id: STV_ID_NULL }) ); } promises.push( getUserEmoteSetConnectionsDataByConnection(getStvPlatformId(), channelUserId).then((res) => res ?? { id: STV_ID_NULL }).catch((err) => { id: STV_ID_NULL; }) ); const promiseRes = await Promise.allSettled(promises); const stvChannelUser = promiseRes[1].status === "fulfilled" ? promiseRes[1].value : void 0; let activeEmoteSet; if (stvChannelUser && "emote_sets" in stvChannelUser && stvChannelUser.emote_sets) { activeEmoteSet = stvChannelUser.emote_sets.find( (set) => set.id === stvChannelUser.connections?.find((c) => c.platform === platformId)?.emote_set_id ); } const stvMeUserId = !this.cachedStvMeUser || this.cachedStvMeUser.id === STV_ID_NULL ? void 0 : this.cachedStvMeUser.id; const room = this.eventAPI.registerRoom(channelUserId, stvChannelUser?.id, stvMeUserId, activeEmoteSet?.id); if (room && room.stvUserId && room.stvUserId !== STV_ID_NULL) { eventBus.subscribe("ntv.chat.message.new", (message) => { this.eventAPI?.sendPresence(room); }); if (stvChannelUser && activeEmoteSet) { this.eventAPI.addEventListenerByStvUser( room, "emotes_added", ((event) => { const data = event.detail; if (!data.pushed) return; if (data.id !== activeEmoteSet?.id) return; if (!data.actor) return; const emotesAdded = []; for (const pushed of data.pushed) { if (pushed.key === "emotes" && pushed.value) { const unpackedEmote = SevenTVEmoteProvider.unpackUserEmote(pushed.value); if (unpackedEmote) emotesAdded.push(unpackedEmote); } } log35( "EXT:STV", "MAIN", `${data.actor?.display_name} added ${emotesAdded.length} new emotes to emoteset ${activeEmoteSet?.name}:`, emotesAdded ); for (const emote of emotesAdded) { const emoteSetId = "7tv_" + activeEmoteSet.id; emotesManager.addEmoteToEmoteSetById(emote, emoteSetId); eventBus.publish("ntv.channel.moderation.emote_added", { emote, actor: { id: data.actor.id, name: data.actor.display_name } }); } }).bind(this) ); this.eventAPI.addEventListenerByStvUser( room, "emotes_removed", ((event) => { const data = event.detail; if (!data.pulled) return; if (data.id !== activeEmoteSet?.id) return; if (!data.actor) return; const emotesRemoved = []; for (const pulled of data.pulled) { if (pulled.key === "emotes" && pulled.old_value) { const unpackedEmote = SevenTVEmoteProvider.unpackUserEmote(pulled.old_value); if (unpackedEmote) emotesRemoved.push(unpackedEmote); } } log35( "EXT:STV", "MAIN", `${data.actor?.display_name} removed ${emotesRemoved.length} emotes from emoteset ${activeEmoteSet?.name}:`, emotesRemoved ); for (const stvEmote of emotesRemoved) { const emote = emotesManager.getEmoteById(stvEmote.id); if (!emote) continue; const emoteSetId = "7tv_" + activeEmoteSet.id; emotesManager.removeEmoteFromEmoteSetById(emote.id, emoteSetId); eventBus.publish("ntv.channel.moderation.emote_removed", { emote, actor: { id: data.actor.id, name: data.actor.display_name } }); } }).bind(this) ); } } } onSessionDestroy(session) { if (this.eventAPI && session.channelData?.userId) { this.eventAPI.removeRoom(session.channelData.userId); } } registerEmoteProvider(session) { session.emotesManager.registerProvider(SevenTVEmoteProvider); } hookRenderMessagePipeline(datastore) { const { settingsManager } = this.rootContext; const renderMessagePipeline = this.rootContext.renderMessagePipeline; this.renderMessageMiddleware = renderMessagePipeline.use( (message, badgesEl, usernameEl, messageParts, next) => { const user = datastore.getUserByName(message.username); if (!user) return next(); const paintCosmeticsEnabledSetting = settingsManager.getSetting( "shared", "ext.7tv.cosmetics.paints.enabled" ); if (paintCosmeticsEnabledSetting) { const paint = datastore.getUserPaint(user.id); if (paint) { usernameEl.setAttribute("seventv-painted-content", "true"); usernameEl.setAttribute("seventv-paint-id", paint.id); usernameEl.style.removeProperty("color"); } } const badgeCosmeticsEnabledSetting = settingsManager.getSetting( "shared", "ext.7tv.cosmetics.badges.enabled" ); if (badgeCosmeticsEnabledSetting) { const badge = datastore.getUserBadge(user.id); if (badge) { const file = badge.host.files.filter((f) => f.format === "WEBP")[0]; if (file) { const badgeEl = document.createElement("img"); badgeEl.classList.add("ntv__badge"); const hostUrl = badge.host.url; badgeEl.setAttribute( "srcset", `${hostUrl}/1x.webp 32w 32h, ${hostUrl}/2x.webp 64w 64h, ${hostUrl}/3x.webp 96w 96h, ${hostUrl}/4x.webp 128w 128h` ); badgeEl.setAttribute("title", badge.tooltip); badgeEl.setAttribute("loading", "lazy"); badgeEl.setAttribute("decoding", "async"); badgeEl.setAttribute("draggable", "false"); badgeEl.setAttribute("height", "" + file.height); badgeEl.setAttribute("width", "" + file.width); badgesEl.appendChild(badgeEl); } } } next(); } ); } unhookRenderMessagePipeline() { if (this.renderMessageMiddleware) this.rootContext.renderMessagePipeline.remove(this.renderMessageMiddleware); } handlePaintCreated(event) { if (!this.paintSheet) { const styleEl = document.createElement("style"); styleEl.id = "ntv__ext-7tv__paint-styles"; (document.head || document.documentElement).appendChild(styleEl); this.paintSheet = styleEl.sheet; if (!this.paintSheet) return error36("EXT:STV", "MAIN", "Failed to create CSSStyleSheet", styleEl); this.paintSheet.insertRule( `[seventv-painted-content="true"] { background-color: currentcolor; }`, this.paintSheet.cssRules.length ); this.paintSheet.insertRule( `[seventv-paint-id] { -webkit-text-fill-color: transparent; background-clip: text !important; -webkit-background-clip: text !important; font-weight: 700; }`, this.paintSheet.cssRules.length ); } const paint = event.detail; const selector = `[seventv-paint-id="${paint.id}"]`; for (let i = 0; i < this.paintSheet.cssRules.length; i++) { const rule = this.paintSheet.cssRules[i]; if (rule instanceof CSSStyleRule && rule.selectorText === selector) return; } const paintShadowsEnabledSetting = this.rootContext.settingsManager.getSetting( "shared", "ext.7tv.cosmetics.paints.shadows.enabled" ); const cssRules = SevenTVPaintStyleGenerator.generateCSSRules(paint, paintShadowsEnabledSetting); this.paintSheet.insertRule(`${selector} {${cssRules}}`, this.paintSheet.cssRules.length); } handlePaintEntitled(event) { const user = event.detail; const paint = this.datastore?.getUserPaint(user.id); if (!paint) return; const displayName = user.display_name.replaceAll('"', """).replaceAll("'", "'"); const chatMessages = document.querySelectorAll( `.ntv__chat-message__username[ntv-username="${displayName}"]` ); for (const message of chatMessages) { message.setAttribute("seventv-painted-content", "true"); message.setAttribute("seventv-paint-id", paint.id); message.style.removeProperty("color"); } } }; // src/Extensions/Botrix/BotrixInputCompletionStrategy.ts var logger37 = new Logger(); var { log: log36, info: info34, error: error37 } = logger37.destruct(); var BotrixInputCompletionStrategy = class extends AbstractInputCompletionStrategy { constructor(rootContext, session, contentEditableEditor, navListWindowManager, { botrixSessionManager }) { super(rootContext, session, contentEditableEditor, navListWindowManager); this.rootContext = rootContext; this.session = session; this.contentEditableEditor = contentEditableEditor; this.navListWindowManager = navListWindowManager; } id = "botrix"; isFullLineStrategy = true; shouldUseStrategy(event, contentEditableEditor) { const firstChar = contentEditableEditor.getFirstCharacter(); return firstChar === "!" || event instanceof KeyboardEvent && event.key === "!" && contentEditableEditor.isInputEmpty(); } maybeCreateNavWindow() { if (this.navWindow) return; this.navWindow = this.navListWindowManager.createNavWindow(this.id); this.navWindow.addEventListener("entry-click", (event) => { this.renderInlineCompletion(); }); } updateCompletionEntries() { if (!this.navWindow) return error37("EXT:BTX", "MAIN", "Tab completion window does not exist yet"); } renderInlineCompletion() { if (!this.navWindow) return error37("EXT:BTX", "MAIN", "Tab completion window does not exist yet"); const selectedEntry = this.navWindow.getSelectedEntry(); if (!selectedEntry) return error37("EXT:BTX", "MAIN", "No selected entry to render completion"); const { name } = selectedEntry; this.contentEditableEditor.clearInput(); this.contentEditableEditor.insertText("!" + name); } moveSelectorUp() { if (!this.navWindow) return error37("EXT:BTX", "MAIN", "No tab completion window to move selector up"); this.navWindow.moveSelectorUp(); this.renderInlineCompletion(); } moveSelectorDown() { if (!this.navWindow) return error37("EXT:BTX", "MAIN", "No tab completion window to move selector down"); this.navWindow.moveSelectorDown(); this.renderInlineCompletion(); } handleKeyDown(event) { } }; // src/Extensions/Botrix/BotrixExecutionStrategy.ts var BotrixExecutionStrategy = class { constructor(rootContext, session) { this.rootContext = rootContext; this.session = session; } shouldUseStrategy(inputIntentDTO) { return inputIntentDTO.input[0] === "!"; } async route(contentEditableEditor, inputIntentDTO, dontClearInput) { const { networkInterface } = this.session; dontClearInput || contentEditableEditor.clearInput(); if (inputIntentDTO.isReply) { if (!inputIntentDTO.replyRefs) throw new Error("ReplyRefs are required for reply messages."); await networkInterface.sendReply( inputIntentDTO.input, inputIntentDTO.replyRefs.messageId, inputIntentDTO.replyRefs.messageContent, inputIntentDTO.replyRefs.senderId, inputIntentDTO.replyRefs.senderUsername, true ); } else { await networkInterface.sendMessage(inputIntentDTO.input, true); } } }; // src/Extensions/Botrix/index.ts var logger38 = new Logger(); var { log: log37, info: info35, error: error38 } = logger38.destruct(); var BotrixNetworkInterface = class { static async fetchUserShopItems(userSlug, platformId) { return REST.get(`https://botrix.live/api/public/shop/items?u=${userSlug}&platform=${platformId}`); } }; var BotrixSessionManager = class { constructor(session) { this.session = session; } userShopItems = []; async getUserShopItems() { if (!this.userShopItems.length) { const userSlug = this.session.channelData.channelName; const platformId = NTV_PLATFORM; const userShopItems = await BotrixNetworkInterface.fetchUserShopItems(userSlug, platformId); log37("EXT:BTX", "MAIN", "User shop items:", userShopItems); } } }; var BotrixExtension = class extends Extension { name = "Botrix"; version = "1.0.0"; description = "Botrix extension for the botrix completion strategy"; sessionCreateCb; constructor(rootContext, sessions) { super(rootContext, sessions); this.sessionCreateCb = this.onSessionCreate.bind(this); } onEnable() { info35("EXT:BTX", "INIT", "Enabling extension:", this.name, this.version); const { eventBus: rootEventBus, settingsManager } = this.rootContext; this.sessions.forEach(this.onSessionCreate.bind(this)); rootEventBus.subscribe("ntv.session.create", this.sessionCreateCb); } onDisable() { info35("EXT:BTX", "INIT", "Disabling extension:", this.name, this.version); const { eventBus: rootEventBus } = this.rootContext; rootEventBus.unsubscribe("ntv.session.create", this.sessionCreateCb); } onSessionCreate(session) { this.registerSessionExecutionStrategy(session); } registerSessionExecutionStrategy(session) { const { rootContext } = this; session.inputExecutionStrategyRegister.registerStrategy(new BotrixExecutionStrategy(rootContext, session)); } registerSessionCompletionStrategy(session) { const inputController = session.userInterface?.getInputController(); if (!inputController) return error38( "EXT:BTX", "MAIN", `No input controller found for extension ${this.name} with session:`, session ); const inputCompletionStrategyManager = session.inputCompletionStrategyManager; if (!inputCompletionStrategyManager) return error38( "EXT:BTX", "MAIN", `No input completion strategy manager found for extension ${this.name} with session:`, session ); session.inputCompletionStrategyRegister.registerStrategy( new BotrixInputCompletionStrategy( this.rootContext, session, inputController.contentEditableEditor, inputCompletionStrategyManager.navListWindowManager, { botrixSessionManager: new BotrixSessionManager(session) } ) ); } }; // src/app.ts var logger39 = new Logger(); var { log: log38, info: info36, error: error39 } = logger39.destruct(); var NipahClient = class { VERSION = "1.5.85"; ENV_VARS = { LOCAL_RESOURCE_ROOT: "http://localhost:3010/", // GITHUB_ROOT: 'https://github.com/Xzensi/NipahTV/raw/master', // GITHUB_ROOT: 'https://cdn.jsdelivr.net/gh/Xzensi/NipahTV@master', GITHUB_ROOT: "https://raw.githubusercontent.com/Xzensi/NipahTV", RELEASE_BRANCH: "master" }; stylesLoaded = false; eventBus = null; emotesManager = null; rootContext = null; loadSettingsManagerPromise = null; database = null; sessions = []; async initialize() { const { ENV_VARS } = this; info36("CORE", "INIT", `Initializing Nipah client [${this.VERSION}]..`); let resourceRoot; if (false) { info36("CORE", "INIT", "Running in debug mode enabled.."); resourceRoot = ENV_VARS.LOCAL_RESOURCE_ROOT; window.NipahTV = this; } else if (false) { info36("CORE", "INIT", "Running in extension mode.."); resourceRoot = browser.runtime.getURL("/"); } else { resourceRoot = ENV_VARS.GITHUB_ROOT + "/" + ENV_VARS.RELEASE_BRANCH + "/"; } let platform = getPlatformId(); if (platform === "kick" /* KICK */) { info36("CORE", "INIT", "Platform detected: Kick"); } else if (platform === "twitch" /* TWITCH */) { info36("CORE", "INIT", "Platform detected: Twitch"); } else if (platform === "youtube" /* YOUTUBE */) { info36("CORE", "INIT", "Platform detected: Youtube"); } else { return error39("CORE", "INIT", "Unsupported platform", window.location.host); } if (true) { window.NTV_APP_VERSION = this.VERSION; window.NTV_PLATFORM = platform; window.NTV_RESOURCE_ROOT = resourceRoot; window.NTV_BROWSER = await getBrowser(); window.NTV_DEVICE = getDevice(); window.NTV_SUPPORTS_AVIF = await hasSupportForAvif(); } else { window.PLATFORM = platform; window.RESOURCE_ROOT = resourceRoot; window.APP_VERSION = this.VERSION; window.BROWSER = await getBrowser(); window.DEVICE = getDevice(); window.SUPPORTS_AVIF = await hasSupportForAvif(); } Object.freeze(NTV_APP_VERSION); Object.freeze(NTV_PLATFORM); Object.freeze(NTV_RESOURCE_ROOT); Object.freeze(NTV_BROWSER); Object.freeze(NTV_DEVICE); Object.freeze(NTV_SUPPORTS_AVIF); this.attachPageNavigationListener(); this.setupDatabase().then(async () => { window.RESTFromMainService = new RESTFromMain(); window.ReactivePropsFromMain = new ReactivePropsFromMain2(); window.LexicalCommandFromMain = new LexicalCommandFromMain2(); await this.injectPageScript(); this.setupClientEnvironment().catch( (err) => error39("CORE", "INIT", "Failed to setup client environment.\n\n", err.message) ); }); } injectPageScript() { return new Promise((resolve, reject) => { if (false) { const s = document.createElement("script"); s.src = browser.runtime.getURL("page.js"); s.onload = function() { s.remove(); resolve(void 0); }; s.onerror = function() { error39("CORE", "INIT", "Failed to load page script.."); reject(void 0); }; (document.head || document.documentElement).appendChild(s); } else { resolve(void 0); } }); } setupDatabase() { return new Promise((resolve, reject) => { const database = true ? DatabaseProxyFactory.create("NipahTV", new Database()) : DatabaseProxyFactory.create("NipahTV"); database.checkCompatibility().then(() => { log38("CORE", "INIT", "Database passed compatibility check."); this.database = database; resolve(void 0); }).catch((err) => { error39("CORE", "INIT", "Failed to open database because:", err); reject(); }); }); } async setupClientEnvironment() { const { database } = this; if (!database) throw new Error("Database is not initialized."); info36("CORE", "INIT", "Setting up client environment.."); const rootEventBus = new Publisher("ROOT"); const settingsManager = new SettingsManager({ database, rootEventBus }); settingsManager.initialize(); let eventService; if (NTV_PLATFORM === "kick" /* KICK */) { eventService = new KickEventService(); } else if (NTV_PLATFORM === "twitch" /* TWITCH */) { eventService = new TwitchEventService(); } else { throw new Error("Unsupported platform"); } this.rootContext = { eventBus: rootEventBus, database, settingsManager, eventService, announcementService: new AnnouncementService(rootEventBus, settingsManager), renderMessagePipeline: new RenderMessagePipeline() }; this.loadExtensions(); this.loadSettingsManagerPromise = settingsManager.loadSettings().then(() => { log38("CORE", "SETUP", "Settings loaded successfully."); const appVersion = settingsManager.getGlobalSetting("app.version"); if (!appVersion || appVersion !== this.VERSION) { settingsManager.setGlobalSetting("app.version", this.VERSION); } const updateAvailableVersion = settingsManager.getGlobalSetting("app.update_available"); if (updateAvailableVersion && updateAvailableVersion <= this.VERSION) { settingsManager.setGlobalSetting("app.update_available", null); } }).catch((err) => { throw new Error(`Couldn't load settings because: ${err}`); }); this.loadAppUpdateBehaviour(rootEventBus); this.doExtensionCompatibilityChecks(); this.createChannelSession(); } loadAppUpdateBehaviour(rootEventBus) { rootEventBus.subscribe("ntv.app.update", () => { info36("CORE", "MAIN", "Extension update has been requested, reloading extension.."); browser.runtime.sendMessage({ action: "runtime.reload" }).then(() => { info36("CORE", "MAIN", "Reloading page after runtime reload.."); location.reload(); }).catch((err) => { error39("CORE", "MAIN", "Failed to reload extension.", err); location.reload(); }); }); } async loadExtensions() { const rootContext = this.rootContext; if (!rootContext) throw new Error("Root context is not initialized."); const { settingsManager } = rootContext; let sevenTVExtension = null; const enableSevenTVExtension = () => { sevenTVExtension = new SevenTVExtension(rootContext, this.sessions); sevenTVExtension.onEnable(); }; const isSevenTVExtensionEnabled = await settingsManager.getSettingFromDatabase("global.shared.ext.7tv.enabled"); if (isSevenTVExtensionEnabled) enableSevenTVExtension(); rootContext.eventBus.subscribe( "ntv.settings.change.ext.7tv.enabled", ({ value, prevValue }) => { if (value && !prevValue) enableSevenTVExtension(); else if (sevenTVExtension) { sevenTVExtension.onDisable(); sevenTVExtension = null; } } ); const enableBotrixExtension = () => { const extension = new BotrixExtension(rootContext, this.sessions); extension.onEnable(); }; const isBotrixExtensionEnabled = true; if (isBotrixExtensionEnabled) enableBotrixExtension(); } doExtensionCompatibilityChecks() { info36("CORE", "INIT", "Checking for extension compatibility issues.."); const rootContext = this.rootContext; if (!rootContext) throw new Error("Root context is not initialized."); const { announcementService, eventBus: rootEventBus } = rootContext; waitForElements(["#seventv-site-hosted"], 6e3).then(() => { log38("CORE", "INIT", "Detected SevenTV extension"); const platformName = NTV_PLATFORM[0].toUpperCase() + NTV_PLATFORM.slice(1); announcementService.registerAnnouncement({ id: "seventv_conflict", title: "NipahTV Compatibility Warning", showDontShowAgainButton: true, showCloseButton: true, message: ` <h2>\u26A0\uFE0F <strong>7TV Extension Conflict!</strong> \u26A0\uFE0F</h2> <p>The 7TV extension has been found to be enabled on ${platformName}. 7TV is not compatible with NipahTV and will cause issues if both are enabled at the same time. It is possible to keep the 7TV extension for <b>other</b> streaming websites if you want, by disabling the extension for only ${platformName}.</p> <h4>How to disable 7TV extension <i>only</i> just for ${platformName}?</h4> <p>If you want to keep 7TV for other streaming websites instead of uninstalling it completely, please follow the instructions on <a href="https://nipahtv.com/seventv_compatibility" target="_blank">https://nipahtv.com/seventv_compatibility</a>.</p> <p>Feel free to join the <a href="https://discord.gg/KZZZYM6ESs" target="_blank">NipahTV Discord community</a> if you need help with this.</p> <br> <p>You can ignore this warning if you want, but expect weird issues such as blank and empty messages.</p> ` }); announcementService.displayAnnouncement("seventv_conflict"); }).catch(() => { }); } async createChannelSession() { if (!this.shouldLoadPage()) { log38("CORE", "MAIN", "Not a stream page, nothing to do here."); return; } log38("CORE", "MAIN", `Creating new session for ${window.location.href}...`); const rootContext = this.rootContext; if (!rootContext) throw new Error("Root context is not initialized."); const { settingsManager, eventBus: rootEventBus } = rootContext; const eventBus = new Publisher("SESSION"); const session = { eventBus, inputCompletionStrategyRegister: new InputCompletionStrategyRegister(), inputExecutionStrategyRegister: new InputExecutionStrategyRegister() }; session.usersManager = new UsersManager(rootContext, session); if (NTV_PLATFORM === "kick" /* KICK */) { session.networkInterface = new KickNetworkInterface(session); } else if (NTV_PLATFORM === "twitch" /* TWITCH */) { throw new Error("Twitch platform is not supported yet."); } else { throw new Error("Unsupported platform"); } const networkInterface = session.networkInterface; const promiseRes = await Promise.allSettled([ this.loadSettingsManagerPromise, networkInterface.loadMeData().catch((err) => { throw `Couldn't load me data because: ${err.message}`; }), networkInterface.loadChannelData().catch((err) => { throw `Couldn't load channel data because: ${err.message}`; }) ]); for (const res of promiseRes) { if (res.status === "rejected") return error39("CORE", "MAIN", "Failed to create session because:", res.reason); } if (!session.meData) throw new Error("Failed to load me user data."); if (!session.channelData) throw new Error("Failed to load channel data."); this.sessions.push(session); const channelData = session.channelData; eventBus.publish("ntv.channel.loaded.channel_data", channelData); const disableModCreatorView = settingsManager.getSetting( channelData.channelId, "moderators.mod_creator_view.disable_ntv" ); if (disableModCreatorView && (channelData.isModView || channelData.isCreatorView)) { info36("CORE", "MAIN", "NipahTV is disabled for this channel in mod/creator view."); return; } this.attachEventServiceListeners(rootContext, session); session.badgeProvider = new KickBadgeProvider(rootContext, channelData); session.badgeProvider.initialize(); const emotesManager = this.emotesManager = new EmotesManager(rootContext, session); emotesManager.initialize(); session.emotesManager = emotesManager; session.inputExecutionStrategyRegister.registerStrategy(new DefaultExecutionStrategy(rootContext, session)); session.inputExecutionStrategyRegister.registerStrategy(new CommandExecutionStrategy(rootContext, session)); let userInterface; if (NTV_PLATFORM === "kick" /* KICK */) { userInterface = new KickUserInterface(rootContext, session); } else { return error39("CORE", "MAIN", "Platform has no user interface implemented..", NTV_PLATFORM); } session.userInterface = userInterface; if (NTV_PLATFORM === "kick" /* KICK */) { emotesManager.registerProvider(KickEmoteProvider); } else if (NTV_PLATFORM === "twitch" /* TWITCH */) { throw new Error("Twitch platform is not supported yet."); } else { throw new Error("Unsupported platform"); } rootEventBus.publish("ntv.session.create", session); if (!this.stylesLoaded) { this.loadStyles().then(() => { this.stylesLoaded = true; userInterface.loadInterface(); }).catch((response) => error39("CORE", "INIT", "Failed to load styles.", response)); } else { userInterface.loadInterface(); } emotesManager.loadProviderEmotes(channelData); eventBus.subscribe( "ntv.session.reload", debounce(() => { if (session.isDestroyed) return; this.cleanupSessions(true); this.createChannelSession(); }, 1e3) ); } shouldLoadPage() { const pathnames = window.location.pathname.split("/"); if (window.location.hostname === "kick.com" && ("/" === window.location.pathname || ["browse", "settings", "transactions", "following"].includes(pathnames[1] || ""))) { return false; } if (window.location.hostname.split(".")[0] === "dashboard" && pathnames[1] !== "stream" && pathnames[1] !== "moderator") { return false; } if ("/" === window.location.pathname || ["browse", "settings", "transactions", "following"].includes(pathnames[1] || "")) { return false; } return true; } attachEventServiceListeners(rootContext, session) { const { eventBus, channelData, meData } = session; if (channelData.isVod) return; rootContext.eventService.subToChatroomEvents(channelData); rootContext.eventService.addEventListener(channelData, "MESSAGE", (message) => { eventBus.publish("ntv.chat.message.new", message, true); }); rootContext.eventService.addEventListener(channelData, "CHATROOM_UPDATED", (chatroomData) => { const oldChatroomData = channelData.chatroom; if (oldChatroomData?.emotesMode?.enabled !== chatroomData.emotesMode?.enabled) { eventBus.publish("ntv.channel.chatroom.emotes_mode.updated", chatroomData.emotesMode); } else if (oldChatroomData?.subscribersMode?.enabled !== chatroomData.subscribersMode?.enabled) { eventBus.publish("ntv.channel.chatroom.subscribers_mode.updated", chatroomData.subscribersMode); } else if (oldChatroomData?.followersMode?.enabled !== chatroomData.followersMode?.enabled || oldChatroomData?.followersMode?.min_duration !== chatroomData.followersMode?.min_duration) { eventBus.publish("ntv.channel.chatroom.followers_mode.updated", chatroomData.followersMode); } else if (oldChatroomData?.slowMode?.enabled !== chatroomData.slowMode?.enabled || oldChatroomData?.slowMode?.messageInterval !== chatroomData.slowMode?.messageInterval) { eventBus.publish("ntv.channel.chatroom.slow_mode.updated", chatroomData.slowMode); } channelData.chatroom = chatroomData; eventBus.publish("ntv.channel.chatroom.updated", chatroomData); }); let unbanTimeoutHandle = null; rootContext.eventService.addEventListener(channelData, "USER_BANNED", (data) => { eventBus.publish("ntv.channel.chatroom.user.banned", data); if (data.user.id === meData.userId) { log38("CORE", "MAIN", "You have been banned from the channel.."); session.channelData.me.isBanned = { bannedAt: (/* @__PURE__ */ new Date()).toISOString(), expiresAt: data.expiresAt, permanent: data.permanent, reason: "" // Reason is not provided by Kick here }; eventBus.publish("ntv.channel.chatroom.me.banned", data); if (unbanTimeoutHandle) clearTimeout(unbanTimeoutHandle); if (!data.permanent) { unbanTimeoutHandle = setTimeout( () => { delete session.channelData.me.isBanned; eventBus.publish("ntv.channel.chatroom.me.unbanned"); }, data.duration * 60 * 1e3 ); } } }); rootContext.eventService.addEventListener(channelData, "USER_UNBANNED", (data) => { eventBus.publish("ntv.channel.chatroom.user.unbanned", data); if (data.user.id === meData.userId) { if (unbanTimeoutHandle) clearTimeout(unbanTimeoutHandle); log38("CORE", "MAIN", "You have been unbanned from the channel.."); delete session.channelData.me.isBanned; eventBus.publish("ntv.channel.chatroom.me.unbanned"); } }); } loadStyles() { if (false) return Promise.resolve(); return new Promise((resolve, reject) => { info36("CORE", "INIT", "Injecting styles.."); if (false) { GM_xmlhttpRequest({ method: "GET", url: NTV_RESOURCE_ROOT + "dist/userscript/kick.css", onerror: () => reject("Failed to load local stylesheet"), onload: function(response) { log38("CORE", "MAIN", "Loaded styles from local resource.."); GM_addStyle(response.responseText); resolve(void 0); } }); } else { let style; switch (NTV_PLATFORM) { case "kick" /* KICK */: style = "KICK_CSS"; break; default: return reject("Unsupported platform"); } const stylesheet = GM_getResourceText(style); if (!stylesheet) return reject("Failed to load stylesheet"); if (stylesheet.substring(0, 4) === "http") { reject("Invalid stylesheet resource."); } GM_addStyle(stylesheet); resolve(void 0); } }); } attachPageNavigationListener() { info36("CORE", "MAIN", "Current URL:", window.location.href); let locationURL = window.location.href; const navigateFn = () => { if (locationURL === window.location.href) return; if (window.location.pathname.match("^/[a-zA-Z0-9]{8}(?:-[a-zA-Z0-9]{4,12}){4}/.+")) return; const prevLocation = locationURL; const newLocation = window.location.href; locationURL = newLocation; log38("CORE", "MAIN", "Navigated to:", newLocation); const prevSession = this.sessions[0]; if (prevSession) { const prevChannelName = prevSession.channelData.channelName; const newLocationChannelName = prevSession.networkInterface.getChannelName(); const newLocationIsVod = prevSession.networkInterface.isVOD(); if (!newLocationIsVod && !prevSession.isDestroyed && !prevSession.userInterface?.isContentEditableEditorDestroyed() && prevChannelName === newLocationChannelName) return log38("CORE", "MAIN", "Session UI is not destroyed, only part of page has changed.."); } info36("CORE", "MAIN", "Navigated to:", locationURL); this.cleanupSessions(); log38("CORE", "MAIN", "Cleaned up old session for", prevLocation); this.createChannelSession(); this.doExtensionCompatibilityChecks(); }; if (window.navigation) { window.navigation.addEventListener("navigate", debounce(navigateFn, 100)); } else { setInterval(navigateFn, 200); } window.addEventListener("beforeunload", () => { info36("CORE", "MAIN", "User is navigating away from the page, cleaning up sessions before leaving.."); this.rootContext?.eventService.disconnectAll(); this.cleanupSessions(); }); } cleanupSessions(restoreOriginalUI = false) { for (const session of this.sessions) { log38( "CORE", "MAIN", `Cleaning up previous session for channel ${session?.channelData?.channelName || "[CHANNEL NOT LOADED]"}...` ); session.isDestroyed = true; session.eventBus.publish("ntv.session.destroy"); if (restoreOriginalUI) session.eventBus.publish("ntv.session.restore_original"); this.rootContext?.eventBus.publish("ntv.session.destroy", session); session.eventBus.destroy(); } this.sessions = []; } }; (() => { if (window.location.pathname.match("^/[a-zA-Z0-9]{8}(?:-[a-zA-Z0-9]{4,12}){4}/.+")) { log38("CORE", "MAIN", "KPSDK URL detected, bailing out.."); return; } if (true) { info36("CORE", "INIT", "Running in userscript mode.."); } if (false) { if (!window["browser"] && !globalThis["browser"]) { if (void 0 === chrome) { return error39("CORE", "INIT", "Unsupported browser, please use a modern browser to run NipahTV."); } window.browser = chrome; } } if (false) { globalThis["EventTarget"] = window["EventTarget"]; } const nipahClient = new NipahClient(); nipahClient.initialize(); })(); //! Temporary migration code //! Temporary delete old settings records //! Temporary patch for setting format changes //! Dirty reload UI hack to check if UI elements of session is destroyed and reload it /*! Bundled license information: pusher-js/dist/web/pusher.js: (*! * Pusher JavaScript Library v8.4.0-rc2 * https://pusher.com/ * * Copyright 2020, Pusher * Released under the MIT licence. *) dexie/dist/dexie.js: (*! ***************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** *) */