Skip to content

Commit fa1d708

Browse files
authored
fix(view): reliably reveal PodNotes view via command + ribbon icon (#55) (#199)
* fix(view): reliably reveal PodNotes view via command + ribbon (#55) The Show PodNotes command was gated to only appear when no view leaf existed, and it never revealed the leaf it set. Once the view was open but hidden (collapsed sidebar, sidebar-icon overflow, or dragged out of sight) there was no way to bring it back: the command disappeared from the palette and did nothing when bound to a hotkey. Add an activateView() helper that reuses the existing leaf when present and always calls revealLeaf, wire the Show PodNotes command to it as a plain always-available callback, and add a left-ribbon podcast icon as a persistent, overflow-proof entry point. Update the command docs. Closes #55 * test(view): lock Show PodNotes command+ribbon wiring; soften ribbon docs (#55) Add an onload wiring test that asserts the podnotes-show-leaf command is registered as a plain always-available callback (never the old leaf-gated checkCallback) and that both the command and the ribbon icon route to activateView. activateView's own unit tests stay green under a refactor that reintroduced the gate, so this locks the actual #55 regression. Soften the ribbon docs: the icon is hideable via Manage ribbon actions and appears in the ribbon menu on mobile, so drop the 'always visible' claim. Addresses review nits; no behavior change.
1 parent 5953625 commit fa1d708

3 files changed

Lines changed: 206 additions & 11 deletions

File tree

docs/docs/commands.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
## Show PodNotes
2-
This command is only accessible if there doesn't exist a pane with PodNotes already.
2+
Opens the PodNotes pane and brings it into focus.
33

4-
Activating it will spawn a new pane with PodNotes in the right sidebar.
4+
If the pane already exists but is hidden — for example in a collapsed sidebar or out of view because the right sidebar has too many icons — this command reveals it. If it does not exist yet, the command creates it in the right sidebar. You can run it from the command palette or bind it to a hotkey.
55

6-
If you are having issues with PodNotes not being shown, feel free to create an [issue](https://github.com/chhoumann/PodNotes/issues/new). However, do make sure to check that the icon isn't just out of view by scrolling on the right sidebar.
6+
PodNotes also adds a **podcast icon to the left ribbon** as a reliable way to reopen the pane. On mobile it appears in the ribbon menu, and you can hide it via Obsidian's *Manage ribbon actions* if you prefer to use the command instead.
7+
8+
If you are having issues with PodNotes not being shown, feel free to create an [issue](https://github.com/chhoumann/PodNotes/issues/new).
79

810
## Play Podcast
911
This will start playback if the current episode is paused.

src/main.activateView.test.ts

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2+
import PodNotes from "./main";
3+
import { VIEW_TYPE } from "./constants";
4+
5+
// Regression coverage for #55: "Show PodNotes" / the ribbon icon must reliably
6+
// surface the view. The bug was that the command was gated on the leaf NOT
7+
// existing and never revealed it, so an already-open-but-hidden view (collapsed
8+
// or overflowing sidebar) could not be brought back. activateView reuses the
9+
// existing leaf when present and always reveals it.
10+
11+
function makeLeaf() {
12+
return {
13+
setViewState: vi.fn().mockResolvedValue(undefined),
14+
};
15+
}
16+
17+
function setupPlugin({
18+
existingLeaves = [] as ReturnType<typeof makeLeaf>[],
19+
rightLeaf = makeLeaf() as ReturnType<typeof makeLeaf> | null,
20+
} = {}) {
21+
const workspace = {
22+
getLeavesOfType: vi.fn().mockReturnValue(existingLeaves),
23+
getRightLeaf: vi.fn().mockReturnValue(rightLeaf),
24+
revealLeaf: vi.fn().mockResolvedValue(undefined),
25+
};
26+
27+
// Build a bare instance so we exercise activateView without running the full
28+
// onload() side effects (store wiring, command registration, etc.).
29+
const plugin = Object.create(PodNotes.prototype) as PodNotes;
30+
(plugin as unknown as { app: { workspace: typeof workspace } }).app = {
31+
workspace,
32+
};
33+
34+
return { plugin, workspace, rightLeaf };
35+
}
36+
37+
describe("PodNotes.activateView", () => {
38+
beforeEach(() => {
39+
vi.clearAllMocks();
40+
});
41+
42+
it("reuses an existing leaf and reveals it without creating a new one", async () => {
43+
const existing = makeLeaf();
44+
const { plugin, workspace } = setupPlugin({ existingLeaves: [existing] });
45+
46+
await plugin.activateView();
47+
48+
expect(workspace.getRightLeaf).not.toHaveBeenCalled();
49+
expect(existing.setViewState).not.toHaveBeenCalled();
50+
expect(workspace.revealLeaf).toHaveBeenCalledTimes(1);
51+
expect(workspace.revealLeaf).toHaveBeenCalledWith(existing);
52+
});
53+
54+
it("creates a right-sidebar leaf when none exists, then reveals it", async () => {
55+
const { plugin, workspace, rightLeaf } = setupPlugin();
56+
57+
await plugin.activateView();
58+
59+
expect(workspace.getRightLeaf).toHaveBeenCalledWith(false);
60+
expect(rightLeaf?.setViewState).toHaveBeenCalledWith({
61+
type: VIEW_TYPE,
62+
active: true,
63+
});
64+
expect(workspace.revealLeaf).toHaveBeenCalledWith(rightLeaf);
65+
});
66+
67+
it("does not throw or reveal when no right leaf is available", async () => {
68+
const { plugin, workspace } = setupPlugin({ rightLeaf: null });
69+
70+
await expect(plugin.activateView()).resolves.toBeUndefined();
71+
expect(workspace.revealLeaf).not.toHaveBeenCalled();
72+
});
73+
});
74+
75+
// Locks the actual #55 wiring (not just activateView's internals): the
76+
// "Show PodNotes" command must stay always-available (a plain callback, never
77+
// a leaf-gated checkCallback) and the ribbon icon must route to activateView.
78+
// A refactor that reintroduced the old checkCallback gate or unwired the ribbon
79+
// would reproduce the bug while activateView's own unit tests stayed green.
80+
describe("PodNotes onload wiring (#55)", () => {
81+
// onload() wires real module-level stores to controllers; unload them after
82+
// each test so leaked subscriptions don't fire into a disposed plugin.
83+
const loaded: PodNotes[] = [];
84+
85+
beforeEach(() => {
86+
vi.clearAllMocks();
87+
});
88+
89+
afterEach(() => {
90+
for (const p of loaded.splice(0)) {
91+
p.onunload();
92+
}
93+
vi.restoreAllMocks();
94+
});
95+
96+
async function loadPlugin() {
97+
const activateSpy = vi
98+
.spyOn(PodNotes.prototype, "activateView")
99+
.mockResolvedValue(undefined);
100+
101+
const commands: Array<Record<string, unknown>> = [];
102+
const ribbonCalls: Array<{
103+
icon: string;
104+
title: string;
105+
handler: (evt: unknown) => unknown;
106+
}> = [];
107+
108+
const plugin = Object.create(PodNotes.prototype) as PodNotes;
109+
Object.assign(plugin, {
110+
loadData: vi.fn().mockResolvedValue({}),
111+
saveData: vi.fn().mockResolvedValue(undefined),
112+
addCommand: vi.fn((cmd: Record<string, unknown>) => {
113+
commands.push(cmd);
114+
return cmd;
115+
}),
116+
addRibbonIcon: vi.fn(
117+
(icon: string, title: string, handler: (evt: unknown) => unknown) => {
118+
ribbonCalls.push({ icon, title, handler });
119+
return document.createElement("div");
120+
},
121+
),
122+
addSettingTab: vi.fn(),
123+
registerView: vi.fn(),
124+
registerObsidianProtocolHandler: vi.fn(),
125+
registerEvent: vi.fn(),
126+
app: {
127+
workspace: {
128+
onLayoutReady: vi.fn(),
129+
on: vi.fn(() => ({})),
130+
getLeavesOfType: vi.fn(() => []),
131+
getRightLeaf: vi.fn(() => null),
132+
revealLeaf: vi.fn(),
133+
detachLeavesOfType: vi.fn(),
134+
},
135+
},
136+
});
137+
138+
await plugin.onload();
139+
loaded.push(plugin);
140+
141+
return { commands, ribbonCalls, activateSpy };
142+
}
143+
144+
it("registers Show PodNotes as an always-available callback, not a leaf-gated checkCallback", async () => {
145+
const { commands } = await loadPlugin();
146+
147+
const showCmd = commands.find((c) => c.id === "podnotes-show-leaf");
148+
expect(showCmd).toBeDefined();
149+
expect(typeof showCmd?.callback).toBe("function");
150+
expect(showCmd?.checkCallback).toBeUndefined();
151+
});
152+
153+
it("Show PodNotes command and ribbon icon both route to activateView", async () => {
154+
const { commands, ribbonCalls, activateSpy } = await loadPlugin();
155+
156+
const showCmd = commands.find((c) => c.id === "podnotes-show-leaf");
157+
(showCmd?.callback as () => void)();
158+
expect(activateSpy).toHaveBeenCalledTimes(1);
159+
160+
const ribbon = ribbonCalls.find((r) => r.title === "Show PodNotes");
161+
expect(ribbon).toBeDefined();
162+
expect(ribbon?.icon).toBe("podcast");
163+
ribbon?.handler(new MouseEvent("click"));
164+
expect(activateSpy).toHaveBeenCalledTimes(2);
165+
});
166+
});

src/main.ts

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -155,14 +155,13 @@ export default class PodNotes extends Plugin implements IPodNotes {
155155
id: "podnotes-show-leaf",
156156
name: "Show PodNotes",
157157
icon: "podcast" as IconType,
158-
checkCallback: (checking: boolean) => {
159-
if (checking) {
160-
return !this.app.workspace.getLeavesOfType(VIEW_TYPE).length;
161-
}
162-
163-
this.app.workspace.getRightLeaf(false)?.setViewState({
164-
type: VIEW_TYPE,
165-
});
158+
// Always available, and always reveals the view. The previous
159+
// checkCallback hid this command whenever a leaf already existed, so
160+
// once the view was open-but-hidden (collapsed sidebar, sidebar
161+
// overflow, dragged out of sight) there was no way to bring it back
162+
// (#55). activateView reuses the existing leaf and reveals it.
163+
callback: () => {
164+
void this.activateView();
166165
},
167166
});
168167

@@ -377,6 +376,14 @@ export default class PodNotes extends Plugin implements IPodNotes {
377376
return this.view;
378377
});
379378

379+
// Persistent, discoverable entry point in the left ribbon. The right
380+
// sidebar header can overflow and hide the view's tab icon (the original
381+
// report in #55), but the ribbon is always reachable, so users can always
382+
// reopen PodNotes.
383+
this.addRibbonIcon("podcast" as IconType, "Show PodNotes", () => {
384+
void this.activateView();
385+
});
386+
380387
this.app.workspace.onLayoutReady(this.onLayoutReady.bind(this));
381388

382389
this.registerObsidianProtocolHandler("podnotes", (action) =>
@@ -415,6 +422,26 @@ export default class PodNotes extends Plugin implements IPodNotes {
415422
}
416423
}
417424

425+
// Reveal the PodNotes view, creating its leaf when needed. Reusing an
426+
// existing leaf (instead of gating on its absence) plus revealLeaf is what
427+
// makes "Show PodNotes" and the ribbon icon reliably surface the view even
428+
// when it is already open but hidden in a collapsed/overflowing sidebar (#55).
429+
async activateView(): Promise<void> {
430+
const { workspace } = this.app;
431+
432+
const existing = workspace.getLeavesOfType(VIEW_TYPE);
433+
let leaf: WorkspaceLeaf | null = existing[0] ?? null;
434+
435+
if (!leaf) {
436+
leaf = workspace.getRightLeaf(false);
437+
await leaf?.setViewState({ type: VIEW_TYPE, active: true });
438+
}
439+
440+
if (leaf) {
441+
await workspace.revealLeaf(leaf);
442+
}
443+
}
444+
418445
private getTranscriptionService(): TranscriptionService {
419446
if (!this.transcriptionService) {
420447
this.transcriptionService = new TranscriptionService(this);

0 commit comments

Comments
 (0)