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
39 changes: 37 additions & 2 deletions apps/desktop/src/components/main/body/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ function Header({ tabs }: { tabs: Tab[] }) {
closeAll,
pin,
unpin,
pendingCloseConfirmationTab,
setPendingCloseConfirmationTab,
} = useTabs(
useShallow((state) => ({
select: state.select,
Expand All @@ -104,6 +106,8 @@ function Header({ tabs }: { tabs: Tab[] }) {
closeAll: state.closeAll,
pin: state.pin,
unpin: state.unpin,
pendingCloseConfirmationTab: state.pendingCloseConfirmationTab,
setPendingCloseConfirmationTab: state.setPendingCloseConfirmationTab,
})),
);

Expand Down Expand Up @@ -193,6 +197,8 @@ function Header({ tabs }: { tabs: Tab[] }) {
handlePin={pin}
handleUnpin={unpin}
tabIndex={1}
pendingCloseConfirmationTab={pendingCloseConfirmationTab}
setPendingCloseConfirmationTab={setPendingCloseConfirmationTab}
/>
</div>
)}
Expand Down Expand Up @@ -247,6 +253,10 @@ function Header({ tabs }: { tabs: Tab[] }) {
handlePin={pin}
handleUnpin={unpin}
tabIndex={shortcutIndex}
pendingCloseConfirmationTab={pendingCloseConfirmationTab}
setPendingCloseConfirmationTab={
setPendingCloseConfirmationTab
}
/>
</Reorder.Item>
);
Expand Down Expand Up @@ -298,6 +308,8 @@ function TabItem({
handlePin,
handleUnpin,
tabIndex,
pendingCloseConfirmationTab,
setPendingCloseConfirmationTab,
}: {
tab: Tab;
handleClose: (tab: Tab) => void;
Expand All @@ -307,6 +319,8 @@ function TabItem({
handlePin: (tab: Tab) => void;
handleUnpin: (tab: Tab) => void;
tabIndex?: number;
pendingCloseConfirmationTab?: Tab | null;
setPendingCloseConfirmationTab?: (tab: Tab | null) => void;
}) {
const handleCloseOthers = () => handleCloseOthersCallback(tab);
const handlePinThis = () => handlePin(tab);
Expand All @@ -323,6 +337,8 @@ function TabItem({
handleCloseAll={handleCloseAll}
handlePinThis={handlePinThis}
handleUnpinThis={handleUnpinThis}
pendingCloseConfirmationTab={pendingCloseConfirmationTab}
setPendingCloseConfirmationTab={setPendingCloseConfirmationTab}
/>
);
}
Expand Down Expand Up @@ -716,6 +732,7 @@ function useTabsShortcuts() {
restoreLastClosedTab,
openNew,
unpin,
setPendingCloseConfirmationTab,
} = useTabs(
useShallow((state) => ({
tabs: state.tabs,
Expand All @@ -727,8 +744,13 @@ function useTabsShortcuts() {
restoreLastClosedTab: state.restoreLastClosedTab,
openNew: state.openNew,
unpin: state.unpin,
setPendingCloseConfirmationTab: state.setPendingCloseConfirmationTab,
})),
);
const liveSessionId = useListener((state) => state.live.sessionId);
const liveStatus = useListener((state) => state.live.status);
const isListening = liveStatus === "active" || liveStatus === "finalizing";

const newNote = useNewNote({ behavior: "new" });
const newNoteCurrent = useNewNote({ behavior: "current" });
const newEmptyTab = useNewEmptyTab();
Expand Down Expand Up @@ -765,7 +787,13 @@ function useTabsShortcuts() {
"mod+w",
async () => {
if (currentTab) {
if (currentTab.pinned) {
const isCurrentTabListening =
isListening &&
currentTab.type === "sessions" &&
currentTab.id === liveSessionId;
if (isCurrentTabListening) {
setPendingCloseConfirmationTab(currentTab);
} else if (currentTab.pinned) {
unpin(currentTab);
} else {
close(currentTab);
Expand All @@ -777,7 +805,14 @@ function useTabsShortcuts() {
enableOnFormTags: true,
enableOnContentEditable: true,
},
[currentTab, close, unpin],
[
currentTab,
close,
unpin,
isListening,
liveSessionId,
setPendingCloseConfirmationTab,
],
);

useHotkeys(
Expand Down
32 changes: 31 additions & 1 deletion apps/desktop/src/components/main/body/sessions/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { useStartListening } from "../../../../hooks/useStartListening";
import { useSTTConnection } from "../../../../hooks/useSTTConnection";
import { useTitleGeneration } from "../../../../hooks/useTitleGeneration";
import * as main from "../../../../store/tinybase/store/main";
import { listenerStore } from "../../../../store/zustand/listener/instance";
import {
rowIdfromTab,
type Tab,
Expand Down Expand Up @@ -48,6 +49,8 @@ export const TabItemNote: TabItem<Extract<Tab, { type: "sessions" }>> = ({
handleCloseAll,
handlePinThis,
handleUnpinThis,
pendingCloseConfirmationTab,
setPendingCloseConfirmationTab,
}) => {
const title = main.UI.useCell(
"sessions",
Expand All @@ -56,11 +59,36 @@ export const TabItemNote: TabItem<Extract<Tab, { type: "sessions" }>> = ({
main.STORE_ID,
);
const sessionMode = useListener((state) => state.getSessionMode(tab.id));
const stop = useListener((state) => state.stop);
const isEnhancing = useIsSessionEnhancing(tab.id);
const isActive = sessionMode === "active" || sessionMode === "finalizing";
const isFinalizing = sessionMode === "finalizing";
const showSpinner = !tab.active && (isFinalizing || isEnhancing);

const showCloseConfirmation =
pendingCloseConfirmationTab?.type === "sessions" &&
pendingCloseConfirmationTab?.id === tab.id;

const handleCloseConfirmationChange = (show: boolean) => {
if (!show) {
setPendingCloseConfirmationTab?.(null);
}
};

const handleCloseWithStop = () => {
if (isActive) {
stop();
const unsubscribe = listenerStore.subscribe((state) => {
if (state.live.status !== "active") {
unsubscribe();
handleCloseThis(tab);
}
});
} else {
handleCloseThis(tab);
}
};

return (
<TabItemBase
icon={<StickyNoteIcon className="w-4 h-4" />}
Expand All @@ -70,7 +98,9 @@ export const TabItemNote: TabItem<Extract<Tab, { type: "sessions" }>> = ({
finalizing={showSpinner}
pinned={tab.pinned}
tabIndex={tabIndex}
handleCloseThis={() => handleCloseThis(tab)}
showCloseConfirmation={showCloseConfirmation}
onCloseConfirmationChange={handleCloseConfirmationChange}
handleCloseThis={handleCloseWithStop}
handleSelectThis={() => handleSelectThis(tab)}
handleCloseOthers={handleCloseOthers}
handleCloseAll={handleCloseAll}
Expand Down
115 changes: 108 additions & 7 deletions apps/desktop/src/components/main/body/shared.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { Pin, X } from "lucide-react";
import { useState } from "react";
import { useCallback, useEffect, useState } from "react";

import { Button } from "@hypr/ui/components/ui/button";
import { Kbd } from "@hypr/ui/components/ui/kbd";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@hypr/ui/components/ui/popover";
import { Spinner } from "@hypr/ui/components/ui/spinner";
import { cn } from "@hypr/utils";

Expand All @@ -16,6 +22,8 @@ type TabItemProps<T extends Tab = Tab> = { tab: T; tabIndex?: number } & {
handleCloseAll: () => void;
handlePinThis: (tab: T) => void;
handleUnpinThis: (tab: T) => void;
pendingCloseConfirmationTab?: Tab | null;
setPendingCloseConfirmationTab?: (tab: Tab | null) => void;
};

type TabItemBaseProps = {
Expand All @@ -28,6 +36,8 @@ type TabItemBaseProps = {
allowPin?: boolean;
isEmptyTab?: boolean;
tabIndex?: number;
showCloseConfirmation?: boolean;
onCloseConfirmationChange?: (show: boolean) => void;
} & {
handleCloseThis: () => void;
handleSelectThis: () => void;
Expand All @@ -51,6 +61,8 @@ export function TabItemBase({
allowPin = true,
isEmptyTab = false,
tabIndex,
showCloseConfirmation = false,
onCloseConfirmationChange,
handleCloseThis,
handleSelectThis,
handleCloseOthers,
Expand All @@ -60,6 +72,51 @@ export function TabItemBase({
}: TabItemBaseProps) {
const isCmdPressed = useCmdKeyPressed();
const [isHovered, setIsHovered] = useState(false);
const [localShowConfirmation, setLocalShowConfirmation] = useState(false);

const isConfirmationOpen = showCloseConfirmation || localShowConfirmation;

useEffect(() => {
if (showCloseConfirmation) {
setLocalShowConfirmation(true);
}
}, [showCloseConfirmation]);

const handleCloseConfirmationChange = (open: boolean) => {
setLocalShowConfirmation(open);
onCloseConfirmationChange?.(open);
};

const handleAttemptClose = () => {
if (active) {
handleCloseConfirmationChange(true);
} else {
handleCloseThis();
}
};

const handleConfirmClose = useCallback(() => {
setLocalShowConfirmation(false);
onCloseConfirmationChange?.(false);
handleCloseThis();
}, [handleCloseThis, onCloseConfirmationChange]);

useEffect(() => {
if (!isConfirmationOpen) return;

const handleKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === "w") {
e.preventDefault();
e.stopPropagation();
handleConfirmClose();
}
};

window.addEventListener("keydown", handleKeyDown, { capture: true });
return () => {
window.removeEventListener("keydown", handleKeyDown, { capture: true });
};
}, [isConfirmationOpen, handleConfirmClose]);

const handleMouseDown = (e: React.MouseEvent) => {
if (e.button === 1 && !active) {
Expand All @@ -72,7 +129,7 @@ export function TabItemBase({
const contextMenu =
active || (selected && !isEmptyTab)
? [
{ id: "close-tab", text: "Close", action: handleCloseThis },
{ id: "close-tab", text: "Close", action: handleAttemptClose },
...(allowPin
? [
{ separator: true as const },
Expand All @@ -87,7 +144,7 @@ export function TabItemBase({
: []),
]
: [
{ id: "close-tab", text: "Close", action: handleCloseThis },
{ id: "close-tab", text: "Close", action: handleAttemptClose },
{
id: "close-others",
text: "Close others",
Expand All @@ -114,7 +171,7 @@ export function TabItemBase({
<div
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
className="h-full"
className="h-full relative"
>
<InteractiveButton
asChild
Expand Down Expand Up @@ -145,7 +202,7 @@ export function TabItemBase({
<div
className={cn([
"absolute inset-0 flex items-center justify-center transition-opacity duration-200",
isHovered ? "opacity-0" : "opacity-100",
isHovered || isConfirmationOpen ? "opacity-0" : "opacity-100",
])}
>
{finalizing ? (
Expand Down Expand Up @@ -176,13 +233,13 @@ export function TabItemBase({
<div
className={cn([
"absolute inset-0 flex items-center justify-center transition-opacity duration-200",
isHovered ? "opacity-100" : "opacity-0",
isHovered || isConfirmationOpen ? "opacity-100" : "opacity-0",
])}
>
<button
onClick={(e) => {
e.stopPropagation();
handleCloseThis();
handleAttemptClose();
}}
className={cn([
"flex items-center justify-center transition-colors",
Expand All @@ -207,6 +264,50 @@ export function TabItemBase({
</div>
)}
</InteractiveButton>
<Popover
open={active && isConfirmationOpen}
onOpenChange={handleCloseConfirmationChange}
>
<PopoverTrigger asChild>
<div className="absolute inset-0 pointer-events-none" />
</PopoverTrigger>
<PopoverContent
side="bottom"
align="start"
className="w-48 p-3 rounded-xl"
sideOffset={2}
onOpenAutoFocus={(e) => e.preventDefault()}
>
<div className="flex flex-col gap-2">
<p className="text-sm text-neutral-700">
Are you sure you want to close this tab? This will stop Hyprnote
from listening.
</p>
<Button
variant="destructive"
size="sm"
className="w-full rounded-lg flex items-center justify-center relative group"
onClick={(e) => {
e.stopPropagation();
handleConfirmClose();
}}
>
<span>Close</span>
<Kbd
className={cn([
"absolute right-2",
"bg-red-200/20 border-red-200/30 text-red-100",
"transition-all duration-100",
"group-hover:-translate-y-0.5 group-hover:shadow-[0_2px_0_0_rgba(0,0,0,0.15),inset_0_1px_0_0_rgba(255,255,255,0.8)]",
"group-active:translate-y-0.5 group-active:shadow-none",
])}
>
⌘ W
</Kbd>
</Button>
</div>
</PopoverContent>
</Popover>
</div>
);
}
Loading