setup fixes + pair_chat_dialog fix#790
Conversation
📝 WalkthroughWalkthroughThe PR addresses issue Changes
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)
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
✨ Finishing Touches🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
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 | 🔴 CriticalDon't route into
AppRoutes.tabviewbeforecontroller.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 | 🟠 MajorRemove the unconditional override of the debug signing config, or conditionally assign the release config only when keystore properties exist.
The
releasebuildType unconditionally assignssigningConfigs.getByName("release")on line 62, overriding the debug fallback from line 61. Whenkey.propertiesis missing, the release config has a nullstoreFileand empty passwords, causing the Android Gradle Plugin'svalidateSigningtask to fail the build with "Keystore file not set for signing config 'release'". A fresh clone withoutkey.propertiescannot 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 | 🟠 MajorIncorrect 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.onBackgroundMessagewill fail without Firebase initialization.
FirebaseMessaging.onBackgroundMessageis registered on line 53, butFirebase.initializeAppis 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:
- Comment out the
onBackgroundMessageregistration as well, or- 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 | 🟠 MajorGuard 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 | 🟠 MajorRollback 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 | 🟡 MinorCheck 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 (AppwriteExceptionwithcode == 404ortype == '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 | 🟡 MinorRemove the empty lyrics panel while playback is disabled.
With the
LyricsReaderblock 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 | 🟠 MajorLabel 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 | 🟡 MinorDebouncer in StatelessWidget may cause issues.
The
Debounceris instantiated as an instance field of aStatelessWidget. 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 theEditProfileControlleror converting this to aStatefulWidget.🛠️ 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 | 🔴 CriticalThe optional Firebase path still crashes here.
messaging?.getToken()can now legitimately returnnull, 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 | 🟠 MajorDelete the storage file and clear the image ID in the removeImage branch.
The remove branch only clears
profileImageUrlbut leaves the old storage file orphaned (nostorage.deleteFile()call) and keepsprofileImageIDin 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 clearprofileImageID: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 | 🟠 MajorThe denormalized
likescounter can drift from the likes table.These methods write the like row and
stories.likesin 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 | 🟠 MajorSkip stale likes instead of failing the whole liked-stories load.
tables.getRow()throwsAppwriteExceptionwith 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 inroom_chat_controller.dartwhere 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 | 🟠 MajorCreate the pair request before opening realtime subscriptions.
Both
quickMatch()andchoosePartner()are declared asasync voidand open realtime subscriptions before awaitingtablesDB.createRow(). If the request creation fails, the subscriptions (getRealtimeStream()andcheckForNewUsers()) will remain active listening for events on a request that was never created. Additionally, these methods lack error handling, so exceptions fromcreateRow()will not propagate.Also,
cancelRequest()navigates away in thefinallyblock regardless of whether thedeleteRow()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 | 🟠 MajorDon't serialize
[null]token lists when messaging is unavailable.When Firebase is disabled,
messaging?.getToken()isnull. 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 | 🟡 MinorMixed 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 | 🟡 MinorThese 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 | 🟡 MinorReset the selected device when it disappears from the refreshed list.
selectedAudioOutputis only initialized once. After a headset/Bluetooth route disappears, the controller can keep pointing at a device that no longer exists inaudioOutputDevices, 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 | 🟡 MinorThe new Kannada entries are still English.
Please update the source
kntranslations 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 | 🟡 MinorTranslate the new Hindi username validation copy.
These two new
hientries 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 | 🟡 MinorSeveral new Marathi entries are still English.
Please update the source
mrtranslations 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 | 🟡 MinorUse the runtime values in these Punjabi helpers.
audioFileSelected/lyricsFileSelecteddropfileName, andchooseThemedropscategory. 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 | 🟡 MinorPunjabi username guidance no longer matches the validator.
usernameCharacterLimitno longer tells the user about the 8-character minimum, andusernameInvalidFormatstill 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 | 🟡 MinorUpdate the Bengali length hint to the new 8-character rule.
This still says
>5characters, while the updated base copy now requires more than 7. Users onbnwill 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.
ChapterPlayerControlleris 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 => awaitpattern is redundant here since_buildControlButton.onPressedis aVoidCallbackthat doesn't return aFuture. 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.
firstWherewithoutorElsewill throw aStateErrorif the message is not found in the localmessageslist. Whilerethrowpropagates 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: Removelatemodifier fromtablesfield.The
tablesfield is initialized in the constructor's initializer list (line 26), so thelatekeyword is unnecessary and misleading. Thelatemodifier 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: Removelatemodifier fromaccountfield.Same issue as
tables- this field is initialized in the constructor's initializer list (line 27), makinglateunnecessary.♻️ 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.
trimmedValueis created on line 146, but line 168 callsvalue.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
AudioDeviceControllerimplementation. If there's anenumerateDevicesor 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.blackis 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)!.deleteas 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.
| 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}, | ||
| ); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
head -n 400 lib/controllers/auth_state_controller.dart | tail -n +260Repository: AOSSIE-Org/Resonate
Length of output: 5044
🏁 Script executed:
rg -n "final.*messaging\s*[=:]|var.*messaging\s*[=:]" lib/controllers/auth_state_controller.dart | head -20Repository: AOSSIE-Org/Resonate
Length of output: 45
🏁 Script executed:
rg -n "messaging" lib/controllers/auth_state_controller.dart -B 2 -A 2Repository: 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.
| // 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(); | ||
| // } | ||
| // } |
There was a problem hiding this comment.
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.
| await tables.createRow( | ||
| databaseId: masterDatabaseId, | ||
| collectionId: friendCallsCollectionId, | ||
| documentId: callModel.docId, | ||
| tableId: friendCallsTableId, | ||
| rowId: callModel.docId, | ||
| data: callModel.toJson(), |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n lib/controllers/friend_calling_controller.dart | head -100Repository: AOSSIE-Org/Resonate
Length of output: 4223
🏁 Script executed:
rg "startCall\(" lib/controllers/friend_calling_controller.dart -B 2 -A 2Repository: AOSSIE-Org/Resonate
Length of output: 198
🏁 Script executed:
rg "\.startCall\(" --type dart -A 3 -B 3Repository: 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 -nRepository: AOSSIE-Org/Resonate
Length of output: 2190
🏁 Script executed:
sed -n '100,160p' lib/views/widgets/friend_request_list_tile.dart | cat -nRepository: AOSSIE-Org/Resonate
Length of output: 3085
🏁 Script executed:
sed -n '95,150p' lib/views/widgets/friend_request_list_tile.dartRepository: AOSSIE-Org/Resonate
Length of output: 2577
🏁 Script executed:
sed -n '70,120p' lib/views/widgets/friend_request_list_tile.dartRepository: AOSSIE-Org/Resonate
Length of output: 2420
🏁 Script executed:
sed -n '50,150p' lib/views/widgets/friend_request_list_tile.dart | head -100Repository: 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.
| await tables.updateRow( | ||
| databaseId: userDatabaseID, | ||
| collectionId: liveChapterAttendeesCollectionId, | ||
| documentId: roomId, | ||
| tableId: liveChapterAttendeesTableId, | ||
| rowId: roomId, | ||
| data: newAttendeesModel.toJson(), |
There was a problem hiding this comment.
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.
| 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); | ||
| } |
There was a problem hiding this comment.
🧩 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.
| 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}, | ||
| ); |
There was a problem hiding this comment.
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.
| const String playStoreUrl = | ||
| "https://play.google.com/store/apps/details?id=com.resonate.resonate"; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's check where playStoreUrl is used in the codebase
rg "playStoreUrl" --type dartRepository: 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 -20Repository: 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.dartRepository: 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 -100Repository: 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 -20Repository: AOSSIE-Org/Resonate
Length of output: 45
🏁 Script executed:
# Look for app store URL references
rg "apple|app.store|itunes|appstore" --type dart -iRepository: 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 -50Repository: AOSSIE-Org/Resonate
Length of output: 2521
🏁 Script executed:
# Find sendUserToAppStore implementation
rg "sendUserToAppStore" --type dart -B 5 -A 10Repository: AOSSIE-Org/Resonate
Length of output: 1740
🏁 Script executed:
# Check the AboutAppScreenController
fd "about_app_screen_controller\.dart" --type f | xargs cat -nRepository: 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 2Repository: 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 -iRepository: AOSSIE-Org/Resonate
Length of output: 45
🏁 Script executed:
# Check if there's a pubspec.yaml file
cat pubspec.yaml | head -50Repository: AOSSIE-Org/Resonate
Length of output: 2342
🏁 Script executed:
# Check for iOS project configuration
ls -la ios/ 2>/dev/null | head -20Repository: 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 -20Repository: 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 dartRepository: 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 -10Repository: 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 dRepository: AOSSIE-Org/Resonate
Length of output: 45
🏁 Script executed:
# Check for iOS-specific constants or configuration
rg "com\.resonate\|bundle" --type dart -i | head -10Repository: 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.
| child: OutlinedButton.icon( | ||
| onPressed: () => SharePlus.instance.share( | ||
| ShareParams( | ||
| text: localizations.inviteToResonate(playStoreUrl), | ||
| ), | ||
| ), | ||
| icon: const Icon(Icons.person_add_outlined), | ||
| label: Text(localizations.inviteFriend), | ||
| ), | ||
| ), |
There was a problem hiding this comment.
🧩 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:
- 1: https://pub.dev/packages/share_plus
- 2: https://pub.dev/documentation/share_plus/latest/
- 3: https://pub.dev/packages/share_plus/versions/12.0.0
- 4: https://github.com/fluttercommunity/plus_plugins/blob/main/packages/share_plus/share_plus/CHANGELOG.md
- 5: https://pub.dev/documentation/share_plus/latest/share_plus/SharePlus-class.html
- 6: https://pub.dev/packages/share_plus/changelog
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.
| 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, | ||
| ); |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
🧩 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 dartRepository: 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:
- Add a TODO comment explaining why playback is disabled and when it should be restored
- Display a placeholder message indicating playback is unavailable (instead of leaving
AnimatedOpacitywidgets empty) - 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).
|
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. |
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:
flutter_lyricGit dependency (repository not available)google-servicesplugin to avoid missing Firebase configuration (google-services.json)FirebaseMessagingusage to avoid[core/no-app]errorskey.propertiesby making signing config null-safeTabViewController)LateInitializationErrorinUpcomingRoomsControllerby initializing storage properlyRenderFlex overflow) inpair_chat_dialog.dartby adding scroll and layout constraintsThese changes improve developer onboarding and allow the app to run without requiring Firebase or backend setup.
Fixes # (issue)
Type of change
How Has This Been Tested?
Cloned the repository on a clean environment
Ran:
Verified that:
Tested on emulator/device with different screen sizes
Checklist:
Maintainer Checklist
Summary by CodeRabbit
Release Notes
New Features
Improvements