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
@@ -57,6 +57,9 @@ import { tryGetFilePath } from "@vrtmrz/livesync-commonlib/compat/common/utils.d
import { configureHiddenFileSyncMode } from "./configureHiddenFileSyncMode.ts";
type SyncDirection = "push" | "pull" | "safe" | "pullForce" | "pushForce";
const HIDDEN_FILE_NOTICE_GROUP = "hidden-file-changes";
const HIDDEN_FILE_NOTICE_DURATION_MS = 20_000;
declare global {
interface OPTIONAL_SYNC_FEATURES {
FETCH: "FETCH";
@@ -1227,6 +1230,8 @@ Offline Changed files: ${files.length}`;
notifyConfigChange() {
const updatedFolders = [...this.queuedNotificationFiles];
this.queuedNotificationFiles.clear();
const noticeGroups = this.services.context.noticeGroups;
let hasNoticeItems = false;
try {
//@ts-ignore
const manifests = Object.values(this.app.plugins.manifests) as unknown as PluginManifest[];
@@ -1238,31 +1243,33 @@ Offline Changed files: ${files.length}`;
// If notified about plug-ins, reloading Obsidian may not be necessary.
const updatePluginId = manifest.id;
const updatePluginName = manifest.name;
this.core.confirm.askInPopup(
`updated-${updatePluginId}`,
`Files in ${updatePluginName} has been updated!\nPress {HERE} to reload ${updatePluginName}, or press elsewhere to dismiss this message.`,
(anchor) => {
anchor.text = "HERE";
anchor.addEventListener("click", () => {
const itemKey = `plugin:${updatePluginId}`;
noticeGroups.setItem(HIDDEN_FILE_NOTICE_GROUP, itemKey, {
message: `Files in ${updatePluginName} were updated.`,
action: {
label: `Reload ${updatePluginName}`,
onSelect: () => {
fireAndForget(async () => {
this._log(
`Unloading plugin: ${updatePluginName}`,
LOG_LEVEL_NOTICE,
"plugin-reload-" + updatePluginId
);
// @ts-ignore
// @ts-ignore -- Obsidian does not expose the plug-in lifecycle methods in its public API.
await this.app.plugins.unloadPlugin(updatePluginId);
// @ts-ignore
// @ts-ignore -- Obsidian does not expose the plug-in lifecycle methods in its public API.
await this.app.plugins.loadPlugin(updatePluginId);
this._log(
`Plugin reloaded: ${updatePluginName}`,
LOG_LEVEL_NOTICE,
"plugin-reload-" + updatePluginId
);
noticeGroups.removeItem(HIDDEN_FILE_NOTICE_GROUP, itemKey);
});
});
}
);
},
},
});
hasNoticeItems = true;
}
} catch (ex) {
this._log("Error on checking plugin status.");
@@ -1272,18 +1279,24 @@ Offline Changed files: ${files.length}`;
// If something changes left, notify for reloading Obsidian.
if (updatedFolders.indexOf(this.services.API.getSystemConfigDir()) >= 0) {
if (!this.services.appLifecycle.isReloadingScheduled()) {
this.core.confirm.askInPopup(
`updated-any-hidden`,
`Some setting files have been modified\nPress {HERE} to schedule a reload of Obsidian, or press elsewhere to dismiss this message.`,
(anchor) => {
anchor.text = "HERE";
anchor.addEventListener("click", () => {
noticeGroups.setItem(HIDDEN_FILE_NOTICE_GROUP, "restart", {
message: "Other Obsidian settings files were updated.",
action: {
label: "Schedule an Obsidian restart",
onSelect: () => {
this.services.appLifecycle.scheduleRestart();
});
}
);
noticeGroups.removeItem(HIDDEN_FILE_NOTICE_GROUP, "restart");
},
},
});
hasNoticeItems = true;
} else {
noticeGroups.removeItem(HIDDEN_FILE_NOTICE_GROUP, "restart");
}
}
if (hasNoticeItems) {
noticeGroups.finish(HIDDEN_FILE_NOTICE_GROUP, { durationMs: HIDDEN_FILE_NOTICE_DURATION_MS });
}
}
queueNotification(key: FilePath) {
@@ -0,0 +1,105 @@
import { describe, expect, it, vi } from "vitest";
vi.mock("@/deps.ts", () => ({}));
vi.mock("@/features/HiddenFileCommon/JsonResolveModal.ts", () => ({
JsonResolveModal: class JsonResolveModal {},
}));
vi.mock("@/features/LiveSyncCommands.ts", () => ({
LiveSyncCommands: class LiveSyncCommands {
plugin!: { app: unknown };
core!: { services: unknown };
get app() {
return this.plugin.app;
}
get services() {
return this.core.services;
}
},
}));
vi.mock("./configureHiddenFileSyncMode.ts", () => ({
configureHiddenFileSyncMode: vi.fn(),
}));
import { HiddenFileSync } from "./CmdHiddenFileSync.ts";
describe("HiddenFileSync configuration-change notices", () => {
it("groups plug-in reloads and an Obsidian restart into one finished Notice", async () => {
const noticeGroups = {
setItem: vi.fn(),
finish: vi.fn(() => true),
removeItem: vi.fn(() => true),
};
const plugin = {
app: {
plugins: {
manifests: {
alpha: {
id: "alpha",
name: "Alpha",
dir: ".obsidian/plugins/alpha",
},
beta: {
id: "beta",
name: "Beta",
dir: ".obsidian/plugins/beta",
},
},
enabledPlugins: new Set(["alpha", "beta"]),
unloadPlugin: vi.fn(async () => undefined),
loadPlugin: vi.fn(async () => undefined),
},
},
};
const core = {
confirm: { askInPopup: vi.fn() },
services: {
context: { noticeGroups },
API: { getSystemConfigDir: vi.fn(() => ".obsidian") },
appLifecycle: {
isReloadingScheduled: vi.fn(() => false),
scheduleRestart: vi.fn(),
},
},
};
const hiddenFileSync = Object.create(HiddenFileSync.prototype) as HiddenFileSync;
Object.assign(hiddenFileSync, {
plugin,
core,
queuedNotificationFiles: new Set([".obsidian/plugins/alpha", ".obsidian/plugins/beta", ".obsidian"]),
_log: vi.fn(),
});
hiddenFileSync.notifyConfigChange();
expect(noticeGroups.setItem).toHaveBeenNthCalledWith(1, "hidden-file-changes", "plugin:alpha", {
message: "Files in Alpha were updated.",
action: expect.objectContaining({ label: "Reload Alpha" }),
});
expect(noticeGroups.setItem).toHaveBeenNthCalledWith(2, "hidden-file-changes", "plugin:beta", {
message: "Files in Beta were updated.",
action: expect.objectContaining({ label: "Reload Beta" }),
});
expect(noticeGroups.setItem).toHaveBeenNthCalledWith(3, "hidden-file-changes", "restart", {
message: "Other Obsidian settings files were updated.",
action: expect.objectContaining({ label: "Schedule an Obsidian restart" }),
});
expect(noticeGroups.setItem.mock.calls.every(([groupKey]) => groupKey === "hidden-file-changes")).toBe(true);
expect(noticeGroups.finish).toHaveBeenCalledWith("hidden-file-changes", { durationMs: 20_000 });
expect(core.confirm.askInPopup).not.toHaveBeenCalled();
const reloadAction = (noticeGroups.setItem.mock.calls[0]?.[2] as { action: { onSelect: () => void } }).action
.onSelect;
reloadAction();
await vi.waitFor(() => {
expect(plugin.app.plugins.unloadPlugin).toHaveBeenCalledWith("alpha");
expect(plugin.app.plugins.loadPlugin).toHaveBeenCalledWith("alpha");
expect(noticeGroups.removeItem).toHaveBeenCalledWith("hidden-file-changes", "plugin:alpha");
});
const restartAction = (noticeGroups.setItem.mock.calls[2]?.[2] as { action: { onSelect: () => void } }).action
.onSelect;
restartAction();
expect(core.services.appLifecycle.scheduleRestart).toHaveBeenCalledOnce();
expect(noticeGroups.removeItem).toHaveBeenCalledWith("hidden-file-changes", "restart");
});
});