Convert `Notification/Handler` to TypeScript
authorAlexander Ebert <ebert@woltlab.com>
Sat, 7 Nov 2020 21:58:23 +0000 (22:58 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Sat, 7 Nov 2020 21:58:23 +0000 (22:58 +0100)
wcfsetup/install/files/js/WoltLabSuite/Core/Notification/Handler.js
wcfsetup/install/files/ts/WoltLabSuite/Core/Notification/Handler.js [deleted file]
wcfsetup/install/files/ts/WoltLabSuite/Core/Notification/Handler.ts [new file with mode: 0644]

index 6f8cb92e53ead6ab2b63abbaedc269677d142291..bee657af116a7eaf8869fd8a2afbb35b68ccd29c 100644 (file)
@@ -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 <http://opensource.org/licenses/lgpl-license.php>
  * @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 (file)
index 0fbb09a..0000000
+++ /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 <http://opensource.org/licenses/lgpl-license.php>
- * @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 (file)
index 0000000..2652304
--- /dev/null
@@ -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 <http://opensource.org/licenses/lgpl-license.php>
+ * @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<void> {
+    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<AjaxCallbackSetup> {
+    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);
+  }
+}