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)