Skip to content

setup fixes + pair_chat_dialog fix#790

Open
mahmoudr80 wants to merge 1 commit intoAOSSIE-Org:masterfrom
mahmoudr80:pair_chat_dialog_fix_and_setup_fix
Open

setup fixes + pair_chat_dialog fix#790
mahmoudr80 wants to merge 1 commit intoAOSSIE-Org:masterfrom
mahmoudr80:pair_chat_dialog_fix_and_setup_fix

Conversation

@mahmoudr80
Copy link
Copy Markdown

@mahmoudr80 mahmoudr80 commented Mar 25, 2026

Description

This PR fixes multiple issues that prevent the app from running on a fresh clone and resolves a UI overflow bug in the pair chat dialog.

Summary of changes:

  • Removed broken flutter_lyric Git dependency (repository not available)
  • Disabled google-services plugin to avoid missing Firebase configuration (google-services.json)
  • Made Firebase initialization and usage optional to prevent runtime crashes
  • Refactored FirebaseMessaging usage to avoid [core/no-app] errors
  • Fixed Gradle crash caused by missing key.properties by making signing config null-safe
  • Handled Appwrite connection failures (local IP not accessible) to prevent runtime crashes
  • Fixed missing GetX dependency injection issues (e.g., TabViewController)
  • Fixed LateInitializationError in UpcomingRoomsController by initializing storage properly
  • Fixed UI overflow (RenderFlex overflow) in pair_chat_dialog.dart by adding scroll and layout constraints

These changes improve developer onboarding and allow the app to run without requiring Firebase or backend setup.

Fixes # (issue)


Type of change

  • [o] Bug fix (non-breaking CHANGE which fixes an issue)
  • [o] New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • [o] Refactor (does not change functionality, e.g. code style improvements, linting)
  • Documentation update

How Has This Been Tested?

  • Cloned the repository on a clean environment

  • Ran:

    flutter pub get
    flutter run
  • Verified that:

    • App builds successfully without errors
    • App launches without Firebase or backend configuration
    • Navigation works (bypassing auth)
    • Pair chat dialog no longer overflows and is scrollable
  • Tested on emulator/device with different screen sizes


Checklist:

  • [o] My code follows the style guidelines of this project
  • [o] I have performed a self-review of my own code
  • [o] I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • [o] My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • [o] Any dependent changes have been merged and published in downstream modules
  • [o] I have checked my code and corrected any misspellings

Maintainer Checklist

Summary by CodeRabbit

Release Notes

  • New Features

    • Added audio device selection during calls to choose your preferred speaker.
    • Added message deletion capability in room chats with confirmation.
    • Added friend discovery and invitation features with empty-state guidance.
  • Improvements

    • Strengthened username validation with minimum 7 characters and stricter format requirements.
    • Expanded localization support to Bengali, Malayalam, Punjabi, and Tamil languages.

@mahmoudr80 mahmoudr80 requested a review from M4dhav as a code owner March 25, 2026 08:34
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 25, 2026

📝 Walkthrough

Walkthrough

The PR addresses issue #788 by disabling Firebase initialization and removing the flutter_lyric dependency. It simultaneously migrates the entire Appwrite data layer from Collections/Documents APIs to Tables/Rows APIs, adds audio output device selection for calls, implements message deletion in chat, enhances username validation, and expands localization support to include Bengali, Malayalam, Punjabi, and Tamil languages with corresponding new UI strings.

Changes

Cohort / File(s) Summary
Appwrite Database Migration
lib/controllers/auth_state_controller.dart, lib/controllers/change_email_controller.dart, lib/controllers/delete_account_controller.dart, lib/controllers/edit_profile_controller.dart, lib/controllers/email_verify_controller.dart, lib/controllers/explore_story_controller.dart, lib/controllers/friend_calling_controller.dart, lib/controllers/friends_controller.dart, lib/controllers/live_chapter_controller.dart, lib/controllers/onboarding_controller.dart, lib/controllers/pair_chat_controller.dart, lib/controllers/room_chat_controller.dart, lib/controllers/rooms_controller.dart, lib/controllers/single_room_controller.dart, lib/controllers/upcomming_rooms_controller.dart, lib/controllers/user_profile_controller.dart, lib/services/appwrite_service.dart, lib/services/room_service.dart, lib/views/widgets/rating_sheet.dart, lib/views/widgets/report_widget.dart, test/controllers/auth_state_controller_test.dart
Replaced all Appwrite Databases/Document APIs with TablesDB/Row equivalents across controllers, services, and tests. Updated parameter names (collectionIdtableId, documentIdrowId), method calls (getDocumentgetRow, etc.), result field access (.documents.rows), and realtime subscription paths. Made FirebaseMessaging nullable in controllers that depend on it.
Firebase & Build Configuration
lib/main.dart, pubspec.yaml, android/app/build.gradle.kts
Commented out Firebase initialization call and corresponding import. Disabled the Google Services Gradle plugin and adjusted signing configuration to defensively handle missing keystore properties with nullable lookups. Commented out flutter_lyric Git dependency.
Audio Device Selection Feature
lib/controllers/audio_device_controller.dart, lib/models/audio_device.dart, lib/utils/enums/audio_device_enum.dart, lib/views/widgets/audio_selector_dialog.dart, test/controllers/audio_device_controller_test.dart
Added new AudioDeviceController with reactive device enumeration (5-second polling), device selection logic, and device name formatting. Introduced AudioDevice model with AudioDeviceType enum mapping device labels to icon/display names. Created reusable AudioDeviceSelectorDialog widget and showAudioDeviceSelector helper function. Integrated into call/chat screens via new audio settings buttons.
Message Deletion & Chat
lib/models/message.dart, lib/controllers/room_chat_controller.dart, lib/views/screens/room_chat_screen.dart
Added isDeleted boolean field to Message model with serialization support. Implemented RoomChatController.deleteMessage() to mark messages as deleted remotely. Updated RoomChatScreen with deletion permissions, long-press context menu, confirmation dialog, and visual indicators for deleted messages.
Call Screens & Audio Integration
lib/controllers/friend_call_screen.dart, lib/controllers/friend_calling_controller.dart, lib/views/screens/live_chapter_screen.dart, lib/views/screens/room_screen.dart
Added audio device selector buttons to friend call, live chapter, and room screens. Updated FriendCallingController to clean up AudioDeviceController on call end/decline. Replaced leave button UI styling with standardized red call-end icon.
Username Validation & Profiles
lib/controllers/edit_profile_controller.dart, lib/views/screens/edit_profile_screen.dart
Enhanced username validation to require minimum 7 characters (from 5) and enforce alphanumeric + dot/dash/underscore pattern. Added debounced async availability checking with progress indicator and improved error messaging. Updated EditProfileController with usernameChecking reactive field.
Friends & Social Features
lib/views/screens/friends_screen.dart, lib/views/screens/friends_empty_screen.dart, lib/views/screens/friend_requests_screen.dart
Added FriendsEmptyState widget for contextual empty states (friends/friend requests) with action buttons to find friends or share invitations. Integrated into friends and friend requests screens when lists are empty. Updated friend requests filtering to use friendsController.authStateController.
Chapter Player Disabling
lib/controllers/chapter_player_controller.dart, lib/views/screens/chapter_play_screen.dart, lib/views/widgets/chapter_player.dart, pubspec.yaml
Fully commented out ChapterPlayerController class and all lyric/playback UI code in chapter play screen and chapter player widget. Disabled flutter_lyric dependency.
Localization Expansion (Core)
lib/l10n/app_localizations.dart, lib/l10n/app_localizations_en.dart, lib/l10n/app_en.arb, lib/utils/constants.dart
Added support for 4 new locales (bn, ml, pa, ta) in localization registry. Updated username character limit validation message to "more than 7". Added new localization keys for audio settings, message deletion, friend discovery, and username validation. Added playStoreUrl constant.
Localization Implementations
lib/l10n/app_localizations_bn.dart, lib/l10n/app_localizations_gu.dart, lib/l10n/app_localizations_hi.dart, lib/l10n/app_localizations_kn.dart, lib/l10n/app_localizations_ml.dart, lib/l10n/app_localizations_mr.dart, lib/l10n/app_localizations_pa.dart, lib/l10n/app_localizations_ta.dart
Added/updated implementation classes for multiple locales with new audio, message deletion, and friend-related strings. Implemented pluralized time-ago formatting and parameterized message builders using intl library.
Localization Resource Files
lib/l10n/app_bn.arb, lib/l10n/app_gu.arb, lib/l10n/app_hi.arb, lib/l10n/app_kn.arb, lib/l10n/app_ml.arb, lib/l10n/app_mr.arb, lib/l10n/app_pa.arb, lib/l10n/app_ta.arb
Added comprehensive ARB resource files for Bengali, Gujarati, Hindi, Kannada, Malayalam, Marathi, Punjabi, and Tamil with UI strings, ICU message formatting, and placeholder schemas.
Infrastructure & Routing
lib/routes/app_pages.dart, lib/services/api_service.dart, lib/themes/theme.dart
Added TabViewBinding to home screen route. Made Functions dependency injectable in ApiService. Added theme color scheme properties (surfaceContainerHighest, onSurfaceVariant, secondaryContainer).
Code Cleanup
lib/views/screens/add_chapter_screen.dart, lib/views/screens/email_verification_screen.dart, lib/views/widgets/pair_chat_dialog.dart, lib/themes/theme_icon_enum.dart, lib/utils/enums/update_enums.dart
Added trailing newlines and fixed EOF formatting. Updated pair chat dialog to be scrollable and use safe display name fallback.
Login Screen & Navigation
lib/views/screens/login_screen.dart, lib/views/screens/story_screen.dart
Removed Firebase login flow (commented out await controller.login(context)), replaced with direct navigation to tabview. Commented out ChapterPlayerController instantiation before chapter selection.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant AudioSelectorDialog as Audio Selector<br/>Dialog
    participant AudioDeviceController as Audio Device<br/>Controller
    participant WebRTC as flutter_webrtc
    participant AppState as Room/Call<br/>Screen

    User->>AudioSelectorDialog: Open audio selector
    AudioSelectorDialog->>AudioDeviceController: showAudioDeviceSelector()
    AudioDeviceController->>AudioDeviceController: Put controller (permanent)
    AudioDeviceController->>WebRTC: enumerateDevices()
    WebRTC-->>AudioDeviceController: List<MediaDeviceInfo>
    AudioDeviceController->>AudioDeviceController: Filter audiooutput devices
    AudioDeviceController->>AudioDeviceController: Set initial selection
    AudioSelectorDialog->>AudioDeviceController: Read audioOutputDevices (Obx)
    AudioSelectorDialog-->>User: Display device list
    User->>AudioSelectorDialog: Select device
    AudioSelectorDialog->>AudioDeviceController: selectAudioOutput(device)
    AudioDeviceController->>AudioDeviceController: Update selectedAudioOutput
    User->>AudioSelectorDialog: Close/Done
    AudioSelectorDialog-->>AppState: Bottom sheet closes
    AppState->>AppState: Selected device ready for use
    AudioDeviceController->>AudioDeviceController: Periodic refresh (every 5s)
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • feat: Upgraded Deprecated Appwrite Code #609 — Performs comprehensive Appwrite API migration from Databases/Document to TablesDB/Row APIs across controllers and services, directly overlapping with the main database layer changes.
  • Output change #597 — Adds audio output device selection feature (AudioDeviceController, AudioDevice model, audio selector widget, and integration into call screens), with code-level overlap.
  • feat: implement delete message feature in chat room #573 — Implements message soft-delete feature (Message.isDeleted field, RoomChatController.deleteMessage method, UI context menu, and localization keys), matching the chat deletion changes.

