Skip to content

Commit

Permalink
Refactor all notifications from extensions and UI toggling such that … (
Browse files Browse the repository at this point in the history
  • Loading branch information
thecalcc authored Aug 14, 2024
1 parent bcfe756 commit fffbc6c
Show file tree
Hide file tree
Showing 10 changed files with 98 additions and 102 deletions.
2 changes: 1 addition & 1 deletion scripts/apps/search/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ angular.module('superdesk.apps.search', [
'sdEmailNotificationsList',
reactToAngular1(
EmailNotificationPreferences,
['toggleEmailNotification', 'preferences'],
['toggleEmailNotification', 'preferences', 'notificationLabels'],
),
)

Expand Down
34 changes: 23 additions & 11 deletions scripts/apps/users/components/EmailNotificationPreferences.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,37 @@
import React from 'react';
import {CheckGroup, Checkbox} from 'superdesk-ui-framework/react';
import {IUser} from 'superdesk-api';
import {gettext} from 'core/utils';

interface IProps {
toggleEmailNotification: (notificationId: string) => void;
preferences?: {[key: string]: any};
preferences: {
notifications: IUser['user_preferences']['notifications'];
};
notificationLabels: Dictionary<string, string>;
}

export class EmailNotificationPreferences extends React.PureComponent<IProps> {
render(): React.ReactNode {
return (
<CheckGroup orientation="vertical">
{Object.entries(this.props.preferences ?? []).map(([key, value]) => (
<Checkbox
key={key}
label={{text: value.label}}
onChange={() => {
this.props.toggleEmailNotification(key);
}}
checked={value?.enabled ?? value?.default ?? false}
/>
))}
{Object.entries(this.props.preferences.notifications)
.map(([notificationId, notificationSettings]) => {
return (
<Checkbox
key={notificationId}
label={{
text: gettext(
'Send {{name}} notifications',
{name: this.props.notificationLabels[notificationId]},
)}}
onChange={() => {
this.props.toggleEmailNotification(notificationId);
}}
checked={notificationSettings.email}
/>
);
})}
</CheckGroup>
);
}
Expand Down
90 changes: 46 additions & 44 deletions scripts/apps/users/directives/UserPreferencesDirective.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {gettext} from 'core/utils';
import {appConfig, extensions, getUserInterfaceLanguage} from 'appConfig';
import {applyDefault} from 'core/helpers/typescript-helpers';
import {DEFAULT_EDITOR_THEME} from 'apps/authoring/authoring/services/AuthoringThemesService';
import {cloneDeep, pick} from 'lodash';
import {IExtensionActivationResult} from 'superdesk-api';

/**
* @ngdoc directive
Expand Down Expand Up @@ -43,73 +45,56 @@ export function UserPreferencesDirective(
link: function(scope, element, attrs) {
const userLang = getUserInterfaceLanguage().replace('_', '-');
const body = angular.element('body');
const NOTIFICATIONS_KEY = 'notifications';

scope.activeNavigation = null;

scope.activeTheme = localStorage.getItem('theme');
const registeredNotifications: IExtensionActivationResult['contributions']['notifications'] = (() => {
const result = {};

for (const extension of Object.values(extensions)) {
for (const [notificationId, notification] of Object.entries(extension.activationResult.contributions?.notifications ?? [])) {
result[notificationId] = notification;
}
}

return result;
})();

/*
* Set this to true after adding all the preferences to the scope. If done before, then the
* directives which depend on scope variables might fail to load properly.
*/

scope.preferencesLoaded = false;
var orig: {[key: string]: any}; // original preferences, before any changes

scope.emailNotificationsFromExtensions = {};

scope.buildNotificationsFromExtensions = function() {
for (const extension of Object.values(extensions)) {
for (const [key, value] of Object.entries(extension.activationResult.contributions?.notifications ?? [])) {
if (value.type === 'email') {
preferencesService.registerUserPreference(key, 1);
scope.emailNotificationsFromExtensions[key] = preferencesService.getSync(key);
}
}
}
};

scope.buildNotificationsFromExtensions();

// email:notification toggling happens via `ng-model` in a template
// this function only updates child notifications
scope.toggleEmailGroupNotifications = function() {
const isGroupEnabled = scope.preferences['email:notification'].enabled;

Object.keys(scope.emailNotificationsFromExtensions).forEach((notificationId) => {
scope.preferences[notificationId].enabled = isGroupEnabled;
scope.emailNotificationsFromExtensions[notificationId] = {
...scope.emailNotificationsFromExtensions[notificationId],
enabled: isGroupEnabled,
};
});
for (const notificationId of Object.keys(scope.preferences.notifications)) {
scope.preferences[NOTIFICATIONS_KEY][notificationId].email = isGroupEnabled;
}

scope.userPrefs.$setDirty();
scope.$applyAsync();
};

scope.toggleEmailNotification = function(notificationId: string) {
const enabledUpdate = !(scope.preferences[notificationId]?.enabled ?? false);
scope.preferences[NOTIFICATIONS_KEY][notificationId].email =
!scope.preferences[NOTIFICATIONS_KEY][notificationId].email;

scope.preferences[notificationId] = {
...(scope.preferences[notificationId] ?? {}),
enabled: enabledUpdate,
};
scope.emailNotificationsFromExtensions[notificationId] = {
...scope.emailNotificationsFromExtensions[notificationId],
enabled: enabledUpdate,
};

const notificationsForGroupAreOff = Object.values(scope.emailNotificationsFromExtensions)
.every((value: any) => value?.enabled == false);

scope.preferences['email:notification'].enabled = !notificationsForGroupAreOff;
scope.preferences['email:notification'].enabled =
Object.values(scope.preferences[NOTIFICATIONS_KEY]).some((value: any) => value.email === true);

scope.userPrefs.$setDirty();
scope.$applyAsync();
};

preferencesService.get(null, true).then((result) => {
orig = result;
buildPreferences(orig);
buildPreferences(cloneDeep(result));

scope.datelineSource = session.identity.dateline_source;
scope.datelinePreview = scope.preferences['dateline:located'].located;
Expand All @@ -118,7 +103,7 @@ export function UserPreferencesDirective(

scope.cancel = function() {
scope.userPrefs.$setPristine();
buildPreferences(orig);
buildPreferences(cloneDeep(orig));

scope.datelinePreview = scope.preferences['dateline:located'].located;
};
Expand Down Expand Up @@ -159,7 +144,7 @@ export function UserPreferencesDirective(
});
}, () => $q.reject('canceledByModal'))
.then((preferences) => {
// ask for browser permission if desktop notification is enable
// ask for browser permission if desktop notification is enable
if (_.get(preferences, 'desktop:notification.enabled')) {
preferencesService.desktopNotification.requestPermission();
}
Expand Down Expand Up @@ -289,13 +274,13 @@ export function UserPreferencesDirective(

scope.preferences = {};
_.each(data, (val, key) => {
if (val.label && val.category) {
if (key == NOTIFICATIONS_KEY) {
scope.preferences[NOTIFICATIONS_KEY] = pick(val, Object.keys(registeredNotifications));
} else if (val.label && val.category) {
scope.preferences[key] = _.create(val);
}
});

scope.buildNotificationsFromExtensions();

// metadata service initialization is needed if its
// values object is undefined or any of the needed
// data buckets are missing in it
Expand Down Expand Up @@ -400,6 +385,23 @@ export function UserPreferencesDirective(

scope.calendars = helperData.event_calendars;

scope.notificationLabels = {};

if (scope.preferences[NOTIFICATIONS_KEY] == null) {
scope.preferences[NOTIFICATIONS_KEY] = {};
}

for (const [notificationId, notification] of Object.entries(registeredNotifications)) {
if (scope.preferences[NOTIFICATIONS_KEY][notificationId] == null) {
scope.preferences[NOTIFICATIONS_KEY][notificationId] = {
email: true,
desktop: true,
};
}

scope.notificationLabels[notificationId] = notification.name;
}

scope.preferencesLoaded = true;
}

Expand Down
8 changes: 4 additions & 4 deletions scripts/apps/users/views/user-preferences.html
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ <h4 class="sd-heading sd-text-align--left sd-text--sans sd-heading--h4 sd-margin
</div>
</li>

<li class="simple-list__item simple-list__item--stacked simple-list__item--justify-flex-start">
<li ng-if="preferencesLoaded === true" class="simple-list__item simple-list__item--stacked simple-list__item--justify-flex-start">
<h3 id="notifications" class="sd-heading sd-text-align--left sd-text--sans sd-heading--h3" translate>Notifications</h3>
<div class="sd-container sd-container--flex sd-container--gap-none sd-container--direction-column sd-radius--medium sd-panel-bg--000 sd-shadow--z2 sd-padding--3 sd-state--focus sd-margin-b--1">
<div class="sd-switch__group sd-switch__group--vertical">
Expand All @@ -82,10 +82,10 @@ <h3 id="notifications" class="sd-heading sd-text-align--left sd-text--sans sd-he

<div class="item ms-1 ps-5">
<sd-email-notifications-list
data-preferences="emailNotificationsFromExtensions"
data-preferences="preferences"
data-toggle-email-notification="toggleEmailNotification"
>
</sd-email-notifications-list>
data-notification-labels="notificationLabels"
></sd-email-notifications-list>
</div>
<div sd-info-item>
<span ng-hide="preferences['desktop:notification'].allowed" sd-switch ng-model="preferences['desktop:notification'].enabled"></span>
Expand Down
10 changes: 5 additions & 5 deletions scripts/core/menu/notifications/notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import _ from 'lodash';
import {gettext} from 'core/utils';
import {AuthoringWorkspaceService} from 'apps/authoring/authoring/services/AuthoringWorkspaceService';
import {extensions} from 'appConfig';
import {IDesktopNotification} from 'superdesk-api';
import {IExtensionActivationResult} from 'superdesk-api';
import {logger} from 'core/services/logger';
import emptyState from 'superdesk-ui-framework/dist/empty-state--small-2.svg';

Expand Down Expand Up @@ -296,7 +296,9 @@ angular.module('superdesk.core.menu.notifications', ['superdesk.core.services.as
scope.emptyState = emptyState;

// merged from all extensions
const notificationsKeyed: {[key: string]: IDesktopNotification['handler']} = {};
const notificationsKeyed: {
[key: string]: IExtensionActivationResult['contributions']['notifications'][0]['handler']
} = {};

for (const extension of Object.values(extensions)) {
const notificationsFromExtensions = extension.activationResult.contributions?.notifications;
Expand All @@ -306,9 +308,7 @@ angular.module('superdesk.core.menu.notifications', ['superdesk.core.services.as
if (notificationsKeyed[key] == null) {
const notificationValue = notificationsFromExtensions[key];

if (notificationValue.type == 'desktop') {
notificationsKeyed[key] = notificationValue.handler;
}
notificationsKeyed[key] = notificationValue.handler;
} else {
logger.error(new Error(`Notification key ${key} already registered.`));
}
Expand Down
3 changes: 2 additions & 1 deletion scripts/core/services/preferencesService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export default angular.module('superdesk.core.preferences', ['superdesk.core.not
userPreferences = {
'feature:preview': 1,
'archive:view': 1,
'notifications': 1,
'email:notification': 1,
'desktop:notification': 1,
'slack:notification': 1,
Expand Down Expand Up @@ -90,7 +91,7 @@ export default angular.module('superdesk.core.preferences', ['superdesk.core.not
},
// ask for permission and send a desktop notification
send: (msg) => {
if (_.get(preferences, 'user_preferences.desktop:notification.enabled')) {
if (preferences.user_preferences['desktop:notification'].enabled) {
if ('Notification' in window && Notification.permission !== 'denied') {
Notification.requestPermission((permission) => {
if (permission === 'granted') {
Expand Down
29 changes: 15 additions & 14 deletions scripts/core/superdesk-api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -705,19 +705,6 @@ declare module 'superdesk-api' {
preview?: React.ComponentType<IIngestRuleHandlerPreviewProps>;
}

interface IEmailNotification {
type: 'email';
}

export interface IDesktopNotification {
type: 'desktop';
label: string;
handler: (notification: any) => {
body: string;
actions: Array<{label: string; onClick: () => void;}>;
};
}

export interface IExtensionActivationResult {
contributions?: {
globalMenuHorizontal?: Array<React.ComponentType>;
Expand Down Expand Up @@ -756,7 +743,13 @@ declare module 'superdesk-api' {
workspaceMenuItems?: Array<IWorkspaceMenuItem>;
customFieldTypes?: Array<ICustomFieldType>;
notifications?: {
[id: string]: IEmailNotification | IDesktopNotification;
[id: string]: {
name: string;
handler?: (notification: any) => {
body: string;
actions: Array<{label: string; onClick: () => void;}>;
};
};
};
entities?: {
article?: {
Expand Down Expand Up @@ -1421,6 +1414,14 @@ declare module 'superdesk-api' {
invisible_stages: Array<any>;
slack_username: string;
slack_user_id: string;
user_preferences: {
notifications: {
[key: string]: {
email: boolean;
desktop: boolean;
};
};
};
last_activity_at?: string;
}

Expand Down
3 changes: 1 addition & 2 deletions scripts/extensions/broadcasting/src/notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ type IExtensionNotifications = Required<Required<IExtensionActivationResult>['co

export const notifications: IExtensionNotifications = {
'rundown-item-comment': {
label: gettext('Open item'),
type: 'desktop',
name: gettext('Open item'),
handler: (notification: IRundownItemCommentNotification) => ({
body: notification.message,
actions: [{
Expand Down
17 changes: 1 addition & 16 deletions scripts/extensions/markForUser/src/extension.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,7 @@ const extension: IExtension = {
},
notifications: {
'item:marked': {
type: 'desktop',
label: gettext('open item'),
name: gettext('Mark for User'),
handler: (notification: any) => ({
body: notification.message,
actions: [{
Expand All @@ -58,20 +57,6 @@ const extension: IExtension = {
}],
}),
},
'item:unmarked': {
label: gettext('open item'),
type: 'desktop',
handler: (notification: any) => ({
body: notification.message,
actions: [{
label: gettext('open item'),
onClick: () => superdesk.ui.article.view(notification.item),
}],
}),
},
'mark_for_user:notification': {
type: 'email',
},
},
entities: {
article: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,6 @@ export function getMarkedForMeComponent(superdesk: ISuperdesk) {
});

this.removeMarkedListener = superdesk.addWebsocketMessageListener('item:marked', this.queryAndSetArticles);
this.removeUnmarkedListener = superdesk.addWebsocketMessageListener(
'item:unmarked',
this.queryAndSetArticles,
);
}
componentWillUnmount() {
this.removeMarkedListener();
Expand Down

0 comments on commit fffbc6c

Please sign in to comment.