Skip to content

Commit f148d86

Browse files
Add "Do Not Disturb" Mode (microsoft#149645)
* Add mute icon to notification center toolbar * Add placeholder mute method * Add config service support * Refactor mute toggle behavior * More refactoring * Add hack for switching actions on toggle * Use do-not-disturb icons * Update status bar icon * Add comment for hack todo * Add experimental tag * Update setting name references * Fix typo * Update status bar icon and filter all errors * Update icons * Updates icon and toggle behavior * Update codicons ttf * cleanups * Use UI state instead of setting * Show window progress instead of notification * Update Storage scopes * Update Storage scopes * Refactor to use NotificationService * Minor fixes * Update tests * Address PR feedback * Update tests * 💄 * 💄 * zen - use dnd mode for filtering * set filters right on startup Co-authored-by: Benjamin Pasero <[email protected]>
1 parent 9b1c6cb commit f148d86

File tree

15 files changed

+149
-42
lines changed

15 files changed

+149
-42
lines changed

src/vs/editor/standalone/browser/standaloneServices.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import { IKeybindingItem, KeybindingsRegistry } from 'vs/platform/keybinding/com
4040
import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem';
4141
import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding';
4242
import { ILabelService, ResourceLabelFormatter, IFormatterChangeEvent } from 'vs/platform/label/common/label';
43-
import { INotification, INotificationHandle, INotificationService, IPromptChoice, IPromptOptions, NoOpNotification, IStatusMessageOptions, NotificationsFilter } from 'vs/platform/notification/common/notification';
43+
import { INotification, INotificationHandle, INotificationService, IPromptChoice, IPromptOptions, NoOpNotification, IStatusMessageOptions } from 'vs/platform/notification/common/notification';
4444
import { IProgressRunner, IEditorProgressService } from 'vs/platform/progress/common/progress';
4545
import { ITelemetryInfo, ITelemetryService, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry';
4646
import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier, IWorkspace, IWorkspaceContextService, IWorkspaceFolder, IWorkspaceFoldersChangeEvent, IWorkspaceFoldersWillChangeEvent, WorkbenchState, WorkspaceFolder } from 'vs/platform/workspace/common/workspace';
@@ -232,8 +232,12 @@ export class StandaloneNotificationService implements INotificationService {
232232

233233
readonly onDidRemoveNotification: Event<INotification> = Event.None;
234234

235+
readonly onDidChangeDoNotDisturbMode: Event<void> = Event.None;
236+
235237
public _serviceBrand: undefined;
236238

239+
public doNotDisturbMode: boolean = false;
240+
237241
private static readonly NO_OP: INotificationHandle = new NoOpNotification();
238242

239243
public info(message: string): INotificationHandle {
@@ -271,8 +275,6 @@ export class StandaloneNotificationService implements INotificationService {
271275
public status(message: string | Error, options?: IStatusMessageOptions): IDisposable {
272276
return Disposable.None;
273277
}
274-
275-
public setFilter(filter: NotificationsFilter): void { }
276278
}
277279

278280
export class StandaloneCommandService implements ICommandService {

src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,10 @@ suite('AbstractKeybindingService', () => {
141141

142142
const notificationService: INotificationService = {
143143
_serviceBrand: undefined,
144+
doNotDisturbMode: false,
144145
onDidAddNotification: undefined!,
145146
onDidRemoveNotification: undefined!,
147+
onDidChangeDoNotDisturbMode: undefined!,
146148
notify: (notification: INotification) => {
147149
showMessageCalls.push({ sev: notification.severity, message: notification.message });
148150
return new NoOpNotification();
@@ -169,8 +171,7 @@ suite('AbstractKeybindingService', () => {
169171
statusMessageCallsDisposed!.push(message);
170172
}
171173
};
172-
},
173-
setFilter() { }
174+
}
174175
};
175176

176177
const resolver = new KeybindingResolver(items, [], () => { });

src/vs/platform/notification/common/notification.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,13 @@ export interface INotificationService {
319319

320320
readonly _serviceBrand: undefined;
321321

322+
/**
323+
* The DND mode can be enabled or disabled
324+
* and will result in all info and warning
325+
* notifications to be silent.
326+
*/
327+
doNotDisturbMode: boolean;
328+
322329
/**
323330
* Emitted when a new notification is added.
324331
*/
@@ -329,6 +336,11 @@ export interface INotificationService {
329336
*/
330337
readonly onDidRemoveNotification: Event<INotification>;
331338

339+
/**
340+
* Emitted when a do not disturb mode has changed.
341+
*/
342+
readonly onDidChangeDoNotDisturbMode: Event<void>;
343+
332344
/**
333345
* Show the provided notification to the user. The returned `INotificationHandle`
334346
* can be used to control the notification afterwards.
@@ -381,13 +393,6 @@ export interface INotificationService {
381393
* @returns a disposable to hide the status message
382394
*/
383395
status(message: NotificationMessage, options?: IStatusMessageOptions): IDisposable;
384-
385-
/**
386-
* Allows to configure a filter for notifications.
387-
*
388-
* @param filter the filter to use
389-
*/
390-
setFilter(filter: NotificationsFilter): void;
391396
}
392397

393398
export class NoOpNotification implements INotificationHandle {

src/vs/platform/notification/test/common/testNotificationService.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,20 @@
55

66
import { Event } from 'vs/base/common/event';
77
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
8-
import { INotification, INotificationHandle, INotificationService, IPromptChoice, IPromptOptions, IStatusMessageOptions, NoOpNotification, NotificationsFilter, Severity } from 'vs/platform/notification/common/notification';
8+
import { INotification, INotificationHandle, INotificationService, IPromptChoice, IPromptOptions, IStatusMessageOptions, NoOpNotification, Severity } from 'vs/platform/notification/common/notification';
99

1010
export class TestNotificationService implements INotificationService {
1111

1212
readonly onDidAddNotification: Event<INotification> = Event.None;
1313

1414
readonly onDidRemoveNotification: Event<INotification> = Event.None;
1515

16+
readonly onDidChangeDoNotDisturbMode: Event<void> = Event.None;
17+
1618
declare readonly _serviceBrand: undefined;
1719

20+
doNotDisturbMode: boolean = false;
21+
1822
private static readonly NO_OP: INotificationHandle = new NoOpNotification();
1923

2024
info(message: string): INotificationHandle {
@@ -40,6 +44,4 @@ export class TestNotificationService implements INotificationService {
4044
status(message: string | Error, options?: IStatusMessageOptions): IDisposable {
4145
return Disposable.None;
4246
}
43-
44-
setFilter(filter: NotificationsFilter): void { }
4547
}

src/vs/workbench/api/test/browser/extHostMessagerService.test.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import * as assert from 'assert';
77
import { MainThreadMessageService } from 'vs/workbench/api/browser/mainThreadMessageService';
88
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
9-
import { INotificationService, INotification, NoOpNotification, INotificationHandle, Severity, IPromptChoice, IPromptOptions, IStatusMessageOptions, NotificationsFilter } from 'vs/platform/notification/common/notification';
9+
import { INotificationService, INotification, NoOpNotification, INotificationHandle, Severity, IPromptChoice, IPromptOptions, IStatusMessageOptions } from 'vs/platform/notification/common/notification';
1010
import { ICommandService } from 'vs/platform/commands/common/commands';
1111
import { mock } from 'vs/base/test/common/mock';
1212
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
@@ -24,8 +24,10 @@ const emptyCommandService: ICommandService = {
2424

2525
const emptyNotificationService = new class implements INotificationService {
2626
declare readonly _serviceBrand: undefined;
27+
doNotDisturbMode: boolean = false;
2728
onDidAddNotification: Event<INotification> = Event.None;
2829
onDidRemoveNotification: Event<INotification> = Event.None;
30+
onDidChangeDoNotDisturbMode: Event<void> = Event.None;
2931
notify(...args: any[]): never {
3032
throw new Error('not implemented');
3133
}
@@ -44,19 +46,17 @@ const emptyNotificationService = new class implements INotificationService {
4446
status(message: string | Error, options?: IStatusMessageOptions): IDisposable {
4547
return Disposable.None;
4648
}
47-
setFilter(filter: NotificationsFilter): void {
48-
throw new Error('not implemented.');
49-
}
5049
};
5150

5251
class EmptyNotificationService implements INotificationService {
5352
declare readonly _serviceBrand: undefined;
54-
53+
doNotDisturbMode: boolean = false;
5554
constructor(private withNotify: (notification: INotification) => void) {
5655
}
5756

5857
onDidAddNotification: Event<INotification> = Event.None;
5958
onDidRemoveNotification: Event<INotification> = Event.None;
59+
onDidChangeDoNotDisturbMode: Event<void> = Event.None;
6060
notify(notification: INotification): INotificationHandle {
6161
this.withNotify(notification);
6262

@@ -77,9 +77,6 @@ class EmptyNotificationService implements INotificationService {
7777
status(message: string, options?: IStatusMessageOptions): IDisposable {
7878
return Disposable.None;
7979
}
80-
setFilter(filter: NotificationsFilter): void {
81-
throw new Error('Method not implemented.');
82-
}
8380
}
8481

8582
suite('ExtHostMessageService', function () {

src/vs/workbench/browser/layout.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import { IFileService } from 'vs/platform/files/common/files';
3333
import { isCodeEditor } from 'vs/editor/browser/editorBrowser';
3434
import { coalesce } from 'vs/base/common/arrays';
3535
import { assertIsDefined, isNumber } from 'vs/base/common/types';
36-
import { INotificationService, NotificationsFilter } from 'vs/platform/notification/common/notification';
36+
import { INotificationService } from 'vs/platform/notification/common/notification';
3737
import { IThemeService } from 'vs/platform/theme/common/themeService';
3838
import { WINDOW_ACTIVE_BORDER, WINDOW_INACTIVE_BORDER } from 'vs/workbench/common/theme';
3939
import { LineNumbersType } from 'vs/editor/common/config/editorOptions';
@@ -1087,6 +1087,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi
10871087
if (!restoring) {
10881088
zenModeExitInfo.transitionedToFullScreen = toggleFullScreen;
10891089
zenModeExitInfo.transitionedToCenteredEditorLayout = !this.isEditorLayoutCentered() && config.centerLayout;
1090+
zenModeExitInfo.handleNotificationsDoNotDisturbMode = !this.notificationService.doNotDisturbMode;
10901091
zenModeExitInfo.wasVisible.sideBar = this.isVisible(Parts.SIDEBAR_PART);
10911092
zenModeExitInfo.wasVisible.panel = this.isVisible(Parts.PANEL_PART);
10921093
zenModeExitInfo.wasVisible.auxiliaryBar = this.isVisible(Parts.AUXILIARYBAR_PART);
@@ -1114,13 +1115,15 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi
11141115
this.windowState.runtime.zenMode.transitionDisposables.add(this.editorGroupService.enforcePartOptions({ showTabs: false }));
11151116
}
11161117

1117-
if (config.silentNotifications) {
1118-
this.notificationService.setFilter(NotificationsFilter.ERROR);
1118+
if (config.silentNotifications && zenModeExitInfo.handleNotificationsDoNotDisturbMode) {
1119+
this.notificationService.doNotDisturbMode = true;
11191120
}
11201121
this.windowState.runtime.zenMode.transitionDisposables.add(this.configurationService.onDidChangeConfiguration(e => {
11211122
if (e.affectsConfiguration(WorkbenchLayoutSettings.ZEN_MODE_SILENT_NOTIFICATIONS)) {
1122-
const filter = this.configurationService.getValue(WorkbenchLayoutSettings.ZEN_MODE_SILENT_NOTIFICATIONS) ? NotificationsFilter.ERROR : NotificationsFilter.OFF;
1123-
this.notificationService.setFilter(filter);
1123+
const zenModeSilentNotifications = !!this.configurationService.getValue(WorkbenchLayoutSettings.ZEN_MODE_SILENT_NOTIFICATIONS);
1124+
if (zenModeExitInfo.handleNotificationsDoNotDisturbMode) {
1125+
this.notificationService.doNotDisturbMode = zenModeSilentNotifications;
1126+
}
11241127
}
11251128
}));
11261129

@@ -1155,13 +1158,14 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi
11551158
this.centerEditorLayout(false, true);
11561159
}
11571160

1161+
if (zenModeExitInfo.handleNotificationsDoNotDisturbMode) {
1162+
this.notificationService.doNotDisturbMode = false;
1163+
}
1164+
11581165
setLineNumbers();
11591166

11601167
this.focus();
11611168

1162-
// Clear notifications filter
1163-
this.notificationService.setFilter(NotificationsFilter.OFF);
1164-
11651169
toggleFullScreen = zenModeExitInfo.transitionedToFullScreen && this.windowState.runtime.fullscreen;
11661170
}
11671171

src/vs/workbench/browser/layoutState.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export const LayoutStateKeys = {
4646
ZEN_MODE_EXIT_INFO: new RuntimeStateKey('zenMode.exitInfo', StorageScope.WORKSPACE, StorageTarget.USER, {
4747
transitionedToCenteredEditorLayout: false,
4848
transitionedToFullScreen: false,
49+
handleNotificationsDoNotDisturbMode: false,
4950
wasVisible: {
5051
auxiliaryBar: false,
5152
panel: false,

src/vs/workbench/browser/parts/notifications/notificationsActions.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { localize } from 'vs/nls';
99
import { Action, IAction, ActionRunner, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions';
1010
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
1111
import { INotificationService } from 'vs/platform/notification/common/notification';
12-
import { CLEAR_NOTIFICATION, EXPAND_NOTIFICATION, COLLAPSE_NOTIFICATION, CLEAR_ALL_NOTIFICATIONS, HIDE_NOTIFICATIONS_CENTER } from 'vs/workbench/browser/parts/notifications/notificationsCommands';
12+
import { CLEAR_NOTIFICATION, EXPAND_NOTIFICATION, COLLAPSE_NOTIFICATION, CLEAR_ALL_NOTIFICATIONS, HIDE_NOTIFICATIONS_CENTER, TOGGLE_DO_NOT_DISTURB_MODE } from 'vs/workbench/browser/parts/notifications/notificationsCommands';
1313
import { ICommandService } from 'vs/platform/commands/common/commands';
1414
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
1515
import { Codicon } from 'vs/base/common/codicons';
@@ -23,6 +23,7 @@ const hideIcon = registerIcon('notifications-hide', Codicon.chevronDown, localiz
2323
const expandIcon = registerIcon('notifications-expand', Codicon.chevronUp, localize('expandIcon', 'Icon for the expand action in notifications.'));
2424
const collapseIcon = registerIcon('notifications-collapse', Codicon.chevronDown, localize('collapseIcon', 'Icon for the collapse action in notifications.'));
2525
const configureIcon = registerIcon('notifications-configure', Codicon.gear, localize('configureIcon', 'Icon for the configure action in notifications.'));
26+
const doNotDisturbIcon = registerIcon('notifications-do-not-disturb', Codicon.bellSlash, localize('doNotDisturbIcon', 'Icon for the mute all action in notifications.'));
2627

2728
export class ClearNotificationAction extends Action {
2829

@@ -60,6 +61,24 @@ export class ClearAllNotificationsAction extends Action {
6061
}
6162
}
6263

64+
export class ToggleDoNotDisturbAction extends Action {
65+
66+
static readonly ID = TOGGLE_DO_NOT_DISTURB_MODE;
67+
static readonly LABEL = localize('toggleDoNotDisturbMode', "Toggle Do Not Disturb Mode");
68+
69+
constructor(
70+
id: string,
71+
label: string,
72+
@ICommandService private readonly commandService: ICommandService
73+
) {
74+
super(id, label, ThemeIcon.asClassName(doNotDisturbIcon));
75+
}
76+
77+
override async run(): Promise<void> {
78+
this.commandService.executeCommand(TOGGLE_DO_NOT_DISTURB_MODE);
79+
}
80+
}
81+
6382
export class HideNotificationsCenterAction extends Action {
6483

6584
static readonly ID = HIDE_NOTIFICATIONS_CENTER;

src/vs/workbench/browser/parts/notifications/notificationsCenter.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,12 @@ import { widgetShadow } from 'vs/platform/theme/common/colorRegistry';
1919
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
2020
import { localize } from 'vs/nls';
2121
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
22-
import { ClearAllNotificationsAction, HideNotificationsCenterAction, NotificationActionRunner } from 'vs/workbench/browser/parts/notifications/notificationsActions';
22+
import { ClearAllNotificationsAction, HideNotificationsCenterAction, NotificationActionRunner, ToggleDoNotDisturbAction } from 'vs/workbench/browser/parts/notifications/notificationsActions';
2323
import { IAction } from 'vs/base/common/actions';
2424
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
2525
import { assertAllDefined, assertIsDefined } from 'vs/base/common/types';
2626
import { NotificationsCenterVisibleContext } from 'vs/workbench/common/contextkeys';
27+
import { INotificationService } from 'vs/platform/notification/common/notification';
2728

2829
export class NotificationsCenter extends Themable implements INotificationsCenterController {
2930

@@ -40,6 +41,7 @@ export class NotificationsCenter extends Themable implements INotificationsCente
4041
private workbenchDimensions: Dimension | undefined;
4142
private readonly notificationsCenterVisibleContextKey = NotificationsCenterVisibleContext.bindTo(this.contextKeyService);
4243
private clearAllAction: ClearAllNotificationsAction | undefined;
44+
private toggleDoNotDisturbAction: ToggleDoNotDisturbAction | undefined;
4345

4446
constructor(
4547
private readonly container: HTMLElement,
@@ -49,7 +51,8 @@ export class NotificationsCenter extends Themable implements INotificationsCente
4951
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
5052
@IContextKeyService private readonly contextKeyService: IContextKeyService,
5153
@IEditorGroupsService private readonly editorGroupService: IEditorGroupsService,
52-
@IKeybindingService private readonly keybindingService: IKeybindingService
54+
@IKeybindingService private readonly keybindingService: IKeybindingService,
55+
@INotificationService private readonly notificationService: INotificationService,
5356
) {
5457
super(themeService);
5558

@@ -61,6 +64,13 @@ export class NotificationsCenter extends Themable implements INotificationsCente
6164
private registerListeners(): void {
6265
this._register(this.model.onDidChangeNotification(e => this.onDidChangeNotification(e)));
6366
this._register(this.layoutService.onDidLayout(dimension => this.layout(Dimension.lift(dimension))));
67+
this._register(this.notificationService.onDidChangeDoNotDisturbMode(() => this.onDidChangeDoNotDisturbMode()));
68+
}
69+
70+
private onDidChangeDoNotDisturbMode(): void {
71+
if (this.notificationService.doNotDisturbMode) {
72+
this.hide(); // hide the notification center when do not disturb is enabled
73+
}
6474
}
6575

6676
get isVisible(): boolean {
@@ -154,6 +164,9 @@ export class NotificationsCenter extends Themable implements INotificationsCente
154164
this.clearAllAction = this._register(this.instantiationService.createInstance(ClearAllNotificationsAction, ClearAllNotificationsAction.ID, ClearAllNotificationsAction.LABEL));
155165
notificationsToolBar.push(this.clearAllAction, { icon: true, label: false, keybinding: this.getKeybindingLabel(this.clearAllAction) });
156166

167+
this.toggleDoNotDisturbAction = this._register(this.instantiationService.createInstance(ToggleDoNotDisturbAction, ToggleDoNotDisturbAction.ID, ToggleDoNotDisturbAction.LABEL));
168+
notificationsToolBar.push(this.toggleDoNotDisturbAction, { icon: true, label: false, keybinding: this.getKeybindingLabel(this.toggleDoNotDisturbAction) });
169+
157170
const hideAllAction = this._register(this.instantiationService.createInstance(HideNotificationsCenterAction, HideNotificationsCenterAction.ID, HideNotificationsCenterAction.LABEL));
158171
notificationsToolBar.push(hideAllAction, { icon: true, label: false, keybinding: this.getKeybindingLabel(hideAllAction) });
159172

@@ -316,6 +329,7 @@ export class NotificationsCenter extends Themable implements INotificationsCente
316329
}
317330
}
318331

332+
319333
registerThemingParticipant((theme, collector) => {
320334
const notificationBorderColor = theme.getColor(NOTIFICATIONS_BORDER);
321335
if (notificationBorderColor) {

0 commit comments

Comments
 (0)