Suggested labels

enhancement, bug-fix, localization, breaking-change

Suggested reviewers

  • M4dhav

Poem

🐰 Hops through databases with tables in sight,
Audio devices selected just right,
Messages deleted, friends discovered anew,
Firebase disabled, flutter_lyric adieu!
Many tongues speak now—Bengali to Tamil bloom,
This refactor brings joy to the room! 🎵

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Copy link
Copy Markdown

@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: 15

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (16)
lib/views/screens/login_screen.dart (1)

114-119: ⚠️ Potential issue | 🔴 Critical

Don't route into AppRoutes.tabview before controller.login(context) succeeds.

This now grants access to the authenticated shell after only local form validation, so invalid credentials can still enter the app. If this bypass is only meant to unblock local setup, gate it behind a debug/guest flag instead of replacing the normal login path.

🔐 Suggested fix
                           onPressed: () async {
                             if (!controller.isLoading.value) {
                               if (controller.loginFormKey.currentState!
                                   .validate()) {
-                               // await controller.login(context);
-                                Get.offAllNamed(AppRoutes.tabview);                              }
+                                await controller.login(context);
+                              }
                             }
                           },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/views/screens/login_screen.dart` around lines 114 - 119, The button
currently bypasses authentication by navigating to AppRoutes.tabview after only
local validation; restore and await the actual login flow by calling await
controller.login(context) (or using its returned success flag) and only call
Get.offAllNamed(AppRoutes.tabview) when that call reports success; use
controller.isLoading and controller.loginFormKey to gate duplicate submits, and
if you need a temporary bypass make it conditional on a debug/guest flag (e.g.,
kDebugMode or a controller.isGuest flag) rather than replacing the normal path.
android/app/build.gradle.kts (1)

47-63: ⚠️ Potential issue | 🟠 Major

Remove the unconditional override of the debug signing config, or conditionally assign the release config only when keystore properties exist.

The release buildType unconditionally assigns signingConfigs.getByName("release") on line 62, overriding the debug fallback from line 61. When key.properties is missing, the release config has a null storeFile and empty passwords, causing the Android Gradle Plugin's validateSigning task to fail the build with "Keystore file not set for signing config 'release'". A fresh clone without key.properties cannot build for release. Conditionally create and use the release config only when all required keystore properties are present.

🛠️ Suggested fix
 val keystoreProperties = Properties()
 val keystorePropertiesFile = rootProject.file("key.properties")
 if (keystorePropertiesFile.exists()) {
     keystoreProperties.load(FileInputStream(keystorePropertiesFile))
 }
+val hasReleaseKeystore = listOf(
+    "keyAlias",
+    "keyPassword",
+    "storeFile",
+    "storePassword",
+).all { keystoreProperties[it] != null }
@@
     buildTypes {
         signingConfigs {
-        create("release") {
-            keyAlias = keystoreProperties["keyAlias"] as String? ?: ""
-            keyPassword = keystoreProperties["keyPassword"] as String? ?: ""
-            storeFile = keystoreProperties["storeFile"]?.let { file(it) }
-            storePassword = keystoreProperties["storePassword"] as String? ?: ""
-        }
-    }
+            if (hasReleaseKeystore) {
+                create("release") {
+                    keyAlias = keystoreProperties["keyAlias"] as String
+                    keyPassword = keystoreProperties["keyPassword"] as String
+                    storeFile = file(keystoreProperties["storeFile"] as String)
+                    storePassword = keystoreProperties["storePassword"] as String
+                }
+            }
+        }
         release {
             // TODO: Add your own signing config for the release build.
             // Signing with the debug keys for now, so `flutter run --release` works.
             isMinifyEnabled = false
             isShrinkResources = false
-            signingConfig = signingConfigs.getByName("debug")
-            signingConfig = signingConfigs.getByName("release")
+            signingConfig = if (hasReleaseKeystore) {
+                signingConfigs.getByName("release")
+            } else {
+                signingConfigs.getByName("debug")
+            }
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/build.gradle.kts` around lines 47 - 63, The release buildType
currently overwrites the debug signingConfig unconditionally (signingConfig =
signingConfigs.getByName("release")), causing invalid release configs when key
properties are missing; change this so the "release" signingConfig is only
created/assigned when the keystoreProperties (keyAlias, keyPassword, storeFile,
storePassword) are present and storeFile is non-null — otherwise keep the debug
signingConfig as the fallback. Locate the signingConfigs.create("release") block
and the release { ... signingConfig = ... } block and add a conditional check
around assigning signingConfigs.getByName("release") (or skip creating the
release config entirely) so the release build uses the real keystore only when
all required keystoreProperties exist.
lib/l10n/app_gu.arb (1)

579-611: ⚠️ Potential issue | 🟠 Major

Incorrect placeholder syntax in ARB strings.

Lines 579, 590, and 602 use Dart-style ${username} interpolation instead of ICU message format {username}. ARB files require ICU syntax for placeholders to work correctly with Flutter's localization system.

🐛 Proposed fix
-    "friendRequestAcceptedTo": "તમે હવે ${username} સાથે મિત્ર છો.",
+    "friendRequestAcceptedTo": "તમે હવે {username} સાથે મિત્ર છો.",
     ...
-    "friendRequestDeclinedTo": "તમે ${username} ની મિત્ર વિનંતી નકારી છે.",
+    "friendRequestDeclinedTo": "તમે {username} ની મિત્ર વિનંતી નકારી છે.",
     ...
-    "callDeclinedTo": "યુઝર ${username} એ કૉલ નકારી છે.",
+    "callDeclinedTo": "યુઝર {username} એ કૉલ નકારી છે.",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/l10n/app_gu.arb` around lines 579 - 611, The ARB entries
friendRequestAcceptedTo, friendRequestDeclinedTo, and callDeclinedTo use Dart
`${username}` interpolation which is invalid for ARB/ICU; update those string
values to use ICU placeholders `{username}` (e.g., change "તમે હવે ${username}
સાથે મિત્ર છો." to use "{username}") while keeping the corresponding
"@..."/placeholders blocks intact (no changes needed to placeholder
definitions).
lib/main.dart (1)

51-60: ⚠️ Potential issue | 🔴 Critical

FirebaseMessaging.onBackgroundMessage will fail without Firebase initialization.

FirebaseMessaging.onBackgroundMessage is registered on line 53, but Firebase.initializeApp is commented out (lines 58-60). When a background message arrives, this will throw a [core/no-app] error because Firebase Core isn't initialized.

To make Firebase truly optional, either:

  1. Comment out the onBackgroundMessage registration as well, or
  2. Wrap both in a try-catch or conditional based on Firebase availability
🐛 Option 1: Comment out background message handler registration
 Future<void> main() async {
   WidgetsFlutterBinding.ensureInitialized();
-  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
+  // FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
   Get.testMode = false;
   SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);

   // Initialize Firebase
   // await Firebase.initializeApp(
   //     options: DefaultFirebaseOptions.currentPlatform
   // );
🐛 Option 2: Conditional Firebase initialization with error handling
 Future<void> main() async {
   WidgetsFlutterBinding.ensureInitialized();
-  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
   Get.testMode = false;
   SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);

-  // Initialize Firebase
-  // await Firebase.initializeApp(
-  //     options: DefaultFirebaseOptions.currentPlatform
-  // );
+  // Initialize Firebase (optional - only if firebase_options.dart exists)
+  try {
+    // Uncomment when Firebase is configured:
+    // await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
+    // FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
+  } catch (e) {
+    log('Firebase initialization skipped or failed: $e');
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/main.dart` around lines 51 - 60, FirebaseMessaging.onBackgroundMessage is
being registered before Firebase.initializeApp is called (Firebase.initializeApp
is commented out), which will cause a [core/no-app] error when background
messages arrive; fix by either removing/commenting out the registration of
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler) or
ensure Firebase is initialized first (call Firebase.initializeApp or wrap the
initialize + registration in a try/catch or conditional that checks Firebase
availability) so that _firebaseMessagingBackgroundHandler is only registered
after successful initialization.
lib/views/widgets/rating_sheet.dart (1)

59-71: ⚠️ Potential issue | 🟠 Major

Guard rating submission for guest / unconfigured sessions.

This still assumes a signed-in Appwrite user via authController.uid! and an available backend. The PR explicitly allows startup without auth/backend wiring, so pressing Submit in that state will throw instead of failing gracefully. Please disable the action or short-circuit with a friendly prompt when the user profile is unavailable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/views/widgets/rating_sheet.dart` around lines 59 - 71, The onPressed
handler currently assumes an authenticated user and backend by calling
tablesDB.updateRow with authController.uid! and then
authController.setUserProfileData; guard this path by first checking for a valid
user id and backend availability (e.g., if authController.uid == null or
tablesDB is null/uninitialized) and short-circuit: either disable the Submit
button when no user/profile is present or return early from the onPressed and
show a friendly prompt/Toast informing the user the session is not configured.
Update the onPressed logic around tablesDB.updateRow, authController.uid,
authController.setUserProfileData, and controller.pairRating to only run the
update+setUserProfileData when authController.uid is non-null and tablesDB is
available.
lib/controllers/onboarding_controller.dart (2)

88-131: ⚠️ Potential issue | 🟠 Major

Rollback the username reservation if a later step fails.

The username row is created before the image upload and before the user profile row. If either later operation fails, the username remains permanently claimed even though onboarding never completed, so the user can't retry with the same handle. Please add compensating cleanup for the reserved username (and any uploaded file, if applicable), or move the whole flow behind a transactional backend endpoint.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/controllers/onboarding_controller.dart` around lines 88 - 131, The
username is reserved early via tables.createRow (using usernameController.text)
but not rolled back if subsequent steps (storage.createFile,
authStateController.account.updateName, or the second tables.createRow for
usersTableID) fail; wrap the post-reservation work in a try/catch and on any
error delete the reserved username row (call the corresponding tables.deleteRow
for userDatabaseID/usernameTableID with rowId=usernameController.text.trim())
and also delete any uploaded file (call storage.deleteFile for
userProfileImageBucketId and uniqueIdForProfileImage if storage.createFile
succeeded), then rethrow or surface the original error so onboarding can be
retried cleanly.

190-200: ⚠️ Potential issue | 🟡 Minor

Check for specific not-found exception before returning username availability.

