var TIME_NOW = {@TIME_NOW};
var LAST_UPDATE_TIME = {@LAST_UPDATE_TIME};
var URL_LEGACY_MODE = false;
+ var ENABLE_DEBUG_MODE = {if ENABLE_DEBUG_MODE}true{else}false{/if};
{if ENABLE_DEBUG_MODE}
{* This constant is a compiler option, it does not exist in production. *}
{if $__sessionKeepAlive|isset}
new WCF.System.KeepAlive({@$__sessionKeepAlive});
+
+ require(['WoltLabSuite/Core/Notification/Handler'], function(NotificationHandler) {
+ NotificationHandler.setup({
+ icon: '{@$__wcf->getPath()}images/apple-touch-icon.png',
+ sessionKeepAlive: {@$__sessionKeepAlive}
+ });
+ });
{/if}
});
</script>
var TIME_NOW = {@TIME_NOW};
var LAST_UPDATE_TIME = {@LAST_UPDATE_TIME};
var URL_LEGACY_MODE = false;
+ var ENABLE_DEBUG_MODE = {if ENABLE_DEBUG_MODE}true{else}false{/if};
{if ENABLE_DEBUG_MODE}
{* This constant is a compiler option, it does not exist in production. *}
var newObj = {};
for (var key in obj) {
- if (objOwns(obj, key) && typeof obj[key] !== 'undefined') {
+ if (obj.hasOwnProperty(key) && typeof obj[key] !== 'undefined') {
newObj[key] = _clone(obj[key]);
}
}
return newObj;
};
+ //noinspection JSUnresolvedVariable
+ var _prefix = 'wsc' + window.WCF_PATH.hashCode() + '-';
+
/**
* @exports WoltLabSuite/Core/Core
*/
*
* @param {object} obj target object
* @param {string=} prefix parameter prefix
- * @return encoded parameter string
+ * @return {string} encoded parameter string
*/
serialize: function(obj, prefix) {
var parameters = [];
}
element.dispatchEvent(event);
+ },
+
+ /**
+ * Returns the unique prefix for the localStorage.
+ *
+ * @return {string} prefix for the localStorage
+ */
+ getStoragePrefix: function() {
+ return _prefix;
}
};
--- /dev/null
+define(['Ajax', 'Core', 'EventHandler'], function(Ajax, Core, EventHandler) {
+ "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;
+ var _lastRequestTimestamp = window.TIME_NOW;
+ var _requestTimer = null;
+ var _sessionKeepAlive = 0;
+
+ return {
+ setup: function (options) {
+ options = Core.extend({
+ icon: '',
+ sessionKeepAlive: 0
+ }, options);
+
+ _icon = options.icon;
+ _sessionKeepAlive = options.sessionKeepAlive * 60;
+
+ console.log("DEBUG ONLY");
+ var x = this._dispatchRequest.bind(this);
+ //this._prepareNextRequest();
+
+ document.addEventListener('visibilitychange', this._onVisibilityChange.bind(this));
+ window.addEventListener('storage', this._onStorage.bind(this));
+
+ this._onVisibilityChange();
+
+ Notification.requestPermission().then(function (result) {
+ if (result === 'granted') {
+ _allowNotification = true;
+ console.log("DEBUG ONLY");
+ x();
+ }
+ });
+ },
+
+ _onVisibilityChange: function() {
+ _inactiveSince = (document.hidden) ? Date.now() : 0;
+ },
+
+ _getNextDelay: function() {
+ if (_inactiveSince === 0) return 5;
+
+ // milliseconds -> minutes
+ var inactiveMins = ~~((Date.now() - _inactiveSince) / 60000);
+ if (inactiveMins < 15) {
+ return 5;
+ }
+ else if (inactiveMins < 30) {
+ return 10;
+ }
+
+ return 15;
+ },
+
+ _prepareNextRequest: function() {
+ var delay = Math.min(this._getNextDelay(), _sessionKeepAlive);
+
+ _requestTimer = window.setTimeout(this._dispatchRequest.bind(this), delay * 60000);
+ },
+
+ _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
+ });
+ },
+
+ _onStorage: function() {
+ window.clearTimeout(_requestTimer);
+ this._prepareNextRequest();
+
+ // TODO: update counters and stuff, this is not the requesting tab!
+ },
+
+ _ajaxSuccess: function(data) {
+ // forward keep alive data
+ window.WCF.System.PushNotification.executeCallbacks(data.returnValues.keepAliveData);
+
+ var abort = false;
+ var pollData = data.returnValues.pollData;
+
+ // store response data in session storage
+ try {
+ window.localStorage.setItem(Core.getStoragePrefix() + 'notification', JSON.stringify(pollData));
+ }
+ 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);
+ },
+
+ _showNotification: function(pollData) {
+ if (!_allowNotification) {
+ return;
+ }
+
+ if (typeof pollData.notification === 'object' && typeof pollData.notification.message === 'string') {
+ new Notification(pollData.notification.title, {
+ body: pollData.notification.message,
+ icon: _icon
+ })
+ }
+ },
+
+ _ajaxSetup: function() {
+ return {
+ data: {
+ actionName: 'poll',
+ className: 'wcf\\data\\session\\SessionAction'
+ },
+ ignoreError: !window.ENABLE_DEBUG_MODE,
+ silent: !window.ENABLE_DEBUG_MODE
+ };
+ }
+ }
+});
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @module WoltLabSuite/Core/Ui/Redactor/Autosave
*/
-define(['EventHandler', 'Language', 'Dom/Traverse', './Metacode'], function(EventHandler, Language, DomTraverse, UiRedactorMetacode) {
+define(['Core', 'EventHandler', 'Language', 'Dom/Traverse', './Metacode'], function(Core, EventHandler, Language, DomTraverse, UiRedactorMetacode) {
"use strict";
if (!COMPILER_TARGET_DEFAULT) {
// time between save requests in seconds
var _frequency = 15;
- //noinspection JSUnresolvedVariable
- var _prefix = 'wsc' + window.WCF_PATH.hashCode() + '-';
-
/**
* @param {Element} element textarea element
* @constructor
this._container = null;
this._editor = null;
this._element = element;
- this._key = _prefix + elData(this._element, 'autosave');
+ this._key = Core.getStoragePrefix() + elData(this._element, 'autosave');
this._lastMessage = '';
this._originalMessage = '';
this._overlay = null;
key = window.localStorage.key(i);
// check if key matches our prefix
- if (key.indexOf(_prefix) !== 0) {
+ if (key.indexOf(Core.getStoragePrefix()) !== 0) {
continue;
}
use wcf\system\event\EventHandler;
use wcf\system\session\SessionHandler;
use wcf\system\user\notification\UserNotificationHandler;
+use wcf\system\user\storage\UserStorageHandler;
use wcf\system\WCF;
/**
public function keepAlive() {
// ignore sessions created by this request
if (WCF::getSession()->lastActivityTime == TIME_NOW) {
- return [];
+ // TODO: DEBUG ONLY
+ //return [];
}
// update last activity time
return $this->keepAliveData;
}
+
+ /**
+ * Validates parameters to poll notification data.
+ */
+ public function validatePoll() {
+ $this->readInteger('lastRequestTimestamp');
+ }
+
+ /**
+ * Polls notification data, including values provided by `keepAlive()`.
+ *
+ * @return array[]
+ */
+ public function poll() {
+ $pollData = [];
+
+ // trigger session keep alive
+ $keepAliveData = (new SessionAction([], 'keepAlive'))->executeAction()['returnValues'];
+
+ // get notifications
+ if (!empty($keepAliveData['userNotificationCount'])) {
+ // We can synchronize notification polling between tabs of the same domain, but
+ // this doesn't work for different origins, that is different sub-domains that
+ // belong to the same instance.
+ //
+ // Storing the time of the last request on the server has the benefit of avoiding
+ // the same notification being presented to the client by different tabs.
+ $lastRequestTime = UserStorageHandler::getInstance()->getField('__notification_lastRequestTime');
+ if ($lastRequestTime === null || $lastRequestTime < $this->parameters['lastRequestTimestamp']) {
+ $lastRequestTime = $this->parameters['lastRequestTimestamp'];
+ }
+
+ $pollData['notification'] = UserNotificationHandler::getInstance()->getLatestNotification($lastRequestTime);
+
+ if (!empty($pollData['notification'])) {
+ UserStorageHandler::getInstance()->update(WCF::getUser()->userID, '__notification_lastRequestTime', TIME_NOW);
+ }
+ }
+
+ // notify 3rd party components
+ EventHandler::getInstance()->fireAction($this, 'poll', $pollData);
+
+ return [
+ 'keepAliveData' => $keepAliveData,
+ 'lastRequestTimestamp' => TIME_NOW,
+ 'pollData' => $pollData
+ ];
+ }
}
if ($row === false) return false;
return $row['mailNotificationType'];
}
+
+ /**
+ * Returns the title and text-only message body for the latest notification,
+ * that is both unread and newer than `$lastRequestTimestamp`. May return an
+ * empty array if there is no new notification.
+ *
+ * @param integer $lastRequestTimestamp
+ * @return string[]
+ */
+ public function getLatestNotification($lastRequestTimestamp) {
+ $notifications = $this->fetchNotifications(1, 0, 0);
+ if (!empty($notifications) && reset($notifications)->time > $lastRequestTimestamp) {
+ $notifications = $this->processNotifications($notifications);
+
+ if (isset($notifications['notifications'][0])) {
+ /** @var IUserNotificationEvent $event */
+ $event = $notifications['notifications'][0]['event'];
+
+ return [
+ 'title' => strip_tags($event->getTitle()),
+ 'message' => strip_tags($event->getMessage())
+ ];
+ }
+ }
+
+ return [];
+ }
}