Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor all notifications from extensions and UI toggling such that … #4589

Merged
merged 13 commits into from
Aug 14, 2024
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) {
tomaskikutis marked this conversation as resolved.
Show resolved Hide resolved
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'),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this unified notification notify both of marking and unmarking? Check with if API exposes this information and coordinate with Petr if not

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, works for marking and unmarking. Only difference between the two previously was the body message, handler was indeed the same one.

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
Loading