Path: blob/main/src/vs/workbench/services/notification/common/notificationService.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { localize } from '../../../../nls.js';6import { INotificationService, INotification, INotificationHandle, Severity, NotificationMessage, INotificationActions, IPromptChoice, IPromptOptions, IStatusMessageOptions, NoOpNotification, NeverShowAgainScope, NotificationsFilter, INeverShowAgainOptions, INotificationSource, INotificationSourceFilter, isNotificationSource, IStatusHandle } from '../../../../platform/notification/common/notification.js';7import { NotificationsModel, ChoiceAction, NotificationChangeType } from '../../../common/notifications.js';8import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';9import { Emitter, Event } from '../../../../base/common/event.js';10import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';11import { IAction, Action } from '../../../../base/common/actions.js';12import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';1314export class NotificationService extends Disposable implements INotificationService {1516declare readonly _serviceBrand: undefined;1718readonly model = this._register(new NotificationsModel());1920constructor(21@IStorageService private readonly storageService: IStorageService22) {23super();2425this.mapSourceToFilter = (() => {26const map = new Map<string, INotificationSourceFilter>();2728for (const sourceFilter of this.storageService.getObject<INotificationSourceFilter[]>(NotificationService.PER_SOURCE_FILTER_SETTINGS_KEY, StorageScope.APPLICATION, [])) {29map.set(sourceFilter.id, sourceFilter);30}3132return map;33})();3435this.globalFilterEnabled = this.storageService.getBoolean(NotificationService.GLOBAL_FILTER_SETTINGS_KEY, StorageScope.APPLICATION, false);3637this.updateFilters();38this.registerListeners();39}4041private registerListeners(): void {42this._register(this.model.onDidChangeNotification(e => {43switch (e.kind) {44case NotificationChangeType.ADD: {45const source = typeof e.item.sourceId === 'string' && typeof e.item.source === 'string' ? { id: e.item.sourceId, label: e.item.source } : e.item.source;4647// Make sure to track sources for notifications by registering48// them with our do not disturb system which is backed by storage4950if (isNotificationSource(source)) {51if (!this.mapSourceToFilter.has(source.id)) {52this.setFilter({ ...source, filter: NotificationsFilter.OFF });53} else {54this.updateSourceFilter(source);55}56}5758break;59}60}61}));62}6364//#region Filters6566private static readonly GLOBAL_FILTER_SETTINGS_KEY = 'notifications.doNotDisturbMode';67private static readonly PER_SOURCE_FILTER_SETTINGS_KEY = 'notifications.perSourceDoNotDisturbMode';6869private readonly _onDidChangeFilter = this._register(new Emitter<void>());70readonly onDidChangeFilter = this._onDidChangeFilter.event;7172private globalFilterEnabled: boolean;7374private readonly mapSourceToFilter: Map<string /** source id */, INotificationSourceFilter>;7576setFilter(filter: NotificationsFilter | INotificationSourceFilter): void {77if (typeof filter === 'number') {78if (this.globalFilterEnabled === (filter === NotificationsFilter.ERROR)) {79return; // no change80}8182// Store into model and persist83this.globalFilterEnabled = filter === NotificationsFilter.ERROR;84this.storageService.store(NotificationService.GLOBAL_FILTER_SETTINGS_KEY, this.globalFilterEnabled, StorageScope.APPLICATION, StorageTarget.MACHINE);8586// Update model87this.updateFilters();8889// Events90this._onDidChangeFilter.fire();91} else {92const existing = this.mapSourceToFilter.get(filter.id);93if (existing?.filter === filter.filter && existing.label === filter.label) {94return; // no change95}9697// Store into model and persist98this.mapSourceToFilter.set(filter.id, { id: filter.id, label: filter.label, filter: filter.filter });99this.saveSourceFilters();100101// Update model102this.updateFilters();103}104}105106getFilter(source?: INotificationSource): NotificationsFilter {107if (source) {108return this.mapSourceToFilter.get(source.id)?.filter ?? NotificationsFilter.OFF;109}110111return this.globalFilterEnabled ? NotificationsFilter.ERROR : NotificationsFilter.OFF;112}113114private updateSourceFilter(source: INotificationSource): void {115const existing = this.mapSourceToFilter.get(source.id);116if (!existing) {117return; // nothing to do118}119120// Store into model and persist121if (existing.label !== source.label) {122this.mapSourceToFilter.set(source.id, { id: source.id, label: source.label, filter: existing.filter });123this.saveSourceFilters();124}125}126127private saveSourceFilters(): void {128this.storageService.store(NotificationService.PER_SOURCE_FILTER_SETTINGS_KEY, JSON.stringify([...this.mapSourceToFilter.values()]), StorageScope.APPLICATION, StorageTarget.MACHINE);129}130131getFilters(): INotificationSourceFilter[] {132return [...this.mapSourceToFilter.values()];133}134135private updateFilters(): void {136this.model.setFilter({137global: this.globalFilterEnabled ? NotificationsFilter.ERROR : NotificationsFilter.OFF,138sources: new Map([...this.mapSourceToFilter.values()].map(source => [source.id, source.filter]))139});140}141142removeFilter(sourceId: string): void {143if (this.mapSourceToFilter.delete(sourceId)) {144145// Persist146this.saveSourceFilters();147148// Update model149this.updateFilters();150}151}152153//#endregion154155info(message: NotificationMessage | NotificationMessage[]): void {156if (Array.isArray(message)) {157for (const messageEntry of message) {158this.info(messageEntry);159}160161return;162}163164this.model.addNotification({ severity: Severity.Info, message });165}166167warn(message: NotificationMessage | NotificationMessage[]): void {168if (Array.isArray(message)) {169for (const messageEntry of message) {170this.warn(messageEntry);171}172173return;174}175176this.model.addNotification({ severity: Severity.Warning, message });177}178179error(message: NotificationMessage | NotificationMessage[]): void {180if (Array.isArray(message)) {181for (const messageEntry of message) {182this.error(messageEntry);183}184185return;186}187188this.model.addNotification({ severity: Severity.Error, message });189}190191notify(notification: INotification): INotificationHandle {192const toDispose = new DisposableStore();193194// Handle neverShowAgain option accordingly195196if (notification.neverShowAgain) {197const scope = this.toStorageScope(notification.neverShowAgain);198const id = notification.neverShowAgain.id;199200// If the user already picked to not show the notification201// again, we return with a no-op notification here202if (this.storageService.getBoolean(id, scope)) {203return new NoOpNotification();204}205206const neverShowAgainAction = toDispose.add(new Action(207'workbench.notification.neverShowAgain',208localize('neverShowAgain', "Don't Show Again"),209undefined, true, async () => {210211// Close notification212handle.close();213214// Remember choice215this.storageService.store(id, true, scope, StorageTarget.USER);216}));217218// Insert as primary or secondary action219const actions = {220primary: notification.actions?.primary || [],221secondary: notification.actions?.secondary || []222};223if (!notification.neverShowAgain.isSecondary) {224actions.primary = [neverShowAgainAction, ...actions.primary]; // action comes first225} else {226actions.secondary = [...actions.secondary, neverShowAgainAction]; // actions comes last227}228229notification.actions = actions;230}231232// Show notification233const handle = this.model.addNotification(notification);234235// Cleanup when notification gets disposed236Event.once(handle.onDidClose)(() => toDispose.dispose());237238return handle;239}240241private toStorageScope(options: INeverShowAgainOptions): StorageScope {242switch (options.scope) {243case NeverShowAgainScope.APPLICATION:244return StorageScope.APPLICATION;245case NeverShowAgainScope.PROFILE:246return StorageScope.PROFILE;247case NeverShowAgainScope.WORKSPACE:248return StorageScope.WORKSPACE;249default:250return StorageScope.APPLICATION;251}252}253254prompt(severity: Severity, message: string, choices: IPromptChoice[], options?: IPromptOptions): INotificationHandle {255256// Handle neverShowAgain option accordingly257if (options?.neverShowAgain) {258const scope = this.toStorageScope(options.neverShowAgain);259const id = options.neverShowAgain.id;260261// If the user already picked to not show the notification262// again, we return with a no-op notification here263if (this.storageService.getBoolean(id, scope)) {264return new NoOpNotification();265}266267const neverShowAgainChoice = {268label: localize('neverShowAgain', "Don't Show Again"),269run: () => this.storageService.store(id, true, scope, StorageTarget.USER),270isSecondary: options.neverShowAgain.isSecondary271};272273// Insert as primary or secondary action274if (!options.neverShowAgain.isSecondary) {275choices = [neverShowAgainChoice, ...choices]; // action comes first276} else {277choices = [...choices, neverShowAgainChoice]; // actions comes last278}279}280281let choiceClicked = false;282const toDispose = new DisposableStore();283284285// Convert choices into primary/secondary actions286const primaryActions: IAction[] = [];287const secondaryActions: IAction[] = [];288choices.forEach((choice, index) => {289const action = new ChoiceAction(`workbench.dialog.choice.${index}`, choice);290if (!choice.isSecondary) {291primaryActions.push(action);292} else {293secondaryActions.push(action);294}295296// React to action being clicked297toDispose.add(action.onDidRun(() => {298choiceClicked = true;299300// Close notification unless we are told to keep open301if (!choice.keepOpen) {302handle.close();303}304}));305306toDispose.add(action);307});308309// Show notification with actions310const actions: INotificationActions = { primary: primaryActions, secondary: secondaryActions };311const handle = this.notify({ severity, message, actions, sticky: options?.sticky, priority: options?.priority });312313Event.once(handle.onDidClose)(() => {314315// Cleanup when notification gets disposed316toDispose.dispose();317318// Indicate cancellation to the outside if no action was executed319if (options && typeof options.onCancel === 'function' && !choiceClicked) {320options.onCancel();321}322});323324return handle;325}326327status(message: NotificationMessage, options?: IStatusMessageOptions): IStatusHandle {328return this.model.showStatusMessage(message, options);329}330}331332registerSingleton(INotificationService, NotificationService, InstantiationType.Delayed);333334335