superdesk/superdesk-client-core

View on GitHub
scripts/core/notification/notification.ts

Summary

Maintainability
F
3 days
Test Coverage
/**
* 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);