diff --git a/apps/desktop2/src/components/main/body/daily.tsx b/apps/desktop2/src/components/main/body/daily.tsx
deleted file mode 100644
index caec209c3..000000000
--- a/apps/desktop2/src/components/main/body/daily.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-import { format } from "date-fns";
-import { CalendarIcon, CheckSquare, Mail, Sun } from "lucide-react";
-
-import { type Tab } from "../../../store/zustand/tabs";
-import { type TabItem, TabItemBase } from "./shared";
-
-export const TabItemDaily: TabItem = (
- {
- tab,
- handleCloseThis,
- handleSelectThis,
- handleCloseOthers,
- handleCloseAll,
- },
-) => {
- return (
- }
- title={tab.type === "daily" ? format(tab.date, "MMM d, yyyy") : "Daily Note"}
- active={tab.active}
- handleCloseThis={() => handleCloseThis(tab)}
- handleSelectThis={() => handleSelectThis(tab)}
- handleCloseOthers={handleCloseOthers}
- handleCloseAll={handleCloseAll}
- />
- );
-};
-
-export function TabContentDaily({ tab }: { tab: Tab }) {
- if (tab.type !== "daily") {
- return null;
- }
-
- return (
-
-
{format(tab.date, "MMM d, yyyy")}
-
-
-
-
Task
-
- {[1, 2, 3].map((i) => (
-
-
- task {i}
-
- ))}
-
-
-
-
-
Email
-
- {[1, 2, 3].map((i) => (
-
-
- email {i}
-
- ))}
-
-
-
-
-
Event
-
- {[1, 2, 3].map((i) => (
-
-
- event {i}
-
- ))}
-
-
-
-
- );
-}
diff --git a/apps/desktop2/src/components/main/body/index.tsx b/apps/desktop2/src/components/main/body/index.tsx
index bde9c5ad4..63138ce6c 100644
--- a/apps/desktop2/src/components/main/body/index.tsx
+++ b/apps/desktop2/src/components/main/body/index.tsx
@@ -12,7 +12,6 @@ import { id } from "../../../utils";
import { ChatFloatingButton } from "../../chat";
import { TabContentCalendar, TabItemCalendar } from "./calendars";
import { TabContentContact, TabItemContact } from "./contacts";
-import { TabContentDaily, TabItemDaily } from "./daily";
import { TabContentEvent, TabItemEvent } from "./events";
import { TabContentFolder, TabItemFolder } from "./folders";
import { TabContentHuman, TabItemHuman } from "./humans";
@@ -236,17 +235,6 @@ function TabItem(
/>
);
}
- if (tab.type === "daily") {
- return (
-
- );
- }
if (tab.type === "calendars") {
return (
@@ -287,9 +275,6 @@ function Content({ tab }: { tab: Tab }) {
if (tab.type === "humans") {
return ;
}
- if (tab.type === "daily") {
- return ;
- }
if (tab.type === "calendars") {
return ;
diff --git a/apps/desktop2/src/components/main/body/sessions/index.tsx b/apps/desktop2/src/components/main/body/sessions/index.tsx
index 2d03a9952..80187c237 100644
--- a/apps/desktop2/src/components/main/body/sessions/index.tsx
+++ b/apps/desktop2/src/components/main/body/sessions/index.tsx
@@ -1,19 +1,24 @@
import { StickyNoteIcon } from "lucide-react";
-import { useMemo, useState } from "react";
+import { useState } from "react";
-import NoteEditor from "@hypr/tiptap/editor";
import { AudioPlayerProvider } from "../../../../contexts/audio-player";
import * as persisted from "../../../../store/tinybase/persisted";
import { rowIdfromTab, type Tab } from "../../../../store/zustand/tabs";
import { type TabItem, TabItemBase } from "../shared";
import { FloatingActionButtonn } from "./floating-action";
-import { InnerHeader } from "./inner-header";
+import { NoteInput } from "./note-input";
import { OuterHeader } from "./outer-header";
import { AudioPlayer } from "./player";
import { TitleInput } from "./title-input";
export const TabItemNote: TabItem = (
- { tab, handleCloseThis, handleSelectThis, handleCloseOthers, handleCloseAll },
+ {
+ tab,
+ handleCloseThis,
+ handleSelectThis,
+ handleCloseOthers,
+ handleCloseAll,
+ },
) => {
const title = persisted.UI.useCell("sessions", rowIdfromTab(tab), "title", persisted.STORE_ID);
@@ -35,11 +40,6 @@ export function TabContentNote({ tab }: { tab: Tab }) {
const sessionRow = persisted.UI.useRow("sessions", sessionId, persisted.STORE_ID);
const [showAudioPlayer, setShowAudioPlayer] = useState(false);
- const editorKey = useMemo(
- () => `session-${sessionId}-raw`,
- [sessionId],
- );
-
const handleEditTitle = persisted.UI.useSetRowCallback(
"sessions",
sessionId,
@@ -48,14 +48,6 @@ export function TabContentNote({ tab }: { tab: Tab }) {
persisted.STORE_ID,
);
- const handleEditRawMd = persisted.UI.useSetRowCallback(
- "sessions",
- sessionId,
- (input: string, _store) => ({ ...sessionRow, raw_md: input }),
- [sessionRow],
- persisted.STORE_ID,
- );
-
const handleRegenerate = (templateId: string | null) => {
console.log("Regenerate clicked:", templateId);
};
@@ -76,29 +68,9 @@ export function TabContentNote({ tab }: { tab: Tab }) {
handleEditTitle(e.target.value)}
- />
- {}}
- isCurrentlyRecording={false}
- shouldShowTab={true}
- shouldShowEnhancedTab={false}
+ onChange={handleEditTitle}
/>
-
-
- handleEditRawMd(e)}
- mentionConfig={{
- trigger: "@",
- handleSearch: async () => {
- return [];
- },
- }}
- />
-
+
{showAudioPlayer && }
diff --git a/apps/desktop2/src/components/main/body/sessions/inner-header.tsx b/apps/desktop2/src/components/main/body/sessions/inner-header.tsx
deleted file mode 100644
index 177e96359..000000000
--- a/apps/desktop2/src/components/main/body/sessions/inner-header.tsx
+++ /dev/null
@@ -1,107 +0,0 @@
-import { useEffect } from "react";
-
-import { cn } from "@hypr/ui/lib/utils";
-import { type Tab, useTabs } from "../../../../store/zustand/tabs";
-
-interface TabHeaderProps {
- tab: Tab;
- onVisibilityChange?: (isVisible: boolean) => void;
- isCurrentlyRecording: boolean;
- shouldShowTab: boolean;
- shouldShowEnhancedTab: boolean;
-}
-
-export const InnerHeader = ({
- tab,
- onVisibilityChange,
- isCurrentlyRecording,
- shouldShowTab,
- shouldShowEnhancedTab,
-}: TabHeaderProps) => {
- const { updateSessionTabState } = useTabs();
-
- const currentTab = tab.type === "sessions" ? (tab.state.editor ?? "raw") : "raw";
-
- const handleTabChange = (view: "raw" | "enhanced" | "transcript") => {
- updateSessionTabState(tab, { editor: view });
- };
-
- // set default tab to 'raw' for blank notes (no meeting session)
- useEffect(() => {
- if (!shouldShowTab && tab.type === "sessions") {
- updateSessionTabState(tab, { editor: "raw" });
- }
- }, [shouldShowTab, tab, updateSessionTabState]);
-
- // notify parent when visibility changes
- useEffect(() => {
- if (onVisibilityChange) {
- onVisibilityChange(shouldShowTab ?? false);
- }
- }, [shouldShowTab, onVisibilityChange]);
-
- // don't render tabs at all for blank notes (no meeting session)
- if (!shouldShowTab) {
- return null;
- }
-
- return (
-
- {/* Tab container */}
-
-
-
- {/* Raw Note Tab */}
-
- {/* Enhanced Note Tab - show when session ended OR transcript exists OR enhanced memo exists */}
- {shouldShowEnhancedTab && (
-
- )}
-
-
-
- {/* Transcript Tab - always show */}
-
-
-
-
-
- );
-};
diff --git a/apps/desktop2/src/components/main/body/sessions/note-input/enhanced.tsx b/apps/desktop2/src/components/main/body/sessions/note-input/enhanced.tsx
new file mode 100644
index 000000000..c2d01df34
--- /dev/null
+++ b/apps/desktop2/src/components/main/body/sessions/note-input/enhanced.tsx
@@ -0,0 +1,29 @@
+import NoteEditor from "@hypr/tiptap/editor";
+
+import * as persisted from "../../../../../store/tinybase/persisted";
+
+export function EnhancedEditor({ sessionId }: { sessionId: string }) {
+ const value = persisted.UI.useCell("sessions", sessionId, "enhanced_md", persisted.STORE_ID);
+
+ const handleEnhancedChange = persisted.UI.useSetPartialRowCallback(
+ "sessions",
+ sessionId,
+ (input: string) => ({ enhanced_md: input }),
+ [],
+ persisted.STORE_ID,
+ );
+
+ return (
+ {
+ return [];
+ },
+ }}
+ />
+ );
+}
diff --git a/apps/desktop2/src/components/main/body/sessions/note-input/index.tsx b/apps/desktop2/src/components/main/body/sessions/note-input/index.tsx
new file mode 100644
index 000000000..584683c4a
--- /dev/null
+++ b/apps/desktop2/src/components/main/body/sessions/note-input/index.tsx
@@ -0,0 +1,57 @@
+import { cn } from "@hypr/ui/lib/utils";
+import { type Tab, useTabs } from "../../../../../store/zustand/tabs";
+import { EnhancedEditor } from "./enhanced";
+import { RawEditor } from "./raw";
+import { TranscriptEditorWrapper } from "./transcript";
+
+type EditorView = "raw" | "enhanced" | "transcript";
+
+const EDITOR_TABS = [
+ { view: "enhanced" as const, label: "Summary" },
+ { view: "raw" as const, label: "Memos" },
+ { view: "transcript" as const, label: "Transcript" },
+];
+
+export function NoteInput({ tab }: { tab: Tab }) {
+ const { updateSessionTabState } = useTabs();
+
+ const handleTabChange = (view: EditorView) => {
+ updateSessionTabState(tab, { editor: view });
+ };
+
+ if (tab.type !== "sessions") {
+ return null;
+ }
+
+ const sessionId = tab.id;
+ const currentTab = tab.state.editor ?? "raw";
+
+ return (
+
+
+
+ {EDITOR_TABS.map(({ view, label }) => (
+
+ ))}
+
+
+
+
+ {currentTab === "enhanced" && }
+ {currentTab === "raw" && }
+ {currentTab === "transcript" && }
+
+
+ );
+}
diff --git a/apps/desktop2/src/components/main/body/sessions/note-input/raw.tsx b/apps/desktop2/src/components/main/body/sessions/note-input/raw.tsx
new file mode 100644
index 000000000..230db10f6
--- /dev/null
+++ b/apps/desktop2/src/components/main/body/sessions/note-input/raw.tsx
@@ -0,0 +1,29 @@
+import NoteEditor from "@hypr/tiptap/editor";
+
+import * as persisted from "../../../../../store/tinybase/persisted";
+
+export function RawEditor({ sessionId }: { sessionId: string }) {
+ const value = persisted.UI.useCell("sessions", sessionId, "raw_md", persisted.STORE_ID);
+
+ const handleRawChange = persisted.UI.useSetPartialRowCallback(
+ "sessions",
+ sessionId,
+ (input: string) => ({ raw_md: input }),
+ [],
+ persisted.STORE_ID,
+ );
+
+ return (
+ {
+ return [];
+ },
+ }}
+ />
+ );
+}
diff --git a/apps/desktop2/src/components/main/body/sessions/note-input/transcript.tsx b/apps/desktop2/src/components/main/body/sessions/note-input/transcript.tsx
new file mode 100644
index 000000000..e80c161fd
--- /dev/null
+++ b/apps/desktop2/src/components/main/body/sessions/note-input/transcript.tsx
@@ -0,0 +1,47 @@
+import { type Word2 } from "@hypr/plugin-listener";
+import TranscriptEditor, { type SpeakerViewInnerProps } from "@hypr/tiptap/transcript";
+
+import * as persisted from "../../../../../store/tinybase/persisted";
+
+export function TranscriptEditorWrapper({
+ sessionId,
+}: {
+ sessionId: string;
+}) {
+ const value = persisted.UI.useCell("sessions", sessionId, "transcript", persisted.STORE_ID);
+
+ const handleTranscriptChange = persisted.UI.useSetPartialRowCallback(
+ "sessions",
+ sessionId,
+ (input: Word2[]) => ({ transcript: JSON.stringify(input) }),
+ [],
+ persisted.STORE_ID,
+ );
+
+ const parseTranscript = (value: string): Word2[] | null => {
+ if (!value) {
+ return null;
+ }
+ try {
+ const parsed = JSON.parse(value);
+ return parsed.words ?? null;
+ } catch {
+ return null;
+ }
+ };
+
+ return (
+
+ );
+}
+
+function SpeakerSelector({ speakerLabel, speakerIndex }: SpeakerViewInnerProps) {
+ const displayLabel = speakerLabel || `Speaker ${speakerIndex ?? 0}`;
+ return {displayLabel};
+}
diff --git a/apps/desktop2/src/components/main/body/sessions/outer-header/folder.tsx b/apps/desktop2/src/components/main/body/sessions/outer-header/folder.tsx
index 66aeb8d68..7f98abdee 100644
--- a/apps/desktop2/src/components/main/body/sessions/outer-header/folder.tsx
+++ b/apps/desktop2/src/components/main/body/sessions/outer-header/folder.tsx
@@ -5,19 +5,38 @@ import { useTabs } from "../../../../../store/zustand/tabs";
export function FolderChain({ sessionId }: { sessionId: string }) {
const folderId = persisted.UI.useCell("sessions", sessionId, "folder_id", persisted.STORE_ID);
- const title = persisted.UI.useCell("sessions", sessionId, "title", persisted.STORE_ID);
+ const title = persisted.UI.useCell("sessions", sessionId, "title", persisted.STORE_ID) ?? "Untitled";
+
+ const handleChangeTitle = persisted.UI.useSetPartialRowCallback(
+ "sessions",
+ sessionId,
+ (title: string) => ({ title }),
+ [],
+ persisted.STORE_ID,
+ );
return (
{!folderId
- ?
- : }
+ ?
+ : }
);
}
-function RenderIfRootExist({ folderId, title }: { folderId: string; title: string }) {
+function RenderIfRootExist(
+ {
+ folderId,
+ title,
+ handleChangeTitle,
+ }: {
+ folderId: string;
+ title: string;
+ handleChangeTitle: (title: string) => void;
+ },
+) {
const folderIds = useFolderList(folderId);
+
return (
<>
{folderIds.map((id, index) => (
@@ -27,34 +46,31 @@ function RenderIfRootExist({ folderId, title }: { folderId: string; title: strin
))}
/
- {title}
+
>
);
}
-function RenderIfRootNotExist({ sessionId }: { sessionId: string }) {
- const title = persisted.UI.useCell("sessions", sessionId, "title", persisted.STORE_ID);
-
+function RenderIfRootNotExist(
+ {
+ title,
+ handleChangeTitle,
+ }: {
+ title: string;
+ handleChangeTitle: (title: string) => void;
+ },
+) {
return (
<>
/
- {title ?? "Untitled"}
+
>
);
}
-function useFolderList(rootFolderId: string) {
- const folderIds = persisted.UI.useLinkedRowIds(
- "folderToParentFolder",
- rootFolderId,
- persisted.STORE_ID,
- );
- return [...folderIds].reverse();
-}
-
function FolderItem({ folderId }: { folderId: string }) {
const folderName = persisted.UI.useCell("folders", folderId, "name", persisted.STORE_ID);
@@ -72,3 +88,23 @@ function FolderItem({ folderId }: { folderId: string }) {
);
}
+
+function useFolderList(rootFolderId: string) {
+ const folderIds = persisted.UI.useLinkedRowIds(
+ "folderToParentFolder",
+ rootFolderId,
+ persisted.STORE_ID,
+ );
+ return [...folderIds].reverse();
+}
+
+function TitleInput({ title, handleChangeTitle }: { title: string; handleChangeTitle: (title: string) => void }) {
+ return (
+ handleChangeTitle(e.target.value)}
+ />
+ );
+}
diff --git a/apps/desktop2/src/components/main/body/sessions/title-input.tsx b/apps/desktop2/src/components/main/body/sessions/title-input.tsx
index cfa27c316..f0994f2a0 100644
--- a/apps/desktop2/src/components/main/body/sessions/title-input.tsx
+++ b/apps/desktop2/src/components/main/body/sessions/title-input.tsx
@@ -1,14 +1,5 @@
import { cn } from "@hypr/ui/lib/utils";
-import { type ChangeEvent, type KeyboardEvent, useEffect, useRef } from "react";
-
-interface TitleInputProps {
- value: string;
- onChange: (e: ChangeEvent) => void;
- onNavigateToEditor?: () => void;
- editable?: boolean;
- isGenerating?: boolean;
- autoFocus?: boolean;
-}
+import { type KeyboardEvent, useEffect, useRef } from "react";
export function TitleInput({
value,
@@ -17,7 +8,14 @@ export function TitleInput({
editable,
isGenerating = false,
autoFocus = false,
-}: TitleInputProps) {
+}: {
+ value: string;
+ onChange: (value: string) => void;
+ onNavigateToEditor?: () => void;
+ editable?: boolean;
+ isGenerating?: boolean;
+ autoFocus?: boolean;
+}) {
const inputRef = useRef(null);
const handleKeyDown = (e: KeyboardEvent) => {
@@ -50,7 +48,7 @@ export function TitleInput({
disabled={!editable || isGenerating}
id="note-title-input"
type="text"
- onChange={onChange}
+ onChange={(e) => onChange(e.target.value)}
value={value}
placeholder={getPlaceholder()}
className={cn(
diff --git a/apps/desktop2/src/components/main/sidebar/profile/index.tsx b/apps/desktop2/src/components/main/sidebar/profile/index.tsx
index a3022fb82..2f348e9f7 100644
--- a/apps/desktop2/src/components/main/sidebar/profile/index.tsx
+++ b/apps/desktop2/src/components/main/sidebar/profile/index.tsx
@@ -1,5 +1,5 @@
import { clsx } from "clsx";
-import { Calendar, ChevronUpIcon, FileText, FolderOpen, Settings, Users } from "lucide-react";
+import { Calendar, ChevronUpIcon, FolderOpen, Settings, Users } from "lucide-react";
import { useCallback, useState } from "react";
import { commands as windowsCommands } from "@hypr/plugin-windows/v1";
@@ -47,16 +47,10 @@ export function ProfileSection() {
closeMenu();
}, [openNew, closeMenu]);
- const handleClickDailyNote = useCallback(() => {
- openNew({ type: "daily", date: new Date(), active: true });
- closeMenu();
- }, [openNew, closeMenu]);
-
const menuItems = [
{ icon: FolderOpen, label: "Folders", onClick: handleClickFolders },
{ icon: Users, label: "Contacts", onClick: handleClickContacts },
{ icon: Calendar, label: "Calendar", onClick: handleClickCalendar },
- { icon: FileText, label: "Daily note", onClick: handleClickDailyNote },
{ icon: Settings, label: "Settings", onClick: handleClickSettings },
];
diff --git a/apps/desktop2/src/components/main/sidebar/timeline/anchor.ts b/apps/desktop2/src/components/main/sidebar/timeline/anchor.ts
index 26af66c9a..be6f5df24 100644
--- a/apps/desktop2/src/components/main/sidebar/timeline/anchor.ts
+++ b/apps/desktop2/src/components/main/sidebar/timeline/anchor.ts
@@ -1,4 +1,4 @@
-import { useCallback, useEffect, useRef, useState } from "react";
+import { type DependencyList, useCallback, useEffect, useRef, useState } from "react";
export function useAnchor() {
const containerRef = useRef(null);
@@ -62,10 +62,12 @@ export function useAutoScrollToAnchor({
scrollFn,
isVisible,
anchorNode,
+ deps = [],
}: {
scrollFn: () => void;
isVisible: boolean;
anchorNode: HTMLDivElement | null;
+ deps?: DependencyList;
}) {
const hasMountedRef = useRef(false);
const prevAnchorNodeRef = useRef(null);
@@ -95,4 +97,14 @@ export function useAutoScrollToAnchor({
}
});
}, [anchorNode, isVisible, scrollFn]);
+
+ useEffect(() => {
+ if (!anchorNode || isVisible) {
+ return;
+ }
+
+ requestAnimationFrame(() => {
+ scrollFn();
+ });
+ }, deps);
}
diff --git a/apps/desktop2/src/components/main/sidebar/timeline/index.tsx b/apps/desktop2/src/components/main/sidebar/timeline/index.tsx
index b224072f6..e6f4e3d57 100644
--- a/apps/desktop2/src/components/main/sidebar/timeline/index.tsx
+++ b/apps/desktop2/src/components/main/sidebar/timeline/index.tsx
@@ -24,10 +24,16 @@ export function TimelineView() {
anchorNode: todayAnchorNode,
} = useAnchor();
+ const todayBucketLength = useMemo(() => {
+ const b = buckets.find(bucket => bucket.label === "Today");
+ return b?.items.length ?? 0;
+ }, [buckets]);
+
useAutoScrollToAnchor({
scrollFn: scrollToToday,
isVisible: isTodayVisible,
anchorNode: todayAnchorNode,
+ deps: [todayBucketLength],
});
return (
diff --git a/apps/desktop2/src/routes/app/main/_layout.tsx b/apps/desktop2/src/routes/app/main/_layout.tsx
index fff694ca5..45be62687 100644
--- a/apps/desktop2/src/routes/app/main/_layout.tsx
+++ b/apps/desktop2/src/routes/app/main/_layout.tsx
@@ -16,8 +16,8 @@ export const Route = createFileRoute("/app/main/_layout")({
});
function Component() {
- const { persistedStore } = useRouteContext({ from: "__root__" });
- const { registerOnClose } = useTabs();
+ const { persistedStore, internalStore } = useRouteContext({ from: "__root__" });
+ const { registerOnClose, registerOnEmpty, currentTab, openNew } = useTabs();
useEffect(() => {
return registerOnClose((tab) => {
@@ -34,6 +34,21 @@ function Component() {
});
}, [persistedStore, registerOnClose]);
+ useEffect(() => {
+ const createDefaultSession = () => {
+ const user_id = internalStore?.getValue("user_id");
+ const sessionId = id();
+ persistedStore?.setRow("sessions", sessionId, { user_id, created_at: new Date().toISOString() });
+ openNew({ id: sessionId, type: "sessions", active: true, state: { editor: "raw" } });
+ };
+
+ if (!currentTab) {
+ createDefaultSession();
+ }
+
+ return registerOnEmpty(createDefaultSession);
+ }, [currentTab, persistedStore, internalStore, registerOnEmpty, openNew]);
+
return (
@@ -41,7 +56,6 @@ function Component() {
-
@@ -63,20 +77,3 @@ function ToolRegistration() {
return null;
}
-
-// TOOD
-function NotSureAboutThis() {
- const { persistedStore, internalStore } = useRouteContext({ from: "__root__" });
- const { currentTab, openNew } = useTabs();
-
- useEffect(() => {
- if (!currentTab) {
- const user_id = internalStore?.getValue("user_id");
- const sessionId = id();
- persistedStore?.setRow("sessions", sessionId, { user_id, created_at: new Date().toISOString() });
- openNew({ id: sessionId, type: "sessions", active: true, state: { editor: "raw" } });
- }
- }, [currentTab]);
-
- return null;
-}
diff --git a/apps/desktop2/src/store/tinybase/persisted.ts b/apps/desktop2/src/store/tinybase/persisted.ts
index 028c0016a..fb2a9890a 100644
--- a/apps/desktop2/src/store/tinybase/persisted.ts
+++ b/apps/desktop2/src/store/tinybase/persisted.ts
@@ -417,7 +417,16 @@ export const StoreComponent = () => {
INDEXES.eventsByDate,
"events",
(getCell) => {
- const d = new Date(getCell("started_at")!);
+ const cell = getCell("started_at");
+ if (!cell) {
+ return "";
+ }
+
+ const d = new Date(cell);
+ if (isNaN(d.getTime())) {
+ return "";
+ }
+
return format(d, "yyyy-MM-dd");
},
"started_at",
@@ -432,7 +441,16 @@ export const StoreComponent = () => {
return "";
}
- const d = new Date(getCell("created_at")!);
+ const cell = getCell("created_at");
+ if (!cell) {
+ return "";
+ }
+
+ const d = new Date(cell);
+ if (isNaN(d.getTime())) {
+ return "";
+ }
+
return format(d, "yyyy-MM-dd");
},
"created_at",
diff --git a/apps/desktop2/src/store/zustand/tabs/basic.ts b/apps/desktop2/src/store/zustand/tabs/basic.ts
index 69981bd2d..c1ca827dd 100644
--- a/apps/desktop2/src/store/zustand/tabs/basic.ts
+++ b/apps/desktop2/src/store/zustand/tabs/basic.ts
@@ -4,7 +4,7 @@ import type { LifecycleState } from "./lifecycle";
import type { NavigationState } from "./navigation";
import type { Tab, TabHistory } from "./schema";
import { isSameTab, tabSchema } from "./schema";
-import { computeHistoryFlags, getSlotId, notifyTabClose, notifyTabsClose, pushHistory } from "./utils";
+import { computeHistoryFlags, getSlotId, notifyEmpty, notifyTabClose, notifyTabsClose, pushHistory } from "./utils";
export type BasicState = {
currentTab: Tab | null;
@@ -100,19 +100,21 @@ export const createBasicSlice = );
},
close: (tab) => {
- const { tabs, history, onCloseHandlers } = get();
+ const { tabs, history, onCloseHandlers, onEmptyHandlers } = get();
const remainingTabs = tabs.filter((t) => !isSameTab(t, tab));
notifyTabClose(onCloseHandlers, tab);
if (remainingTabs.length === 0) {
- return set({
+ set({
tabs: [] as Tab[],
currentTab: null,
history: new Map(),
canGoBack: false,
canGoNext: false,
} as Partial);
+ notifyEmpty(onEmptyHandlers);
+ return;
}
const closedTabIndex = tabs.findIndex((t) => isSameTab(t, tab));
@@ -162,7 +164,7 @@ export const createBasicSlice = );
},
closeAll: () => {
- const { tabs, onCloseHandlers } = get();
+ const { tabs, onCloseHandlers, onEmptyHandlers } = get();
notifyTabsClose(onCloseHandlers, tabs);
set({
tabs: [],
@@ -171,5 +173,6 @@ export const createBasicSlice = );
+ notifyEmpty(onEmptyHandlers);
},
});
diff --git a/apps/desktop2/src/store/zustand/tabs/lifecycle.ts b/apps/desktop2/src/store/zustand/tabs/lifecycle.ts
index 186a184e2..15cf692b0 100644
--- a/apps/desktop2/src/store/zustand/tabs/lifecycle.ts
+++ b/apps/desktop2/src/store/zustand/tabs/lifecycle.ts
@@ -4,10 +4,12 @@ import type { Tab } from "./schema";
export type LifecycleState = {
onCloseHandlers: Set<(tab: Tab) => void>;
+ onEmptyHandlers: Set<() => void>;
};
export type LifecycleActions = {
registerOnClose: (handler: (tab: Tab) => void) => () => void;
+ registerOnEmpty: (handler: () => void) => () => void;
};
export const createLifecycleSlice = (
@@ -15,6 +17,7 @@ export const createLifecycleSlice = (
get: StoreApi["getState"],
): LifecycleState & LifecycleActions => ({
onCloseHandlers: new Set(),
+ onEmptyHandlers: new Set(),
registerOnClose: (handler) => {
const { onCloseHandlers } = get();
const nextHandlers = new Set(onCloseHandlers);
@@ -27,4 +30,16 @@ export const createLifecycleSlice = (
set({ onCloseHandlers: nextHandlers } as Partial);
};
},
+ registerOnEmpty: (handler) => {
+ const { onEmptyHandlers } = get();
+ const nextHandlers = new Set(onEmptyHandlers);
+ nextHandlers.add(handler);
+ set({ onEmptyHandlers: nextHandlers } as Partial);
+ return () => {
+ const { onEmptyHandlers: currentHandlers } = get();
+ const nextHandlers = new Set(currentHandlers);
+ nextHandlers.delete(handler);
+ set({ onEmptyHandlers: nextHandlers } as Partial);
+ };
+ },
});
diff --git a/apps/desktop2/src/store/zustand/tabs/schema.ts b/apps/desktop2/src/store/zustand/tabs/schema.ts
index e726303a2..360604164 100644
--- a/apps/desktop2/src/store/zustand/tabs/schema.ts
+++ b/apps/desktop2/src/store/zustand/tabs/schema.ts
@@ -45,10 +45,6 @@ export const tabSchema = z.discriminatedUnion("type", [
type: z.literal("calendars"),
month: z.coerce.date(),
}),
- baseTabSchema.extend({
- type: z.literal("daily"),
- date: z.coerce.date(),
- }),
]);
export type Tab = z.infer;
@@ -70,7 +66,6 @@ export const rowIdfromTab = (tab: Tab): string => {
return tab.id;
case "calendars":
case "contacts":
- case "daily":
throw new Error("invalid_resource");
case "folders":
if (!tab.id) {
@@ -94,8 +89,6 @@ export const uniqueIdfromTab = (tab: Tab): string => {
return `calendars-${tab.month.getFullYear()}-${tab.month.getMonth()}`;
case "contacts":
return `contacts`;
- case "daily":
- return `daily-${tab.date.getFullYear()}-${tab.date.getMonth()}-${tab.date.getDate()}`;
case "folders":
return `folders-${tab.id ?? "all"}`;
}
diff --git a/apps/desktop2/src/store/zustand/tabs/utils.ts b/apps/desktop2/src/store/zustand/tabs/utils.ts
index 72a4ca50b..4a29620d3 100644
--- a/apps/desktop2/src/store/zustand/tabs/utils.ts
+++ b/apps/desktop2/src/store/zustand/tabs/utils.ts
@@ -27,6 +27,18 @@ export const notifyTabsClose = (
tabs.forEach((tab) => notifyTabClose(handlers, tab));
};
+export const notifyEmpty = (
+ handlers: Set<() => void>,
+): void => {
+ handlers.forEach((handler) => {
+ try {
+ handler();
+ } catch (error) {
+ console.error("tab onEmpty handler failed", error);
+ }
+ });
+};
+
export const computeHistoryFlags = (
history: Map,
currentTab: Tab | null,
diff --git a/apps/desktop2/src/utils/timeline.ts b/apps/desktop2/src/utils/timeline.ts
index 831270045..8e4d669ee 100644
--- a/apps/desktop2/src/utils/timeline.ts
+++ b/apps/desktop2/src/utils/timeline.ts
@@ -128,7 +128,7 @@ export function buildTimelineBuckets({
return;
}
- if (!isPast(eventStartTime)) {
+ if (eventStartTime && !isPast(eventStartTime)) {
items.push({
type: "event",
id: eventId,
@@ -153,12 +153,14 @@ export function buildTimelineBuckets({
return;
}
- items.push({
- type: "session",
- id: sessionId,
- date: format(date, "yyyy-MM-dd"),
- data: row as unknown as persisted.Session,
- });
+ if (date) {
+ items.push({
+ type: "session",
+ id: sessionId,
+ date: format(date, "yyyy-MM-dd"),
+ data: row as unknown as persisted.Session,
+ });
+ }
});
items.sort((a, b) => {
diff --git a/packages/tiptap/src/transcript/utils.ts b/packages/tiptap/src/transcript/utils.ts
index 988f2acde..b9a2da91f 100644
--- a/packages/tiptap/src/transcript/utils.ts
+++ b/packages/tiptap/src/transcript/utils.ts
@@ -37,7 +37,7 @@ type SpeakerContent = {
export const wordsToSpeakerChunks = (words: Word2[]): { words: Word2[]; speaker: SpeakerIdentity | null }[] => {
return words.reduce<{ cur: SpeakerIdentity | null; acc: { words: Word2[]; speaker: SpeakerIdentity | null }[] }>(
- (state, word, index) => {
+ (state, word, _index) => {
const isFirst = state.acc.length === 0;
const isSameSpeaker = (!state.cur && !word.speaker)