scripts/core/notification/notification.ts
/**
* This file is part of Superdesk.
*
* Copyright 2015 Sourcefabric z.u. and contributors.
*
* For the full copyright and license information, please see the
* AUTHORS and LICENSE files distributed with this source code, or
* at https://www.sourcefabric.org/superdesk/license
*/
import _ from 'lodash';
import {gettext} from 'core/utils';
import {IPublicWebsocketMessages, IWebsocketMessage, IArticle, IUser} from 'superdesk-api';
import {appConfig} from 'appConfig';
// implementing interface to be able to get keys at runtime
const publicWebsocketMessageNames: IPublicWebsocketMessages = {
'content:update': undefined,
'resource:created': undefined,
'resource:updated': undefined,
'resource:deleted': undefined,
};
// implementing interface to be able to get keys at runtime
const internalWebsocketMessageNames: IInternalWebsocketMessages = {
'item:lock': undefined,
'item:spike': undefined,
'item:unspike': undefined,
'item:highlights': undefined,
'item:publish': undefined,
};
export const getWebsocketMessageEventName = (
eventName: string,
extensionName?: string,
) => 'websocket-event--' + eventName + (extensionName == null ? '' : '--' + extensionName);
export const getInternalWebsocketMessageEventName = (eventName: string) =>
'websocket-event-internal--' + eventName;
// can also be private, meaning it could only be accessed in extension the event is addressed to.
export function isWebsocketEventPublic(eventName: string) {
return Object.keys(publicWebsocketMessageNames).includes(eventName);
}
export function isWebsocketEventInternal(eventName: string) {
return Object.keys(internalWebsocketMessageNames).includes(eventName);
}
interface IInternalWebsocketMessages { // not exposed to client API
'item:lock': IWebsocketMessage<{
item: IArticle['_id'];
item_version: string;
lock_session: string;
lock_time: string;
user: IUser['_id'];
_etag: string;
}>;
'item:spike': IWebsocketMessage<never>;
'item:unspike': IWebsocketMessage<never>;
'item:highlights': IWebsocketMessage<{item_id?: string; mark_id?: string; marked: number}>;
'item:publish': IWebsocketMessage<any>;
}
export function addWebsocketEventListener<T extends keyof IPublicWebsocketMessages>(
event: T,
handler: (message: IPublicWebsocketMessages[T]) => void,
): () => void {
const eventName = getWebsocketMessageEventName(event);
const _handler = (e: CustomEvent) => handler(e.detail);
window.addEventListener(eventName, _handler);
return () => window.removeEventListener(eventName, _handler);
}
export function addInternalWebsocketEventListener<T extends keyof IInternalWebsocketMessages>(
event: T,
handler: (message: IInternalWebsocketMessages[T]) => void,
): () => void {
const eventName = getInternalWebsocketMessageEventName(event);
const _handler = (e: CustomEvent) => handler(e.detail);
window.addEventListener(eventName, _handler);
return () => window.removeEventListener(eventName, _handler);
}
WebSocketProxy.$inject = ['$rootScope', '$interval', 'session', 'SESSION_EVENTS'];
function WebSocketProxy($rootScope, $interval, session, SESSION_EVENTS) {
var ws = null;
var connectTimer = -1;
var TIMEOUT = 5000;
var ReloadEvents = [
'user_disabled',
'user_inactivated',
'user_role_changed',
'user_type_changed',
'user_privileges_revoked',
'role_privileges_revoked',
'desk_membership_revoked',
'desk',
'stage',
'stage_visibility_updated',
];
var readyState = {
CONNECTING: 0,
OPEN: 1,
CLOSING: 2,
CLOSED: 3,
};
if (!appConfig.server.ws) {
return;
}
var connect = function() {
if (!ws || ws.readyState === readyState.CLOSED) {
ws = new WebSocket(appConfig.server.ws);
bindEvents();
}
};
var disconnect = function() {
if (ws) {
ws.close();
ws = null;
}
};
var bindEvents = function() {
ws.onmessage = function(event) {
var msg = angular.fromJson(event.data);
// Delay all websocket events to avoid getting old data.
// The server is sending websocket events before it is able to return updated data.
setTimeout(() => {
const addressedForExtension = typeof msg.extra === 'object' && typeof msg.extra.extension === 'string';
if (addressedForExtension || isWebsocketEventPublic(msg.event)) {
window.dispatchEvent(
new CustomEvent(
getWebsocketMessageEventName(
msg.event,
isWebsocketEventPublic(msg.event) ? undefined : msg.extra.extension,
),
{detail: msg},
),
);
}
if (isWebsocketEventInternal(msg.event)) {
window.dispatchEvent(
new CustomEvent(
getInternalWebsocketMessageEventName(msg.event),
{detail: msg},
),
);
}
$rootScope.$broadcast(msg.event, msg.extra);
if (_.includes(ReloadEvents, msg.event)) {
$rootScope.$broadcast('reload', msg);
}
}, 100);
};
ws.onerror = function(event) {
console.error(event);
};
ws.onopen = function(event) {
$interval.cancel(connectTimer);
$rootScope.$broadcast('connected');
};
ws.onclose = function(event) {
$rootScope.$broadcast('disconnected');
$interval.cancel(connectTimer);
connectTimer = $interval(() => {
if (ws && session.sessionId) {
connect(); // Retry to connect for every TIMEOUT interval.
}
}, TIMEOUT, 0, false); // passed invokeApply = false to prevent triggering digest cycle
};
};
connect();
$rootScope.$on(SESSION_EVENTS.LOGOUT, disconnect);
$rootScope.$on(SESSION_EVENTS.LOGIN, connect);
}
/**
* Service for notifying user when websocket connection disconnected or connected.
*/
NotifyConnectionService.$inject = ['$rootScope', 'notify', '$timeout', 'session'];
function NotifyConnectionService($rootScope, notify, $timeout, session) {
var self = this;
self.message = null;
$rootScope.$on('disconnected', (event) => {
self.message = gettext('Disconnected from Notification Server!');
$rootScope.$applyAsync(() => {
notify.warning(self.message);
});
});
$rootScope.$on('connected', (event) => {
self.message = gettext('Connected to Notification Server!');
$rootScope.$applyAsync(() => {
notify.pop(); // removes disconnection warning, once connected.
notify.success(self.message);
});
});
$rootScope.$on('vocabularies:updated', (event, data) => {
if (!data || (data.user && data.user !== session.identity._id)) {
self.message = data.vocabulary + gettext(
' vocabulary has been updated. Please re-login to see updated vocabulary values');
$timeout(() => {
notify.error(self.message);
}, 100);
}
});
}
ReloadService.$inject = ['$window', '$rootScope', 'session', 'desks', 'superdeskFlags'];
function ReloadService($window, $rootScope, session, desks, superdeskFlags) {
var self = this;
self.userDesks = [];
self.result = null;
self.activeDesk = null;
desks.fetchCurrentUserDesks().then((deskList) => {
self.userDesks = deskList;
self.activeDesk = desks.active.desk;
});
var userEvents = {
user_disabled: 'User is disabled',
user_inactivated: 'User is inactivated',
user_role_changed: 'User role is changed',
user_type_changed: 'User type is changed',
user_privileges_revoked: 'User privileges are revoked',
};
var roleEvents = {
role_privileges_revoked: 'Role privileges are revoked',
};
var deskEvents = {
desk_membership_revoked: 'User removed from desk',
desk: 'Desk is deleted/updated',
};
var stageEvents = {
stage: 'Stage is created/updated/deleted',
stage_visibility_updated: 'Stage visibility change',
};
$rootScope.$on('reload', (event, msg) => {
self.result = self.reloadIdentifier(msg);
self.reload(self.result);
});
this.reload = function(result) {
if (result.reload) {
if (superdeskFlags.flags.authoring) {
self.broadcast(gettext(result.message));
} else {
this.forceReload();
}
}
};
this.forceReload = function() {
return $window.location.reload(true);
};
this.broadcast = function(msg) {
$rootScope.$broadcast('savework', msg);
};
this.reloadIdentifier = function(msg) {
var result = {
reload: false,
message: null,
};
if (_.has(userEvents, msg.event) && !_.isNil(msg.extra.user_id) &&
msg.extra.user_id.indexOf(session.identity._id) !== -1) {
result.message = userEvents[msg.event];
result.reload = true;
}
if (_.has(roleEvents, msg.event) &&
msg.extra.role_id.indexOf(session.identity.role) !== -1) {
result.message = roleEvents[msg.event];
result.reload = true;
}
if (_.has(deskEvents, msg.event) &&
!_.isNil(msg.extra.desk_id) && !_.isNil(msg.extra.user_ids) &&
!_.isNil(_.find(self.userDesks, {_id: msg.extra.desk_id})) &&
msg.extra.user_ids.indexOf(session.identity._id) !== -1) {
result.message = deskEvents[msg.event];
result.reload = true;
}
if (_.has(stageEvents, msg.event) && !_.isNil(msg.extra.desk_id)) {
if (msg.event === 'stage_visibility_updated') {
if (_.isNil(_.find(self.userDesks, {_id: msg.extra.desk_id})) &&
!_.isNil($window.location.hash.match('/search'))
|| !_.isNil($window.location.hash.match('/authoring/'))) {
result.message = stageEvents[msg.event];
result.reload = true;
}
} else if (msg.event === 'stage') {
if (!_.isNil(_.find(self.userDesks, {_id: msg.extra.desk_id}))
&& self.activeDesk === msg.extra.desk_id) {
result.message = stageEvents[msg.event];
result.reload = true;
}
}
}
return result;
};
}
/**
* @ngdoc module
* @module superdesk.core.notification
* @name superdesk.core.notification
* @packageName superdesk.core
* @description The notification package holds various types of notifications.
*/
export default angular.module('superdesk.apps.notification', ['superdesk.apps.desks', 'superdesk.core.menu'])
.service('reloadService', ReloadService)
.service('notifyConnectionService', NotifyConnectionService)
.run(WebSocketProxy);