Skip to content

Commit 57dcb57

Browse files
author
ComputelessComputer
committed
fix: open summary notifications in the summary tab
Tag summary-ready notifications with a dedicated key, route notification clicks back to the enhanced note instead of starting a new recording, and cover the listener behavior with tests.
1 parent 5d9b1f7 commit 57dcb57

5 files changed

Lines changed: 182 additions & 24 deletions

File tree

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
3+
const sessionMocks = vi.hoisted(() => ({
4+
createSession: vi.fn().mockReturnValue("new-session"),
5+
getOrCreateSessionForEventId: vi
6+
.fn()
7+
.mockImplementation(
8+
(_store: unknown, eventId: string) => `event-${eventId}`,
9+
),
10+
}));
11+
12+
vi.mock("~/store/tinybase/store/sessions", () => ({
13+
createSession: sessionMocks.createSession,
14+
getOrCreateSessionForEventId: sessionMocks.getOrCreateSessionForEventId,
15+
}));
16+
17+
import { getNotificationOpenConfig } from "./event-listeners";
18+
import { createSummaryReadyNotificationKey } from "./summary-ready-notification";
19+
20+
describe("getNotificationOpenConfig", () => {
21+
beforeEach(() => {
22+
sessionMocks.createSession.mockClear();
23+
sessionMocks.getOrCreateSessionForEventId.mockClear();
24+
});
25+
26+
it("opens summary notifications in the enhanced note without autostart", () => {
27+
const store = {} as never;
28+
29+
expect(
30+
getNotificationOpenConfig(
31+
{
32+
key: createSummaryReadyNotificationKey("session-1", "note-1"),
33+
source: null,
34+
},
35+
store,
36+
),
37+
).toEqual({
38+
id: "session-1",
39+
state: {
40+
view: { type: "enhanced", id: "note-1" },
41+
autoStart: null,
42+
},
43+
});
44+
expect(sessionMocks.createSession).not.toHaveBeenCalled();
45+
expect(sessionMocks.getOrCreateSessionForEventId).not.toHaveBeenCalled();
46+
});
47+
48+
it("opens calendar event notifications in their linked session and autostarts", () => {
49+
const store = {} as never;
50+
51+
expect(
52+
getNotificationOpenConfig(
53+
{
54+
key: "event-1",
55+
source: { type: "calendar_event", event_id: "event-1" },
56+
},
57+
store,
58+
),
59+
).toEqual({
60+
id: "event-event-1",
61+
state: { view: null, autoStart: true },
62+
});
63+
expect(sessionMocks.getOrCreateSessionForEventId).toHaveBeenCalledWith(
64+
store,
65+
"event-1",
66+
);
67+
});
68+
69+
it("falls back to a new session for generic notification clicks", () => {
70+
const store = {} as never;
71+
72+
expect(
73+
getNotificationOpenConfig(
74+
{
75+
key: "generic-notification",
76+
source: null,
77+
},
78+
store,
79+
),
80+
).toEqual({
81+
id: "new-session",
82+
state: { view: null, autoStart: true },
83+
});
84+
expect(sessionMocks.createSession).toHaveBeenCalledWith(store);
85+
});
86+
});

apps/desktop/src/services/event-listeners.tsx

Lines changed: 59 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,61 @@
11
import { type UnlistenFn } from "@tauri-apps/api/event";
22
import { useEffect, useRef } from "react";
33

4-
import { events as notificationEvents } from "@hypr/plugin-notification";
4+
import {
5+
events as notificationEvents,
6+
type NotificationSource,
7+
} from "@hypr/plugin-notification";
58
import {
69
commands as updaterCommands,
710
events as updaterEvents,
811
} from "@hypr/plugin-updater2";
912
import { getCurrentWebviewWindowLabel } from "@hypr/plugin-windows";
1013

14+
import { parseSummaryReadyNotificationKey } from "./summary-ready-notification";
15+
1116
import * as main from "~/store/tinybase/store/main";
1217
import {
1318
createSession,
1419
getOrCreateSessionForEventId,
1520
} from "~/store/tinybase/store/sessions";
1621
import { useTabs } from "~/store/zustand/tabs";
1722

