diff --git a/3CX_TAPI.user.js b/3CX_TAPI.user.js
index 056d2f0..1915297 100644
--- a/3CX_TAPI.user.js
+++ b/3CX_TAPI.user.js
@@ -122,6 +122,98 @@ module.exports = function (i) {
return i[1];
};
+/***/ },
+
+/***/ "./node_modules/css-loader/dist/cjs.js!./src/availability.css"
+(module, __webpack_exports__, __webpack_require__) {
+
+"use strict";
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ A: () => (__WEBPACK_DEFAULT_EXPORT__)
+/* harmony export */ });
+/* harmony import */ var _node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./node_modules/css-loader/dist/runtime/noSourceMaps.js");
+/* harmony import */ var _node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0__);
+/* harmony import */ var _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__("./node_modules/css-loader/dist/runtime/api.js");
+/* harmony import */ var _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1__);
+// Imports
+
+
+var ___CSS_LOADER_EXPORT___ = _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1___default()((_node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0___default()));
+// Module
+___CSS_LOADER_EXPORT___.push([module.id, `.tapi-availability {
+ grid-column: 2;
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ gap: 4px;
+ font-size: 9px;
+ opacity: 0.7;
+ line-height: 1;
+ white-space: nowrap;
+ overflow: hidden;
+ margin-top: -4px;
+ margin-bottom: 6px;
+}
+
+.tapi-availability small {
+ font-size: inherit;
+}
+
+.tapi-dot {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ display: inline-block;
+ flex-shrink: 0;
+}
+
+.tapi-dot-on {
+ background-color: #5cb85c;
+}
+
+.tapi-dot-off {
+ background-color: #d9534f;
+}
+
+.tapi-avatar-square {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ width: 5px;
+ z-index: 10;
+}
+
+people-tile .avatar-size-m,
+people-tile .avatar-size-m .avatar-container,
+people-tile .avatar-size-m .avatar-content {
+ width: 57px !important;
+ height: 57px !important;
+ line-height: 57px !important;
+ margin-top: 10px;
+}
+
+people-tile {
+ grid-template-rows: 29px 23px;
+}
+
+people-tile app-avatar {
+ grid-row: 1 / span 2 !important;
+ align-self: center !important;
+}
+
+.tapi-square-on {
+ background-color: #5cb85c;
+}
+
+.tapi-square-off {
+ background-color: #d9534f;
+}
+`, ""]);
+// Exports
+/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (___CSS_LOADER_EXPORT___);
+
+
/***/ },
/***/ "./node_modules/css-loader/dist/cjs.js!./src/presence.css"
@@ -658,6 +750,335 @@ var __webpack_exports__ = {};
(() => {
"use strict";
+// EXTERNAL MODULE: ./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js
+var injectStylesIntoStyleTag = __webpack_require__("./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js");
+var injectStylesIntoStyleTag_default = /*#__PURE__*/__webpack_require__.n(injectStylesIntoStyleTag);
+// EXTERNAL MODULE: ./node_modules/style-loader/dist/runtime/styleDomAPI.js
+var styleDomAPI = __webpack_require__("./node_modules/style-loader/dist/runtime/styleDomAPI.js");
+var styleDomAPI_default = /*#__PURE__*/__webpack_require__.n(styleDomAPI);
+// EXTERNAL MODULE: ./node_modules/style-loader/dist/runtime/insertBySelector.js
+var insertBySelector = __webpack_require__("./node_modules/style-loader/dist/runtime/insertBySelector.js");
+var insertBySelector_default = /*#__PURE__*/__webpack_require__.n(insertBySelector);
+// EXTERNAL MODULE: ./node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js
+var setAttributesWithoutAttributes = __webpack_require__("./node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js");
+var setAttributesWithoutAttributes_default = /*#__PURE__*/__webpack_require__.n(setAttributesWithoutAttributes);
+// EXTERNAL MODULE: ./node_modules/style-loader/dist/runtime/insertStyleElement.js
+var insertStyleElement = __webpack_require__("./node_modules/style-loader/dist/runtime/insertStyleElement.js");
+var insertStyleElement_default = /*#__PURE__*/__webpack_require__.n(insertStyleElement);
+// EXTERNAL MODULE: ./node_modules/style-loader/dist/runtime/styleTagTransform.js
+var styleTagTransform = __webpack_require__("./node_modules/style-loader/dist/runtime/styleTagTransform.js");
+var styleTagTransform_default = /*#__PURE__*/__webpack_require__.n(styleTagTransform);
+// EXTERNAL MODULE: ./node_modules/css-loader/dist/cjs.js!./src/availability.css
+var availability = __webpack_require__("./node_modules/css-loader/dist/cjs.js!./src/availability.css");
+;// ./src/availability.css
+
+
+
+
+
+
+
+
+
+
+
+var options = {};
+
+options.styleTagTransform = (styleTagTransform_default());
+options.setAttributes = (setAttributesWithoutAttributes_default());
+options.insert = insertBySelector_default().bind(null, "head");
+options.domAPI = (styleDomAPI_default());
+options.insertStyleElement = (insertStyleElement_default());
+
+var update = injectStylesIntoStyleTag_default()(availability/* default */.A, options);
+
+
+
+
+ /* harmony default export */ const src_availability = (availability/* default */.A && availability/* default */.A.locals ? availability/* default */.A.locals : undefined);
+
+;// ./src/availability.ts
+
+class Availability {
+ _service;
+ _availabilities = [];
+ constructor(service) {
+ this._service = service;
+ }
+ start() {
+ this._service.subscribe((avs) => {
+ this._availabilities = avs;
+ this.updateAllIndicators();
+ });
+ waitForKeyElements('people-tile', (element) => { this.decorateTile(element); }, false);
+ }
+ decorateTile(tile) {
+ if (tile.querySelector('.tapi-availability')) {
+ return;
+ }
+ var extEl = tile.querySelector('span[data-id="extPhone"]');
+ if (!extEl) {
+ return;
+ }
+ var extension = extEl.textContent.trim();
+ var indicator = document.createElement('div');
+ indicator.classList.add('tapi-availability');
+ indicator.dataset.tapiExtension = extension;
+ tile.appendChild(indicator);
+ this.updateIndicator(indicator);
+ var avatarContainer = tile.querySelector('app-avatar .avatar-container');
+ if (avatarContainer) {
+ var square = document.createElement('i');
+ square.classList.add('tapi-avatar-square');
+ square.dataset.tapiExtension = extension;
+ avatarContainer.appendChild(square);
+ this.updateSquare(square);
+ }
+ }
+ updateAllIndicators() {
+ var indicators = document.getElementsByClassName('tapi-availability');
+ for (var i of indicators) {
+ this.updateIndicator(i);
+ }
+ var squares = document.getElementsByClassName('tapi-avatar-square');
+ for (var s of squares) {
+ this.updateSquare(s);
+ }
+ }
+ updateIndicator(indicator) {
+ var extension = indicator.dataset.tapiExtension;
+ var entry = this._availabilities.find(a => a.extension === extension);
+ if (!entry) {
+ indicator.innerHTML = '';
+ return;
+ }
+ var dotClass = entry.loggedIn ? 'tapi-dot-on' : 'tapi-dot-off';
+ var mockedTime = '13.04. 08:30';
+ indicator.innerHTML = '' + mockedTime + '';
+ }
+ updateSquare(square) {
+ var extension = square.dataset.tapiExtension;
+ var entry = this._availabilities.find(a => a.extension === extension);
+ square.classList.remove('tapi-square-on', 'tapi-square-off');
+ if (!entry) {
+ square.style.display = 'none';
+ return;
+ }
+ square.style.display = '';
+ square.classList.add(entry.loggedIn ? 'tapi-square-on' : 'tapi-square-off');
+ }
+}
+
+;// ./src/config.ts
+class _Config {
+ tapi_server_url = 'https://3cxtapi.cp-austria.at';
+}
+const Config = new _Config();
+
+;// ./node_modules/@trim21/gm-fetch/dist/index.mjs
+function parseRawHeaders(h) {
+ const s = h.trim();
+ if (!s) {
+ return new Headers();
+ }
+ const array = s.split("\r\n").map((value) => {
+ let s = value.split(":");
+ return [s[0].trim(), s[1].trim()];
+ });
+ return new Headers(array);
+}
+function parseGMResponse(req, res) {
+ // workaround TamperMonkey bug(?) where sometimes response is string despite responseType being "blob"
+ const headers = parseRawHeaders(res.responseHeaders);
+ const body = typeof res.response === "string"
+ ? new Blob([res.response], { type: headers.get("Content-Type") || "text/plain" })
+ : res.response;
+ return new ResImpl(body, {
+ statusCode: res.status,
+ statusText: res.statusText,
+ headers,
+ finalUrl: res.finalUrl,
+ redirected: res.finalUrl === req.url,
+ });
+}
+class ResImpl {
+ constructor(body, init) {
+ this.rawBody = body;
+ this.init = init;
+ this.body = body.stream();
+ const { headers, statusCode, statusText, finalUrl, redirected } = init;
+ this.headers = headers;
+ this.status = statusCode;
+ this.statusText = statusText;
+ this.url = finalUrl;
+ this.type = "basic";
+ this.redirected = redirected;
+ this._bodyUsed = false;
+ }
+ get bodyUsed() {
+ return this._bodyUsed;
+ }
+ get ok() {
+ return this.status < 300;
+ }
+ arrayBuffer() {
+ if (this.bodyUsed) {
+ throw new TypeError("Failed to execute 'arrayBuffer' on 'Response': body stream already read");
+ }
+ this._bodyUsed = true;
+ return this.rawBody.arrayBuffer();
+ }
+ blob() {
+ if (this.bodyUsed) {
+ throw new TypeError("Failed to execute 'blob' on 'Response': body stream already read");
+ }
+ this._bodyUsed = true;
+ // `slice` will use empty string as default value, so need to pass all arguments.
+ return Promise.resolve(this.rawBody.slice(0, this.rawBody.size, this.rawBody.type));
+ }
+ clone() {
+ if (this.bodyUsed) {
+ throw new TypeError("Failed to execute 'clone' on 'Response': body stream already read");
+ }
+ return new ResImpl(this.rawBody, this.init);
+ }
+ formData() {
+ if (this.bodyUsed) {
+ throw new TypeError("Failed to execute 'formData' on 'Response': body stream already read");
+ }
+ this._bodyUsed = true;
+ return this.rawBody.text().then(decode);
+ }
+ async json() {
+ if (this.bodyUsed) {
+ throw new TypeError("Failed to execute 'json' on 'Response': body stream already read");
+ }
+ this._bodyUsed = true;
+ return JSON.parse(await this.rawBody.text());
+ }
+ text() {
+ if (this.bodyUsed) {
+ throw new TypeError("Failed to execute 'text' on 'Response': body stream already read");
+ }
+ this._bodyUsed = true;
+ return this.rawBody.text();
+ }
+ async bytes() {
+ if (this.bodyUsed) {
+ throw new TypeError("Failed to execute 'bytes' on 'Response': body stream already read");
+ }
+ this._bodyUsed = true;
+ return new Uint8Array(await this.rawBody.arrayBuffer());
+ }
+}
+function decode(body) {
+ const form = new FormData();
+ body
+ .trim()
+ .split("&")
+ .forEach(function (bytes) {
+ if (bytes) {
+ const split = bytes.split("=");
+ const name = split.shift()?.replace(/\+/g, " ");
+ const value = split.join("=").replace(/\+/g, " ");
+ form.append(decodeURIComponent(name), decodeURIComponent(value));
+ }
+ });
+ return form;
+}
+
+async function GM_fetch(input, init) {
+ const request = new Request(input, init);
+ let data;
+ if (init?.body) {
+ data = await request.text();
+ }
+ return await XHR(request, init, data);
+}
+function XHR(request, init, data) {
+ return new Promise((resolve, reject) => {
+ if (request.signal && request.signal.aborted) {
+ return reject(new DOMException("Aborted", "AbortError"));
+ }
+ GM.xmlHttpRequest({
+ url: request.url,
+ method: gmXHRMethod(request.method.toUpperCase()),
+ headers: Object.fromEntries(new Headers(init?.headers).entries()),
+ data: data,
+ responseType: "blob",
+ onload(res) {
+ try {
+ resolve(parseGMResponse(request, res));
+ }
+ catch (e) {
+ reject(e);
+ }
+ },
+ onabort() {
+ reject(new DOMException("Aborted", "AbortError"));
+ },
+ ontimeout() {
+ reject(new TypeError("Network request failed, timeout"));
+ },
+ onerror(err) {
+ reject(new TypeError("Failed to fetch: " + err.finalUrl));
+ },
+ });
+ });
+}
+const httpMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "TRACE", "OPTIONS", "CONNECT"];
+// a ts type helper to narrow type
+function includes(array, element) {
+ return array.includes(element);
+}
+function gmXHRMethod(method) {
+ if (includes(httpMethods, method)) {
+ return method;
+ }
+ throw new Error(`unsupported http method ${method}`);
+}
+
+
+//# sourceMappingURL=index.mjs.map
+
+;// ./src/availability-service.ts
+
+
+class AvailabilityService {
+ _availabilities = [];
+ _listeners = [];
+ _started = false;
+ start() {
+ if (this._started) {
+ return;
+ }
+ this._started = true;
+ this.fetch();
+ setInterval(() => this.fetch(), 30000);
+ }
+ subscribe(listener) {
+ this._listeners.push(listener);
+ if (this._availabilities.length > 0) {
+ listener(this._availabilities);
+ }
+ }
+ get availabilities() {
+ return this._availabilities;
+ }
+ async fetch() {
+ try {
+ var response = await GM_fetch(Config.tapi_server_url + '/availability');
+ if (response.status === 200) {
+ this._availabilities = await response.json();
+ this._listeners.forEach(l => l(this._availabilities));
+ }
+ }
+ catch (error) {
+ console.log(error);
+ }
+ }
+}
+
;// ./node_modules/chrono-node/dist/esm/types.js
var Meridiem;
(function (Meridiem) {
@@ -4248,178 +4669,6 @@ function fireChangeEvents(element) {
console.debug('change event dispatched for element: ' + element.id);
}
-;// ./node_modules/@trim21/gm-fetch/dist/index.mjs
-function parseRawHeaders(h) {
- const s = h.trim();
- if (!s) {
- return new Headers();
- }
- const array = s.split("\r\n").map((value) => {
- let s = value.split(":");
- return [s[0].trim(), s[1].trim()];
- });
- return new Headers(array);
-}
-function parseGMResponse(req, res) {
- // workaround TamperMonkey bug(?) where sometimes response is string despite responseType being "blob"
- const headers = parseRawHeaders(res.responseHeaders);
- const body = typeof res.response === "string"
- ? new Blob([res.response], { type: headers.get("Content-Type") || "text/plain" })
- : res.response;
- return new ResImpl(body, {
- statusCode: res.status,
- statusText: res.statusText,
- headers,
- finalUrl: res.finalUrl,
- redirected: res.finalUrl === req.url,
- });
-}
-class ResImpl {
- constructor(body, init) {
- this.rawBody = body;
- this.init = init;
- this.body = body.stream();
- const { headers, statusCode, statusText, finalUrl, redirected } = init;
- this.headers = headers;
- this.status = statusCode;
- this.statusText = statusText;
- this.url = finalUrl;
- this.type = "basic";
- this.redirected = redirected;
- this._bodyUsed = false;
- }
- get bodyUsed() {
- return this._bodyUsed;
- }
- get ok() {
- return this.status < 300;
- }
- arrayBuffer() {
- if (this.bodyUsed) {
- throw new TypeError("Failed to execute 'arrayBuffer' on 'Response': body stream already read");
- }
- this._bodyUsed = true;
- return this.rawBody.arrayBuffer();
- }
- blob() {
- if (this.bodyUsed) {
- throw new TypeError("Failed to execute 'blob' on 'Response': body stream already read");
- }
- this._bodyUsed = true;
- // `slice` will use empty string as default value, so need to pass all arguments.
- return Promise.resolve(this.rawBody.slice(0, this.rawBody.size, this.rawBody.type));
- }
- clone() {
- if (this.bodyUsed) {
- throw new TypeError("Failed to execute 'clone' on 'Response': body stream already read");
- }
- return new ResImpl(this.rawBody, this.init);
- }
- formData() {
- if (this.bodyUsed) {
- throw new TypeError("Failed to execute 'formData' on 'Response': body stream already read");
- }
- this._bodyUsed = true;
- return this.rawBody.text().then(decode);
- }
- async json() {
- if (this.bodyUsed) {
- throw new TypeError("Failed to execute 'json' on 'Response': body stream already read");
- }
- this._bodyUsed = true;
- return JSON.parse(await this.rawBody.text());
- }
- text() {
- if (this.bodyUsed) {
- throw new TypeError("Failed to execute 'text' on 'Response': body stream already read");
- }
- this._bodyUsed = true;
- return this.rawBody.text();
- }
- async bytes() {
- if (this.bodyUsed) {
- throw new TypeError("Failed to execute 'bytes' on 'Response': body stream already read");
- }
- this._bodyUsed = true;
- return new Uint8Array(await this.rawBody.arrayBuffer());
- }
-}
-function decode(body) {
- const form = new FormData();
- body
- .trim()
- .split("&")
- .forEach(function (bytes) {
- if (bytes) {
- const split = bytes.split("=");
- const name = split.shift()?.replace(/\+/g, " ");
- const value = split.join("=").replace(/\+/g, " ");
- form.append(decodeURIComponent(name), decodeURIComponent(value));
- }
- });
- return form;
-}
-
-async function GM_fetch(input, init) {
- const request = new Request(input, init);
- let data;
- if (init?.body) {
- data = await request.text();
- }
- return await XHR(request, init, data);
-}
-function XHR(request, init, data) {
- return new Promise((resolve, reject) => {
- if (request.signal && request.signal.aborted) {
- return reject(new DOMException("Aborted", "AbortError"));
- }
- GM.xmlHttpRequest({
- url: request.url,
- method: gmXHRMethod(request.method.toUpperCase()),
- headers: Object.fromEntries(new Headers(init?.headers).entries()),
- data: data,
- responseType: "blob",
- onload(res) {
- try {
- resolve(parseGMResponse(request, res));
- }
- catch (e) {
- reject(e);
- }
- },
- onabort() {
- reject(new DOMException("Aborted", "AbortError"));
- },
- ontimeout() {
- reject(new TypeError("Network request failed, timeout"));
- },
- onerror(err) {
- reject(new TypeError("Failed to fetch: " + err.finalUrl));
- },
- });
- });
-}
-const httpMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "TRACE", "OPTIONS", "CONNECT"];
-// a ts type helper to narrow type
-function includes(array, element) {
- return array.includes(element);
-}
-function gmXHRMethod(method) {
- if (includes(httpMethods, method)) {
- return method;
- }
- throw new Error(`unsupported http method ${method}`);
-}
-
-
-//# sourceMappingURL=index.mjs.map
-
-;// ./src/config.ts
-class _Config {
- tapi_server_url = 'https://3cxtapi.cp-austria.at';
-}
-const Config = new _Config();
-
;// ./src/call-history.ts
@@ -4531,24 +4780,6 @@ class CallNotification {
}
}
-// EXTERNAL MODULE: ./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js
-var injectStylesIntoStyleTag = __webpack_require__("./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js");
-var injectStylesIntoStyleTag_default = /*#__PURE__*/__webpack_require__.n(injectStylesIntoStyleTag);
-// EXTERNAL MODULE: ./node_modules/style-loader/dist/runtime/styleDomAPI.js
-var styleDomAPI = __webpack_require__("./node_modules/style-loader/dist/runtime/styleDomAPI.js");
-var styleDomAPI_default = /*#__PURE__*/__webpack_require__.n(styleDomAPI);
-// EXTERNAL MODULE: ./node_modules/style-loader/dist/runtime/insertBySelector.js
-var insertBySelector = __webpack_require__("./node_modules/style-loader/dist/runtime/insertBySelector.js");
-var insertBySelector_default = /*#__PURE__*/__webpack_require__.n(insertBySelector);
-// EXTERNAL MODULE: ./node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js
-var setAttributesWithoutAttributes = __webpack_require__("./node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js");
-var setAttributesWithoutAttributes_default = /*#__PURE__*/__webpack_require__.n(setAttributesWithoutAttributes);
-// EXTERNAL MODULE: ./node_modules/style-loader/dist/runtime/insertStyleElement.js
-var insertStyleElement = __webpack_require__("./node_modules/style-loader/dist/runtime/insertStyleElement.js");
-var insertStyleElement_default = /*#__PURE__*/__webpack_require__.n(insertStyleElement);
-// EXTERNAL MODULE: ./node_modules/style-loader/dist/runtime/styleTagTransform.js
-var styleTagTransform = __webpack_require__("./node_modules/style-loader/dist/runtime/styleTagTransform.js");
-var styleTagTransform_default = /*#__PURE__*/__webpack_require__.n(styleTagTransform);
// EXTERNAL MODULE: ./node_modules/css-loader/dist/cjs.js!./src/presence.css
var presence = __webpack_require__("./node_modules/css-loader/dist/cjs.js!./src/presence.css");
;// ./src/presence.css
@@ -4563,15 +4794,15 @@ var presence = __webpack_require__("./node_modules/css-loader/dist/cjs.js!./src/
-var options = {};
+var presence_options = {};
-options.styleTagTransform = (styleTagTransform_default());
-options.setAttributes = (setAttributesWithoutAttributes_default());
-options.insert = insertBySelector_default().bind(null, "head");
-options.domAPI = (styleDomAPI_default());
-options.insertStyleElement = (insertStyleElement_default());
+presence_options.styleTagTransform = (styleTagTransform_default());
+presence_options.setAttributes = (setAttributesWithoutAttributes_default());
+presence_options.insert = insertBySelector_default().bind(null, "head");
+presence_options.domAPI = (styleDomAPI_default());
+presence_options.insertStyleElement = (insertStyleElement_default());
-var update = injectStylesIntoStyleTag_default()(presence/* default */.A, options);
+var presence_update = injectStylesIntoStyleTag_default()(presence/* default */.A, presence_options);
@@ -4864,48 +5095,53 @@ var status_update = injectStylesIntoStyleTag_default()(cjs_js_src_status/* defau
;// ./src/status.ts
-
-
const zcIcon = __webpack_require__("./src/stopwatch-regular-full.svg");
class Status {
+ _service;
_user;
_enabled = false;
_statusOn = 'menuAvailable';
_statusOff = 'menuAway';
_currentStatus = undefined;
+ _subscribed = false;
+ constructor(service) {
+ this._service = service;
+ }
async showStatus(element) {
this._user = await GM.getValue('tapi-zc-user', '');
this._enabled = await GM.getValue('tapi-zc-enabled', false);
this._statusOn = await GM.getValue('tapi-zc-on', 'menuAvailable');
this._statusOff = await GM.getValue('tapi-zc-off', 'menuAvailable');
console.log('tapi-zc-user', this._user, 'tapi-zc-enabled', this._enabled, 'tapi-zc-on', this._statusOn, 'tapi-zc-off', this._statusOff);
- this.checkStatus();
+ this.subscribeOnce();
waitForKeyElements("wc-account-menu > div > ul", (element) => { this.addZcStatusPopup(element); }, true);
}
- async checkStatus() {
- if (this._enabled) {
- try {
- var response = await GM_fetch(Config.tapi_server_url + '/availability/' + encodeURIComponent(this._user));
- if (response.status == 200) {
- var status = await response.json();
- if (this._currentStatus !== status.loggedIn) {
- this._currentStatus = status.loggedIn;
- console.log('New status, loggedIn', this._currentStatus);
- var accMenu = document.getElementsByTagName("wc-account-menu")[0];
- var avatar = accMenu.getElementsByTagName("app-avatar")[0];
- avatar.click();
- setTimeout(() => {
- var statusId = this._currentStatus ? this._statusOn : this._statusOff;
- console.log('Clicking status', statusId);
- document.getElementById(statusId).click();
- }, 1000);
- }
- }
- }
- catch (error) {
- console.log(error);
- }
- setTimeout(() => this.checkStatus(), 30000);
+ subscribeOnce() {
+ if (this._subscribed) {
+ return;
+ }
+ this._subscribed = true;
+ this._service.subscribe((avs) => this.onAvailabilities(avs));
+ }
+ onAvailabilities(avs) {
+ if (!this._enabled || !this._user) {
+ return;
+ }
+ var entry = avs.find(a => a.user === this._user);
+ if (!entry) {
+ return;
+ }
+ if (this._currentStatus !== entry.loggedIn) {
+ this._currentStatus = entry.loggedIn;
+ console.log('New status, loggedIn', this._currentStatus);
+ var accMenu = document.getElementsByTagName("wc-account-menu")[0];
+ var avatar = accMenu.getElementsByTagName("app-avatar")[0];
+ avatar.click();
+ setTimeout(() => {
+ var statusId = this._currentStatus ? this._statusOn : this._statusOff;
+ console.log('Clicking status', statusId);
+ document.getElementById(statusId).click();
+ }, 1000);
}
}
addZcStatusPopup(element) {
@@ -4998,7 +5234,9 @@ class Status {
GM.setValue('tapi-zc-enabled', this._enabled);
console.log('tapi-zc-enabled', this._enabled);
this._currentStatus = undefined;
- this.checkStatus();
+ if (this._enabled) {
+ this.onAvailabilities(this._service.availabilities);
+ }
};
var zcOn = document.getElementById('tapi-zc-on');
zcOn.value = this._statusOn;
@@ -5054,6 +5292,8 @@ class Status {
+
+
console.log('script start');
const src_search_0 = new Search();
// eslint-disable-next-line no-undef
@@ -5075,7 +5315,9 @@ const callHistory = new CallHistory();
waitForKeyElements('call', element => {
callHistory.showCallHistory(element);
}, false);
-const src_status_0 = new Status();
+const availabilityService = new AvailabilityService();
+availabilityService.start();
+const src_status_0 = new Status(availabilityService);
// eslint-disable-next-line no-undef
waitForKeyElements('wc-account-menu', element => {
src_status_0.showStatus(element);
@@ -5083,6 +5325,8 @@ waitForKeyElements('wc-account-menu', element => {
waitForKeyElements('wc-account-menu i.status-indicator', element => {
src_status_0.watchStatus(element);
}, false);
+const src_availability_0 = new Availability(availabilityService);
+src_availability_0.start();
})();
/******/ })()
diff --git a/client/src/availability-info.ts b/client/src/availability-info.ts
new file mode 100644
index 0000000..06a96ef
--- /dev/null
+++ b/client/src/availability-info.ts
@@ -0,0 +1,5 @@
+export class AvailabilityInfo {
+ public user: string;
+ public loggedIn: boolean;
+ public extension: string;
+}
diff --git a/client/src/availability-service.ts b/client/src/availability-service.ts
new file mode 100644
index 0000000..7f72dec
--- /dev/null
+++ b/client/src/availability-service.ts
@@ -0,0 +1,43 @@
+import { Config } from './config'
+import { AvailabilityInfo } from './availability-info'
+import GM_fetch from '@trim21/gm-fetch'
+
+type Listener = (availabilities: AvailabilityInfo[]) => void
+
+export class AvailabilityService {
+ private _availabilities: AvailabilityInfo[] = []
+ private _listeners: Listener[] = []
+ private _started = false
+
+ public start() {
+ if (this._started) {
+ return
+ }
+ this._started = true
+ this.fetch()
+ setInterval(() => this.fetch(), 30000)
+ }
+
+ public subscribe(listener: Listener) {
+ this._listeners.push(listener)
+ if (this._availabilities.length > 0) {
+ listener(this._availabilities)
+ }
+ }
+
+ public get availabilities(): AvailabilityInfo[] {
+ return this._availabilities
+ }
+
+ private async fetch() {
+ try {
+ var response = await GM_fetch(Config.tapi_server_url + '/availability')
+ if (response.status === 200) {
+ this._availabilities = await response.json() as AvailabilityInfo[]
+ this._listeners.forEach(l => l(this._availabilities))
+ }
+ } catch (error) {
+ console.log(error)
+ }
+ }
+}
diff --git a/client/src/availability.css b/client/src/availability.css
new file mode 100644
index 0000000..81d8027
--- /dev/null
+++ b/client/src/availability.css
@@ -0,0 +1,69 @@
+.tapi-availability {
+ grid-column: 2;
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ gap: 4px;
+ font-size: 9px;
+ opacity: 0.7;
+ line-height: 1;
+ white-space: nowrap;
+ overflow: hidden;
+ margin-top: -4px;
+ margin-bottom: 6px;
+}
+
+.tapi-availability small {
+ font-size: inherit;
+}
+
+.tapi-dot {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ display: inline-block;
+ flex-shrink: 0;
+}
+
+.tapi-dot-on {
+ background-color: #5cb85c;
+}
+
+.tapi-dot-off {
+ background-color: #d9534f;
+}
+
+.tapi-avatar-square {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ width: 5px;
+ z-index: 10;
+}
+
+people-tile .avatar-size-m,
+people-tile .avatar-size-m .avatar-container,
+people-tile .avatar-size-m .avatar-content {
+ width: 57px !important;
+ height: 57px !important;
+ line-height: 57px !important;
+ margin-top: 10px;
+}
+
+people-tile {
+ grid-template-rows: 29px 23px;
+}
+
+people-tile app-avatar {
+ grid-row: 1 / span 2 !important;
+ align-self: center !important;
+}
+
+.tapi-square-on {
+ background-color: #5cb85c;
+}
+
+.tapi-square-off {
+ background-color: #d9534f;
+}
diff --git a/client/src/availability.ts b/client/src/availability.ts
new file mode 100644
index 0000000..275aadd
--- /dev/null
+++ b/client/src/availability.ts
@@ -0,0 +1,83 @@
+import './availability.css'
+import { AvailabilityInfo } from './availability-info'
+import { AvailabilityService } from './availability-service'
+
+declare function waitForKeyElements(selectorOrFunction: any, callback: any, waitOnce: boolean): any;
+
+export class Availability {
+ private _service: AvailabilityService
+ private _availabilities: AvailabilityInfo[] = []
+
+ constructor(service: AvailabilityService) {
+ this._service = service
+ }
+
+ public start() {
+ this._service.subscribe((avs) => {
+ this._availabilities = avs
+ this.updateAllIndicators()
+ })
+ waitForKeyElements('people-tile', (element: HTMLElement) => { this.decorateTile(element) }, false)
+ }
+
+ private decorateTile(tile: HTMLElement) {
+ if (tile.querySelector('.tapi-availability')) {
+ return
+ }
+ var extEl = tile.querySelector('span[data-id="extPhone"]') as HTMLElement
+ if (!extEl) {
+ return
+ }
+ var extension = extEl.textContent.trim()
+
+ var indicator = document.createElement('div')
+ indicator.classList.add('tapi-availability')
+ indicator.dataset.tapiExtension = extension
+ tile.appendChild(indicator)
+ this.updateIndicator(indicator)
+
+ var avatarContainer = tile.querySelector('app-avatar .avatar-container') as HTMLElement
+ if (avatarContainer) {
+ var square = document.createElement('i')
+ square.classList.add('tapi-avatar-square')
+ square.dataset.tapiExtension = extension
+ avatarContainer.appendChild(square)
+ this.updateSquare(square)
+ }
+ }
+
+ private updateAllIndicators() {
+ var indicators = document.getElementsByClassName('tapi-availability')
+ for (var i of indicators) {
+ this.updateIndicator(i as HTMLElement)
+ }
+ var squares = document.getElementsByClassName('tapi-avatar-square')
+ for (var s of squares) {
+ this.updateSquare(s as HTMLElement)
+ }
+ }
+
+ private updateIndicator(indicator: HTMLElement) {
+ var extension = indicator.dataset.tapiExtension
+ var entry = this._availabilities.find(a => a.extension === extension)
+ if (!entry) {
+ indicator.innerHTML = ''
+ return
+ }
+ var dotClass = entry.loggedIn ? 'tapi-dot-on' : 'tapi-dot-off'
+ var mockedTime = '13.04. 08:30'
+ indicator.innerHTML = '' + mockedTime + ''
+ }
+
+ private updateSquare(square: HTMLElement) {
+ var extension = square.dataset.tapiExtension
+ var entry = this._availabilities.find(a => a.extension === extension)
+ square.classList.remove('tapi-square-on', 'tapi-square-off')
+ if (!entry) {
+ square.style.display = 'none'
+ return
+ }
+ square.style.display = ''
+ square.classList.add(entry.loggedIn ? 'tapi-square-on' : 'tapi-square-off')
+ }
+}
diff --git a/client/src/index.js b/client/src/index.js
index 2bf8e71..7c16033 100644
--- a/client/src/index.js
+++ b/client/src/index.js
@@ -1,5 +1,7 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import * as chrono from 'chrono-node'
+import { Availability } from './availability'
+import { AvailabilityService } from './availability-service'
import { CallHistory } from './call-history'
import { CallNotification } from './call-notification'
import { Presence } from './presence'
@@ -24,8 +26,14 @@ const callHistory = new CallHistory()
// eslint-disable-next-line no-undef
waitForKeyElements('call', (element) => { callHistory.showCallHistory(element) }, false)
-const status = new Status()
+const availabilityService = new AvailabilityService()
+availabilityService.start()
+
+const status = new Status(availabilityService)
// eslint-disable-next-line no-undef
waitForKeyElements('wc-account-menu', (element) => { status.showStatus(element) }, false)
waitForKeyElements('wc-account-menu i.status-indicator', (element) => { status.watchStatus(element) }, false)
+
+const availability = new Availability(availabilityService)
+availability.start()
diff --git a/client/src/status.ts b/client/src/status.ts
index 85c8d00..b44feb8 100644
--- a/client/src/status.ts
+++ b/client/src/status.ts
@@ -1,17 +1,22 @@
-import { Config } from './config';
import './status.css';
-import { ZcStatus } from './zc-status';
-import GM_fetch from "@trim21/gm-fetch";
+import { AvailabilityInfo } from './availability-info';
+import { AvailabilityService } from './availability-service';
const zcIcon = require('./stopwatch-regular-full.svg');
declare function waitForKeyElements(selectorOrFunction: any, callback: any, waitOnce: boolean): any;
export class Status {
+ private _service: AvailabilityService;
private _user: string;
private _enabled = false;
private _statusOn = 'menuAvailable';
private _statusOff = 'menuAway';
private _currentStatus: boolean = undefined;
+ private _subscribed = false;
+
+ constructor(service: AvailabilityService) {
+ this._service = service;
+ }
public async showStatus(element: HTMLElement) {
this._user = await GM.getValue('tapi-zc-user', '');
@@ -20,34 +25,38 @@ export class Status {
this._statusOff = await GM.getValue('tapi-zc-off', 'menuAvailable');
console.log('tapi-zc-user', this._user, 'tapi-zc-enabled', this._enabled, 'tapi-zc-on', this._statusOn, 'tapi-zc-off', this._statusOff);
- this.checkStatus();
+ this.subscribeOnce();
waitForKeyElements("wc-account-menu > div > ul", (element: HTMLElement) => { this.addZcStatusPopup(element) }, true);
}
- private async checkStatus() {
- if (this._enabled) {
- try {
- var response = await GM_fetch(Config.tapi_server_url + '/availability/' + encodeURIComponent(this._user));
- if (response.status == 200) {
- var status = await response.json() as ZcStatus;
- if (this._currentStatus !== status.loggedIn) {
- this._currentStatus = status.loggedIn;
- console.log('New status, loggedIn', this._currentStatus);
- var accMenu = document.getElementsByTagName("wc-account-menu")[0];
- var avatar = accMenu.getElementsByTagName("app-avatar")[0] as HTMLAnchorElement;
- avatar.click();
- setTimeout(() => {
- var statusId = this._currentStatus ? this._statusOn : this._statusOff;
- console.log('Clicking status', statusId);
- (document.getElementById(statusId) as HTMLSpanElement).click();
- }, 1000);
- }
- }
- } catch (error) {
- console.log(error);
- }
- setTimeout(() => this.checkStatus(), 30000);
+ private subscribeOnce() {
+ if (this._subscribed) {
+ return;
+ }
+ this._subscribed = true;
+ this._service.subscribe((avs) => this.onAvailabilities(avs));
+ }
+
+ private onAvailabilities(avs: AvailabilityInfo[]) {
+ if (!this._enabled || !this._user) {
+ return;
+ }
+ var entry = avs.find(a => a.user === this._user);
+ if (!entry) {
+ return;
+ }
+ if (this._currentStatus !== entry.loggedIn) {
+ this._currentStatus = entry.loggedIn;
+ console.log('New status, loggedIn', this._currentStatus);
+ var accMenu = document.getElementsByTagName("wc-account-menu")[0];
+ var avatar = accMenu.getElementsByTagName("app-avatar")[0] as HTMLAnchorElement;
+ avatar.click();
+ setTimeout(() => {
+ var statusId = this._currentStatus ? this._statusOn : this._statusOff;
+ console.log('Clicking status', statusId);
+ (document.getElementById(statusId) as HTMLSpanElement).click();
+ }, 1000);
}
}
@@ -149,7 +158,9 @@ export class Status {
GM.setValue('tapi-zc-enabled', this._enabled);
console.log('tapi-zc-enabled', this._enabled);
this._currentStatus = undefined;
- this.checkStatus();
+ if (this._enabled) {
+ this.onAvailabilities(this._service.availabilities);
+ }
}
var zcOn = document.getElementById('tapi-zc-on') as HTMLSelectElement;
diff --git a/client/src/zc-status.ts b/client/src/zc-status.ts
deleted file mode 100644
index df5316c..0000000
--- a/client/src/zc-status.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export class ZcStatus {
- public user: string;
- public loggedIn: boolean;
-}