The current implementation catches all exceptions and returns true, which masks network, authentication, or server failures as username being available. Based on patterns used elsewhere in the codebase (e.g., room_chat_controller.dart), only treat a not-found error (AppwriteException with code == 404 or type == 'document_not_found') as username available. Rethrow all other exceptions so validation failures are properly surfaced instead of silently passing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/controllers/onboarding_controller.dart` around lines 190 - 200, In
isUsernameAvailable, don't catch all exceptions as "available": call
tables.getRow (used in isUsernameAvailable) and only treat an AppwriteException
with code == 404 or type == 'document_not_found' as username available (return
true); for any other exception (network/auth/server) rethrow it so callers can
handle failures; ensure you import/use AppwriteException or check
exception.runtimeType/name appropriately and log as needed before rethrowing.
lib/views/screens/chapter_play_screen.dart (1)

104-164: ⚠️ Potential issue | 🟡 Minor

Remove the empty lyrics panel while playback is disabled.

With the LyricsReader block commented out, this section now renders as a blank 200px card on every chapter. Either collapse it or replace it with an explicit unavailable-state so the screen doesn't look broken.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/views/screens/chapter_play_screen.dart` around lines 104 - 164, The
container currently leaves a blank 200px card because the LyricsReader block is
commented out; update the UI to conditionally render either the LyricsReader
(when lyrics exist and playback/lyrics are enabled) or an explicit unavailable
state (or collapse it) instead of an empty box. Locate the Container around the
commented LyricsReader and replace the unconditional Container(height: 200, ...)
with a conditional build that checks controller.lyricModel (or
controller.isPlaying / controller.lyricProgress as appropriate) and returns
either the LyricsReader (with the existing props: model: controller.lyricModel,
position: controller.lyricProgress.value, playing: controller.isPlaying.value,
lyricUi, etc.) or a compact unavailable widget (e.g., SizedBox.shrink() or a
small Card with an icon and AppLocalizations.of(context)!.noLyrics message) so
the screen no longer shows an empty panel.
lib/views/screens/room_screen.dart (1)

194-216: ⚠️ Potential issue | 🟠 Major

Label the icon-only room actions for accessibility.

The leave control lost its text label, and the new audio-settings FAB is icon-only as well. TalkBack/VoiceOver will treat these as unlabeled buttons unless you add a tooltip/semantics label, which makes key room actions hard to use non-visually.