23+
type NotificationTarget = {
24+
key: string;
25+
source: NotificationSource | null;
26+
};
27+
28+
type MainStore = NonNullable<ReturnType<typeof main.UI.useStore>>;
29+
30+
export function getNotificationOpenConfig(
31+
notification: NotificationTarget,
32+
store: MainStore,
33+
) {
34+
const summaryTarget = parseSummaryReadyNotificationKey(notification.key);
35+
if (summaryTarget) {
36+
return {
37+
id: summaryTarget.sessionId,
38+
state: {
39+
view: { type: "enhanced" as const, id: summaryTarget.enhancedNoteId },
40+
autoStart: null,
41+
},
42+
};
43+
}
44+
45+
const eventId =
46+
notification.source?.type === "calendar_event"
47+
? notification.source.event_id
48+
: null;
49+
const sessionId = eventId
50+
? getOrCreateSessionForEventId(store, eventId)
51+
: createSession(store);
52+
53+
return {
54+
id: sessionId,
55+
state: { view: null, autoStart: true },
56+
};
57+
}
58+
1859
function useUpdaterEvents() {
1960
const openNew = useTabs((state) => state.openNew);
2061

@@ -46,7 +87,7 @@ function useUpdaterEvents() {
4687
function useNotificationEvents() {
4788
const store = main.UI.useStore(main.STORE_ID);
4889
const openNew = useTabs((state) => state.openNew);
49-
const pendingAutoStart = useRef<{ eventId: string | null } | null>(null);
90+
const pendingNotification = useRef<NotificationTarget | null>(null);
5091
const storeRef = useRef(store);
5192
const openNewRef = useRef(openNew);
5293

@@ -56,16 +97,14 @@ function useNotificationEvents() {
5697
}, [store, openNew]);
5798

5899
useEffect(() => {
59-
if (pendingAutoStart.current && store) {
60-
const { eventId } = pendingAutoStart.current;
61-
pendingAutoStart.current = null;
62-
const sessionId = eventId
63-
? getOrCreateSessionForEventId(store, eventId)
64-
: createSession(store);
100+
if (pendingNotification.current && store) {
101+
const notification = pendingNotification.current;
102+
pendingNotification.current = null;
103+
const { id, state } = getNotificationOpenConfig(notification, store);
65104
openNew({
66105
type: "sessions",
67-
id: sessionId,
68-
state: { view: null, autoStart: true },
106+
id,
107+
state,
69108
});
70109
}
71110
}, [store, openNew]);
@@ -84,22 +123,22 @@ function useNotificationEvents() {
84123
payload.type === "notification_confirm" ||
85124
payload.type === "notification_accept"
86125
) {
87-
const eventId =
88-
payload.source?.type === "calendar_event"
89-
? payload.source.event_id
90-
: null;
91126
const currentStore = storeRef.current;
92127
if (!currentStore) {
93-
pendingAutoStart.current = { eventId };
128+
pendingNotification.current = {
129+
key: payload.key,
130+
source: payload.source,
131+
};
94132
return;
95133
}
96-
const sessionId = eventId
97-
? getOrCreateSessionForEventId(currentStore, eventId)
98-
: createSession(currentStore);
134+
const { id, state } = getNotificationOpenConfig(
135+
{ key: payload.key, source: payload.source },
136+
currentStore,
137+
);
99138
openNewRef.current({
100139
type: "sessions",
101-
id: sessionId,
102-
state: { view: null, autoStart: true },
140+
id,
141+
state,
103142
});
104143
} else if (payload.type === "notification_option_selected") {
105144
const currentStore = storeRef.current;
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
const SUMMARY_READY_NOTIFICATION_KEY_PREFIX = "summary-ready:";
2+
3+
export function createSummaryReadyNotificationKey(
4+
sessionId: string,
5+
enhancedNoteId: string,
6+
) {
7+
return `${SUMMARY_READY_NOTIFICATION_KEY_PREFIX}${sessionId}:${enhancedNoteId}`;
8+
}
9+
10+
export function parseSummaryReadyNotificationKey(key: string) {
11+
if (!key.startsWith(SUMMARY_READY_NOTIFICATION_KEY_PREFIX)) {
12+
return null;
13+
}
14+
15+
const payload = key.slice(SUMMARY_READY_NOTIFICATION_KEY_PREFIX.length);
16+
const separatorIndex = payload.indexOf(":");
17+
if (separatorIndex === -1) {
18+
return null;
19+
}
20+
21+
const sessionId = payload.slice(0, separatorIndex);
22+
const enhancedNoteId = payload.slice(separatorIndex + 1);
23+
24+
if (!sessionId || !enhancedNoteId) {
25+
return null;
26+
}
27+
28+
return { sessionId, enhancedNoteId };
29+
}

apps/desktop/src/store/zustand/ai-task/task-configs/enhance-success.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
44
import type { TaskConfig } from ".";
55
import { enhanceSuccess } from "./enhance-success";
66

7+
import { createSummaryReadyNotificationKey } from "~/services/summary-ready-notification";
8+
79
const mocks = vi.hoisted(() => ({
810
isFocused: vi.fn().mockResolvedValue(true),
911
showNotification: vi.fn().mockResolvedValue({ status: "ok", data: null }),
@@ -142,15 +144,15 @@ describe("enhanceSuccess.onSuccess", () => {
142144
await enhanceSuccess.onSuccess?.(params);
143145

144146
expect(mocks.showNotification).toHaveBeenCalledWith({
145-
key: null,
147+
key: createSummaryReadyNotificationKey("session-1", "note-1"),
146148
title: "Summary ready",
147149
message: "Weekly sync",
148150
timeout: null,
149151
source: null,
150152
start_time: null,
151153
participants: null,
152154
event_details: null,
153-
action_label: null,
155+
action_label: "Open summary",
154156
options: null,
155157
});
156158
});

apps/desktop/src/store/zustand/ai-task/task-configs/enhance-success.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { md2json } from "@hypr/tiptap/shared";
55

66
import { createTaskId, type TaskConfig } from ".";
77

8+
import { createSummaryReadyNotificationKey } from "~/services/summary-ready-notification";
9+
810
async function maybeShowSummaryReadyNotification(
911
store: Parameters<
1012
NonNullable<TaskConfig<"enhance">["onSuccess"]>
@@ -34,15 +36,15 @@ async function maybeShowSummaryReadyNotification(
3436
typeof rawSessionTitle === "string" ? rawSessionTitle.trim() : "";
3537

3638
void notificationCommands.showNotification({
37-
key: null,
39+
key: createSummaryReadyNotificationKey(args.sessionId, args.enhancedNoteId),
3840
title: `${noteTitle} ready`,
3941
message: sessionTitle || "Your meeting summary has been generated.",
4042
timeout: null,
4143
source: null,
4244
start_time: null,
4345
participants: null,
4446
event_details: null,
45-
action_label: null,
47+
action_label: "Open summary",
4648
options: null,
4749
});
4850
}

0 commit comments

Comments
 (0)