From d5ce26fea383725ecc74d9941dcb493c2ee73cb7 Mon Sep 17 00:00:00 2001 From: ComputelessComputer <63365510+ComputelessComputer@users.noreply.github.com> Date: Fri, 22 May 2026 16:03:06 +0900 Subject: [PATCH] fix(notes): add self participant to new notes Create plain sessions through one helper so new notes include the current user participant while self-only blanks still clean up. --- apps/desktop/src/shared/useNewNote.ts | 75 ++++------------ .../src/store/tinybase/store/sessions.test.ts | 90 +++++++++++++++++++ .../src/store/tinybase/store/sessions.ts | 83 +++++++++++++++-- 3 files changed, 183 insertions(+), 65 deletions(-) create mode 100644 apps/desktop/src/store/tinybase/store/sessions.test.ts diff --git a/apps/desktop/src/shared/useNewNote.ts b/apps/desktop/src/shared/useNewNote.ts index db5430ce17..d4ef2e323c 100644 --- a/apps/desktop/src/shared/useNewNote.ts +++ b/apps/desktop/src/shared/useNewNote.ts @@ -4,9 +4,7 @@ import { open as selectFile } from "@tauri-apps/plugin-dialog"; import { useCallback } from "react"; import { useShallow } from "zustand/shallow"; -import { commands as analyticsCommands } from "@hypr/plugin-analytics"; - -import { id } from "~/shared/utils"; +import { createSession } from "~/store/tinybase/store/sessions"; import { useTabs } from "~/store/zustand/tabs"; import { useListener } from "~/stt/contexts"; import { setPendingUpload } from "~/stt/pending-upload"; @@ -16,7 +14,7 @@ export function useNewNote({ }: { behavior?: "new" | "current"; } = {}) { - const { persistedStore, internalStore } = useRouteContext({ + const { persistedStore } = useRouteContext({ from: "__root__", }); const { openNew, openCurrent } = useTabs( @@ -27,23 +25,14 @@ export function useNewNote({ ); const handler = useCallback(() => { - const user_id = internalStore?.getValue("user_id"); - const sessionId = id(); - - persistedStore?.setRow("sessions", sessionId, { - user_id, - created_at: new Date().toISOString(), - title: "", - }); - - void analyticsCommands.event({ - event: "note_created", - has_event_id: false, - }); + if (!persistedStore) { + return; + } + const sessionId = createSession(persistedStore); const ff = behavior === "new" ? openNew : openCurrent; ff({ type: "sessions", id: sessionId }); - }, [persistedStore, internalStore, openNew, openCurrent, behavior]); + }, [persistedStore, openNew, openCurrent, behavior]); return handler; } @@ -53,7 +42,7 @@ export function useNewNoteAndListen({ }: { behavior?: "new" | "current"; } = {}) { - const { persistedStore, internalStore } = useRouteContext({ + const { persistedStore } = useRouteContext({ from: "__root__", }); const { openNew, openCurrent } = useTabs( @@ -74,35 +63,18 @@ export function useNewNoteAndListen({ return; } - const user_id = internalStore?.getValue("user_id"); - const sessionId = id(); - - persistedStore?.setRow("sessions", sessionId, { - user_id, - created_at: new Date().toISOString(), - title: "", - }); - - void analyticsCommands.event({ - event: "note_created", - has_event_id: false, - }); + if (!persistedStore) { + return; + } + const sessionId = createSession(persistedStore); const ff = behavior === "new" ? openNew : openCurrent; ff({ type: "sessions", id: sessionId, state: { view: null, autoStart: true }, }); - }, [ - status, - liveSessionId, - persistedStore, - internalStore, - openNew, - openCurrent, - behavior, - ]); + }, [status, liveSessionId, persistedStore, openNew, openCurrent, behavior]); return handler; } @@ -113,7 +85,7 @@ const AUDIO_FILTERS = [ const TRANSCRIPT_FILTERS = [{ name: "Transcript", extensions: ["vtt", "srt"] }]; export function useNewNoteAndUpload() { - const { persistedStore, internalStore } = useRouteContext({ + const { persistedStore } = useRouteContext({ from: "__root__", }); const openNew = useTabs((state) => state.openNew); @@ -134,20 +106,11 @@ export function useNewNoteAndUpload() { return; } - const user_id = internalStore?.getValue("user_id"); - const sessionId = id(); - - persistedStore?.setRow("sessions", sessionId, { - user_id, - created_at: new Date().toISOString(), - title: "", - }); - - void analyticsCommands.event({ - event: "note_created", - has_event_id: false, - }); + if (!persistedStore) { + return; + } + const sessionId = createSession(persistedStore); setPendingUpload(sessionId, { kind, filePath }); openNew({ type: "sessions", @@ -155,7 +118,7 @@ export function useNewNoteAndUpload() { state: { view: null, autoStart: null }, }); }, - [persistedStore, internalStore, openNew], + [persistedStore, openNew], ); return handler; diff --git a/apps/desktop/src/store/tinybase/store/sessions.test.ts b/apps/desktop/src/store/tinybase/store/sessions.test.ts new file mode 100644 index 0000000000..002bade55f --- /dev/null +++ b/apps/desktop/src/store/tinybase/store/sessions.test.ts @@ -0,0 +1,90 @@ +import { beforeEach, describe, expect, test, vi } from "vitest"; + +import { createSession, isSessionEmpty } from "./sessions"; + +import { createTestMainStore } from "~/store/tinybase/persister/testing/mocks"; + +const analyticsEventMock = vi.hoisted(() => vi.fn()); + +vi.mock("@hypr/plugin-analytics", () => ({ + commands: { + event: analyticsEventMock, + }, +})); + +type Store = Parameters[0]; + +describe("createSession", () => { + let store: Store; + + beforeEach(() => { + store = createTestMainStore() as Store; + store.setValue("user_id", "user-1"); + store.setRow("humans", "user-1", { + user_id: "user-1", + name: "John", + email: "john@example.com", + org_id: "", + job_title: "", + linkedin_username: "", + memo: "", + pinned: false, + }); + analyticsEventMock.mockClear(); + }); + + test("adds the current user as a participant", () => { + const sessionId = createSession(store); + const participants = Object.values( + store.getTable("mapping_session_participant"), + ); + + expect(store.getRow("sessions", sessionId)).toEqual( + expect.objectContaining({ + user_id: "user-1", + title: "", + raw_md: "", + }), + ); + expect(participants).toEqual([ + expect.objectContaining({ + user_id: "user-1", + session_id: sessionId, + human_id: "user-1", + source: "manual", + }), + ]); + expect(analyticsEventMock).toHaveBeenCalledWith({ + event: "note_created", + has_event_id: false, + }); + }); + + test("keeps a note with only the default user participant empty", () => { + const sessionId = createSession(store); + + expect(isSessionEmpty(store, sessionId)).toBe(true); + }); + + test("treats additional manual participants as note content", () => { + const sessionId = createSession(store); + store.setRow("humans", "participant-1", { + user_id: "user-1", + name: "Anand", + email: "anand@example.com", + org_id: "", + job_title: "", + linkedin_username: "", + memo: "", + pinned: false, + }); + store.setRow("mapping_session_participant", "mapping-1", { + user_id: "user-1", + session_id: sessionId, + human_id: "participant-1", + source: "manual", + }); + + expect(isSessionEmpty(store, sessionId)).toBe(false); + }); +}); diff --git a/apps/desktop/src/store/tinybase/store/sessions.ts b/apps/desktop/src/store/tinybase/store/sessions.ts index 26efe7281c..23d279df08 100644 --- a/apps/desktop/src/store/tinybase/store/sessions.ts +++ b/apps/desktop/src/store/tinybase/store/sessions.ts @@ -18,12 +18,19 @@ type Store = NonNullable>; export function createSession(store: Store, title?: string): string { const sessionId = id(); - store.setRow("sessions", sessionId, { - title: title ?? "", - created_at: new Date().toISOString(), - raw_md: "", - user_id: DEFAULT_USER_ID, + const userId = getCurrentUserId(store); + + store.transaction(() => { + store.setRow("sessions", sessionId, { + title: title ?? "", + created_at: new Date().toISOString(), + raw_md: "", + user_id: userId, + }); + + addCurrentUserParticipant(store, sessionId, userId); }); + void analyticsCommands.event({ event: "note_created", has_event_id: false, @@ -71,7 +78,7 @@ export function getOrCreateSessionForEventId( title: title ?? sessionEvent.title, created_at: new Date().toISOString(), raw_md: "", - user_id: DEFAULT_USER_ID, + user_id: getCurrentUserId(store), }); createParticipantsFromEvent(store, sessionId, event); @@ -132,10 +139,15 @@ export function isSessionEmpty(store: Store, sessionId: string): boolean { return false; } + const currentUserId = getCurrentUserId(store); let hasManualParticipant = false; store.forEachRow("mapping_session_participant", (rowId, _forEachCell) => { const row = store.getRow("mapping_session_participant", rowId); - if (row?.session_id === sessionId && row.source !== "auto") { + if ( + row?.session_id === sessionId && + row.source !== "auto" && + row.human_id !== currentUserId + ) { hasManualParticipant = true; } }); @@ -157,6 +169,58 @@ export function isSessionEmpty(store: Store, sessionId: string): boolean { return true; } +function getCurrentUserId(store: Store): string { + const userId = store.getValue("user_id"); + return typeof userId === "string" && userId ? userId : DEFAULT_USER_ID; +} + +function ensureCurrentUserHuman(store: Store, userId: string): void { + if (store.hasRow("humans", userId)) { + return; + } + + store.setRow("humans", userId, { + user_id: userId, + name: "", + email: "", + org_id: "", + job_title: "", + linkedin_username: "", + memo: "", + pinned: false, + } satisfies HumanStorage); +} + +function addCurrentUserParticipant( + store: Store, + sessionId: string, + userId: string, +): void { + let hasCurrentUserParticipant = false; + store.forEachRow("mapping_session_participant", (rowId, _forEachCell) => { + const row = store.getRow("mapping_session_participant", rowId); + if ( + row?.session_id === sessionId && + row.human_id === userId && + row.source !== "excluded" + ) { + hasCurrentUserParticipant = true; + } + }); + + if (hasCurrentUserParticipant) { + return; + } + + ensureCurrentUserHuman(store, userId); + store.setRow("mapping_session_participant", id(), { + user_id: userId, + session_id: sessionId, + human_id: userId, + source: "manual", + } satisfies MappingSessionParticipantStorage); +} + function createParticipantsFromEvent( store: Store, sessionId: string, @@ -173,6 +237,7 @@ function createParticipantsFromEvent( if (!Array.isArray(participants) || participants.length === 0) return; + const userId = getCurrentUserId(store); const humansByEmail = new Map(); store.forEachRow("humans", (humanId, _forEachCell) => { const human = store.getRow("humans", humanId); @@ -191,7 +256,7 @@ function createParticipantsFromEvent( if (!humanId) { humanId = id(); store.setRow("humans", humanId, { - user_id: DEFAULT_USER_ID, + user_id: userId, name: participant.name || participant.email, email: participant.email, org_id: "", @@ -204,7 +269,7 @@ function createParticipantsFromEvent( } store.setRow("mapping_session_participant", id(), { - user_id: DEFAULT_USER_ID, + user_id: userId, session_id: sessionId, human_id: humanId, source: "auto",