Skip to content

Conversation

@ucswift
Copy link
Member

@ucswift ucswift commented Feb 5, 2026

Summary by CodeRabbit

  • New Features

    • Auto-connect when selecting a transmission channel.
    • Audio feedback for start/stop transmission.
  • Bug Fixes

    • Clearer microphone error messages and improved web permission handling.
  • Refactor

    • Simplified PTT interface and connection state handling.
    • Use built-in packaging flag for dev/production detection.
  • Chores

    • CI step to auto-update Electron package version before builds.
  • Translations

    • Added "Disconnected" translations (en/ar/es).
  • Tests

    • Added logger warn mock in store tests.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 5, 2026

📝 Walkthrough

Walkthrough

Updates add CI steps to set Electron package version, replace an external is-dev check with Electron's app.isPackaged, change productName, remove audio-stream UI and onOpenAudioStreams prop, implement auto-connect and audio feedback for PTT, and add web microphone permission handling and "disconnected" translations.

Changes

Cohort / File(s) Summary
CI Workflows
\.github/workflows/react-native-cicd.yml
Adds a step to install jq, compute an Electron version (1.{run_number}.0), backup and update package.json .version before Electron build steps.
Electron config & main
electron/main.js, package.json
Replaces electron-is-dev usage with !app.isPackaged; updates build.productName from "Dispatch" to "Resgrid Dispatch".
PTT UI & Hooks
src/components/dispatch-console/ptt-interface.tsx, src/hooks/use-ptt.ts
Removes audio-stream panel and onOpenAudioStreams prop; changes default channel to "Disconnected"; implements auto-connect on PTT press and channel selection; integrates audioService to play start/stop transmit sounds; improves microphone error messages.
LiveKit store (permissions & connect)
src/stores/app/livekit-store.ts
Adds web MediaDevices microphone permission requests with specific error logging; calls requestPermissions() before connecting; wraps microphone/camera state toggles in try/catch.
Translations
src/translations/en.json, src/translations/ar.json, src/translations/es.json
Adds dispatch.disconnected translations for English, Arabic, and Spanish.
Tests
src/stores/app/__tests__/livekit-store.test.ts
Adds a logger.warn mock and minor formatting adjustments in test setup.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant PTTInterface
    participant LiveKitStore
    participant AudioService
    participant LiveKit

    User->>PTTInterface: Press PTT
    PTTInterface->>PTTInterface: Check isConnected
    alt Not connected
        PTTInterface->>PTTInterface: Select default/first channel
        PTTInterface->>LiveKitStore: requestPermissions()
        LiveKitStore->>LiveKitStore: navigator.mediaDevices.getUserMedia(audio)
        LiveKitStore-->>PTTInterface: Permission result
        PTTInterface->>LiveKitStore: connectToRoom()
        LiveKit-->>LiveKitStore: Connected
    end
    PTTInterface->>PTTInterface: Enable microphone
    PTTInterface->>AudioService: playStartTransmittingSound()
    AudioService-->>User: Play start sound
    User->>PTTInterface: Release PTT
    PTTInterface->>PTTInterface: Disable microphone
    PTTInterface->>AudioService: playStopTransmittingSound()
    AudioService-->>User: Play stop sound
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

  • Develop #83: Edits the same Electron main process and CI workflow files; likely related adjustments to Electron startup and CI/versioning.

Poem

🐰 I hopped to tweak the sound,

Auto-connect when PTT's found,
A beep to start, a chime to end,
Permissions asked from web to friend,
Disconnected shows — then we resound 🎧

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Develop' is vague and generic, providing no meaningful information about the actual changes in this pull request. Replace the title with a clear, specific summary of the main changes, such as 'Add auto-connect PTT flow, update Electron versioning, and remove audio stream UI components' or similar.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch develop

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
.github/workflows/react-native-cicd.yml (1)

209-218: ⚠️ Potential issue | 🟠 Major

Align package.json version to full semver (MAJOR.MINOR.PATCH).

