From: Alexander Ebert Date: Sat, 7 Nov 2020 21:58:23 +0000 (+0100) Subject: Convert `Notification/Handler` to TypeScript X-Git-Tag: 5.4.0_Alpha_1~579^2 X-Git-Url: https://git.stricted.de/?a=commitdiff_plain;h=8750a546f279144b3d9ce901fa605da93e886065;p=GitHub%2FWoltLab%2FWCF.git Convert `Notification/Handler` to TypeScript --- diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Notification/Handler.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Notification/Handler.js index 6f8cb92e53..bee657af11 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Notification/Handler.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Notification/Handler.js @@ -3,86 +3,76 @@ * increasing request delay on inactivity. * * @author Alexander Ebert - * @copyright 2001-2019 WoltLab GmbH + * @copyright 2001-2019 WoltLab GmbH * @license GNU Lesser General Public License * @module WoltLabSuite/Core/Notification/Handler */ -define(['Ajax', 'Core', 'EventHandler', 'StringUtil'], function (Ajax, Core, EventHandler, StringUtil) { +define(["require", "exports", "tslib", "../Ajax", "../Core", "../Event/Handler", "../StringUtil"], function (require, exports, tslib_1, Ajax, Core, EventHandler, StringUtil) { "use strict"; - if (!('Promise' in window) || !('Notification' in window)) { - // fake object exposed to ancient browsers (*cough* IE11 *cough*) - return { - setup: function () { } - }; - } - var _allowNotification = false; - var _icon = ''; - var _inactiveSince = 0; - //noinspection JSUnresolvedVariable - var _lastRequestTimestamp = window.TIME_NOW; - var _requestTimer = null; - /** - * @exports WoltLabSuite/Core/Notification/Handler - */ - return { + Object.defineProperty(exports, "__esModule", { value: true }); + exports.setup = void 0; + Ajax = tslib_1.__importStar(Ajax); + Core = tslib_1.__importStar(Core); + EventHandler = tslib_1.__importStar(EventHandler); + StringUtil = tslib_1.__importStar(StringUtil); + class NotificationHandler { /** * Initializes the desktop notification system. - * - * @param {Object} options initialization options */ - setup: function (options) { + constructor(options) { + this.inactiveSince = 0; + this.lastRequestTimestamp = window.TIME_NOW; + this.requestTimer = undefined; options = Core.extend({ enableNotifications: false, - icon: '', + icon: "", }, options); - _icon = options.icon; - this._prepareNextRequest(); - document.addEventListener('visibilitychange', this._onVisibilityChange.bind(this)); - window.addEventListener('storage', this._onStorage.bind(this)); - this._onVisibilityChange(null); + this.icon = options.icon; + this.prepareNextRequest(); + document.addEventListener("visibilitychange", (ev) => this.onVisibilityChange(ev)); + window.addEventListener("storage", () => this.onStorage()); + this.onVisibilityChange(); if (options.enableNotifications) { - switch (window.Notification.permission) { - case 'granted': - _allowNotification = true; - break; - case 'default': - window.Notification.requestPermission(function (result) { - if (result === 'granted') { - _allowNotification = true; - } - }); - break; + void this.enableNotifications(); + } + } + async enableNotifications() { + switch (window.Notification.permission) { + case "granted": + this.allowNotification = true; + break; + case "default": { + const result = await window.Notification.requestPermission(); + if (result === "granted") { + this.allowNotification = true; + } + break; } } - }, + } /** * Detects when this window is hidden or restored. - * - * @param {Event} event - * @protected */ - _onVisibilityChange: function (event) { + onVisibilityChange(event) { // document was hidden before - if (event !== null && !document.hidden) { - var difference = (Date.now() - _inactiveSince) / 60000; + if (event && !document.hidden) { + const difference = (Date.now() - this.inactiveSince) / 60000; if (difference > 4) { - this._resetTimer(); - this._dispatchRequest(); + this.resetTimer(); + this.dispatchRequest(); } } - _inactiveSince = (document.hidden) ? Date.now() : 0; - }, + this.inactiveSince = document.hidden ? Date.now() : 0; + } /** * Returns the delay in minutes before the next request should be dispatched. - * - * @return {int} - * @protected */ - _getNextDelay: function () { - if (_inactiveSince === 0) + getNextDelay() { + if (this.inactiveSince === 0) { return 5; + } // milliseconds -> minutes - var inactiveMinutes = ~~((Date.now() - _inactiveSince) / 60000); + const inactiveMinutes = ~~((Date.now() - this.inactiveSince) / 60000); if (inactiveMinutes < 15) { return 5; } @@ -90,55 +80,49 @@ define(['Ajax', 'Core', 'EventHandler', 'StringUtil'], function (Ajax, Core, Eve return 10; } return 15; - }, + } /** * Resets the request delay timer. - * - * @protected */ - _resetTimer: function () { - if (_requestTimer !== null) { - window.clearTimeout(_requestTimer); - _requestTimer = null; + resetTimer() { + if (this.requestTimer) { + window.clearTimeout(this.requestTimer); + this.requestTimer = undefined; } - }, + } /** * Schedules the next request using a calculated delay. - * - * @protected */ - _prepareNextRequest: function () { - this._resetTimer(); - _requestTimer = window.setTimeout(this._dispatchRequest.bind(this), this._getNextDelay() * 60000); - }, + prepareNextRequest() { + this.resetTimer(); + this.requestTimer = window.setTimeout(this.dispatchRequest.bind(this), this.getNextDelay() * 60000); + } /** * Requests new data from the server. - * - * @protected */ - _dispatchRequest: function () { - var parameters = {}; - EventHandler.fire('com.woltlab.wcf.notification', 'beforePoll', parameters); + dispatchRequest() { + const parameters = {}; + EventHandler.fire("com.woltlab.wcf.notification", "beforePoll", parameters); // this timestamp is used to determine new notifications and to avoid // notifications being displayed multiple times due to different origins // (=subdomains) used, because we cannot synchronize them in the client - parameters.lastRequestTimestamp = _lastRequestTimestamp; + parameters.lastRequestTimestamp = this.lastRequestTimestamp; Ajax.api(this, { - parameters: parameters + parameters: parameters, }); - }, + } /** * Notifies subscribers for updated data received by another tab. - * - * @protected */ - _onStorage: function () { + onStorage() { // abort and re-schedule periodic request - this._prepareNextRequest(); - var pollData, keepAliveData, abort = false; + this.prepareNextRequest(); + let pollData; + let keepAliveData; + let abort = false; try { - pollData = window.localStorage.getItem(Core.getStoragePrefix() + 'notification'); - keepAliveData = window.localStorage.getItem(Core.getStoragePrefix() + 'keepAliveData'); + pollData = window.localStorage.getItem(Core.getStoragePrefix() + "notification"); + keepAliveData = window.localStorage.getItem(Core.getStoragePrefix() + "keepAliveData"); pollData = JSON.parse(pollData); keepAliveData = JSON.parse(keepAliveData); } @@ -146,22 +130,22 @@ define(['Ajax', 'Core', 'EventHandler', 'StringUtil'], function (Ajax, Core, Eve abort = true; } if (!abort) { - EventHandler.fire('com.woltlab.wcf.notification', 'onStorage', { + EventHandler.fire("com.woltlab.wcf.notification", "onStorage", { pollData: pollData, - keepAliveData: keepAliveData + keepAliveData: keepAliveData, }); } - }, - _ajaxSuccess: function (data) { - var abort = false; - var keepAliveData = data.returnValues.keepAliveData; - var pollData = data.returnValues.pollData; + } + _ajaxSuccess(data) { + const keepAliveData = data.returnValues.keepAliveData; + const pollData = data.returnValues.pollData; // forward keep alive data window.WCF.System.PushNotification.executeCallbacks({ returnValues: keepAliveData }); // store response data in local storage + let abort = false; try { - window.localStorage.setItem(Core.getStoragePrefix() + 'notification', JSON.stringify(pollData)); - window.localStorage.setItem(Core.getStoragePrefix() + 'keepAliveData', JSON.stringify(keepAliveData)); + window.localStorage.setItem(Core.getStoragePrefix() + "notification", JSON.stringify(pollData)); + window.localStorage.setItem(Core.getStoragePrefix() + "keepAliveData", JSON.stringify(keepAliveData)); } catch (e) { // storage is unavailable, e.g. in private mode, log error and disable polling @@ -169,47 +153,50 @@ define(['Ajax', 'Core', 'EventHandler', 'StringUtil'], function (Ajax, Core, Eve window.console.log(e); } if (!abort) { - this._prepareNextRequest(); + this.prepareNextRequest(); } - _lastRequestTimestamp = data.returnValues.lastRequestTimestamp; - EventHandler.fire('com.woltlab.wcf.notification', 'afterPoll', pollData); - this._showNotification(pollData); - }, + this.lastRequestTimestamp = data.returnValues.lastRequestTimestamp; + EventHandler.fire("com.woltlab.wcf.notification", "afterPoll", pollData); + this.showNotification(pollData); + } /** * Displays a desktop notification. - * - * @param {Object} pollData - * @protected */ - _showNotification: function (pollData) { - if (!_allowNotification) { + showNotification(pollData) { + if (!this.allowNotification) { return; } - //noinspection JSUnresolvedVariable - if (typeof pollData.notification === 'object' && typeof pollData.notification.message === 'string') { - //noinspection JSUnresolvedVariable - var notification = new window.Notification(pollData.notification.title, { + if (typeof pollData.notification === "object" && typeof pollData.notification.message === "string") { + const notification = new window.Notification(pollData.notification.title, { body: StringUtil.unescapeHTML(pollData.notification.message), - icon: _icon + icon: this.icon, }); - notification.onclick = function () { + notification.onclick = () => { window.focus(); notification.close(); - //noinspection JSUnresolvedVariable - window.location = pollData.notification.link; + window.location.href = pollData.notification.link; }; } - }, - _ajaxSetup: function () { - //noinspection JSUnresolvedVariable + } + _ajaxSetup() { return { data: { - actionName: 'poll', - className: 'wcf\\data\\session\\SessionAction' + actionName: "poll", + className: "wcf\\data\\session\\SessionAction", }, ignoreError: !window.ENABLE_DEBUG_MODE, - silent: !window.ENABLE_DEBUG_MODE + silent: !window.ENABLE_DEBUG_MODE, }; } - }; + } + let notificationHandler; + /** + * Initializes the desktop notification system. + */ + function setup(options) { + if (!notificationHandler) { + notificationHandler = new NotificationHandler(options); + } + } + exports.setup = setup; }); diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Notification/Handler.js b/wcfsetup/install/files/ts/WoltLabSuite/Core/Notification/Handler.js deleted file mode 100644 index 0fbb09a5de..0000000000 --- a/wcfsetup/install/files/ts/WoltLabSuite/Core/Notification/Handler.js +++ /dev/null @@ -1,249 +0,0 @@ -/** - * Provides desktop notifications via periodic polling with an - * increasing request delay on inactivity. - * - * @author Alexander Ebert - * @copyright 2001-2019 WoltLab GmbH - * @license GNU Lesser General Public License - * @module WoltLabSuite/Core/Notification/Handler - */ -define(['Ajax', 'Core', 'EventHandler', 'StringUtil'], function(Ajax, Core, EventHandler, StringUtil) { - "use strict"; - - if (!('Promise' in window) || !('Notification' in window)) { - // fake object exposed to ancient browsers (*cough* IE11 *cough*) - return { - setup: function () {} - } - } - - var _allowNotification = false; - var _icon = ''; - var _inactiveSince = 0; - //noinspection JSUnresolvedVariable - var _lastRequestTimestamp = window.TIME_NOW; - var _requestTimer = null; - - /** - * @exports WoltLabSuite/Core/Notification/Handler - */ - return { - /** - * Initializes the desktop notification system. - * - * @param {Object} options initialization options - */ - setup: function (options) { - options = Core.extend({ - enableNotifications: false, - icon: '', - }, options); - - _icon = options.icon; - - this._prepareNextRequest(); - - document.addEventListener('visibilitychange', this._onVisibilityChange.bind(this)); - window.addEventListener('storage', this._onStorage.bind(this)); - - this._onVisibilityChange(null); - - if (options.enableNotifications) { - switch (window.Notification.permission) { - case 'granted': - _allowNotification = true; - break; - case 'default': - window.Notification.requestPermission(function (result) { - if (result === 'granted') { - _allowNotification = true; - } - }); - break; - } - } - }, - - /** - * Detects when this window is hidden or restored. - * - * @param {Event} event - * @protected - */ - _onVisibilityChange: function(event) { - // document was hidden before - if (event !== null && !document.hidden) { - var difference = (Date.now() - _inactiveSince) / 60000; - if (difference > 4) { - this._resetTimer(); - this._dispatchRequest(); - } - } - - _inactiveSince = (document.hidden) ? Date.now() : 0; - }, - - /** - * Returns the delay in minutes before the next request should be dispatched. - * - * @return {int} - * @protected - */ - _getNextDelay: function() { - if (_inactiveSince === 0) return 5; - - // milliseconds -> minutes - var inactiveMinutes = ~~((Date.now() - _inactiveSince) / 60000); - if (inactiveMinutes < 15) { - return 5; - } - else if (inactiveMinutes < 30) { - return 10; - } - - return 15; - }, - - /** - * Resets the request delay timer. - * - * @protected - */ - _resetTimer: function() { - if (_requestTimer !== null) { - window.clearTimeout(_requestTimer); - _requestTimer = null; - } - }, - - /** - * Schedules the next request using a calculated delay. - * - * @protected - */ - _prepareNextRequest: function() { - this._resetTimer(); - - _requestTimer = window.setTimeout(this._dispatchRequest.bind(this), this._getNextDelay() * 60000); - }, - - /** - * Requests new data from the server. - * - * @protected - */ - _dispatchRequest: function() { - var parameters = {}; - EventHandler.fire('com.woltlab.wcf.notification', 'beforePoll', parameters); - - // this timestamp is used to determine new notifications and to avoid - // notifications being displayed multiple times due to different origins - // (=subdomains) used, because we cannot synchronize them in the client - parameters.lastRequestTimestamp = _lastRequestTimestamp; - - Ajax.api(this, { - parameters: parameters - }); - }, - - /** - * Notifies subscribers for updated data received by another tab. - * - * @protected - */ - _onStorage: function() { - // abort and re-schedule periodic request - this._prepareNextRequest(); - - var pollData, keepAliveData, abort = false; - try { - pollData = window.localStorage.getItem(Core.getStoragePrefix() + 'notification'); - keepAliveData = window.localStorage.getItem(Core.getStoragePrefix() + 'keepAliveData'); - - pollData = JSON.parse(pollData); - keepAliveData = JSON.parse(keepAliveData); - } - catch (e) { - abort = true; - } - - if (!abort) { - EventHandler.fire('com.woltlab.wcf.notification', 'onStorage', { - pollData: pollData, - keepAliveData: keepAliveData - }); - } - }, - - _ajaxSuccess: function(data) { - var abort = false; - var keepAliveData = data.returnValues.keepAliveData; - var pollData = data.returnValues.pollData; - - // forward keep alive data - window.WCF.System.PushNotification.executeCallbacks({returnValues: keepAliveData}); - - // store response data in local storage - try { - window.localStorage.setItem(Core.getStoragePrefix() + 'notification', JSON.stringify(pollData)); - window.localStorage.setItem(Core.getStoragePrefix() + 'keepAliveData', JSON.stringify(keepAliveData)); - } - catch (e) { - // storage is unavailable, e.g. in private mode, log error and disable polling - abort = true; - - window.console.log(e); - } - - if (!abort) { - this._prepareNextRequest(); - } - - _lastRequestTimestamp = data.returnValues.lastRequestTimestamp; - - EventHandler.fire('com.woltlab.wcf.notification', 'afterPoll', pollData); - - this._showNotification(pollData); - }, - - /** - * Displays a desktop notification. - * - * @param {Object} pollData - * @protected - */ - _showNotification: function(pollData) { - if (!_allowNotification) { - return; - } - - //noinspection JSUnresolvedVariable - if (typeof pollData.notification === 'object' && typeof pollData.notification.message === 'string') { - //noinspection JSUnresolvedVariable - var notification = new window.Notification(pollData.notification.title, { - body: StringUtil.unescapeHTML(pollData.notification.message), - icon: _icon - }); - notification.onclick = function () { - window.focus(); - notification.close(); - - //noinspection JSUnresolvedVariable - window.location = pollData.notification.link; - }; - } - }, - - _ajaxSetup: function() { - //noinspection JSUnresolvedVariable - return { - data: { - actionName: 'poll', - className: 'wcf\\data\\session\\SessionAction' - }, - ignoreError: !window.ENABLE_DEBUG_MODE, - silent: !window.ENABLE_DEBUG_MODE - }; - } - } -}); diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Notification/Handler.ts b/wcfsetup/install/files/ts/WoltLabSuite/Core/Notification/Handler.ts new file mode 100644 index 0000000000..2652304b94 --- /dev/null +++ b/wcfsetup/install/files/ts/WoltLabSuite/Core/Notification/Handler.ts @@ -0,0 +1,260 @@ +/** + * Provides desktop notifications via periodic polling with an + * increasing request delay on inactivity. + * + * @author Alexander Ebert + * @copyright 2001-2019 WoltLab GmbH + * @license GNU Lesser General Public License + * @module WoltLabSuite/Core/Notification/Handler + */ + +import * as Ajax from "../Ajax"; +import { AjaxCallbackSetup } from "../Ajax/Data"; +import * as Core from "../Core"; +import * as EventHandler from "../Event/Handler"; +import * as StringUtil from "../StringUtil"; + +interface NotificationHandlerOptions { + enableNotifications: boolean; + icon: string; +} + +interface PollingResult { + notification: { + link: string; + message?: string; + title: string; + }; +} + +interface AjaxResponse { + returnValues: { + keepAliveData: unknown; + lastRequestTimestamp: number; + pollData: PollingResult; + }; +} + +class NotificationHandler { + private allowNotification: boolean; + private readonly icon: string; + private inactiveSince = 0; + private lastRequestTimestamp = window.TIME_NOW; + private requestTimer?: number = undefined; + + /** + * Initializes the desktop notification system. + */ + constructor(options: NotificationHandlerOptions) { + options = Core.extend( + { + enableNotifications: false, + icon: "", + }, + options, + ) as NotificationHandlerOptions; + + this.icon = options.icon; + + this.prepareNextRequest(); + + document.addEventListener("visibilitychange", (ev) => this.onVisibilityChange(ev)); + window.addEventListener("storage", () => this.onStorage()); + + this.onVisibilityChange(); + + if (options.enableNotifications) { + void this.enableNotifications(); + } + } + + private async enableNotifications(): Promise { + switch (window.Notification.permission) { + case "granted": + this.allowNotification = true; + break; + + case "default": { + const result = await window.Notification.requestPermission(); + if (result === "granted") { + this.allowNotification = true; + } + break; + } + } + } + + /** + * Detects when this window is hidden or restored. + */ + private onVisibilityChange(event?: Event) { + // document was hidden before + if (event && !document.hidden) { + const difference = (Date.now() - this.inactiveSince) / 60_000; + if (difference > 4) { + this.resetTimer(); + this.dispatchRequest(); + } + } + + this.inactiveSince = document.hidden ? Date.now() : 0; + } + + /** + * Returns the delay in minutes before the next request should be dispatched. + */ + private getNextDelay(): number { + if (this.inactiveSince === 0) { + return 5; + } + + // milliseconds -> minutes + const inactiveMinutes = ~~((Date.now() - this.inactiveSince) / 60_000); + if (inactiveMinutes < 15) { + return 5; + } else if (inactiveMinutes < 30) { + return 10; + } + + return 15; + } + + /** + * Resets the request delay timer. + */ + private resetTimer(): void { + if (this.requestTimer) { + window.clearTimeout(this.requestTimer); + this.requestTimer = undefined; + } + } + + /** + * Schedules the next request using a calculated delay. + */ + private prepareNextRequest(): void { + this.resetTimer(); + + this.requestTimer = window.setTimeout(this.dispatchRequest.bind(this), this.getNextDelay() * 60_000); + } + + /** + * Requests new data from the server. + */ + private dispatchRequest(): void { + const parameters: ArbitraryObject = {}; + + EventHandler.fire("com.woltlab.wcf.notification", "beforePoll", parameters); + + // this timestamp is used to determine new notifications and to avoid + // notifications being displayed multiple times due to different origins + // (=subdomains) used, because we cannot synchronize them in the client + parameters.lastRequestTimestamp = this.lastRequestTimestamp; + + Ajax.api(this, { + parameters: parameters, + }); + } + + /** + * Notifies subscribers for updated data received by another tab. + */ + private onStorage(): void { + // abort and re-schedule periodic request + this.prepareNextRequest(); + + let pollData; + let keepAliveData; + let abort = false; + try { + pollData = window.localStorage.getItem(Core.getStoragePrefix() + "notification"); + keepAliveData = window.localStorage.getItem(Core.getStoragePrefix() + "keepAliveData"); + + pollData = JSON.parse(pollData); + keepAliveData = JSON.parse(keepAliveData); + } catch (e) { + abort = true; + } + + if (!abort) { + EventHandler.fire("com.woltlab.wcf.notification", "onStorage", { + pollData: pollData, + keepAliveData: keepAliveData, + }); + } + } + + _ajaxSuccess(data: AjaxResponse): void { + const keepAliveData = data.returnValues.keepAliveData; + const pollData = data.returnValues.pollData; + + // forward keep alive data + window.WCF.System.PushNotification.executeCallbacks({ returnValues: keepAliveData }); + + // store response data in local storage + let abort = false; + try { + window.localStorage.setItem(Core.getStoragePrefix() + "notification", JSON.stringify(pollData)); + window.localStorage.setItem(Core.getStoragePrefix() + "keepAliveData", JSON.stringify(keepAliveData)); + } catch (e) { + // storage is unavailable, e.g. in private mode, log error and disable polling + abort = true; + + window.console.log(e); + } + + if (!abort) { + this.prepareNextRequest(); + } + + this.lastRequestTimestamp = data.returnValues.lastRequestTimestamp; + + EventHandler.fire("com.woltlab.wcf.notification", "afterPoll", pollData); + + this.showNotification(pollData); + } + + /** + * Displays a desktop notification. + */ + private showNotification(pollData: PollingResult): void { + if (!this.allowNotification) { + return; + } + + if (typeof pollData.notification === "object" && typeof pollData.notification.message === "string") { + const notification = new window.Notification(pollData.notification.title, { + body: StringUtil.unescapeHTML(pollData.notification.message), + icon: this.icon, + }); + notification.onclick = () => { + window.focus(); + notification.close(); + + window.location.href = pollData.notification.link; + }; + } + } + + _ajaxSetup(): ReturnType { + return { + data: { + actionName: "poll", + className: "wcf\\data\\session\\SessionAction", + }, + ignoreError: !window.ENABLE_DEBUG_MODE, + silent: !window.ENABLE_DEBUG_MODE, + }; + } +} + +let notificationHandler: NotificationHandler; + +/** + * Initializes the desktop notification system. + */ +export function setup(options: NotificationHandlerOptions): void { + if (!notificationHandler) { + notificationHandler = new NotificationHandler(options); + } +}