Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions apps/desktop/src/services/event-listeners.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { beforeEach, describe, expect, it, vi } from "vitest";

const sessionMocks = vi.hoisted(() => ({
createSession: vi.fn().mockReturnValue("new-session"),
getOrCreateSessionForEventId: vi
.fn()
.mockImplementation(
(_store: unknown, eventId: string) => `event-${eventId}`,
),
}));

vi.mock("~/store/tinybase/store/sessions", () => ({
createSession: sessionMocks.createSession,
getOrCreateSessionForEventId: sessionMocks.getOrCreateSessionForEventId,
}));

import { getNotificationOpenConfig } from "./event-listeners";
import { createSummaryReadyNotificationKey } from "./summary-ready-notification";

describe("getNotificationOpenConfig", () => {
beforeEach(() => {
sessionMocks.createSession.mockClear();
sessionMocks.getOrCreateSessionForEventId.mockClear();
});

it("opens summary notifications in the enhanced note without autostart", () => {
const store = {} as never;

expect(
getNotificationOpenConfig(
{
key: createSummaryReadyNotificationKey("session-1", "note-1"),
source: null,
},
store,
),
).toEqual({
id: "session-1",
state: {
view: { type: "enhanced", id: "note-1" },
autoStart: null,
},
});
expect(sessionMocks.createSession).not.toHaveBeenCalled();
expect(sessionMocks.getOrCreateSessionForEventId).not.toHaveBeenCalled();
});

it("opens calendar event notifications in their linked session and autostarts", () => {
const store = {} as never;

expect(
getNotificationOpenConfig(
{
key: "event-1",
source: { type: "calendar_event", event_id: "event-1" },
},
store,
),
).toEqual({
id: "event-event-1",
state: { view: null, autoStart: true },
});
expect(sessionMocks.getOrCreateSessionForEventId).toHaveBeenCalledWith(
store,
"event-1",
);
});

it("falls back to a new session for generic notification clicks", () => {
const store = {} as never;

expect(
getNotificationOpenConfig(
{
key: "generic-notification",
source: null,
},
store,
),
).toEqual({
id: "new-session",
state: { view: null, autoStart: true },
});
expect(sessionMocks.createSession).toHaveBeenCalledWith(store);
});
});
79 changes: 59 additions & 20 deletions apps/desktop/src/services/event-listeners.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,61 @@
import { type UnlistenFn } from "@tauri-apps/api/event";
import { useEffect, useRef } from "react";

import { events as notificationEvents } from "@hypr/plugin-notification";
import {
events as notificationEvents,
type NotificationSource,
} from "@hypr/plugin-notification";
import {
commands as updaterCommands,
events as updaterEvents,
} from "@hypr/plugin-updater2";
import { getCurrentWebviewWindowLabel } from "@hypr/plugin-windows";

import { parseSummaryReadyNotificationKey } from "./summary-ready-notification";

import * as main from "~/store/tinybase/store/main";
import {
createSession,
getOrCreateSessionForEventId,
} from "~/store/tinybase/store/sessions";
import { useTabs } from "~/store/zustand/tabs";

type NotificationTarget = {
key: string;
source: NotificationSource | null;
};

type MainStore = NonNullable<ReturnType<typeof main.UI.useStore>>;

export function getNotificationOpenConfig(
notification: NotificationTarget,
store: MainStore,
) {
const summaryTarget = parseSummaryReadyNotificationKey(notification.key);
if (summaryTarget) {
return {
id: summaryTarget.sessionId,
state: {
view: { type: "enhanced" as const, id: summaryTarget.enhancedNoteId },
autoStart: null,
},
};
}

const eventId =
notification.source?.type === "calendar_event"
? notification.source.event_id
: null;
const sessionId = eventId
? getOrCreateSessionForEventId(store, eventId)
: createSession(store);

return {
id: sessionId,
state: { view: null, autoStart: true },
};
}

