refactor(notifications): extract routing predicates and fix pre-overlay

- Extract shouldShowOsd/Overlay/Desktop into notification-routing.ts (was duplicated in 3 files)
- Add resolveOverlayReadinessNotificationType: preserves system channel when overlay not ready (both→osd-system, system→system, overlay→osd)
- Route overlay loading status through showConfiguredStatusNotification instead of raw OSD
This commit is contained in:
2026-06-04 22:24:56 -07:00
parent 9247248d48
commit a01fc57053
7 changed files with 94 additions and 48 deletions
+9 -5
View File
@@ -609,6 +609,7 @@ import {
notifyConfiguredStatus, notifyConfiguredStatus,
type ConfiguredStatusNotificationOptions, type ConfiguredStatusNotificationOptions,
} from './main/runtime/configured-status-notification'; } from './main/runtime/configured-status-notification';
import { resolveOverlayReadinessNotificationType } from './main/runtime/notification-routing';
import { createUpdateDialogPresenter } from './main/runtime/update/update-dialogs'; import { createUpdateDialogPresenter } from './main/runtime/update/update-dialogs';
import { import {
runUpdateCliCommand, runUpdateCliCommand,
@@ -3337,10 +3338,7 @@ function isVisibleOverlayContentReady(): boolean {
function getConfiguredStatusNotificationType(): NotificationType { function getConfiguredStatusNotificationType(): NotificationType {
const configuredType = getResolvedConfig().ankiConnect.behavior.notificationType; const configuredType = getResolvedConfig().ankiConnect.behavior.notificationType;
if (configuredType === 'none' || isVisibleOverlayContentReady()) { return resolveOverlayReadinessNotificationType(configuredType, isVisibleOverlayContentReady());
return configuredType;
}
return 'osd';
} }
function sendOverlayNotificationEvent(payload: OverlayNotificationEventPayload): void { function sendOverlayNotificationEvent(payload: OverlayNotificationEventPayload): void {
@@ -3414,7 +3412,13 @@ function showYoutubeFlowStatusNotification(message: string): void {
} }
function showOverlayLoadingStatusNotification(message: string): void { function showOverlayLoadingStatusNotification(message: string): void {
showMpvOsd(message); showConfiguredStatusNotification(message, {
id: 'overlay-loading-status',
title: 'SubMiner',
variant: 'progress',
persistent: true,
desktop: false,
});
} }
const buildBroadcastRuntimeOptionsChangedMainDepsHandler = const buildBroadcastRuntimeOptionsChangedMainDepsHandler =
@@ -1,6 +1,7 @@
import type { CharacterDictionaryAutoSyncStatusEvent } from './character-dictionary-auto-sync'; import type { CharacterDictionaryAutoSyncStatusEvent } from './character-dictionary-auto-sync';
import type { StartupOsdSequencerCharacterDictionaryEvent } from './startup-osd-sequencer'; import type { StartupOsdSequencerCharacterDictionaryEvent } from './startup-osd-sequencer';
import type { NotificationType, OverlayNotificationPayload } from '../../types/notification'; import type { NotificationType, OverlayNotificationPayload } from '../../types/notification';
import { shouldShowDesktop, shouldShowOverlay, shouldShowOsd } from './notification-routing';
export type CharacterDictionaryAutoSyncNotificationEvent = CharacterDictionaryAutoSyncStatusEvent; export type CharacterDictionaryAutoSyncNotificationEvent = CharacterDictionaryAutoSyncStatusEvent;
@@ -16,18 +17,6 @@ export interface CharacterDictionaryAutoSyncNotificationDeps {
}; };
} }
function shouldShowOsd(type: NotificationType): boolean {
return type === 'osd' || type === 'osd-system';
}
function shouldShowOverlay(type: NotificationType): boolean {
return type === 'overlay' || type === 'both';
}
function shouldShowDesktop(type: NotificationType): boolean {
return type === 'system' || type === 'both' || type === 'osd-system';
}
function isTerminalPhase(phase: CharacterDictionaryAutoSyncNotificationEvent['phase']): boolean { function isTerminalPhase(phase: CharacterDictionaryAutoSyncNotificationEvent['phase']): boolean {
return phase === 'ready' || phase === 'failed'; return phase === 'ready' || phase === 'failed';
} }
@@ -27,7 +27,7 @@ test('notifyConfiguredStatus routes both to overlay and system without osd', ()
]); ]);
}); });
test('notifyConfiguredStatus routes pre-overlay status to osd only', () => { test('notifyConfiguredStatus routes pre-overlay both status to osd and desktop', () => {
const calls: string[] = []; const calls: string[] = [];
notifyConfiguredStatus('Overlay loading...', { notifyConfiguredStatus('Overlay loading...', {
@@ -42,7 +42,25 @@ test('notifyConfiguredStatus routes pre-overlay status to osd only', () => {
calls.push(`desktop:${title}:${options.body ?? ''}`), calls.push(`desktop:${title}:${options.body ?? ''}`),
}); });
assert.deepEqual(calls, ['osd:Overlay loading...']); assert.deepEqual(calls, ['osd:Overlay loading...', 'desktop:SubMiner:Overlay loading...']);
});
test('notifyConfiguredStatus routes pre-overlay system status to desktop only', () => {
const calls: string[] = [];
notifyConfiguredStatus('Overlay loading...', {
getNotificationType: () => 'system',
isOverlayReady: () => false,
showOsd: (message) => {
calls.push(`osd:${message}`);
},
showOverlayNotification: (payload) =>
calls.push(`overlay:${payload.id ?? ''}:${payload.body ?? ''}`),
showDesktopNotification: (title, options) =>
calls.push(`desktop:${title}:${options.body ?? ''}`),
});
assert.deepEqual(calls, ['desktop:SubMiner:Overlay loading...']);
}); });
test('notifyConfiguredStatus keeps osd-system on legacy surfaces', () => { test('notifyConfiguredStatus keeps osd-system on legacy surfaces', () => {
@@ -1,4 +1,5 @@
import type { NotificationType, OverlayNotificationPayload } from '../../types/notification'; import type { NotificationType, OverlayNotificationPayload } from '../../types/notification';
import { shouldShowDesktop, shouldShowOverlay, shouldShowOsd } from './notification-routing';
export interface ConfiguredStatusNotificationDeps { export interface ConfiguredStatusNotificationDeps {
getNotificationType: () => NotificationType | undefined; getNotificationType: () => NotificationType | undefined;
@@ -17,18 +18,6 @@ export interface ConfiguredStatusNotificationOptions {
delivery?: 'notification' | 'feedback'; delivery?: 'notification' | 'feedback';
} }
function shouldShowOverlay(type: NotificationType): boolean {
return type === 'overlay' || type === 'both';
}
function shouldShowOsd(type: NotificationType): boolean {
return type === 'osd' || type === 'osd-system';
}
function shouldShowDesktop(type: NotificationType): boolean {
return type === 'system' || type === 'both' || type === 'osd-system';
}
export function getPlaybackFeedbackNotificationOptions( export function getPlaybackFeedbackNotificationOptions(
message: string, message: string,
): ConfiguredStatusNotificationOptions { ): ConfiguredStatusNotificationOptions {
@@ -60,12 +49,9 @@ export function notifyConfiguredStatus(
return; return;
} }
if (deps.isOverlayReady?.() === false) { const overlayReady = deps.isOverlayReady?.() !== false;
deps.showOsd(message);
return;
}
if (showOverlay) { if (showOverlay && overlayReady) {
if (deps.showOverlayNotification) { if (deps.showOverlayNotification) {
deps.showOverlayNotification({ deps.showOverlayNotification({
id: options.id, id: options.id,
@@ -77,6 +63,8 @@ export function notifyConfiguredStatus(
} else if (desktopEnabled && !shouldShowDesktop(type)) { } else if (desktopEnabled && !shouldShowDesktop(type)) {
deps.showDesktopNotification(options.title ?? 'SubMiner', { body: message }); deps.showDesktopNotification(options.title ?? 'SubMiner', { body: message });
} }
} else if (showOverlay && !showOsd) {
deps.showOsd(message);
} }
if (showOsd) { if (showOsd) {
@@ -0,0 +1,29 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
resolveOverlayReadinessNotificationType,
shouldShowDesktop,
shouldShowOverlay,
shouldShowOsd,
} from './notification-routing';
test('notification routing preserves system notification while overlay is not ready', () => {
assert.equal(resolveOverlayReadinessNotificationType('system', false), 'system');
});
test('notification routing preserves both as osd plus system while overlay is not ready', () => {
assert.equal(resolveOverlayReadinessNotificationType('both', false), 'osd-system');
});
test('notification routing falls back overlay-only notification to osd while overlay is not ready', () => {
assert.equal(resolveOverlayReadinessNotificationType('overlay', false), 'osd');
});
test('notification routing predicates classify delivery channels', () => {
assert.equal(shouldShowOverlay('both'), true);
assert.equal(shouldShowOverlay('system'), false);
assert.equal(shouldShowOsd('osd-system'), true);
assert.equal(shouldShowOsd('both'), false);
assert.equal(shouldShowDesktop('osd-system'), true);
assert.equal(shouldShowDesktop('overlay'), false);
});
+29
View File
@@ -0,0 +1,29 @@
import type { NotificationType } from '../../types/notification';
export function shouldShowOsd(type: NotificationType): boolean {
return type === 'osd' || type === 'osd-system';
}
export function shouldShowOverlay(type: NotificationType): boolean {
return type === 'overlay' || type === 'both';
}
export function shouldShowDesktop(type: NotificationType): boolean {
return type === 'system' || type === 'both' || type === 'osd-system';
}
export function resolveOverlayReadinessNotificationType(
type: NotificationType,
overlayReady: boolean,
): NotificationType {
if (overlayReady) {
return type;
}
if (type === 'overlay') {
return 'osd';
}
if (type === 'both') {
return 'osd-system';
}
return type;
}
+1 -12
View File
@@ -1,4 +1,5 @@
import type { NotificationType, OverlayNotificationPayload } from '../../types/notification'; import type { NotificationType, OverlayNotificationPayload } from '../../types/notification';
import { shouldShowDesktop, shouldShowOverlay, shouldShowOsd } from './notification-routing';
export interface StartupOsdSequencerCharacterDictionaryEvent { export interface StartupOsdSequencerCharacterDictionaryEvent {
phase: 'checking' | 'generating' | 'syncing' | 'building' | 'importing' | 'ready' | 'failed'; phase: 'checking' | 'generating' | 'syncing' | 'building' | 'importing' | 'ready' | 'failed';
@@ -21,18 +22,6 @@ interface StartupStatusNotificationOptions {
desktop?: boolean; desktop?: boolean;
} }
function shouldShowOsd(type: NotificationType): boolean {
return type === 'osd' || type === 'osd-system';
}
function shouldShowOverlay(type: NotificationType): boolean {
return type === 'overlay' || type === 'both';
}
function shouldShowDesktop(type: NotificationType): boolean {
return type === 'system' || type === 'both' || type === 'osd-system';
}
export function createStartupOsdSequencer(deps: StartupOsdSequencerDeps): { export function createStartupOsdSequencer(deps: StartupOsdSequencerDeps): {
reset: () => void; reset: () => void;
showTokenizationLoading: (message: string) => void; showTokenizationLoading: (message: string) => void;