The current code writes 1.${{ github.run_number }} (2 segments) to package.json, which violates semver requirements enforced by npm, EAS/Expo, and Electron Builder. npm requires versions parseable by node-semver (3-part core). EAS's autoIncrement: "version" expects MAJOR.MINOR.PATCH. Electron Builder rejects non-SemVer formats with "App version is not valid semver". Update to 1.${{ github.run_number }}.0 (3 segments) to match the pattern already used for ELECTRON_VERSION elsewhere in the workflow and prevent build failures.

🔧 Suggested fix
-            jq --arg version "1.${{ github.run_number }}" --argjson versionCode "$androidVersionCode" '.version = $version | .versionCode = $versionCode' package.json > package.json.tmp && mv package.json.tmp package.json
+            jq --arg version "1.${{ github.run_number }}.0" --argjson versionCode "$androidVersionCode" '.version = $version | .versionCode = $versionCode' package.json > package.json.tmp && mv package.json.tmp package.json
src/components/dispatch-console/ptt-interface.tsx (1)

27-80: ⚠️ Potential issue | 🟡 Minor

Wrap the hard-coded 'Disconnected' default in t() for proper internationalization.

The parameter default currentChannel: externalChannel = 'Disconnected' bypasses translation when isConnected is true but pttChannel?.Name is falsy. Remove the hard-coded default and use a translated fallback instead.