Possible fix
-        return ElevatedButton(
+        return Tooltip(
+          message: AppLocalizations.of(context)!.leaveButton,
+          child: Semantics(
+            label: AppLocalizations.of(context)!.leaveButton,
+            button: true,
+            child: ElevatedButton(
               onPressed: () async {
                 await _deleteRoomDialog(
                   controller.appwriteRoom.isUserAdmin
                       ? AppLocalizations.of(context)!.delete
                       : AppLocalizations.of(context)!.leave,
@@
               style: ElevatedButton.styleFrom(
                 backgroundColor: Colors.redAccent,
                 foregroundColor: Colors.white,
                 shape: RoundedRectangleBorder(
                   borderRadius: BorderRadius.circular(20),
                 ),
               ),
               child: const Icon(Icons.call_end, size: 24),
-        );
+            ),
+          ),
+        );
-    return FloatingActionButton(
-      onPressed: () async => await showAudioDeviceSelector(context),
-      backgroundColor: Theme.of(context).colorScheme.onSecondary,
-      child: const Icon(Icons.volume_up),
-    );
+    return Tooltip(
+      message: AppLocalizations.of(context)!.audioOptions,
+      child: FloatingActionButton(
+        onPressed: () async => await showAudioDeviceSelector(context),
+        backgroundColor: Theme.of(context).colorScheme.onSecondary,
+        child: const Icon(Icons.volume_up),
+      ),
+    );

Also applies to: 296-301

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/views/screens/room_screen.dart` around lines 194 - 216, The icon-only
room action buttons (the ElevatedButton that calls _deleteRoomDialog and the
audio-settings FAB referenced around lines 296-301) are missing accessibility
labels; wrap the button child in a Tooltip or Semantics (or provide a
semanticLabel on the Icon) so TalkBack/VoiceOver exposes a meaningful label
(e.g., use AppLocalizations strings for "leave"/"delete" or "audio settings"),
leaving the existing onPressed behavior that calls
controller.deleteRoom/leaveRoom via _deleteRoomDialog unchanged.
lib/views/screens/edit_profile_screen.dart (1)

17-27: ⚠️ Potential issue | 🟡 Minor

Debouncer in StatelessWidget may cause issues.

The Debouncer is instantiated as an instance field of a StatelessWidget. Since Flutter can rebuild widgets and create new instances, this could lead to multiple debouncer instances or lost debounce state. Consider moving the debouncer to the EditProfileController or converting this to a StatefulWidget.

🛠️ Recommended approach: Move debouncer to controller

In EditProfileController:

final debouncer = Debouncer(milliseconds: 800);

Then in EditProfileScreen, access it via:

editProfileController.debouncer.run(() async { ... });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/views/screens/edit_profile_screen.dart` around lines 17 - 27, The
Debouncer is currently an instance field on the StatelessWidget
EditProfileScreen which can create multiple/lost debounce state on rebuilds;
move the Debouncer into the EditProfileController (e.g., add a final debouncer =
Debouncer(milliseconds: 800); inside EditProfileController) and remove the
debouncer field from EditProfileScreen, then update all usages in
edit_profile_screen.dart to call editProfileController.debouncer.run(() async {
... }); instead; alternatively, if you prefer widget-local state, convert
EditProfileScreen to a StatefulWidget and create the Debouncer in the State
class, disposing or retaining it appropriately.
lib/controllers/user_profile_controller.dart (1)

144-151: ⚠️ Potential issue | 🔴 Critical

The optional Firebase path still crashes here.

messaging?.getToken() can now legitimately return null, but Line 151 still force-unwraps it. On fresh clones / builds without Firebase configured, following a creator throws before the row is written, which undercuts the PR’s “Firebase is optional” goal.

Suggested fix
     final fcmToken = await authStateController.messaging?.getToken();
     final FollowerUserModel follower = FollowerUserModel(
       docId: ID.unique(),
       uid: authStateController.uid!,
       username: authStateController.userName!,
       profileImageUrl: authStateController.profileImageUrl!,
       name: authStateController.displayName!,
-      fcmToken: fcmToken!,
+      fcmToken: fcmToken ?? '',
       followingUserId: creatorId,
       followerRating:
           authStateController.ratingTotal / authStateController.ratingCount,
     );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/controllers/user_profile_controller.dart` around lines 144 - 151, The
code force-unwraps the FCM token (fcmToken!) even though messaging?.getToken()
can return null; update the creation of the FollowerUserModel to handle a
nullable fcmToken safely (e.g., allow FollowerUserModel.fcmToken to be nullable
or provide a default like empty string) and remove the force-unwrap, obtaining
fcmToken via await authStateController.messaging?.getToken() and passing it
as-is or with a null-coalescing default; adjust the FollowerUserModel
constructor/type if necessary to accept a nullable value so the follow flow
doesn't crash when Firebase messaging is unavailable.
lib/controllers/edit_profile_controller.dart (1)

255-264: ⚠️ Potential issue | 🟠 Major

Delete the storage file and clear the image ID in the removeImage branch.

The remove branch only clears profileImageUrl but leaves the old storage file orphaned (no storage.deleteFile() call) and keeps profileImageID in the database pointing to it. This is inconsistent with the upload branch (lines 218–221) which properly deletes the old file before creating a new one. Add the storage deletion and update the database to also clear profileImageID:

Example fix
      if (removeImage) {
        try {
          await storage.deleteFile(
            bucketId: userProfileImageBucketId,
            fileId: authStateController.profileImageID!,
          );
        } catch (e) {
          log(e.toString());
        }

        imageController.text = "";

        // Update user profile picture URL in Database
        await tables.updateRow(
          databaseId: userDatabaseID,
          tableId: usersTableID,
          rowId: authStateController.uid!,
          data: {"profileImageUrl": imageController.text, "profileImageID": null},
        );
      }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/controllers/edit_profile_controller.dart` around lines 255 - 264, The
removeImage branch currently only clears imageController.text and updates
profileImageUrl, leaving the old storage file and profileImageID orphaned;
update the removeImage block to call storage.deleteFile(bucketId:
userProfileImageBucketId, fileId: authStateController.profileImageID) (wrapped
in try/catch/log), then clear imageController.text and call tables.updateRow
(databaseId: userDatabaseID, tableId: usersTableID, rowId:
authStateController.uid!) to set both "profileImageUrl" to "" and
"profileImageID" to null so the DB no longer references the deleted file.
lib/controllers/explore_story_controller.dart (2)

582-605: ⚠️ Potential issue | 🟠 Major

The denormalized likes counter can drift from the likes table.

These methods write the like row and stories.likes in separate requests. Any failure between them—or two clients liking at the same time—leaves the counter wrong, and the catch-and-continue flow guarantees drift on those error paths.

Also applies to: 608-648

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/controllers/explore_story_controller.dart` around lines 582 - 605, The
likeStoryFromUserAccount flow writes a like row (tables.createRow) and updates
the denormalized counter (tables.updateRow) in separate requests, allowing
counter drift on failures or concurrent likes; change this by making the
operation atomic or idempotent: either perform both actions inside a single
server-side transaction/function (move logic into a backend/cloud function or
use Appwrite server-side transactions if available) so the like row creation and
stories.likes increment succeed or fail together, or replace the direct counter
update with a conditional/incremental update (compare-and-set or an atomic
increment API) to avoid lost updates, and ensure fetchUserLikedStories is called
only after success. Locate and update likeStoryFromUserAccount, and replicate
the same fix for the similar block handling unlikes/other ranges (around lines
608–648).

455-476: ⚠️ Potential issue | 🟠 Major

Skip stale likes instead of failing the whole liked-stories load.

tables.getRow() throws AppwriteException with code 404 when a story doesn't exist. If a like row points to a deleted story, Future.wait() will abort the entire liked-stories fetch on the first failure. deleteStory() already treats like cleanup as best-effort, so stale likes can persist under partial failures or race conditions. Follow the existing pattern in room_chat_controller.dart where 404 errors are caught and handled gracefully.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/controllers/explore_story_controller.dart` around lines 455 - 476,
fetchUserLikedStories currently uses Future.wait on tables.getRow which will
abort on the first AppwriteException (404) for stale likes; change the mapping
so each tables.getRow call is wrapped in a try/catch that catches
AppwriteException (check error.code == 404) and returns null (or skips) for
missing stories, then await Future.wait on the mapped futures, filter out nulls
before calling convertAppwriteDocListToStoryList, and assign the resulting list
to userLikedStories; follow the same per-row try/catch pattern used in
room_chat_controller.dart to make likes cleanup best-effort and avoid failing
the whole fetch.
lib/controllers/pair_chat_controller.dart (1)

43-68: ⚠️ Potential issue | 🟠 Major

Create the pair request before opening realtime subscriptions.

Both quickMatch() and choosePartner() are declared as async void and open realtime subscriptions before awaiting tablesDB.createRow(). If the request creation fails, the subscriptions (getRealtimeStream() and checkForNewUsers()) will remain active listening for events on a request that was never created. Additionally, these methods lack error handling, so exceptions from createRow() will not propagate.

Also, cancelRequest() navigates away in the finally block regardless of whether the deleteRow() call succeeds, which can mask deletion failures and falsely indicate successful cancellation to the user.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/controllers/pair_chat_controller.dart` around lines 43 - 68, Change
quickMatch() and choosePartner() from async void to Future<void>, await
tablesDB.createRow(...) before calling getRealtimeStream() or
checkForNewUsers(), and wrap the createRow call in try/catch to handle and
surface errors (use processLogger/Get.snackbar or rethrow) so subscriptions are
only opened after the request is successfully created (reference quickMatch,
choosePartner, tablesDB.createRow, getRealtimeStream, checkForNewUsers). For
cancelRequest(), await deleteRow and only perform navigation in the success path
(or show an error and keep the UI if deleteRow fails), and add try/catch/finally
to properly report deletion failures instead of always navigating away
(reference cancelRequest and deleteRow).
lib/controllers/upcomming_rooms_controller.dart (1)

111-121: ⚠️ Potential issue | 🟠 Major

Don't serialize [null] token lists when messaging is unavailable.

When Firebase is disabled, messaging?.getToken() is null. Both payloads currently turn that into [null], which makes the row payload invalid or at least noisy instead of cleanly omitting the token field.

Suggested fix
   Future<void> addUserToSubscriberList(String upcomingRoomId) async {
     final fcmToken = await messaging?.getToken();
     await tablesDB.createRow(
       databaseId: upcomingRoomsDatabaseId,
       tableId: subscribedUserTableId,
       rowId: ID.unique(),
       data: {
         "userID": authStateController.uid,
         "upcomingRoomId": upcomingRoomId,
-        "registrationTokens": [fcmToken],
+        if (fcmToken != null && fcmToken.isNotEmpty)
+          "registrationTokens": [fcmToken],
         "userProfileUrl": authStateController.profileImageUrl,
       },
     );
@@
   Future<void> createUpcomingRoom() async {
@@
     try {
       final fcmToken = await messaging?.getToken();
       await tablesDB.createRow(
         databaseId: upcomingRoomsDatabaseId,
         tableId: upcomingRoomsTableId,
         rowId: ID.unique(),
         data: {
           "name": createRoomController.nameController.text,
           "scheduledDateTime": scheduledDateTime,
           "tags": createRoomController.tagsController.getTags,
           "description": createRoomController.descriptionController.text,
           "creatorUid": authStateController.uid,
-          "creator_fcm_tokens": [fcmToken],
+          if (fcmToken != null && fcmToken.isNotEmpty)
+            "creator_fcm_tokens": [fcmToken],
         },
       );

Also applies to: 269-281

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/controllers/upcomming_rooms_controller.dart` around lines 111 - 121, The
current code assigns fcmToken = await messaging?.getToken() and always inserts
"registrationTokens": [fcmToken] into the data map, which serializes as [null]
when messaging is unavailable; change the createRow payload to only include the
"registrationTokens" key when fcmToken is non-null (e.g., build the data map,
add "registrationTokens": [fcmToken] only if fcmToken != null) so you don't
store [null]; apply the same fix for the other occurrence that uses
messaging?.getToken() and "registrationTokens" (the block around the second
createRow).
🟡 Minor comments (9)
lib/l10n/app_pa.arb-521-524 (1)

521-524: ⚠️ Potential issue | 🟡 Minor

Mixed script characters in Punjabi translation.

Line 521 contains "ਠੀक" which mixes Gurmukhi script (ਠੀ) with Devanagari script (क). For proper Punjabi (Gurmukhi), this should be "ਠੀਕ" (all Gurmukhi characters).

🔤 Proposed fix
-  "enterValidDOB": "ਠੀक ਜਨਮ ਤਾਰੀਖ ਦਾਖਲ ਕਰੋ",
+  "enterValidDOB": "ਠੀਕ ਜਨਮ ਤਾਰੀਖ ਦਾਖਲ ਕਰੋ",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/l10n/app_pa.arb` around lines 521 - 524, The Punjabi translation for the
"enterValidDOB" ARB entry mixes scripts; replace the mixed-script word with the
correct all-Gurmukhi form for that key in lib/l10n/app_pa.arb by updating the
value of "enterValidDOB" so it uses only Gurmukhi characters (ensure the string
for "enterValidDOB" is corrected to the proper Gurmukhi spelling and leave the
"@enterValidDOB" description unchanged).
lib/l10n/app_localizations_gu.dart-996-997 (1)

996-997: ⚠️ Potential issue | 🟡 Minor

These new Gujarati translations are still English.

The newly added audio-device and friends-empty-state strings return English text, so Gujarati users will see a mixed-language UI for the new flows. Please update the source ARB with Gujarati copy and regenerate this file.

Also applies to: 1349-1400

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/l10n/app_localizations_gu.dart` around lines 996 - 997, The Gujarati
localization entries are still in English (e.g., the getter audioOptions =>
'Audio Options' and the new audio-device and friends-empty-state strings);
update the source ARB with the correct Gujarati translations for those keys
(audioOptions, audioDevice, friendsEmptyState or their exact ARB keys) and then
regenerate the Dart localization file so lib/l10n/app_localizations_gu.dart
contains the Gujarati strings instead of English.
lib/controllers/audio_device_controller.dart-38-39 (1)

38-39: ⚠️ Potential issue | 🟡 Minor

Reset the selected device when it disappears from the refreshed list.

selectedAudioOutput is only initialized once. After a headset/Bluetooth route disappears, the controller can keep pointing at a device that no longer exists in audioOutputDevices, so the UI/state drifts until the user manually reselects something.

Suggested fix
       audioOutputDevices.value = outputs;
-      selectedAudioOutput.value ??= outputs.firstOrNull;
+      final selectedId = selectedAudioOutput.value?.deviceId;
+      if (selectedId == null ||
+          outputs.every((output) => output.deviceId != selectedId)) {
+        selectedAudioOutput.value = outputs.isEmpty ? null : outputs.first;
+      }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/controllers/audio_device_controller.dart` around lines 38 - 39, The
selectedAudioOutput is only set once and can remain pointing at a device that no
longer exists after refreshing audioOutputDevices; update the refresh logic in
audio_device_controller.dart so that after assigning audioOutputDevices.value =
outputs you check whether selectedAudioOutput.value is contained in outputs and,
if not, set selectedAudioOutput.value to either outputs.firstOrNull (or null) to
reset selection; reference the variables audioOutputDevices and
selectedAudioOutput to find where to add this containment check and reset.
lib/l10n/app_localizations_kn.dart-1001-1002 (1)

1001-1002: ⚠️ Potential issue | 🟡 Minor

The new Kannada entries are still English.

Please update the source kn translations before regenerating. As written, the audio-output, delete-message, friends, invite, and username-validation flows will show mixed-language UI to Kannada users.

Also applies to: 1356-1415

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/l10n/app_localizations_kn.dart` around lines 1001 - 1002, The Kannada
localization file contains English text for several getters (e.g., the String
getter audioOptions and other keys in the same class such as deleteMessage,
friends, invite, usernameValidation and the entries in the block around lines
1356–1415); replace each English string value with the correct Kannada
translation for those getters in the AppLocalizationsKn class and any other
getters in that block, ensuring all audio-output, delete-message, friends,
invite, and username-validation related getters return Kannada strings before
regenerating the localization artifacts.
lib/l10n/app_localizations_hi.dart-1406-1412 (1)

1406-1412: ⚠️ Potential issue | 🟡 Minor

Translate the new Hindi username validation copy.

These two new hi entries are still English, so the username validation flow will show mixed-language error messages.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/l10n/app_localizations_hi.dart` around lines 1406 - 1412, The Hindi
localization file contains two English strings for username errors: replace the
English text for the getters usernameInvalidFormat and usernameAlreadyTaken in
AppLocalizationsHi with proper Hindi translations; update the return values of
the usernameInvalidFormat and usernameAlreadyTaken getters to their Hindi
equivalents so the username validation flow shows consistent Hindi messages.
lib/l10n/app_localizations_mr.dart-997-998 (1)

997-998: ⚠️ Potential issue | 🟡 Minor

Several new Marathi entries are still English.

Please update the source mr translations before regenerating. The audio-output, friends empty-state, and invite flows will otherwise ship mixed-language copy.

Also applies to: 1350-1363, 1377-1400

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/l10n/app_localizations_mr.dart` around lines 997 - 998, The Marathi
localization contains several keys still in English (notably the getter
audioOptions and all keys covering the audio-output, friends empty-state, and
invite flows); update each corresponding String getter in
app_localizations_mr.dart to proper Marathi translations (replace the English
literals for audioOptions and the other getters in those sections with
translated strings), then run the localization generation/regeneration step so
the Dart localization output is rebuilt with the corrected Marathi source
entries.
lib/l10n/app_localizations_pa.dart-843-850 (1)

843-850: ⚠️ Potential issue | 🟡 Minor

Use the runtime values in these Punjabi helpers.

audioFileSelected/lyricsFileSelected drop fileName, and chooseTheme drops category. The UI will only show generic text instead of the selected file or the translated theme label.

Suggested fix
   String audioFileSelected(String fileName) {
-    return 'ਆਡੀਓ ਫਾਈਲ ਚੁਣੀ ਗਈ';
+    return 'ਚੁਣੀ ਗਈ ਆਡੀਓ ਫਾਈਲ: $fileName';
   }

   `@override`
   String lyricsFileSelected(String fileName) {
-    return 'ਲਿਰਿਕਸ ਫਾਈਲ ਚੁਣੀ ਗਈ';
+    return 'ਚੁਣੀ ਗਈ ਲਿਰਿਕਸ ਫਾਈਲ: $fileName';
   }
@@
   `@override`
   String chooseTheme(String category) {
-    return 'ਥੀਮ ਚੁਣੋ';
+    String _temp0 = intl.Intl.selectLogic(category, {
+      'classicTheme': 'ਕਲਾਸਿਕ',
+      'timeTheme': 'ਸਮਾਂ',
+      'vintageTheme': 'ਵਿੰਟੇਜ',
+      'amberTheme': 'ਐਂਬਰ',
+      'forestTheme': 'ਫਾਰਸਟ',
+      'creamTheme': 'ਕਰੀਮ',
+      'other': 'ਹੋਰ',
+    });
+    return '$_temp0';
   }

Also applies to: 1044-1047

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/l10n/app_localizations_pa.dart` around lines 843 - 850, The Punjabi
localization helpers are ignoring their runtime parameters—update
audioFileSelected(String fileName) and lyricsFileSelected(String fileName) to
include the fileName in the returned string, and similarly include the category
parameter in chooseTheme(String category) (and the other occurrences noted
around lines ~1044–1047) so the UI displays the actual file name and translated
theme label; locate those methods in app_localizations_pa.dart (methods
audioFileSelected, lyricsFileSelected, chooseTheme) and return strings that
interpolate the provided parameters into the Punjabi message.
lib/l10n/app_localizations_pa.dart-715-717 (1)

715-717: ⚠️ Potential issue | 🟡 Minor

Punjabi username guidance no longer matches the validator.

usernameCharacterLimit no longer tells the user about the 8-character minimum, and usernameInvalidFormat still bans dots/hyphens even though the updated rule allows them.

Suggested fix
   `@override`
   String get usernameCharacterLimit =>
-      'ਯੂਜ਼ਰਨੇਮ ਅੱਖਰ ਸੀਮਾ';
+      'ਯੂਜ਼ਰਨੇਮ ਘੱਟੋ-ਘੱਟ 8 ਅੱਖਰਾਂ ਦਾ ਹੋਣਾ ਚਾਹੀਦਾ ਹੈ।';
@@
   `@override`
   String get usernameInvalidFormat =>
-      'ਯੂਜ਼ਰਨੇਮ ਵਿੱਚ ਸਿਰਫ਼ ਅੱਖਰ, ਨੰਬਰ ਅਤੇ ਅੰਡਰਸਕੋਰ ਹੋ ਸਕਦੇ ਹਨ';
+      'ਕਿਰਪਾ ਕਰਕੇ ਵੈਧ ਯੂਜ਼ਰਨੇਮ ਦਾਖਲ ਕਰੋ। ਸਿਰਫ਼ ਅੱਖਰ, ਨੰਬਰ, ਡਾਟ, ਅੰਡਰਸਕੋਰ ਅਤੇ ਹਾਈਫਨ ਮਨਜ਼ੂਰ ਹਨ।';

Also applies to: 1365-1367

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/l10n/app_localizations_pa.dart` around lines 715 - 717, Update the
Punjabi localization strings to match the validator: change the
usernameCharacterLimit (method name: usernameCharacterLimit) to include the
"minimum 8 characters" guidance in Punjabi, and update usernameInvalidFormat
(method name: usernameInvalidFormat) so it no longer states that dots or hyphens
are prohibited — instead reflect the current rule (e.g., only disallowed
characters or allowed pattern including letters, numbers, dots and hyphens) in
Punjabi; apply the same corrections to the other occurrence noted around lines
1365-1367 by editing those same getters so messages exactly mirror the validator
rules.
lib/l10n/app_localizations_bn.dart-729-731 (1)

729-731: ⚠️ Potential issue | 🟡 Minor

Update the Bengali length hint to the new 8-character rule.

This still says >5 characters, while the updated base copy now requires more than 7. Users on bn will get contradictory guidance during username validation.

Suggested fix
-  String get usernameCharacterLimit => 'ইউজারনেম ৫টির বেশি অক্ষরের হতে হবে।';
+  String get usernameCharacterLimit => 'ইউজারনেম কমপক্ষে ৮ অক্ষরের হতে হবে।';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/l10n/app_localizations_bn.dart` around lines 729 - 731, Update the
Bengali username length hint in the usernameCharacterLimit getter
(app_localizations_bn.dart) to reflect the new rule requiring more than 7
characters (minimum 8) instead of saying ">5"; locate the String returned by the
usernameCharacterLimit getter and change the message text so it clearly states
the 8-character minimum in Bengali.
🧹 Nitpick comments (11)
lib/views/screens/story_screen.dart (1)

7-7: Remove unused import after commenting out controller instantiation.

ChapterPlayerController is imported at line 7 but the only usage at line 384 is commented out. The import should be removed or commented out with a TODO explaining when it will be restored.

Suggested cleanup
-import 'package:resonate/controllers/chapter_player_controller.dart';
+// TODO: Re-enable when flutter_lyric dependency is restored
+// import 'package:resonate/controllers/chapter_player_controller.dart';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/views/screens/story_screen.dart` at line 7, Remove the unused import of
ChapterPlayerController since its only usage (the commented-out controller
instantiation) is no longer active; either delete the import line for
ChapterPlayerController or comment it out and add a brief TODO noting why it's
commented and when to restore it (e.g., restore when re-enabling the
ChapterPlayerController instantiation).
lib/controllers/friend_call_screen.dart (1)

132-140: Consider simplifying the async lambda.

The async => await pattern is redundant here since _buildControlButton.onPressed is a VoidCallback that doesn't return a Future. This works but is unnecessarily verbose.

♻️ Simplified callback
 _buildControlButton(
   icon: Icons.settings_voice,
   label: AppLocalizations.of(context)!.audioOptions,
-  onPressed: () async => await showAudioDeviceSelector(context),
+  onPressed: () => showAudioDeviceSelector(context),
   backgroundColor: _getControlButtonBackgroundColor(
     currentBrightness,
   ),
   heroTag: "audio-settings",
 ),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/controllers/friend_call_screen.dart` around lines 132 - 140, The
onPressed handler passed to _buildControlButton uses the redundant "async =>
await" pattern when calling showAudioDeviceSelector; because onPressed is a
VoidCallback, replace the async closure with a simple non-async closure that
directly invokes showAudioDeviceSelector (e.g., a plain () =>
showAudioDeviceSelector(context) or a short block that calls
showAudioDeviceSelector(context)) to remove unnecessary async/await and keep the
callback synchronous.
lib/views/screens/live_chapter_screen.dart (1)

266-272: Consider simplifying the async lambda (same as friend_call_screen.dart).

♻️ Simplified callback
   Widget _buildAudioSettingsButton() {
     return FloatingActionButton(
-      onPressed: () async => await showAudioDeviceSelector(context),
+      onPressed: () => showAudioDeviceSelector(context),
       backgroundColor: Theme.of(context).colorScheme.secondary,
       child: const Icon(Icons.settings_voice),
     );
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/views/screens/live_chapter_screen.dart` around lines 266 - 272, The
onPressed callback in _buildAudioSettingsButton unnecessarily wraps
showAudioDeviceSelector in an async-await lambda; replace "onPressed: () async
=> await showAudioDeviceSelector(context)" with a simple direct call like
"onPressed: () => showAudioDeviceSelector(context)" (or a short void closure) to
simplify the callback while keeping behavior identical for the
FloatingActionButton and the showAudioDeviceSelector call.
lib/controllers/room_chat_controller.dart (1)

48-67: Consider handling case when message is not found in local list.

firstWhere without orElse will throw a StateError if the message is not found in the local messages list. While rethrow propagates errors, consider adding explicit handling or logging for this edge case.

🛡️ Optional: Add explicit not-found handling
 Future<void> deleteMessage(String messageId) async {
   try {
-    Message messageToDelete = messages.firstWhere(
-      (msg) => msg.messageId == messageId,
-    );
+    final index = messages.indexWhere((msg) => msg.messageId == messageId);
+    if (index == -1) {
+      log('Message not found locally: $messageId');
+      return;
+    }
+    Message messageToDelete = messages[index];
     messageToDelete = messageToDelete.copyWith(content: '', isDeleted: true);

     await tablesDB.updateRow(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/controllers/room_chat_controller.dart` around lines 48 - 67, The
deleteMessage method uses messages.firstWhere without orElse which throws
StateError when the messageId isn't present; update deleteMessage to handle the
not-found case explicitly (e.g., use firstWhere(..., orElse: () => null) or
messages.indexWhere and check >=0), log a clear "message not found" or similar
via log when the message is missing, and return early instead of attempting to
copy/update/rethrow; keep the existing try/catch for other errors and ensure you
reference the Message lookup in deleteMessage, the messages list, and the
tablesDB.updateRow call when applying the fix.
lib/views/screens/friends_empty_screen.dart (1)

65-70: Consider adding error handling for TabViewController lookup.

Get.find<TabViewController>() will throw if the controller is not registered. While this is likely always available in the app's navigation flow, defensive programming could prevent crashes in edge cases.

🛡️ Optional: Add defensive check
 onPressed: () {
+  if (!Get.isRegistered<TabViewController>()) {
+    Get.offAllNamed(AppRoutes.tabview);
+    return;
+  }
   Get.find<TabViewController>().setIndex(1);
   Get.until(
     (route) => route.settings.name == AppRoutes.tabview,
   );
 },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/views/screens/friends_empty_screen.dart` around lines 65 - 70, The
current onPressed uses Get.find<TabViewController>() which will throw if the
controller isn't registered; wrap this lookup with a defensive check (e.g., use
Get.isRegistered<TabViewController>() or try/catch around Get.find) and only
call setIndex on the TabViewController if it exists; if the controller is
missing, fall back to a safe action such as navigating directly to
AppRoutes.tabview with Get.toNamed or logging an error before calling Get.until
so the app doesn't crash. Ensure references: TabViewController,
Get.find/Get.isRegistered (or try/catch), setIndex, Get.until, and
AppRoutes.tabview.
lib/controllers/change_email_controller.dart (2)

34-34: Remove late modifier from tables field.

The tables field is initialized in the constructor's initializer list (line 26), so the late keyword is unnecessary and misleading. The late modifier indicates delayed initialization, but this field is always initialized when the constructor runs.

♻️ Proposed fix
-  late final TablesDB tables;
+  final TablesDB tables;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/controllers/change_email_controller.dart` at line 34, The tables field is
declared with a redundant late modifier even though it's initialized in the
constructor initializer list; remove the late keyword and declare it as final
TablesDB tables; (refer to the tables field in the ChangeEmailController class
and its constructor initializer where tables is already set) to reflect
immediate initialization and avoid misleading delayed-initialization semantics.

35-35: Remove late modifier from account field.

Same issue as tables - this field is initialized in the constructor's initializer list (line 27), making late unnecessary.

♻️ Proposed fix
-  late final Account account;
+  final Account account;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/controllers/change_email_controller.dart` at line 35, The account field
is declared with an unnecessary late modifier because it is already initialized
in the constructor initializer list (same pattern as tables); remove the late
keyword from the declaration of account so it becomes a non-late final field,
leaving its initialization in the constructor (constructor and Account type
remain unchanged).
lib/views/screens/edit_profile_screen.dart (1)

143-186: Minor redundancy in trimming logic.

trimmedValue is created on line 146, but line 168 calls value.trim() again. Use the already-trimmed value for consistency.

♻️ Proposed fix
                          debouncer.run(() async {
                            final available = await controller
-                               .isUsernameAvailable(value.trim());
+                               .isUsernameAvailable(trimmedValue);

                            controller.usernameChecking.value = false;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/views/screens/edit_profile_screen.dart` around lines 143 - 186, The
onChanged handler creates trimmedValue but redundantly calls value.trim() later;
update the debouncer call to use trimmedValue (i.e., pass trimmedValue into
controller.isUsernameAvailable and anywhere else trim is re-applied) so the
existing trimmedValue is reused consistently—look for usages in the onChanged
closure (trimmedValue, controller.isUsernameAvailable, debouncer.run) and
replace value.trim() with trimmedValue.
test/controllers/audio_device_controller_test.dart (1)

30-64: Test logic duplicates production code filtering.

The test manually implements the filtering logic (lines 52-57) instead of calling a controller method. This tests the concept but not the actual AudioDeviceController implementation. If there's an enumerateDevices or similar method on the controller, consider using it instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/controllers/audio_device_controller_test.dart` around lines 30 - 64, The
test duplicates production filtering logic by manually converting
MediaDeviceInfo to AudioDevice and adding outputs to
controller.audioOutputDevices instead of exercising the controller method;
update the test to call the controller's device enumeration/filtering entrypoint
(e.g., AudioDeviceController.enumerateDevices or whatever method populates
audioOutputDevices) with the mockDevices (or a mocked dependency that returns
them), then assert controller.audioOutputDevices.length == 2 and that every
device.kind == 'audiooutput' to verify the controller's behavior rather than
reimplementing the filter; use AudioDevice.fromMediaDeviceInfo only if needed to
construct mock return values for the controller's dependency.
lib/views/screens/room_chat_screen.dart (2)

400-402: Hardcoded color may not work well with dark themes.

Colors.black is hardcoded for the reply content text, which will have poor contrast on dark theme backgrounds. Consider using a theme-aware color.

♻️ Proposed fix
                                            Text(
                                              widget.message.replyTo!.content,
                                              maxLines: 1,
                                              style: TextStyle(
-                                               color: Colors.black,
+                                               color: Theme.of(context).colorScheme.onSecondaryContainer,
                                              ),
                                              overflow: TextOverflow.ellipsis,
                                            ),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/views/screens/room_chat_screen.dart` around lines 400 - 402, The
TextStyle for the reply content currently hardcodes color: Colors.black which
breaks dark mode; locate the reply content Text widget (in RoomChatScreen where
TextStyle(color: Colors.black) is used) and replace the hardcoded color with a
theme-aware color such as Theme.of(context).textTheme.bodyText2?.color or
Theme.of(context).colorScheme.onBackground (fallback to a safe default like
Colors.black using ?? if needed) so the text adapts to light/dark themes and
maintains contrast.

122-126: Success snackbar message could be more descriptive.

The success snackbar uses AppLocalizations.of(context)!.delete as the message body, which likely just says "Delete". Consider using a more descriptive message like "Message deleted successfully" for better user feedback.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/views/screens/room_chat_screen.dart` around lines 122 - 126, Replace the
generic delete message passed to customSnackbar with a more descriptive
localized message (e.g., use AppLocalizations.of(context)!.messageDeleted or
create a new key like messageDeletedSuccessfully) so the snackbar reads "Message
deleted successfully"; update the call in the code that invokes customSnackbar
to pass that more specific localization value, and if the new key is needed, add
it to the localization files and regenerate localization artifacts.

Comment on lines +273 to 315
final fcmToken = await messaging?.getToken();

//subscribed Upcoming Rooms
List<Document> subscribedUpcomingRooms = await databases
.listDocuments(
List<Row> subscribedUpcomingRooms = await tables
.listRows(
databaseId: upcomingRoomsDatabaseId,
collectionId: subscribedUserCollectionId,
tableId: subscribedUserTableId,
queries: [
Query.equal("userID", [uid]),
],
)
.then((value) => value.documents);
.then((value) => value.rows);
for (var subscription in subscribedUpcomingRooms) {
List<dynamic> registrationTokens =
subscription.data['registrationTokens'];
registrationTokens.add(fcmToken!);
databases.updateDocument(
tables.updateRow(
databaseId: upcomingRoomsDatabaseId,
collectionId: subscribedUserCollectionId,
documentId: subscription.$id,
tableId: subscribedUserTableId,
rowId: subscription.$id,
data: {"registrationTokens": registrationTokens},
);
}

//created Upcoming Rooms
List<Document> createdUpcomingRooms = await databases
.listDocuments(
List<Row> createdUpcomingRooms = await tables
.listRows(
databaseId: upcomingRoomsDatabaseId,
collectionId: upcomingRoomsCollectionId,
tableId: upcomingRoomsTableId,
queries: [
Query.equal("creatorUid", [uid]),
],
)
.then((value) => value.documents);
.then((value) => value.rows);
for (var upcomingRoom in createdUpcomingRooms) {
List<dynamic> creatorFcmTokens = upcomingRoom.data['creator_fcm_tokens'];
creatorFcmTokens.add(fcmToken!);
databases.updateDocument(
tables.updateRow(
databaseId: upcomingRoomsDatabaseId,
collectionId: upcomingRoomsCollectionId,
documentId: upcomingRoom.$id,
tableId: upcomingRoomsTableId,
rowId: upcomingRoom.$id,
data: {"creator_fcm_tokens": creatorFcmTokens},
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

head -n 400 lib/controllers/auth_state_controller.dart | tail -n +260

Repository: AOSSIE-Org/Resonate

Length of output: 5044


🏁 Script executed:

rg -n "final.*messaging\s*[=:]|var.*messaging\s*[=:]" lib/controllers/auth_state_controller.dart | head -20

Repository: AOSSIE-Org/Resonate

Length of output: 45


🏁 Script executed:

rg -n "messaging" lib/controllers/auth_state_controller.dart -B 2 -A 2

Repository: AOSSIE-Org/Resonate

Length of output: 1188


Fix null-coalescing crashes and add await to token persist calls.

Both addRegistrationTokentoSubscribedandCreatedUpcomingRooms() and removeRegistrationTokenFromSubscribedUpcomingRooms() call messaging?.getToken() (nullable result), then force-unwrap with fcmToken!. When Firebase is disabled or permission denied, this crashes on login/logout. Additionally, all tables.updateRow() calls lack await, allowing the method to return before database writes complete.

Add a null-check guard at the start of each method and await all updateRow() calls:

Suggested fix
  Future<void> addRegistrationTokentoSubscribedandCreatedUpcomingRooms() async {
    final fcmToken = await messaging?.getToken();
+   if (fcmToken == null || fcmToken.isEmpty) return;

    //subscribed Upcoming Rooms
    List<Row> subscribedUpcomingRooms = await tables
        .listRows(
          databaseId: upcomingRoomsDatabaseId,
          tableId: subscribedUserTableId,
          queries: [
            Query.equal("userID", [uid]),
          ],
        )
        .then((value) => value.rows);
    for (var subscription in subscribedUpcomingRooms) {
      List<dynamic> registrationTokens =
          subscription.data['registrationTokens'];
-      registrationTokens.add(fcmToken!);
-      tables.updateRow(
+      registrationTokens.add(fcmToken);
+      await tables.updateRow(
        databaseId: upcomingRoomsDatabaseId,
        tableId: subscribedUserTableId,
        rowId: subscription.$id,
        data: {"registrationTokens": registrationTokens},
      );
    }

    //created Upcoming Rooms
    List<Row> createdUpcomingRooms = await tables
        .listRows(
          databaseId: upcomingRoomsDatabaseId,
          tableId: upcomingRoomsTableId,
          queries: [
            Query.equal("creatorUid", [uid]),
          ],
        )
        .then((value) => value.rows);
    for (var upcomingRoom in createdUpcomingRooms) {
      List<dynamic> creatorFcmTokens = upcomingRoom.data['creator_fcm_tokens'];
-      creatorFcmTokens.add(fcmToken!);
-      tables.updateRow(
+      creatorFcmTokens.add(fcmToken);
+      await tables.updateRow(
        databaseId: upcomingRoomsDatabaseId,
        tableId: upcomingRoomsTableId,
        rowId: upcomingRoom.$id,
        data: {"creator_fcm_tokens": creatorFcmTokens},
      );
    }
  }

  Future<void> removeRegistrationTokenFromSubscribedUpcomingRooms() async {
    final fcmToken = await messaging?.getToken();
+   if (fcmToken == null || fcmToken.isEmpty) return;

    //subscribed Upcoming Rooms
    List<Row> subscribedUpcomingRooms = await tables
        .listRows(
          databaseId: upcomingRoomsDatabaseId,
          tableId: subscribedUserTableId,
          queries: [
            Query.equal("userID", [uid]),
          ],
        )
        .then((value) => value.rows);
    for (var subscription in subscribedUpcomingRooms) {
      List<dynamic> registrationTokens =
          subscription.data['registrationTokens'];
-      registrationTokens.remove(fcmToken!);
-      tables.updateRow(
+      registrationTokens.remove(fcmToken);
+      await tables.updateRow(
        databaseId: upcomingRoomsDatabaseId,
        tableId: subscribedUserTableId,
        rowId: subscription.$id,
        data: {"registrationTokens": registrationTokens},
      );
    }

    //created Upcoming Rooms
    List<Row> createdUpcomingRooms = await tables
        .listRows(
          databaseId: upcomingRoomsDatabaseId,
          tableId: upcomingRoomsTableId,
          queries: [
            Query.equal("creatorUid", [uid]),
          ],
        )
        .then((value) => value.rows);
    for (var upcomingRoom in createdUpcomingRooms) {
      List<dynamic> creatorFcmTokens = upcomingRoom.data['creator_fcm_tokens'];
-      creatorFcmTokens.remove(fcmToken!);
-      tables.updateRow(
+      creatorFcmTokens.remove(fcmToken);
+      await tables.updateRow(
        databaseId: upcomingRoomsDatabaseId,
        tableId: upcomingRoomsTableId,
        rowId: upcomingRoom.$id,
        data: {"creator_fcm_tokens": creatorFcmTokens},
      );
    }
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/controllers/auth_state_controller.dart` around lines 273 - 315, Both
addRegistrationTokentoSubscribedandCreatedUpcomingRooms() and
removeRegistrationTokenFromSubscribedUpcomingRooms() are force-unwrapping a
nullable fcmToken from messaging?.getToken() and performing non-awaited DB
writes; add an early null-check/guard after calling messaging?.getToken()
(return early if token is null) to avoid fcmToken! crashes, and add await before
every tables.updateRow(...) call in both methods so database writes complete
before the method returns; update all references to registrationTokens and
creator_fcm_tokens handling inside those functions accordingly.

Comment on lines +1 to +48
// import 'package:get/get.dart';
// import 'package:audioplayers/audioplayers.dart';
// import 'package:flutter_lyric/lyrics_reader_model.dart';
//
// class ChapterPlayerController extends GetxController {
// Rx<int> currentPage = 0.obs;
// Rx<int> lyricProgress = 0.obs;
// Rx<double> sliderProgress = 0.0.obs;
// Rx<bool> isPlaying = false.obs;
// AudioPlayer? audioPlayer;
// late Duration chapterDuration;
// late LyricsReaderModel lyricModel;
//
// void initialize(
// AudioPlayer player,
// LyricsReaderModel model,
// Duration duration,
// ) {
// audioPlayer = player;
// lyricModel = model;
// chapterDuration = duration;
// audioPlayer?.setReleaseMode(ReleaseMode.stop);
//
// audioPlayer?.onPositionChanged.listen((Duration event) {
// sliderProgress.value = event.inMilliseconds.toDouble();
// lyricProgress.value = event.inMilliseconds;
// });
//
// audioPlayer?.onPlayerStateChanged.listen((PlayerState state) {
// isPlaying.value = state == PlayerState.playing;
// });
// }
//
// void togglePlayPause() {
// if (isPlaying.value) {
// audioPlayer?.pause();
// } else {
// audioPlayer?.resume();
// }
// isPlaying.value = !isPlaying.value;
// }
//
// @override
// void onClose() {
// audioPlayer?.release();
// super.onClose();
// }
// }
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Keep a minimal chapter player instead of commenting out the whole controller.

This removes all playback state, position listeners, and audio cleanup, so the chapter player screen is effectively downgraded to a static view. If flutter_lyric has to be removed, keep the audioplayers part alive and stub only the lyric-specific integration instead of disabling the feature wholesale.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/controllers/chapter_player_controller.dart` around lines 1 - 48, Restore
a minimal ChapterPlayerController that keeps audio playback, position listeners,
play/pause state and cleanup but stubs out the lyric-specific integration:
uncomment and reintroduce the class ChapterPlayerController, the fields
audioPlayer, isPlaying, sliderProgress and lyricProgress, and the methods
initialize, togglePlayPause and onClose; in initialize keep AudioPlayer setup,
onPositionChanged and onPlayerStateChanged listeners and setting
chapterDuration, but remove the hard dependency on LyricsReaderModel by making
lyricModel optional/null-able or replacing it with a simple stub type and skip
any lyric-specific calls; ensure onClose still calls audioPlayer?.release() and
that togglePlayPause resumes/pauses audioPlayer and updates isPlaying.

Comment on lines +65 to 69
await tables.createRow(
databaseId: masterDatabaseId,
collectionId: friendCallsCollectionId,
documentId: callModel.docId,
tableId: friendCallsTableId,
rowId: callModel.docId,
data: callModel.toJson(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n lib/controllers/friend_calling_controller.dart | head -100

Repository: AOSSIE-Org/Resonate

Length of output: 4223


🏁 Script executed:

rg "startCall\(" lib/controllers/friend_calling_controller.dart -B 2 -A 2

Repository: AOSSIE-Org/Resonate

Length of output: 198


🏁 Script executed:

rg "\.startCall\(" --type dart -A 3 -B 3

Repository: AOSSIE-Org/Resonate

Length of output: 2662


🏁 Script executed:

sed -n '1,300p' lib/views/widgets/friend_request_list_tile.dart | cat -n | grep -A 20 "startCall"

Repository: AOSSIE-Org/Resonate

Length of output: 1934


🏁 Script executed:

sed -n '100,140p' lib/views/widgets/friend_request_list_tile.dart | cat -n

Repository: AOSSIE-Org/Resonate

Length of output: 2190


🏁 Script executed:

sed -n '100,160p' lib/views/widgets/friend_request_list_tile.dart | cat -n

Repository: AOSSIE-Org/Resonate

Length of output: 3085


🏁 Script executed:

sed -n '95,150p' lib/views/widgets/friend_request_list_tile.dart

Repository: AOSSIE-Org/Resonate

Length of output: 2577


🏁 Script executed:

sed -n '70,120p' lib/views/widgets/friend_request_list_tile.dart

Repository: AOSSIE-Org/Resonate

Length of output: 2420


🏁 Script executed:

sed -n '50,150p' lib/views/widgets/friend_request_list_tile.dart | head -100

Repository: AOSSIE-Org/Resonate

Length of output: 4456


🏁 Script executed:

cat -n lib/controllers/friend_calling_controller.dart | grep -A 30 "Future<void>"

Repository: AOSSIE-Org/Resonate

Length of output: 5569


Wrap call creation and notification send in error handling to prevent orphaned call records.

The waiting row is persisted at lines 65-70 before the notification is sent at lines 86-89. If createExecution() fails, the callee never receives the call but the database retains a stale waiting record. Delete the row or mark it failed in the error path before rethrowing.

Also applies to: 86-89

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/controllers/friend_calling_controller.dart` around lines 65 - 69, Wrap
the createRow + notification/send logic in a try/catch so that if
createExecution() or the notification send fails you clean up the previously
persisted "waiting" row; specifically, after calling tables.createRow(...) with
callModel.docId (and before calling createExecution()/sendNotification),
surround the subsequent createExecution()/sendNotification calls with a try
block and in the catch call tables.deleteRow(databaseId: masterDatabaseId,
tableId: friendCallsTableId, rowId: callModel.docId) or tables.updateRow(...) to
mark the row as failed, then rethrow the error; update the code paths around
createRow, createExecution, and the notification send to ensure no orphaned
waiting records remain.

Comment on lines +162 to 166
await tables.updateRow(
databaseId: userDatabaseID,
collectionId: liveChapterAttendeesCollectionId,
documentId: roomId,
tableId: liveChapterAttendeesTableId,
rowId: roomId,
data: newAttendeesModel.toJson(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

This attendee write can drop concurrent joins/leaves.

Both paths overwrite the full attendees row from a client-side snapshot. If two participants join or leave at roughly the same time, the later updateRow() wins and silently removes the earlier change. This needs a server-side mutation path or optimistic concurrency/version checks before writing.

Also applies to: 228-232

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/controllers/live_chapter_controller.dart` around lines 162 - 166, The
current use of tables.updateRow with newAttendeesModel.toJson() (for
liveChapterAttendeesTableId / rowId roomId in live_chapter_controller.dart)
overwrites the entire attendees row and can lose concurrent joins/leaves;
replace this with a server-side-safe mutation or an optimistic-concurrency
retry: implement an atomic add/remove attendee endpoint or use the table API's
conditional update/ETag/version field (read current row, merge the change into
its attendees set, then attempt a conditional update; on version mismatch retry)
for both places where updateRow is used (the block shown and the analogous
228-232 block). Ensure you reference and update the same version/etag field and
retry loop so concurrent joins/leaves are merged instead of clobbered.

Comment on lines +241 to 259
Future<void> cancelRequest() async {
try {
if (requestDocId != null) {
await tablesDB.deleteRow(
databaseId: masterDatabaseId,
tableId: pairRequestTableId,
rowId: requestDocId!,
);
}

requestDocId = null;

await subscription?.close();
await userAddedSubscription?.close();
} catch (e) {
log('Cancel request failed: $e');
} finally {
Get.offNamedUntil(AppRoutes.tabview, (route) => false);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n lib/controllers/pair_chat_controller.dart | sed -n '241,259p'

Repository: AOSSIE-Org/Resonate

Length of output: 658


Move the state cleanup outside the database operation or restructure error handling to ensure cleanup always runs.

Lines 251–254 are skipped if deleteRow throws an exception, leaving requestDocId set and subscriptions open while the finally block navigates away. This causes the application to appear to succeed while leaving orphaned state and active listeners.

Either execute cleanup unconditionally (e.g., in finally or before navigation) or ensure all cleanup steps complete even if the delete fails.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/controllers/pair_chat_controller.dart` around lines 241 - 259, The
cancelRequest method can leave requestDocId and subscriptions uncleared if
tablesDB.deleteRow throws; move or duplicate the state cleanup into a guaranteed
path so it always runs: ensure requestDocId = null and both
subscription?.close() and userAddedSubscription?.close() are executed in the
finally block (or before calling Get.offNamedUntil) regardless of deleteRow
success, and keep the deleteRow attempt inside the try so errors are still
caught and logged; update cancelRequest to call tablesDB.deleteRow inside try,
then perform requestDocId reset and subscription closes in finally, then
navigate with Get.offNamedUntil.

Comment on lines +68 to 84
Row roomDoc = await roomsController.tablesDB.getRow(
databaseId: masterDatabaseId,
collectionId: roomsCollectionId,
documentId: roomId,
tableId: roomsTableId,
rowId: roomId,
);

// Increment the totalParticipants Attribute
int newParticipantCount =
roomDoc.data["totalParticipants"] -
participantDocsRef.documents.length +
participantDocsRef.rows.length +
1;
await roomsController.databases.updateDocument(
await roomsController.tablesDB.updateRow(
databaseId: masterDatabaseId,
collectionId: roomsCollectionId,
documentId: roomId,
tableId: roomsTableId,
rowId: roomId,
data: {"totalParticipants": newParticipantCount},
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

totalParticipants is still updated with a race-prone read/modify/write.

Both the join and leave paths read the room row, do local arithmetic, then write the new count back. Concurrent joins/leaves will lose updates, and the leave path can make delete/update decisions from a stale participant count. This counter needs to be mutated atomically on the backend, or derived from participant rows instead of being maintained client-side.

Also applies to: 245-265

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/services/room_service.dart` around lines 68 - 84, The code currently
reads the room row via roomsController.tablesDB.getRow, computes
totalParticipants from roomDoc.data["totalParticipants"] and
participantDocsRef.rows.length, then writes it back with updateRow, which is
race-prone; change this to an atomic update or a derived count: either (1) use
your database's atomic increment/decrement API on roomsController.tablesDB
(e.g., an increment operator) to adjust "totalParticipants" instead of
read/modify/write, or (2) stop maintaining the counter and derive the value from
participant rows when needed (use participantDocsRef.rows.length as the source
of truth and remove writes to "totalParticipants"); apply the same fix for the
leave path referenced around lines 245-265 so both join and leave use atomic
updates or derive from participant rows.

Comment on lines +103 to +104
const String playStoreUrl =
"https://play.google.com/store/apps/details?id=com.resonate.resonate";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's check where playStoreUrl is used in the codebase
rg "playStoreUrl" --type dart

Repository: AOSSIE-Org/Resonate

Length of output: 237


🏁 Script executed:

# Let's also search for share/invite related functionality
rg -i "invite|share" --type dart -l | head -20

Repository: AOSSIE-Org/Resonate

Length of output: 591


🏁 Script executed:

# Check the constants file to see the context
cat -n lib/utils/constants.dart | sed -n '100,110p'

Repository: AOSSIE-Org/Resonate

Length of output: 696


🏁 Script executed:

# Check the friends_empty_screen.dart to see how inviteToResonate uses playStoreUrl
cat -n lib/views/screens/friends_empty_screen.dart | grep -A 10 -B 10 "inviteToResonate"

Repository: AOSSIE-Org/Resonate

Length of output: 1056


🏁 Script executed:

# Check the app_localizations to see how inviteToResonate is implemented
rg -A 5 "inviteToResonate" lib/l10n/app_localizations.dart

Repository: AOSSIE-Org/Resonate

Length of output: 353


🏁 Script executed:

# Search for other share/invite related code in the entire lib directory
rg "share|Share" --type dart -B 3 -A 3 | head -100

Repository: AOSSIE-Org/Resonate

Length of output: 6811


🏁 Script executed:

# Check pubspec.yaml for platform support information
cat pubspec.yaml | grep -A 20 "^platforms:"

Repository: AOSSIE-Org/Resonate

Length of output: 45


🏁 Script executed:

# Check for iOS/web specific code or platform checks
rg "Platform\.|kIsWeb|defaultTargetPlatform" --type dart | head -20

Repository: AOSSIE-Org/Resonate

Length of output: 45


🏁 Script executed:

# Look for app store URL references
rg "apple|app.store|itunes|appstore" --type dart -i

Repository: AOSSIE-Org/Resonate

Length of output: 283


🏁 Script executed:

# Check main.dart or similar to see what platforms are supported
fd "main\.dart|app\.dart" --type f | head -5 | xargs cat -n 2>/dev/null | head -50

Repository: AOSSIE-Org/Resonate

Length of output: 2521


🏁 Script executed:

# Find sendUserToAppStore implementation
rg "sendUserToAppStore" --type dart -B 5 -A 10

Repository: AOSSIE-Org/Resonate

Length of output: 1740


🏁 Script executed:

# Check the AboutAppScreenController
fd "about_app_screen_controller\.dart" --type f | xargs cat -n

Repository: AOSSIE-Org/Resonate

Length of output: 3499


🏁 Script executed:

# Check if there are platform-specific conditionals anywhere
rg "dart:io|Platform\." --type dart -B 2 -A 2

Repository: AOSSIE-Org/Resonate

Length of output: 2645


🏁 Script executed:

# Look for iOS or App Store related constants/URLs
rg "iosUrl|appStoreUrl|ios.*store|apple.*store" --type dart -i

Repository: AOSSIE-Org/Resonate

Length of output: 45


🏁 Script executed:

# Check if there's a pubspec.yaml file
cat pubspec.yaml | head -50

Repository: AOSSIE-Org/Resonate

Length of output: 2342


🏁 Script executed:

# Check for iOS project configuration
ls -la ios/ 2>/dev/null | head -20

Repository: AOSSIE-Org/Resonate

Length of output: 695


🏁 Script executed:

# Search for any iOS specific code or store references
rg "ios" --type dart -i | grep -i "store\|url\|app" | head -20

Repository: AOSSIE-Org/Resonate

Length of output: 332


🏁 Script executed:

# Check if there's an App Store URL or any mention of App Store
rg "appstore|app.store|app_store" -i --type dart

Repository: AOSSIE-Org/Resonate

Length of output: 283


🏁 Script executed:

# Check iOS configuration files for bundle ID or app store details
cat ios/Runner.xcodeproj/project.pbxproj 2>/dev/null | grep -i "bundle\|appstore\|identifier" | head -10

Repository: AOSSIE-Org/Resonate

Length of output: 227


🏁 Script executed:

# Check for any fastlane or CI/CD configuration that might indicate iOS deployment
fd "fastlane|\.github|\.gitlab" --type d

Repository: AOSSIE-Org/Resonate

Length of output: 45


🏁 Script executed:

# Check for iOS-specific constants or configuration
rg "com\.resonate\|bundle" --type dart -i | head -10

Repository: AOSSIE-Org/Resonate

Length of output: 45


Use platform-specific URLs or a neutral invite link for the share flow.

The hardcoded Android Play Store URL breaks the invite experience for iOS and web users. Since the app supports iOS (confirmed by bundle ID com.resonate.resonate in the iOS project), recipients on those platforms cannot access the Play Store link. Either define an iOS App Store URL and route based on platform, or use a neutral link (like the GitHub repo) that works across all platforms.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/utils/constants.dart` around lines 103 - 104, The constant playStoreUrl
currently hardcodes an Android Play Store link (playStoreUrl) which breaks
sharing on iOS/web; add an iosAppStoreUrl (and optionally a neutral fallback
like repoUrl) and update the share flow to choose the appropriate URL based on
platform (e.g., Platform.isAndroid, Platform.isIOS, kIsWeb) or fall back to the
neutral link; locate usages of playStoreUrl in the share logic and replace with
a small platform-routing helper that returns the correct URL.

Comment on lines +78 to +87
child: OutlinedButton.icon(
onPressed: () => SharePlus.instance.share(
ShareParams(
text: localizations.inviteToResonate(playStoreUrl),
),
),
icon: const Icon(Icons.person_add_outlined),
label: Text(localizations.inviteFriend),
),
),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

share_plus 12.0.1 SharePlus.instance.share ShareParams API

💡 Result:

The share_plus Flutter package version 12.0.1 provides the SharePlus.instance.share(ShareParams params) API for sharing content via the platform's native share sheet (ACTION_SEND on Android, UIActivityViewController on iOS, etc.). Key details: - Import: import 'package:share_plus/share_plus.dart'; - Basic usage: SharePlus.instance.share(ShareParams(text: 'check out my website https://example.com')); - ShareParams supports: text, subject, title, uri, files (List), sharePositionOrigin (Rect, required for iOS 26 to avoid crashes if zero-sized), excludedCupertinoActivities. - Returns Future for checking share outcome: final result = await SharePlus.instance.share(params); - Previous static Share.share methods are deprecated; migrate by wrapping params in ShareParams. - Version 12.0.1 fixes iOS 26 crash when sharePositionOrigin param is missing or zero on iPhones. Example sharing text and file: final params = ShareParams( text: 'Great picture', files: [XFile('${directory.path}/image.jpg')], ); await SharePlus.instance.share(params); For iOS 26 compatibility: final box = context.findRenderObject as RenderBox?; SharePlus.instance.share( ShareParams( text: text, sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), ); Or use Rect.fromLTWH(0, 0, 1, 1) for simple cases.

Citations:


Add sharePositionOrigin parameter to prevent iOS 26+ crashes.

The SharePlus.instance.share(ShareParams(...)) API usage is correct for version 12.0.1, but the code is missing the sharePositionOrigin parameter. This parameter is required for iOS 26+ (iOS 16+) to avoid crashes; version 12.0.1 specifically addresses this by preventing crashes when the parameter is missing or zero-sized.

Update the code to include the parameter:

Suggested fix
onPressed: () {
  final box = context.findRenderObject() as RenderBox?;
  if (box != null) {
    SharePlus.instance.share(
      ShareParams(
        text: localizations.inviteToResonate(playStoreUrl),
        sharePositionOrigin: box.localToGlobal(Offset.zero) & box.size,
      ),
    );
  }
},

Alternatively, use a simple fallback:

sharePositionOrigin: Rect.fromLTWH(0, 0, 1, 1),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/views/screens/friends_empty_screen.dart` around lines 78 - 87, The
ShareParams call in the OutlinedButton.icon onPressed is missing
sharePositionOrigin which can crash iOS 16+; update the onPressed handler in the
OutlinedButton.icon to compute the button's global Rect via
context.findRenderObject() as RenderBox, then pass that Rect into
ShareParams.sharePositionOrigin (or use a safe fallback like
Rect.fromLTWH(0,0,1,1)) when calling SharePlus.instance.share; reference the
OutlinedButton.icon onPressed, SharePlus.instance.share, ShareParams,
context.findRenderObject, and RenderBox to locate where to add this logic.

Comment on lines +248 to +256
Future<void> showAudioDeviceSelector(BuildContext context) async {
final controller = Get.put(AudioDeviceController(), permanent: true);
await controller.refreshDevices();

Get.bottomSheet(
AudioDeviceSelectorDialog(controller: controller),
isScrollControlled: true,
backgroundColor: Colors.transparent,
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t create a permanent polling controller from a transient bottom sheet.

AudioDeviceController starts a 5-second timer in onInit(), and this helper never disposes it. Opening the selector from a screen that does not explicitly Get.delete<AudioDeviceController>() leaves a permanent poller running after the sheet is closed. Reuse an existing owner-scoped instance if present; otherwise create one for the sheet and clean it up on dismiss.

Suggested fix
 Future<void> showAudioDeviceSelector(BuildContext context) async {
-  final controller = Get.put(AudioDeviceController(), permanent: true);
+  final hadController = Get.isRegistered<AudioDeviceController>();
+  final controller =
+      hadController
+          ? Get.find<AudioDeviceController>()
+          : Get.put(AudioDeviceController>());
   await controller.refreshDevices();
 
-  Get.bottomSheet(
+  await Get.bottomSheet(
     AudioDeviceSelectorDialog(controller: controller),
     isScrollControlled: true,
     backgroundColor: Colors.transparent,
   );
+
+  if (!hadController && Get.isRegistered<AudioDeviceController>()) {
+    Get.delete<AudioDeviceController>();
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/views/widgets/audio_selector_dialog.dart` around lines 248 - 256,
showAudioDeviceSelector currently creates a permanent AudioDeviceController via
Get.put(..., permanent: true) which starts a 5s timer and is never disposed;
change the logic to first try to reuse an existing owner-scoped instance
(Get.isRegistered<AudioDeviceController>() / Get.find<AudioDeviceController>()),
and only create a non-permanent controller when none exists (omit permanent:
true), call controller.refreshDevices(), then await the result of
Get.bottomSheet(AudioDeviceSelectorDialog(controller: controller), ...); after
the bottom sheet completes, if you created the controller in this function
delete it (Get.delete<AudioDeviceController>(force: true) or equivalent) to stop
the poller so it doesn’t remain running after the sheet is closed.

ChapterPlayer({super.key, required this.chapter, required this.progress});

final ChapterPlayerController controller = Get.find();
//final ChapterPlayerController controller = Get.find();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check if ChapterPlayerController is also disabled and understand the scope of this change

echo "=== ChapterPlayerController status ==="
rg -n "class ChapterPlayerController" --type dart -A5

echo -e "\n=== flutter_lyric dependency ==="
cat pubspec.yaml 2>/dev/null | grep -A3 "flutter_lyric" || fd pubspec.yaml --exec cat {} | grep -A3 "flutter_lyric"

echo -e "\n=== Usage of ChapterPlayerController ==="
rg -n "ChapterPlayerController" --type dart

Repository: AOSSIE-Org/Resonate

Length of output: 1519


Chapter player is non-functional with all playback controls commented out.

The ChapterPlayerController lookup on line 14 and all Obx-wrapped playback controls (slider, play/pause buttons, progress display) are disabled across the codebase. The entire ChapterPlayerController class is also commented out, disabling core playback functionality. This leaves the widget as a static display with no interactivity.

Since this appears to be a deliberate workaround (likely related to the commented flutter_lyric dependency), either:

  1. Add a TODO comment explaining why playback is disabled and when it should be restored
  2. Display a placeholder message indicating playback is unavailable (instead of leaving AnimatedOpacity widgets empty)
  3. Create a tracking issue to restore functionality
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/views/widgets/chapter_player.dart` at line 14, The chapter player has all
playback controls and the ChapterPlayerController lookup commented out (e.g.,
the commented line with ChapterPlayerController controller = Get.find(),
Obx-wrapped slider and play/pause controls, and AnimatedOpacity areas), leaving
the widget non-interactive; restore a clear UX by adding a visible placeholder
and TODO: uncomment the controller lookup and control widgets only when playback
support is restored, otherwise replace the empty AnimatedOpacity blocks with a
centered message like "Playback unavailable" and add a TODO referencing
ChapterPlayerController and the flutter_lyric change so future work can
re-enable the Obx-wrapped slider, play/pause buttons and progress display (or
alternatively uncomment and rewire the controller if playback is meant to be
active).

@mahmoudr80
Copy link
Copy Markdown
Author

Thanks for the review. I realize now that I misunderstood the missing dependencies and modified the code incorrectly. I’ll revisit the setup and align my changes with the intended architecture.

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.

App fails to run due to missing flutter_lyric repo and firebase_options.dart

1 participant