function useUpdaterEvents() {
const openNew = useTabs((state) => state.openNew);

Expand Down Expand Up @@ -46,7 +87,7 @@ function useUpdaterEvents() {
function useNotificationEvents() {
const store = main.UI.useStore(main.STORE_ID);
const openNew = useTabs((state) => state.openNew);
const pendingAutoStart = useRef<{ eventId: string | null } | null>(null);
const pendingNotification = useRef<NotificationTarget | null>(null);
const storeRef = useRef(store);
const openNewRef = useRef(openNew);

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

useEffect(() => {
if (pendingAutoStart.current && store) {
const { eventId } = pendingAutoStart.current;
pendingAutoStart.current = null;
const sessionId = eventId
? getOrCreateSessionForEventId(store, eventId)
: createSession(store);
if (pendingNotification.current && store) {
const notification = pendingNotification.current;
pendingNotification.current = null;
const { id, state } = getNotificationOpenConfig(notification, store);
openNew({
type: "sessions",
id: sessionId,
state: { view: null, autoStart: true },
id,
state,
});
}
}, [store, openNew]);
Expand All @@ -84,22 +123,22 @@ function useNotificationEvents() {
payload.type === "notification_confirm" ||
payload.type === "notification_accept"
) {
const eventId =
payload.source?.type === "calendar_event"
? payload.source.event_id
: null;
const currentStore = storeRef.current;
if (!currentStore) {
pendingAutoStart.current = { eventId };
pendingNotification.current = {
key: payload.key,
source: payload.source,
};
return;
}
const sessionId = eventId
? getOrCreateSessionForEventId(currentStore, eventId)
: createSession(currentStore);
const { id, state } = getNotificationOpenConfig(
{ key: payload.key, source: payload.source },
currentStore,
);
openNewRef.current({
type: "sessions",
id: sessionId,
state: { view: null, autoStart: true },
id,
state,
});
} else if (payload.type === "notification_option_selected") {
const currentStore = storeRef.current;
Expand Down
29 changes: 29 additions & 0 deletions apps/desktop/src/services/summary-ready-notification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const SUMMARY_READY_NOTIFICATION_KEY_PREFIX = "summary-ready:";

export function createSummaryReadyNotificationKey(
sessionId: string,
enhancedNoteId: string,
) {
return `${SUMMARY_READY_NOTIFICATION_KEY_PREFIX}${sessionId}:${enhancedNoteId}`;
}

export function parseSummaryReadyNotificationKey(key: string) {
if (!key.startsWith(SUMMARY_READY_NOTIFICATION_KEY_PREFIX)) {
return null;
}

const payload = key.slice(SUMMARY_READY_NOTIFICATION_KEY_PREFIX.length);
const separatorIndex = payload.indexOf(":");
if (separatorIndex === -1) {
return null;
}

const sessionId = payload.slice(0, separatorIndex);
const enhancedNoteId = payload.slice(separatorIndex + 1);

if (!sessionId || !enhancedNoteId) {
return null;
}

return { sessionId, enhancedNoteId };
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import type { TaskConfig } from ".";
import { enhanceSuccess } from "./enhance-success";

import { createSummaryReadyNotificationKey } from "~/services/summary-ready-notification";

const mocks = vi.hoisted(() => ({
isFocused: vi.fn().mockResolvedValue(true),
showNotification: vi.fn().mockResolvedValue({ status: "ok", data: null }),
Expand Down Expand Up @@ -142,15 +144,15 @@ describe("enhanceSuccess.onSuccess", () => {
await enhanceSuccess.onSuccess?.(params);

expect(mocks.showNotification).toHaveBeenCalledWith({
key: null,
key: createSummaryReadyNotificationKey("session-1", "note-1"),
title: "Summary ready",
message: "Weekly sync",
timeout: null,
source: null,
start_time: null,
participants: null,
event_details: null,
action_label: null,
action_label: "Open summary",
options: null,
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { md2json } from "@hypr/tiptap/shared";

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

import { createSummaryReadyNotificationKey } from "~/services/summary-ready-notification";

async function maybeShowSummaryReadyNotification(
store: Parameters<
NonNullable<TaskConfig<"enhance">["onSuccess"]>
Expand Down Expand Up @@ -34,15 +36,15 @@ async function maybeShowSummaryReadyNotification(
typeof rawSessionTitle === "string" ? rawSessionTitle.trim() : "";

void notificationCommands.showNotification({
key: null,
key: createSummaryReadyNotificationKey(args.sessionId, args.enhancedNoteId),
title: `${noteTitle} ready`,
message: sessionTitle || "Your meeting summary has been generated.",
timeout: null,
source: null,
start_time: null,
participants: null,
event_details: null,
action_label: null,
action_label: "Open summary",
options: null,
});
}
Expand Down
Loading