Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/services/notification/common/notificationService.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { localize } from '../../../../nls.js';
7
import { INotificationService, INotification, INotificationHandle, Severity, NotificationMessage, INotificationActions, IPromptChoice, IPromptOptions, IStatusMessageOptions, NoOpNotification, NeverShowAgainScope, NotificationsFilter, INeverShowAgainOptions, INotificationSource, INotificationSourceFilter, isNotificationSource, IStatusHandle } from '../../../../platform/notification/common/notification.js';
8
import { NotificationsModel, ChoiceAction, NotificationChangeType } from '../../../common/notifications.js';
9
import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';
10
import { Emitter, Event } from '../../../../base/common/event.js';
11
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
12
import { IAction, Action } from '../../../../base/common/actions.js';
13
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
14
15
export class NotificationService extends Disposable implements INotificationService {
16
17
declare readonly _serviceBrand: undefined;
18
19
readonly model = this._register(new NotificationsModel());
20
21
constructor(
22
@IStorageService private readonly storageService: IStorageService
23
) {
24
super();
25
26
this.mapSourceToFilter = (() => {
27
const map = new Map<string, INotificationSourceFilter>();
28
29
for (const sourceFilter of this.storageService.getObject<INotificationSourceFilter[]>(NotificationService.PER_SOURCE_FILTER_SETTINGS_KEY, StorageScope.APPLICATION, [])) {
30
map.set(sourceFilter.id, sourceFilter);
31
}
32
33
return map;
34
})();
35
36
this.globalFilterEnabled = this.storageService.getBoolean(NotificationService.GLOBAL_FILTER_SETTINGS_KEY, StorageScope.APPLICATION, false);
37
38
this.updateFilters();
39
this.registerListeners();
40
}
41
42
private registerListeners(): void {
43
this._register(this.model.onDidChangeNotification(e => {
44
switch (e.kind) {
45
case NotificationChangeType.ADD: {
46
const source = typeof e.item.sourceId === 'string' && typeof e.item.source === 'string' ? { id: e.item.sourceId, label: e.item.source } : e.item.source;
47
48
// Make sure to track sources for notifications by registering
49
// them with our do not disturb system which is backed by storage
50
51
if (isNotificationSource(source)) {
52
if (!this.mapSourceToFilter.has(source.id)) {
53
this.setFilter({ ...source, filter: NotificationsFilter.OFF });
54
} else {
55
this.updateSourceFilter(source);
56
}
57
}
58
59
break;
60
}
61
}
62
}));
63
}
64
65
//#region Filters
66
67
private static readonly GLOBAL_FILTER_SETTINGS_KEY = 'notifications.doNotDisturbMode';
68
private static readonly PER_SOURCE_FILTER_SETTINGS_KEY = 'notifications.perSourceDoNotDisturbMode';
69
70
private readonly _onDidChangeFilter = this._register(new Emitter<void>());
71
readonly onDidChangeFilter = this._onDidChangeFilter.event;
72
73
private globalFilterEnabled: boolean;
74
75
private readonly mapSourceToFilter: Map<string /** source id */, INotificationSourceFilter>;
76
77
setFilter(filter: NotificationsFilter | INotificationSourceFilter): void {
78
if (typeof filter === 'number') {
79
if (this.globalFilterEnabled === (filter === NotificationsFilter.ERROR)) {
80
return; // no change
81
}
82
83
// Store into model and persist
84
this.globalFilterEnabled = filter === NotificationsFilter.ERROR;
85
this.storageService.store(NotificationService.GLOBAL_FILTER_SETTINGS_KEY, this.globalFilterEnabled, StorageScope.APPLICATION, StorageTarget.MACHINE);
86
87
// Update model
88
this.updateFilters();
89
90
// Events
91
this._onDidChangeFilter.fire();
92
} else {
93
const existing = this.mapSourceToFilter.get(filter.id);
94
if (existing?.filter === filter.filter && existing.label === filter.label) {
95
return; // no change
96
}
97
98
// Store into model and persist
99
this.mapSourceToFilter.set(filter.id, { id: filter.id, label: filter.label, filter: filter.filter });
100
this.saveSourceFilters();
101
102
// Update model
103
this.updateFilters();
104
}
105
}
106
107
getFilter(source?: INotificationSource): NotificationsFilter {
108
if (source) {
109
return this.mapSourceToFilter.get(source.id)?.filter ?? NotificationsFilter.OFF;
110
}
111
112
return this.globalFilterEnabled ? NotificationsFilter.ERROR : NotificationsFilter.OFF;
113
}
114
115
private updateSourceFilter(source: INotificationSource): void {
116
const existing = this.mapSourceToFilter.get(source.id);
117
if (!existing) {
118
return; // nothing to do
119
}
120
121
// Store into model and persist
122
if (existing.label !== source.label) {
123
this.mapSourceToFilter.set(source.id, { id: source.id, label: source.label, filter: existing.filter });
124
this.saveSourceFilters();
125
}
126
}
127
128
private saveSourceFilters(): void {
129
this.storageService.store(NotificationService.PER_SOURCE_FILTER_SETTINGS_KEY, JSON.stringify([...this.mapSourceToFilter.values()]), StorageScope.APPLICATION, StorageTarget.MACHINE);
130
}
131
132
getFilters(): INotificationSourceFilter[] {
133
return [...this.mapSourceToFilter.values()];
134
}
135
136
private updateFilters(): void {
137
this.model.setFilter({
138
global: this.globalFilterEnabled ? NotificationsFilter.ERROR : NotificationsFilter.OFF,
139
sources: new Map([...this.mapSourceToFilter.values()].map(source => [source.id, source.filter]))
140
});
141
}
142
143
removeFilter(sourceId: string): void {
144
if (this.mapSourceToFilter.delete(sourceId)) {
145
146
// Persist
147
this.saveSourceFilters();
148
149
// Update model
150
this.updateFilters();
151
}
152
}
153
154
//#endregion
155
156
info(message: NotificationMessage | NotificationMessage[]): void {
157
if (Array.isArray(message)) {
158
for (const messageEntry of message) {
159
this.info(messageEntry);
160
}
161
162
return;
163
}
164
165
this.model.addNotification({ severity: Severity.Info, message });
166
}
167
168
warn(message: NotificationMessage | NotificationMessage[]): void {
169
if (Array.isArray(message)) {
170
for (const messageEntry of message) {
171
this.warn(messageEntry);
172
}
173
174
return;
175
}
176
177
this.model.addNotification({ severity: Severity.Warning, message });
178
}
179
180
error(message: NotificationMessage | NotificationMessage[]): void {
181
if (Array.isArray(message)) {
182
for (const messageEntry of message) {
183
this.error(messageEntry);
184
}
185
186
return;
187
}
188
189
this.model.addNotification({ severity: Severity.Error, message });
190
}
191
192
notify(notification: INotification): INotificationHandle {
193
const toDispose = new DisposableStore();
194
195
// Handle neverShowAgain option accordingly
196
197
if (notification.neverShowAgain) {
198
const scope = this.toStorageScope(notification.neverShowAgain);
199
const id = notification.neverShowAgain.id;
200
201
// If the user already picked to not show the notification
202
// again, we return with a no-op notification here
203
if (this.storageService.getBoolean(id, scope)) {
204
return new NoOpNotification();
205
}
206
207
const neverShowAgainAction = toDispose.add(new Action(
208
'workbench.notification.neverShowAgain',
209
localize('neverShowAgain', "Don't Show Again"),
210
undefined, true, async () => {
211
212
// Close notification
213
handle.close();
214
215
// Remember choice
216
this.storageService.store(id, true, scope, StorageTarget.USER);
217
}));
218
219
// Insert as primary or secondary action
220
const actions = {
221
primary: notification.actions?.primary || [],
222
secondary: notification.actions?.secondary || []
223
};
224
if (!notification.neverShowAgain.isSecondary) {
225
actions.primary = [neverShowAgainAction, ...actions.primary]; // action comes first
226
} else {
227
actions.secondary = [...actions.secondary, neverShowAgainAction]; // actions comes last
228
}
229
230
notification.actions = actions;
231
}
232
233
// Show notification
234
const handle = this.model.addNotification(notification);
235
236
// Cleanup when notification gets disposed
237
Event.once(handle.onDidClose)(() => toDispose.dispose());
238
239
return handle;
240
}
241
242
private toStorageScope(options: INeverShowAgainOptions): StorageScope {
243
switch (options.scope) {
244
case NeverShowAgainScope.APPLICATION:
245
return StorageScope.APPLICATION;
246
case NeverShowAgainScope.PROFILE:
247
return StorageScope.PROFILE;
248
case NeverShowAgainScope.WORKSPACE:
249
return StorageScope.WORKSPACE;
250
default:
251
return StorageScope.APPLICATION;
252
}
253
}
254
255
prompt(severity: Severity, message: string, choices: IPromptChoice[], options?: IPromptOptions): INotificationHandle {
256
257
// Handle neverShowAgain option accordingly
258
if (options?.neverShowAgain) {
259
const scope = this.toStorageScope(options.neverShowAgain);
260
const id = options.neverShowAgain.id;
261
262
// If the user already picked to not show the notification
263
// again, we return with a no-op notification here
264
if (this.storageService.getBoolean(id, scope)) {
265
return new NoOpNotification();
266
}
267
268
const neverShowAgainChoice = {
269
label: localize('neverShowAgain', "Don't Show Again"),
270
run: () => this.storageService.store(id, true, scope, StorageTarget.USER),
271
isSecondary: options.neverShowAgain.isSecondary
272
};
273
274
// Insert as primary or secondary action
275
if (!options.neverShowAgain.isSecondary) {
276
choices = [neverShowAgainChoice, ...choices]; // action comes first
277
} else {
278
choices = [...choices, neverShowAgainChoice]; // actions comes last
279
}
280
}
281
282
let choiceClicked = false;
283
const toDispose = new DisposableStore();
284
285
286
// Convert choices into primary/secondary actions
287
const primaryActions: IAction[] = [];
288
const secondaryActions: IAction[] = [];
289
choices.forEach((choice, index) => {
290
const action = new ChoiceAction(`workbench.dialog.choice.${index}`, choice);
291
if (!choice.isSecondary) {
292
primaryActions.push(action);
293
} else {
294
secondaryActions.push(action);
295
}
296
297
// React to action being clicked
298
toDispose.add(action.onDidRun(() => {
299
choiceClicked = true;
300
301
// Close notification unless we are told to keep open
302
if (!choice.keepOpen) {
303
handle.close();
304
}
305
}));
306
307
toDispose.add(action);
308
});
309
310
// Show notification with actions
311
const actions: INotificationActions = { primary: primaryActions, secondary: secondaryActions };
312
const handle = this.notify({ severity, message, actions, sticky: options?.sticky, priority: options?.priority });
313
314
Event.once(handle.onDidClose)(() => {
315
316
// Cleanup when notification gets disposed
317
toDispose.dispose();
318
319
// Indicate cancellation to the outside if no action was executed
320
if (options && typeof options.onCancel === 'function' && !choiceClicked) {
321
options.onCancel();
322
}
323
});
324
325
return handle;
326
}
327
328
status(message: NotificationMessage, options?: IStatusMessageOptions): IStatusHandle {
329
return this.model.showStatusMessage(message, options);
330
}
331
}
332
333
registerSingleton(INotificationService, NotificationService, InstantiationType.Delayed);
334
335