Skip to content
Merged
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
75 changes: 19 additions & 56 deletions apps/desktop/src/shared/useNewNote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -16,7 +14,7 @@ export function useNewNote({
}: {
behavior?: "new" | "current";
} = {}) {
const { persistedStore, internalStore } = useRouteContext({
const { persistedStore } = useRouteContext({
from: "__root__",
});
const { openNew, openCurrent } = useTabs(
Expand All @@ -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;
}
Expand All @@ -53,7 +42,7 @@ export function useNewNoteAndListen({
}: {
behavior?: "new" | "current";
} = {}) {
const { persistedStore, internalStore } = useRouteContext({
const { persistedStore } = useRouteContext({
from: "__root__",
});
const { openNew, openCurrent } = useTabs(
Expand All @@ -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;
}
Expand All @@ -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);
Expand All @@ -134,28 +106,19 @@ 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",
id: sessionId,
state: { view: null, autoStart: null },
});
},
[persistedStore, internalStore, openNew],
[persistedStore, openNew],
);

return handler;
Expand Down
90 changes: 90 additions & 0 deletions apps/desktop/src/store/tinybase/store/sessions.test.ts
Original file line number Diff line number Diff line change
@@ -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<typeof createSession>[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);
});
});
83 changes: 74 additions & 9 deletions apps/desktop/src/store/tinybase/store/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,19 @@ type Store = NonNullable<ReturnType<typeof main.UI.useStore>>;

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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
});
Expand All @@ -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,
Expand All @@ -173,6 +237,7 @@ function createParticipantsFromEvent(

if (!Array.isArray(participants) || participants.length === 0) return;

const userId = getCurrentUserId(store);
const humansByEmail = new Map<string, string>();
store.forEachRow("humans", (humanId, _forEachCell) => {
const human = store.getRow("humans", humanId);
Expand All @@ -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: "",
Expand All @@ -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",
Expand Down
Loading