Proposed fix
-export const PTTInterface: React.FC<PTTInterfaceProps> = ({ onPTTPress, onPTTRelease, isTransmitting: externalTransmitting = false, currentChannel: externalChannel = 'Disconnected' }) => {
+export const PTTInterface: React.FC<PTTInterfaceProps> = ({ onPTTPress, onPTTRelease, isTransmitting: externalTransmitting = false, currentChannel: externalChannel }) => {
   const { t } = useTranslation();
   const { colorScheme } = useColorScheme();
@@
-  const displayChannel = isConnected ? (pttChannel?.Name || externalChannel) : (pttChannel?.Name || t('dispatch.disconnected'));
+  const fallbackChannel = externalChannel ?? t('dispatch.disconnected');
+  const displayChannel = isConnected ? (pttChannel?.Name || fallbackChannel) : (pttChannel?.Name || t('dispatch.disconnected'));
🤖 Fix all issues with AI agents
In `@src/hooks/use-ptt.ts`:
- Around line 388-401: The catch path currently treats failures from
audioService.playStartTransmittingSound() as a transmit error; change this to
make SFX playback best-effort by isolating or wrapping
playStartTransmittingSound() in its own try/catch so playback errors are logged
(e.g., logger.warn) but do not set or report transmit failure or revert the mic
state; apply the same pattern for audioService.playStopTransmittingSound() (the
similar block around lines 425-427) so audio playbacks cannot abort or mark the
transmit flow as failed.

In `@src/stores/app/livekit-store.ts`:
- Around line 124-129: The Biome rule flags the implicit return from the arrow
used in stream.getTracks().forEach((track) => track.stop()); — update the
forEach callback to use a block-bodied arrow function so it does not return a
value (e.g., replace the single-expression arrow with a block that calls
track.stop();). Locate this code in the navigator.mediaDevices.getUserMedia try
block in livekit-store.ts and change the callback for stream.getTracks().forEach
accordingly.
🧹 Nitpick comments (1)
src/stores/app/livekit-store.ts (1)

134-153: Avoid any in permission error handling.
Use unknown and narrow the shape to keep type safety.

🔧 Proposed refactor
-          } catch (mediaError: any) {
-            if (mediaError.name === 'NotAllowedError') {
+          } catch (mediaError: unknown) {
+            const errorName =
+              typeof mediaError === 'object' && mediaError !== null && 'name' in mediaError
+                ? String((mediaError as { name: unknown }).name)
+                : 'UnknownError';
+            if (errorName === 'NotAllowedError') {
               logger.error({
                 message: 'Microphone permission denied by user',
                 context: { platform: 'web', error: mediaError },
               });
               // Only throw on permission denied - this requires user action
               throw mediaError;
-            } else if (mediaError.name === 'NotFoundError') {
+            } else if (errorName === 'NotFoundError') {
               // No microphone found - log warning but continue
               // User can still listen and may connect a microphone later
               logger.warn({
                 message: 'No microphone device found - voice channel will be listen-only until a microphone is connected',
                 context: { platform: 'web' },
               });
As per coding guidelines: Avoid using `any`; strive for precise types.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/stores/app/livekit-store.ts (1)

136-198: ⚠️ Potential issue | 🟠 Major

On web, NotAllowedError is swallowed by the outer catch—connection proceeds despite explicit mic permission denial.

The web flow throws NotAllowedError when the user denies permission (line 141), but the outer catch block (line 184) logs the error without rethrowing. This allows connectToRoom to continue and create a room connection. Android/iOS correctly return early on denial (line 160), but the web platform lacks this guard. Rethrow the error in the outer catch for web permission denials, or better yet, let the error propagate naturally by not catching it for this specific case.

Also, the mediaError parameter is typed as any (line 137), which violates the TypeScript guideline to avoid any types. Use a more specific type like DOMException.

src/stores/app/__tests__/livekit-store.test.ts (1)

114-121: ⚠️ Potential issue | 🟠 Major

Update web-platform test expectations for new warning paths.
requestPermissions now logs warnings on web when navigator.mediaDevices is unavailable or getUserMedia fails, so the "Unsupported platform handling" test expectations are outdated. Mock navigator.mediaDevices and assert the expected warn calls instead of no logging.

🧪 Suggested test update
 describe('Unsupported platform handling', () => {
   beforeEach(() => {
     (Platform as any).OS = 'web';
+    (global as any).navigator = {
+      mediaDevices: {
+        getUserMedia: jest.fn().mockRejectedValue({ name: 'NotFoundError' }),
+      },
+    };
   });

   it('should handle unsupported platform gracefully', async () => {
     const { requestPermissions } = useLiveKitStore.getState();
     await requestPermissions();

     expect(mockGetRecordingPermissionsAsync).not.toHaveBeenCalled();
     expect(mockRequestRecordingPermissionsAsync).not.toHaveBeenCalled();
-    // For unsupported platforms, the function just returns without logging
-    expect(mockLogger.info).not.toHaveBeenCalled();
-    expect(mockLogger.error).not.toHaveBeenCalled();
+    expect(mockLogger.warn).toHaveBeenCalled();
   });
 });
🧹 Nitpick comments (1)
src/stores/app/livekit-store.ts (1)

122-156: Guard navigator and avoid any in the web permission path.
In non-browser contexts (tests/SSR), navigator can be undefined, and any defeats type-safety.

♻️ Proposed adjustment
-        if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
+        const mediaDevices = typeof navigator !== 'undefined' ? navigator.mediaDevices : undefined;
+        if (mediaDevices?.getUserMedia) {
           try {
             // Request microphone access - this will prompt the user for permission
-            const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
+            const stream = await mediaDevices.getUserMedia({ audio: true });
             // Release the stream immediately - LiveKit will get its own stream
             stream.getTracks().forEach((track) => {
               track.stop();
             });
             logger.info({
               message: 'Microphone permission granted successfully',
               context: { platform: 'web' },
             });
-          } catch (mediaError: any) {
-            if (mediaError.name === 'NotAllowedError') {
+          } catch (mediaError: unknown) {
+            const errorName =
+              typeof mediaError === 'object' && mediaError !== null && 'name' in mediaError
+                ? String((mediaError as { name?: unknown }).name)
+                : undefined;
+            if (errorName === 'NotAllowedError') {
               logger.error({
                 message: 'Microphone permission denied by user',
                 context: { platform: 'web', error: mediaError },
               });
               // Only throw on permission denied - this requires user action
               throw mediaError;
-            } else if (mediaError.name === 'NotFoundError') {
+            } else if (errorName === 'NotFoundError') {
               // No microphone found - log warning but continue
               // User can still listen and may connect a microphone later
               logger.warn({
                 message: 'No microphone device found - voice channel will be listen-only until a microphone is connected',
                 context: { platform: 'web' },
               });
             } else {
               logger.warn({
                 message: 'Failed to request microphone permission - continuing with limited audio',
                 context: { platform: 'web', error: mediaError },
               });
             }
           }

As per coding guidelines, avoid using any; strive for precise types.

@ucswift
Copy link
Member Author

ucswift commented Feb 5, 2026

Approve

Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

This PR is approved.

@ucswift ucswift merged commit 1c21d14 into master Feb 5, 2026
11 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant