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
6 changes: 6 additions & 0 deletions apps/desktop/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<title>Hyprnote</title>
<style>
html, body, #root {
background: transparent !important;
background-color: transparent !important;
}
</style>
</head>
<body spellcheck="false">
<div id="root"></div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
VolumeOffIcon,
} from "lucide-react";
import { useEffect, useState } from "react";
import { emit, listen } from "@tauri-apps/api/event";

import SoundIndicator from "@/components/sound-indicator";
import { useHypr } from "@/contexts";
Expand Down Expand Up @@ -227,7 +228,8 @@ export function WhenActive() {

useEffect(() => {
if (showConsent) {
listenerCommands.setSpeakerMuted(true).then(() => {
listenerCommands.setSpeakerMuted(true).then(async () => {
await emit("audio-speaker-state-changed", { muted: true });
audioControls.refetchSpeakerMuted();
});
}
Expand All @@ -237,9 +239,12 @@ export function WhenActive() {
mutationFn: async (recordEveryone: boolean) => {
if (recordEveryone) {
await listenerCommands.setSpeakerMuted(false);
await emit("audio-speaker-state-changed", { muted: false });
} else {
await listenerCommands.setSpeakerMuted(true);
await listenerCommands.setMicMuted(false);
await emit("audio-speaker-state-changed", { muted: true });
await emit("audio-mic-state-changed", { muted: false });
}
setHasShownConsent(true);
},
Expand Down Expand Up @@ -438,13 +443,43 @@ function useAudioControls() {
queryFn: () => listenerCommands.getSpeakerMuted(),
});

// Listen for audio state changes from other windows
useEffect(() => {
const unsubscribeMicState = listen<{ muted: boolean }>("audio-mic-state-changed", ({ payload }) => {
console.log(`[Main Window] Received mic state change:`, payload);
refetchMicMuted();
});

const unsubscribeSpeakerState = listen<{ muted: boolean }>("audio-speaker-state-changed", ({ payload }) => {
console.log(`[Main Window] Received speaker state change:`, payload);
refetchSpeakerMuted();
});

return () => {
unsubscribeMicState.then(unlisten => unlisten());
unsubscribeSpeakerState.then(unlisten => unlisten());
};
}, [refetchMicMuted, refetchSpeakerMuted]);

Comment on lines +446 to +463
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Potential memory-leak & race in event listener cleanup

listen returns Promise<UnlistenFn>.
Storing the promise and later calling .then(unlisten => unlisten()) inside the cleanup works, but if the promise rejects (e.g. window closed) the .catch() branch is swallowed and the listener may linger.
A safer pattern is:

useEffect(() => {
-  const unsub = listen<Payload>("evt", handler);
+  let unlisten: (() => void) | undefined;
+  const unsubPromise = listen<Payload>("evt", handler)
+    .then(fn => (unlisten = fn))
+    .catch(console.error);

  return () => {
-    unsub.then(u => u()).catch(console.error);
+    if (unlisten) unlisten();
  };
}, []);

This guarantees that the actual unlisten function is called or the error is surfaced.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/desktop/src/components/editor-area/note-header/listen-button.tsx around
lines 446 to 463, the cleanup function for event listeners uses .then() on
promises returned by listen(), but does not handle promise rejections, risking
unhandled errors and lingering listeners. To fix this, convert the cleanup
function to async, await the promises from listen(), and call the unlisten
functions inside a try-catch block to properly handle any errors during
unsubscription.

const toggleMicMuted = useMutation({
mutationFn: () => listenerCommands.setMicMuted(!isMicMuted),
mutationFn: async () => {
const newMuted = !isMicMuted;
await listenerCommands.setMicMuted(newMuted);
// Emit event to synchronize with other windows
await emit("audio-mic-state-changed", { muted: newMuted });
return newMuted;
},
onSuccess: () => refetchMicMuted(),
Comment on lines +465 to 472
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Undefined state can flip mic unintentionally

isMicMuted can be undefined until the first query resolves.
!undefined evaluates to true, so the very first click may always mute, regardless of the actual device state.

- const newMuted = !isMicMuted;
+ if (isMicMuted === undefined) {
+   return; // or disable the button until loaded
+ }
+ const newMuted = !isMicMuted;

The same applies to toggleSpeakerMuted.
Also consider passing disabled={isMicMuted === undefined} to <AudioControlButton> to prevent premature clicks.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
mutationFn: async () => {
const newMuted = !isMicMuted;
await listenerCommands.setMicMuted(newMuted);
// Emit event to synchronize with other windows
await emit("audio-mic-state-changed", { muted: newMuted });
return newMuted;
},
onSuccess: () => refetchMicMuted(),
mutationFn: async () => {
if (isMicMuted === undefined) {
return; // or disable the button until loaded
}
const newMuted = !isMicMuted;
await listenerCommands.setMicMuted(newMuted);
// Emit event to synchronize with other windows
await emit("audio-mic-state-changed", { muted: newMuted });
return newMuted;
},
onSuccess: () => refetchMicMuted(),
🤖 Prompt for AI Agents
In apps/desktop/src/components/editor-area/note-header/listen-button.tsx around
lines 465 to 472, the isMicMuted state can be undefined initially, causing the
toggle logic to incorrectly flip the mic state on the first click. To fix this,
add a guard to check if isMicMuted is undefined before toggling and prevent the
mutation if so. Also, pass disabled={isMicMuted === undefined} to the
<AudioControlButton> component to disable the button until the state is
resolved. Apply the same fix for toggleSpeakerMuted to handle its undefined
state similarly.

});

const toggleSpeakerMuted = useMutation({
mutationFn: () => listenerCommands.setSpeakerMuted(!isSpeakerMuted),
mutationFn: async () => {
const newMuted = !isSpeakerMuted;
await listenerCommands.setSpeakerMuted(newMuted);
// Emit event to synchronize with other windows
await emit("audio-speaker-state-changed", { muted: newMuted });
return newMuted;
},
onSuccess: () => refetchSpeakerMuted(),
});

Expand Down
14 changes: 14 additions & 0 deletions apps/desktop/src/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useQuery } from "@tanstack/react-query";
import { CatchNotFound, createRootRouteWithContext, Outlet, useNavigate } from "@tanstack/react-router";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
import { listen } from "@tauri-apps/api/event";
import { lazy, Suspense, useEffect } from "react";

import { CatchNotFoundFallback, ErrorComponent, NotFoundComponent } from "@/components/control";
Expand Down Expand Up @@ -66,6 +67,19 @@ function Component() {
scan({ enabled: false });
}, []);

// Listen for debug events from control window
useEffect(() => {
let unlisten: (() => void) | undefined;

listen<string>("debug", (event) => {
console.log(`[Control Debug] ${event.payload}`);
}).then((fn) => {
unlisten = fn;
});

return () => unlisten?.();
}, []);

return (
<>
<ClipboardHandler />
Expand Down
Loading