Group Obsidian startup notices by operation

This commit is contained in:
vorotamoroz
2026-07-19 07:04:54 +00:00
parent ed3f81e9f9
commit d28282edad
10 changed files with 613 additions and 164 deletions
@@ -0,0 +1,51 @@
import { KeyedNoticeGroupManager } from "@vrtmrz/obsidian-plugin-kit/notice";
export interface ObsidianNoticeGroupItem {
message: string;
action?: {
label: string;
onSelect: () => void;
};
}
interface KeyedNoticeGroupDriver {
setItem(groupKey: string, itemKey: string, item: ObsidianNoticeGroupItem): unknown;
finish(groupKey: string, options?: { durationMs?: number | false }): boolean;
removeItem(groupKey: string, itemKey: string): boolean;
hide(groupKey: string): boolean;
dispose(): void;
}
/** Obsidian-owned interactive Notice capability exposed through the application Context. */
export interface ObsidianNoticeGroups {
setItem(groupKey: string, itemKey: string, item: ObsidianNoticeGroupItem): void;
finish(groupKey: string, options?: { durationMs?: number | false }): boolean;
removeItem(groupKey: string, itemKey: string): boolean;
hide(groupKey: string): boolean;
dispose(): void;
}
/** Adapts Fancy Kit grouped Notices without exposing Obsidian Notice instances to features. */
export class ObsidianNoticeGroupManager implements ObsidianNoticeGroups {
constructor(private readonly manager: KeyedNoticeGroupDriver = new KeyedNoticeGroupManager()) {}
setItem(groupKey: string, itemKey: string, item: ObsidianNoticeGroupItem): void {
this.manager.setItem(groupKey, itemKey, item);
}
finish(groupKey: string, options?: { durationMs?: number | false }): boolean {
return this.manager.finish(groupKey, options);
}
removeItem(groupKey: string, itemKey: string): boolean {
return this.manager.removeItem(groupKey, itemKey);
}
hide(groupKey: string): boolean {
return this.manager.hide(groupKey);
}
dispose(): void {
this.manager.dispose();
}
}
@@ -0,0 +1,32 @@
import { describe, expect, it, vi } from "vitest";
vi.mock("@vrtmrz/obsidian-plugin-kit/notice", () => ({
KeyedNoticeGroupManager: class KeyedNoticeGroupManager {},
}));
import { ObsidianNoticeGroupManager } from "./ObsidianNoticeGroups";
describe("ObsidianNoticeGroupManager", () => {
it("keeps Fancy Kit rendering behind the Context-owned capability", () => {
const driver = {
setItem: vi.fn(),
finish: vi.fn(() => true),
removeItem: vi.fn(() => true),
hide: vi.fn(() => true),
dispose: vi.fn(),
};
const groups = new ObsidianNoticeGroupManager(driver);
const item = {
message: "Complete",
action: { label: "Review", onSelect: vi.fn() },
};
groups.setItem("integrity", "result", item);
expect(driver.setItem).toHaveBeenCalledWith("integrity", "result", item);
expect(groups.finish("integrity", { durationMs: 1_000 })).toBe(true);
expect(groups.removeItem("integrity", "result")).toBe(true);
expect(groups.hide("integrity")).toBe(true);
groups.dispose();
expect(driver.dispose).toHaveBeenCalledOnce();
});
});
@@ -3,17 +3,20 @@ import type { App, Plugin } from "@/deps";
import { ServiceContext } from "@vrtmrz/livesync-commonlib/context";
import { eventHub } from "@/common/events";
import { translateLiveSyncMessage } from "@/common/translation";
import type { ObsidianNoticeGroups } from "./ObsidianNoticeGroups";
/** Host capabilities owned by one Self-hosted LiveSync plug-in instance. */
export class ObsidianServiceContext extends ServiceContext {
app: App;
plugin: Plugin;
liveSyncPlugin: ObsidianLiveSyncPlugin;
readonly noticeGroups: ObsidianNoticeGroups;
constructor(app: App, plugin: Plugin, liveSyncPlugin: ObsidianLiveSyncPlugin) {
constructor(app: App, plugin: Plugin, liveSyncPlugin: ObsidianLiveSyncPlugin, noticeGroups: ObsidianNoticeGroups) {
super({ events: eventHub, translate: translateLiveSyncMessage });
this.app = app;
this.plugin = plugin;
this.liveSyncPlugin = liveSyncPlugin;
this.noticeGroups = noticeGroups;
}
}
@@ -13,7 +13,8 @@ describe("ObsidianServiceContext contract", () => {
const app = {} as Parameters[0];
const plugin = {} as Parameters[1];
const liveSyncPlugin = {} as Parameters[2];
const context = new ObsidianServiceContext(app, plugin, liveSyncPlugin);
const noticeGroups = {} as Parameters[3];
const context = new ObsidianServiceContext(app, plugin, liveSyncPlugin, noticeGroups);
expect(observeServiceContext(context, TRANSLATION_KEY)).toEqual({
translation: translateLiveSyncMessage(TRANSLATION_KEY),
@@ -23,5 +24,6 @@ describe("ObsidianServiceContext contract", () => {
expect(context.app).toBe(app);
expect(context.plugin).toBe(plugin);
expect(context.liveSyncPlugin).toBe(liveSyncPlugin);
expect(context.noticeGroups).toBe(noticeGroups);
});
});
+4 -1
View File
@@ -25,12 +25,14 @@ import { ObsidianUIService } from "./ObsidianUIService";
import { createScreenWakeLockManager } from "octagonal-wheels/browser/wakeLock";
import { PouchDB } from "@vrtmrz/livesync-commonlib/compat/pouchdb/pouchdb-browser";
import { OpenKeyValueDatabase } from "@/common/KeyValueDB";
import { ObsidianNoticeGroupManager } from "./ObsidianNoticeGroups";
// InjectableServiceHub
export class ObsidianServiceHub extends InjectableServiceHub<ObsidianServiceContext> {
constructor(plugin: ObsidianLiveSyncPlugin) {
const context = new ObsidianServiceContext(plugin.app, plugin, plugin);
const noticeGroups = new ObsidianNoticeGroupManager();
const context = new ObsidianServiceContext(plugin.app, plugin, plugin, noticeGroups);
const API = new ObsidianAPIService(context);
const conflict = new ObsidianConflictService(context);
@@ -62,6 +64,7 @@ export class ObsidianServiceHub extends InjectableServiceHub<ObsidianServiceCont
const screenWakeLock = createScreenWakeLockManager();
appLifecycle.onUnload.addHandler(async () => {
await screenWakeLock.dispose();
noticeGroups.dispose();
return true;
});
const database = new ObsidianDatabaseService(context, {