mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-12 01:55:55 -07:00
fix(notification): replace repeated desktop status toasts
- Add optional replaceId to showDesktopNotification; a repeated id closes the prior toast instead of stacking a new one - Character dictionary auto-sync now shares one replaceId across checking/generating/importing/ready phases - Startup OSD sequencer passes its lane id as replaceId so each startup lane keeps a single live notification
This commit is contained in:
@@ -52,9 +52,15 @@ function resolveRuntimeDefaultNotificationIconPath(): string | null {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Live notifications keyed by `replaceId`. Electron exposes no native "replace this notification"
|
||||||
|
* flag, so a repeated status closes its predecessor instead of stacking a fresh toast per update.
|
||||||
|
*/
|
||||||
|
const notificationsByReplaceId = new Map<string, Electron.Notification>();
|
||||||
|
|
||||||
export function showDesktopNotification(
|
export function showDesktopNotification(
|
||||||
title: string,
|
title: string,
|
||||||
options: { body?: string; icon?: string },
|
options: { body?: string; icon?: string; replaceId?: string },
|
||||||
): void {
|
): void {
|
||||||
const notificationOptions: {
|
const notificationOptions: {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -98,5 +104,15 @@ export function showDesktopNotification(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const notification = new Notification(notificationOptions);
|
const notification = new Notification(notificationOptions);
|
||||||
|
const replaceId = options.replaceId?.trim();
|
||||||
|
if (replaceId) {
|
||||||
|
notificationsByReplaceId.get(replaceId)?.close();
|
||||||
|
notificationsByReplaceId.set(replaceId, notification);
|
||||||
|
notification.once('close', () => {
|
||||||
|
if (notificationsByReplaceId.get(replaceId) === notification) {
|
||||||
|
notificationsByReplaceId.delete(replaceId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
notification.show();
|
notification.show();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -214,3 +214,69 @@ test('auto sync notifications let startup sequencer own osd-system desktop deliv
|
|||||||
|
|
||||||
assert.deepEqual(calls, ['osd:importing', 'desktop:SubMiner:importing']);
|
assert.deepEqual(calls, ['osd:importing', 'desktop:SubMiner:importing']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('auto sync desktop notifications reuse one replace id across every phase', () => {
|
||||||
|
const replaceIds: Array<string | undefined> = [];
|
||||||
|
const deps = {
|
||||||
|
getNotificationType: () => 'system' as const,
|
||||||
|
showOsd: () => undefined,
|
||||||
|
showDesktopNotification: (_title: string, options: { body?: string; replaceId?: string }) => {
|
||||||
|
replaceIds.push(options.replaceId);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const phase of ['checking', 'generating', 'importing', 'ready'] as const) {
|
||||||
|
notifyCharacterDictionaryAutoSyncStatus(makeEvent(phase, phase), deps);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.deepEqual(replaceIds, [
|
||||||
|
'character-dictionary-auto-sync',
|
||||||
|
'character-dictionary-auto-sync',
|
||||||
|
'character-dictionary-auto-sync',
|
||||||
|
'character-dictionary-auto-sync',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('overlay-unavailable desktop fallback shares the same replace id', () => {
|
||||||
|
const replaceIds: Array<string | undefined> = [];
|
||||||
|
|
||||||
|
notifyCharacterDictionaryAutoSyncStatus(makeEvent('generating', 'generating'), {
|
||||||
|
getNotificationType: () => 'overlay',
|
||||||
|
showOsd: () => undefined,
|
||||||
|
showDesktopNotification: (_title, options) => {
|
||||||
|
replaceIds.push(options.replaceId);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(replaceIds, ['character-dictionary-auto-sync']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('startup lanes keep one desktop notification per lane', () => {
|
||||||
|
const calls: Array<{ body?: string; replaceId?: string }> = [];
|
||||||
|
const sequencer = createStartupOsdSequencer({
|
||||||
|
getNotificationType: () => 'system',
|
||||||
|
showOsd: () => undefined,
|
||||||
|
showDesktopNotification: (_title, options) => {
|
||||||
|
calls.push(options);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
sequencer.markTokenizationReady();
|
||||||
|
notifyCharacterDictionaryAutoSyncStatus(makeEvent('generating', 'generating one'), {
|
||||||
|
getNotificationType: () => 'osd',
|
||||||
|
showOsd: () => undefined,
|
||||||
|
showDesktopNotification: () => undefined,
|
||||||
|
startupOsdSequencer: sequencer,
|
||||||
|
});
|
||||||
|
notifyCharacterDictionaryAutoSyncStatus(makeEvent('generating', 'generating two'), {
|
||||||
|
getNotificationType: () => 'osd',
|
||||||
|
showOsd: () => undefined,
|
||||||
|
showDesktopNotification: () => undefined,
|
||||||
|
startupOsdSequencer: sequencer,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
calls.map((call) => call.replaceId),
|
||||||
|
['startup-status', 'startup-status'],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export interface CharacterDictionaryAutoSyncNotificationDeps {
|
|||||||
getNotificationType: () => NotificationType | undefined;
|
getNotificationType: () => NotificationType | undefined;
|
||||||
showOsd: (message: string) => boolean | void;
|
showOsd: (message: string) => boolean | void;
|
||||||
showOverlayNotification?: (payload: OverlayNotificationPayload) => void;
|
showOverlayNotification?: (payload: OverlayNotificationPayload) => void;
|
||||||
showDesktopNotification: (title: string, options: { body?: string }) => void;
|
showDesktopNotification: (title: string, options: { body?: string; replaceId?: string }) => void;
|
||||||
startupOsdSequencer?: {
|
startupOsdSequencer?: {
|
||||||
notifyCharacterDictionaryStatus: (
|
notifyCharacterDictionaryStatus: (
|
||||||
event: StartupOsdSequencerCharacterDictionaryEvent,
|
event: StartupOsdSequencerCharacterDictionaryEvent,
|
||||||
@@ -17,6 +17,10 @@ export interface CharacterDictionaryAutoSyncNotificationDeps {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// One live desktop notification for the whole sync: progress updates replace each other and the
|
||||||
|
// terminal ready/failed message replaces the last progress one, matching the overlay toast.
|
||||||
|
const CHARACTER_DICTIONARY_DESKTOP_NOTIFICATION_ID = 'character-dictionary-auto-sync';
|
||||||
|
|
||||||
function isTerminalPhase(phase: CharacterDictionaryAutoSyncNotificationEvent['phase']): boolean {
|
function isTerminalPhase(phase: CharacterDictionaryAutoSyncNotificationEvent['phase']): boolean {
|
||||||
return phase === 'ready' || phase === 'failed';
|
return phase === 'ready' || phase === 'failed';
|
||||||
}
|
}
|
||||||
@@ -53,7 +57,10 @@ export function notifyCharacterDictionaryAutoSyncStatus(
|
|||||||
persistent: !isTerminalPhase(event.phase),
|
persistent: !isTerminalPhase(event.phase),
|
||||||
});
|
});
|
||||||
} else if (!shouldShowDesktop(type)) {
|
} else if (!shouldShowDesktop(type)) {
|
||||||
deps.showDesktopNotification('SubMiner', { body: event.message });
|
deps.showDesktopNotification('SubMiner', {
|
||||||
|
body: event.message,
|
||||||
|
replaceId: CHARACTER_DICTIONARY_DESKTOP_NOTIFICATION_ID,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,6 +76,9 @@ export function notifyCharacterDictionaryAutoSyncStatus(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (shouldShowDesktop(type) && !startupSequencerShown) {
|
if (shouldShowDesktop(type) && !startupSequencerShown) {
|
||||||
deps.showDesktopNotification('SubMiner', { body: event.message });
|
deps.showDesktopNotification('SubMiner', {
|
||||||
|
body: event.message,
|
||||||
|
replaceId: CHARACTER_DICTIONARY_DESKTOP_NOTIFICATION_ID,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export interface StartupOsdSequencerDeps {
|
|||||||
getNotificationType?: () => NotificationType | undefined;
|
getNotificationType?: () => NotificationType | undefined;
|
||||||
showOsd: (message: string) => boolean | void;
|
showOsd: (message: string) => boolean | void;
|
||||||
showOverlayNotification?: (payload: OverlayNotificationPayload) => void;
|
showOverlayNotification?: (payload: OverlayNotificationPayload) => void;
|
||||||
showDesktopNotification?: (title: string, options: { body?: string }) => void;
|
showDesktopNotification?: (title: string, options: { body?: string; replaceId?: string }) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StartupStatusNotificationOptions {
|
interface StartupStatusNotificationOptions {
|
||||||
@@ -62,7 +62,11 @@ export function createStartupOsdSequencer(deps: StartupOsdSequencerDeps): {
|
|||||||
shown = deps.showOsd(options.message) !== false || shown;
|
shown = deps.showOsd(options.message) !== false || shown;
|
||||||
}
|
}
|
||||||
if (options.desktop !== false && shouldShowDesktop(type)) {
|
if (options.desktop !== false && shouldShowDesktop(type)) {
|
||||||
deps.showDesktopNotification?.('SubMiner', { body: options.message });
|
// Each startup lane keeps one live desktop notification instead of one per update.
|
||||||
|
deps.showDesktopNotification?.('SubMiner', {
|
||||||
|
body: options.message,
|
||||||
|
replaceId: options.id,
|
||||||
|
});
|
||||||
shown = true;
|
shown = true;
|
||||||
}
|
}
|
||||||
return shown;
|
return shown;
|
||||||
|
|||||||
Reference in New Issue
Block a user