Skip to content

Implement Friends and Direct calling functionality#524

Merged
M4dhav merged 6 commits intoAOSSIE-Org:devfrom
M4dhav:dev
Sep 6, 2025
Merged

Implement Friends and Direct calling functionality#524
M4dhav merged 6 commits intoAOSSIE-Org:devfrom
M4dhav:dev

Conversation

@M4dhav
Copy link
Contributor

@M4dhav M4dhav commented Sep 6, 2025

Description

This PR Implements all functionality related to Adding friends on the Resonate App and Direct Calls between friends

Fixes #523

Type of change

Please delete options that are not relevant.

  • New feature (non-breaking change which adds functionality)

How Has This Been Tested?

All Automated tests were run and passed and functionality was manually tested.

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • 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
  • Any dependent changes have been merged and published in downstream modules
  • I have checked my code and corrected any misspellings

Maintainer Checklist

Summary by CodeRabbit

  • New Features
    • Friend system: send/accept/decline requests with real-time updates, plus new Friends and Friend Requests screens.
    • Voice calling between friends: ringing and in-call screens with accept/decline, mic/speaker toggles, and native incoming-call UI.
  • Improvements
    • Profile screen: context-aware friend actions and follower handling.
    • Story notifications: clearer “New story added!” messaging.
    • UI tweaks: enhanced user list with ratings; raise-hand button adapts to theme.
    • Expanded English/Hindi translations for friends/calls.
  • Bug Fixes
    • Corrected chat message timestamp display.
  • Android
    • Updated activity launch behavior and added compatibility rules.
  • Documentation
    • Added Google Play badge; README formatting updates.

@M4dhav M4dhav self-assigned this Sep 6, 2025
@M4dhav M4dhav added enhancement New feature or request gsoc Part of Google Summer of Code season labels Sep 6, 2025
@github-actions
Copy link
Contributor

github-actions bot commented Sep 6, 2025

🎉 Welcome @M4dhav!
Thank you for your pull request! Our team will review it soon. 🔍

  • Please ensure your PR follows the contribution guidelines. ✅
  • All automated tests should pass before merging. 🔄
  • If this PR fixes an issue, link it in the description. 🔗

We appreciate your contribution! 🚀

@coderabbitai
Copy link

coderabbitai bot commented Sep 6, 2025

Walkthrough

Introduces friend relationships and direct calling features: new controllers, models, enums, routes, screens, and localizations. Integrates FCM background handling and CallKit for incoming calls. Adds Appwrite constants/collections, ProGuard keep rules, Android launchMode change, and multiple UI screens (ringing/call/friends). Also includes minor bug fixes and widespread formatting.

Changes

Cohort / File(s) Summary
Android call/launch config
android/app/proguard-rules.pro, android/app/src/main/AndroidManifest.xml, lib/main.dart
Keep Flutter CallKit plugin classes; change MainActivity launchMode to singleInstance; add FCM background handler to show CallKit incoming UI and register background messaging.
Friend calling feature
lib/controllers/friend_calling_controller.dart, lib/controllers/friend_call_screen.dart, lib/views/screens/ringing_screen.dart, lib/routes/app_pages.dart, lib/routes/app_routes.dart, lib/utils/enums/friend_call_status.dart, lib/models/friend_call_model.dart, lib/models/friend_call_model.*, lib/controllers/auth_state_controller.dart
New end-to-end friend-call flow: model, enum, controller, ringing and in-call screens, routes; integrates CallKit events and FCM foreground handling via AuthStateController.
Friends system feature
lib/controllers/friends_controller.dart, lib/models/friends_model.dart, lib/models/friends_model.*, lib/views/screens/friends_screen.dart, lib/views/screens/friend_requests_screen.dart, lib/l10n/app_*.arb, lib/l10n/app_localizations*.dart, lib/views/screens/profile_screen.dart, lib/utils/constants.dart
Add friend requests/accept/decline, data model with ratings, friends and requests screens, localization strings/APIs, constants for collections/functions, and profile UI integration for friend actions.
Follower/rating model adjustments
lib/models/follower_user_model.dart, lib/models/follower_user_model.*, lib/models/resonate_user.dart, lib/models/resonate_user.*
Make followerRating nullable with converter; propagate to generated files; unify numeric converter name (toDouble) across models.
Notification/story minor change
lib/controllers/explore_story_controller.dart
Adjust new-story notification title/body format.
UI/logic fixes and tweaks
lib/views/screens/room_chat_screen.dart, lib/views/screens/room_screen.dart, lib/views/screens/create_story_screen.dart, lib/views/screens/explore_screen.dart
Fix chat timestamp source; tweak raise-hand button background color logic; DropdownButtonFormField uses initialValue; change search “no match” condition to check only users.
Constants and config
lib/utils/constants.dart
Change default APPWRITE_BASE_DOMAIN; add friendsCollectionID, friendCallsCollectionId, startFriendCallFunctionID.
Formatting-only refactors
README.md, lib/controllers/* (many), lib/views/screens/* (many), lib/models/* (several), lib/services/*, lib/themes/*, lib/utils/*
Widespread reformatting (trailing commas, multiline params, minor logs/newlines) without behavior changes.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Caller as Caller App
  participant FC as FriendsController
  participant FCall as FriendCallingController
  participant DB as Appwrite (DB/Functions/Realtime)
  participant FCM as Firebase Messaging
  participant CK as CallKit (FlutterCallkitIncoming)
  actor Callee as Callee App

  Caller->>FCall: startCall(reciever...)
  FCall->>DB: create call doc (status=waiting)
  FCall->>DB: functions.execute(startFriendCall)
  DB-->>Callee: FCM push (type=incoming_call, extra)
  Note over Callee,FCM: Background or foreground
  FCM->>Callee: deliver RemoteMessage
  Callee->>CK: showCallkitIncoming(extra)
  alt Accept
    CK-->>Callee: ActionAccept
    Callee->>FCall: onAnswerCall(extra)
    FCall->>DB: update status=connected
    FCall->>DB: subscribe realtime(callId)
    FCall->>FCall: joinCall(livekitRoomId)
    Callee->>Callee: Navigate FriendCallScreen
    Caller-->>FCall: realtime update => joinCall
  else Decline
    CK-->>Callee: ActionDecline
    Callee->>FCall: onDeclinedCall(extra)
    FCall->>DB: update status=declined
    FCall-->>CK: endAllCalls()
  end
Loading
sequenceDiagram
  autonumber
  actor User as User
  participant FC as FriendsController
  participant DB as Appwrite DB
  participant UI as ProfileScreen/Friends* Screens

  User->>FC: sendFriendRequest(target)
  FC->>DB: create FriendsModel(status=sent)
  DB-->>FC: realtime update
  FC->>UI: update friendRequestsList

  User->>FC: acceptFriendRequest(model)
  FC->>DB: update status=accepted
  DB-->>FC: realtime update
  FC->>UI: move to friendsList

  User->>FC: removeFriend(model)
  FC->>DB: delete friend doc
  FC->>UI: update lists
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Assessment against linked issues

Objective Addressed Explanation
Add friends capability: request, accept/decline, list views (#523)
Direct friend-to-friend calling with ringing/accept/decline (#523)
Incoming call handling via FCM + native call UI (CallKit) (#523)
UI routes/screens for friends, requests, ringing/call (#523)
Localization/messages for friend/call flows (#523)

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Change Explore search no-match condition to depend only on users (lib/views/screens/explore_screen.dart, multiple lines) Alters search results behavior unrelated to friends/calling.
Fix timestamp to use message.creationDateTime (lib/views/screens/room_chat_screen.dart, multiple lines) Bug fix in room chat not tied to friends/calling.
Raise-hand button background color tweak (lib/views/screens/room_screen.dart, multiple lines) UI theming change unrelated to objectives.
Dropdown initialValue vs value in Create Story (lib/views/screens/create_story_screen.dart, multiple lines) Form behavior change outside friend/call scope.
Default APPWRITE_BASE_DOMAIN changed (lib/utils/constants.dart, top lines) Environment/config change not specified in the issue.
README badge addition (README.md, badges section) Documentation cosmetic change, unrelated.

Possibly related PRs

Poem

Thump-thump go my paws on keys so bright,
I dial a friend in moonlit night.
A ring, a beep, “Accept?” I cheer—
We chat in code, both far and near.
New pals made fast, requests in queue,
Hop-hop! Our calls connect—woohoo! 🐰📞✨

✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

❤️ Share

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

@M4dhav M4dhav linked an issue Sep 6, 2025 that may be closed by this pull request
@M4dhav
Copy link
Contributor Author

M4dhav commented Sep 6, 2025

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Sep 6, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

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

Caution

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

⚠️ Outside diff range comments (21)
lib/controllers/tabview_controller.dart (1)

30-33: onDispose is not a GetX lifecycle hook; subscription won’t be cancelled

This method is never called by GetX, causing a leak. Use onClose() and call super.onClose().

-  void onDispose() {
-    _linkSubscription?.cancel();
-    super.dispose();
-  }
+  @override
+  void onClose() {
+    _linkSubscription?.cancel();
+    super.onClose();
+  }
lib/views/screens/reset_password_screen.dart (1)

13-16: Dispose TextEditingController to prevent leaks.

The controller is never disposed.

 class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
   final authController = Get.find<AuthenticationController>();
   TextEditingController passwordController = TextEditingController();
 
   @override
   Widget build(BuildContext context) {
@@
   }
 }
+
+@override
+void dispose() {
+  passwordController.dispose();
+  super.dispose();
+}

Also applies to: 62-62

android/app/src/main/AndroidManifest.xml (1)

3-12: Android 13+ notifications permission likely required for FCM/call notifications.

If you surface incoming-call notifications on API 33+, you’ll need POST_NOTIFICATIONS. Add it (and gate at runtime).

     <!-- Permissions -->
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.RECORD_AUDIO" />
+    <!-- Android 13+ notifications -->
+    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
lib/controllers/livekit_controller.dart (3)

11-12: late final on fields prevents reconnection; reassignment will throw LateInitializationError.

You reassign liveKitRoom and listener on reconnect. With late final, the second assignment fails at runtime.

-  late final Room liveKitRoom;
-  late final EventsListener<RoomEvent> listener;
+  late Room liveKitRoom;
+  late EventsListener<RoomEvent> listener;

Also applies to: 53-55, 92-117


47-51: Reset-attempts line is a no-op.

if (reconnectAttempts == 0) reconnectAttempts = 0; never changes state. If intent is a fresh connect, just reset.

-    // Reset attempts for a fresh connection
-    if (reconnectAttempts == 0) reconnectAttempts = 0;
+    // Reset attempts for a fresh connection
+    reconnectAttempts = 0;

51-65: Dispose leaked Room/listener on failed connect; create them as temps and assign only on success.

Currently, Room() and createListener() occur before connect(). On failure, they aren’t disposed, and on retries you create more instances.

   Future<bool> connectToRoom() async {
-    // Reset attempts for a fresh connection
-    if (reconnectAttempts == 0) reconnectAttempts = 0;
+    // Reset attempts for a fresh connection
+    reconnectAttempts = 0;

     while (reconnectAttempts < maxAttempts) {
       try {
-        liveKitRoom = Room();
-        listener = liveKitRoom.createListener();
-
-        await liveKitRoom.connect(
+        Room? tmpRoom;
+        EventsListener<RoomEvent>? tmpListener;
+        tmpRoom = Room();
+        tmpListener = tmpRoom.createListener();
+        await tmpRoom.connect(
           liveKitUri,
           roomToken,
           roomOptions: const RoomOptions(
             adaptiveStream: false,
             dynacast: false,
             defaultVideoPublishOptions: VideoPublishOptions(simulcast: false),
           ),
         );

-        isConnected.value = true;
-        reconnectAttempts = 0; // Reset on success
-        log('Connected to room successfully');
+        // Swap in the successfully connected room/listener
+        try { await listener.dispose(); } catch (_) {}
+        try { await liveKitRoom.dispose(); } catch (_) {}
+        liveKitRoom = tmpRoom;
+        listener = tmpListener;
+        isConnected.value = true;
+        reconnectAttempts = 0; // Reset on success
+        log('Connected to room successfully');
         return true;
       } catch (error) {
         reconnectAttempts++;
-        log(
-          'Connection attempt $reconnectAttempts/$maxAttempts failed: $error',
-        );
+        log('Connection attempt $reconnectAttempts/$maxAttempts failed: $error');
+        // Best-effort cleanup of temps
+        try { await listener.dispose(); } catch (_) {}
+        try { await liveKitRoom.dispose(); } catch (_) {}
 
         if (reconnectAttempts < maxAttempts) {
           await Future.delayed(retryInterval); // Wait before retrying
         } else {
           log('Failed to connect after $maxAttempts attempts');
           Get.snackbar(
             AppLocalizations.of(Get.context!)!.connectionFailed,
             AppLocalizations.of(Get.context!)!.unableToJoinRoom,
             duration: const Duration(seconds: 5),
           );
           return false;
         }
       }
     }
     return false; // Fallback return (shouldn’t hit this due to loop logic)
   }

Also applies to: 70-87

lib/controllers/edit_profile_controller.dart (1)

248-258: Removing profile image leaves stale storage object and ID

When removeImage is true, you only clear the URL; the old file remains in storage and profileImageID isn’t cleared. This leaks storage and leaves inconsistent state.

       if (removeImage) {
-        imageController.text = "";
-
-        // Update user profile picture URL in Database
-        await databases.updateDocument(
-          databaseId: userDatabaseID,
-          collectionId: usersCollectionID,
-          documentId: authStateController.uid!,
-          data: {"profileImageUrl": imageController.text},
-        );
+        imageController.text = "";
+        // Best-effort delete of existing file
+        try {
+          final oldId = authStateController.profileImageID;
+          if (oldId != null && oldId.isNotEmpty) {
+            await storage.deleteFile(
+              bucketId: userProfileImageBucketId,
+              fileId: oldId,
+            );
+          }
+        } catch (e) {
+          log(e.toString());
+        }
+        // Clear both URL and ID in Database
+        await databases.updateDocument(
+          databaseId: userDatabaseID,
+          collectionId: usersCollectionID,
+          documentId: authStateController.uid!,
+          data: {
+            "profileImageUrl": imageController.text,
+            "profileImageID": "",
+          },
+        );
       }
lib/controllers/explore_story_controller.dart (1)

164-168: Guard division by zero; remove stray Future.delayed

ratingCount can be 0/null causing NaN/Infinity. Also the unawaited Future.delayed has no effect.

-      userData['userRating'] =
-          userData['ratingTotal'] / userData['ratingCount'];
-      log(userData['userRating'].toString());
-      Future.delayed(Duration(seconds: 1));
+      final total = (userData['ratingTotal'] ?? 0) as num;
+      final count = (userData['ratingCount'] ?? 0) as num;
+      userData['userRating'] = count > 0 ? (total / count) : 0.0;
+      log(userData['userRating'].toString());
lib/controllers/single_room_controller.dart (1)

94-103: Fix crash when participant isn’t found before update (index = -1).

indexWhere can return -1; direct indexing will throw. Guard and self-heal by adding the participant from payload.

Apply:

-    int toBeUpdatedIndex = participants.indexWhere(
-      (p) => p.value.uid == payload["uid"],
-    );
-    participants[toBeUpdatedIndex].value.isModerator = payload["isModerator"];
+    final toBeUpdatedIndex = participants.indexWhere(
+      (p) => p.value.uid == payload["uid"],
+    );
+    if (toBeUpdatedIndex == -1) {
+      // Update arrived before create; reconcile locally.
+      await addParticipantDataToList(Document.fromMap(payload));
+      return;
+    }
+    participants[toBeUpdatedIndex].value.isModerator = payload["isModerator"];
     participants[toBeUpdatedIndex].value.hasRequestedToBeSpeaker =
         payload["hasRequestedToBeSpeaker"] ?? false;
     participants[toBeUpdatedIndex].value.isMicOn = payload["isMicOn"];
     participants[toBeUpdatedIndex].value.isSpeaker = payload["isSpeaker"];
lib/routes/app_pages.dart (1)

80-88: Duplicate route for resetPassword

resetPassword is registered twice. Duplicate names can cause brittle routing and hard‑to‑trace bugs. Remove one of them.

Apply this diff to drop the second block:

-    GetPage(
-      name: AppRoutes.resetPassword,
-      page: () => const ResetPasswordScreen(),
-      binding: ProfileBinding(),
-    ),
lib/views/screens/edit_profile_screen.dart (1)

266-276: Refactor showBottomSheet to accept BuildContext

Complements the previous change; removes reliance on global context.

-  void showBottomSheet() {
+  void showBottomSheet(BuildContext context) {
     showModalBottomSheet(
-      context: Get.context!,
-      builder: (_) {
-        return changeProfilePictureBottomSheet(Get.context!);
+      context: context,
+      builder: (_) {
+        return changeProfilePictureBottomSheet(context);
       },
       shape: const RoundedRectangleBorder(
         borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
       ),
     );
   }
lib/controllers/room_chat_controller.dart (1)

197-266: Close realtime subscription to prevent leaks.

Add onClose to cancel the subscription.

 class RoomChatController extends GetxController {
@@
   void subscribeToMessages() {
     try {
       ...
     } catch (e) {
       log('Error subscribing to messages: $e');
     }
   }
+
+  @override
+  void onClose() {
+    subscription?.close();
+    super.onClose();
+  }
 }
lib/controllers/authentication_controller.dart (1)

117-119: Do not ship a localhost recovery URL.

Move to configuration and ensure it’s a valid HTTPS endpoint.

-        url:
-            'https://localhost/reset-password', // Replace with actual reset password URL
+        url: kPasswordResetUrl, // e.g., from env/config per flavor
lib/views/screens/pairing_screen.dart (2)

21-23: Avoid force-unwrapping profileImageUrl; add safe fallback.

This can crash if profileImageUrl is null. Prefer a null-aware fallback to the placeholder.

-    final profileImageUrl = controller.isAnonymous.value
-        ? themeController.userProfileImagePlaceholderUrl
-        : Get.find<AuthStateController>().profileImageUrl!;
+    final auth = Get.find<AuthStateController>();
+    final profileImageUrl = controller.isAnonymous.value
+        ? themeController.userProfileImagePlaceholderUrl
+        : (auth.profileImageUrl ?? themeController.userProfileImagePlaceholderUrl);

73-79: Color.withValues may not be available; use withOpacity for broader SDK compat.

Prevents build failures on older Flutter channels.

-            colors: [
-              primaryColor.withValues(alpha: 0.2),
-              primaryColor,
-              primaryColor.withValues(alpha: 0.6),
-            ],
+            colors: [
+              primaryColor.withOpacity(0.2),
+              primaryColor,
+              primaryColor.withOpacity(0.6),
+            ],
lib/controllers/friend_call_screen.dart (1)

1-180: Add missing withValues extension on Color
withValues is invoked across multiple widgets (e.g. settings_screen.dart, ringing_screen.dart, friend_call_screen.dart) but no withValues extension or helper is defined—add a Color.withValues({double alpha}) extension in a shared file and import it globally to prevent build failures.

lib/views/screens/onboarding_screen.dart (1)

105-113: Validator can NPE on null input

Avoid value!. Trim and null-guard.

Apply:

-                        validator: (value) {
-                          if (value!.length > 5) {
-                            return null;
-                          } else {
-                            return AppLocalizations.of(
-                              context,
-                            )!.usernameCharacterLimit;
-                          }
-                        },
+                        validator: (value) {
+                          final v = value?.trim() ?? '';
+                          if (v.length > 5) return null;
+                          return AppLocalizations.of(context)!.usernameCharacterLimit;
+                        },
lib/controllers/auth_state_controller.dart (3)

86-102: Guard against null payloads and missing room lookups to prevent crashes.

notificationResponse.payload! can be null and indexWhere can return -1, leading to a negative scroll offset and potential exceptions.

-  void onDidReceiveNotificationResponse(
-    NotificationResponse notificationResponse,
-  ) async {
-    String name = notificationResponse.payload!;
+  void onDidReceiveNotificationResponse(
+    NotificationResponse notificationResponse,
+  ) async {
+    final payload = notificationResponse.payload;
+    if (payload == null || payload.isEmpty) return;
+    final name = payload;
     UpcomingRoomsController upcomingRoomsController =
         Get.find<UpcomingRoomsController>();
-    int index = upcomingRoomsController.upcomingRooms.indexWhere(
+    final index = upcomingRoomsController.upcomingRooms.indexWhere(
       (upcomingRoom) => upcomingRoom.name == name,
     );
-    upcomingRoomsController.upcomingRoomScrollController.value =
-        ScrollController(initialScrollOffset: UiSizes.height_170 * index);
+    if (index >= 0) {
+      upcomingRoomsController.upcomingRoomScrollController.value =
+          ScrollController(initialScrollOffset: UiSizes.height_170 * index);
+    }

375-396: Fix logout ordering: remove FCM tokens before deleting the Appwrite session.

Currently you delete the session first, then attempt database updates, which will likely fail due to missing auth.

-      onConfirm: () async {
-        await account.deleteSession(sessionId: 'current');
-        await removeRegistrationTokenFromSubscribedUpcomingRooms();
-        Get.offAllNamed(AppRoutes.welcomeScreen);
-      },
+      onConfirm: () async {
+        try {
+          await removeRegistrationTokenFromSubscribedUpcomingRooms();
+        } finally {
+          await account.deleteSession(sessionId: 'current');
+          Get.offAllNamed(AppRoutes.welcomeScreen);
+        }
+      },

96-102: Dispose previous ScrollController before reassigning
At lib/controllers/auth_state_controller.dart:96 you overwrite upcomingRoomScrollController.value with a new ScrollController without disposing the old one. Call .dispose() on the existing controller before assignment (or override onClose() in UpcommingRoomsController to dispose its current controller) to prevent memory leaks.

lib/views/screens/profile_screen.dart (1)

143-151: Fix empty avatar URL crash; always fall back to placeholder.

NetworkImage('') throws; add emptiness checks for both own and other profiles.

-          CircleAvatar(
-            backgroundColor: Theme.of(Get.context!).colorScheme.secondary,
-            backgroundImage: widget.isCreatorProfile != null
-                ? NetworkImage(widget.creator!.profileImageUrl ?? '')
-                : controller.profileImageUrl == null ||
-                      controller.profileImageUrl!.isEmpty
-                ? NetworkImage(themeController.userProfileImagePlaceholderUrl)
-                : NetworkImage(controller.profileImageUrl ?? ''),
-            radius: UiSizes.width_66,
-          ),
+          CircleAvatar(
+            backgroundColor: Theme.of(Get.context!).colorScheme.secondary,
+            backgroundImage: widget.isCreatorProfile != null
+                ? NetworkImage(
+                    (widget.creator!.profileImageUrl?.isNotEmpty ?? false)
+                        ? widget.creator!.profileImageUrl!
+                        : themeController.userProfileImagePlaceholderUrl,
+                  )
+                : NetworkImage(
+                    (controller.profileImageUrl?.isNotEmpty ?? false)
+                        ? controller.profileImageUrl!
+                        : themeController.userProfileImagePlaceholderUrl,
+                  ),
+            radius: UiSizes.width_66,
+          ),

Comment on lines +29 to 33

android:exported="true"
android:launchMode="singleTop"
android:launchMode="singleInstance"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Avoid singleInstance for MainActivity; prefer singleTop (or singleTask) to preserve back stack and deep links.

singleInstance forces a dedicated task and commonly breaks deep links, app links, and expected navigation when re-launched from notifications/CallKit screens. For Flutter entry activities, singleTop is the safe default; singleTask is acceptable when you truly need a single instance across tasks.

Apply:

-            android:launchMode="singleInstance"
+            android:launchMode="singleTop"
📝 Committable suggestion

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

Suggested change
android:exported="true"
android:launchMode="singleTop"
android:launchMode="singleInstance"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
🤖 Prompt for AI Agents
In android/app/src/main/AndroidManifest.xml around lines 29 to 33, the
MainActivity uses android:launchMode="singleInstance" which breaks back stack
and deep links; change the launchMode to android:launchMode="singleTop" (or to
"singleTask" only if you explicitly need a single instance across tasks). Update
the attribute value in the <activity> tag, ensure manifest merges pick up the
change, then rebuild and test deep links, notifications and normal navigation
flows to confirm expected behavior.

Comment on lines +42 to +47
Future<void> createRoom(
String name,
String description,
List<String> tags,
bool fromCreateScreen,
) async {
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Guard dialog closing to avoid unintended route pops.

This method calls Get.back() (Lines 69 and 93) without ensuring a dialog is open. If no dialog was presented by this method’s caller, you may pop the current route.

Apply:

-      // Close the loading dialog
-      Get.back();
+      // Close the loading dialog
+      if (Get.isDialogOpen == true) {
+        Get.back();
+      }
...
-      // Close the loading dialog
-      Get.back();
+      if (Get.isDialogOpen == true) {
+        Get.back();
+      }

Also applies to: 69-70, 93-94

🤖 Prompt for AI Agents
In lib/controllers/create_room_controller.dart around lines 42-47 (and
specifically where Get.back() is called at lines 69-70 and 93-94), the code
calls Get.back() unconditionally which can pop a route if no dialog is open;
guard each call by checking whether a dialog is currently shown (e.g. use if
(Get.isDialogOpen == true) { Get.back(); } or an equivalent check) so you only
close dialogs and never unintentionally pop routes.

Comment on lines 31 to 37
Future<void> deleteUserProfilePicture() async {
try {
await storage.deleteFile(
bucketId: userProfileImageBucketId,
fileId: authStateController.profileImageID!);
bucketId: userProfileImageBucketId,
fileId: authStateController.profileImageID!,
);
} catch (e) {
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid null-assertions and handle 404 deletes gracefully

These deletions currently use null-assertions; a missing ID will crash the flow. Guard early and ignore 404s from Appwrite to make deletion idempotent.

-  Future<void> deleteUserProfilePicture() async {
-    try {
-      await storage.deleteFile(
-        bucketId: userProfileImageBucketId,
-        fileId: authStateController.profileImageID!,
-      );
-    } catch (e) {
-      log(e.toString());
-    }
-  }
+  Future<void> deleteUserProfilePicture() async {
+    final imageId = authStateController.profileImageID;
+    if (imageId == null || imageId.isEmpty) return;
+    try {
+      await storage.deleteFile(
+        bucketId: userProfileImageBucketId,
+        fileId: imageId,
+      );
+    } on AppwriteException catch (e) {
+      // Ignore "not found" to keep delete idempotent
+      if (e.code != 404) log(e.toString());
+    } catch (e) {
+      log(e.toString());
+    }
+  }
📝 Committable suggestion

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

Suggested change
Future<void> deleteUserProfilePicture() async {
try {
await storage.deleteFile(
bucketId: userProfileImageBucketId,
fileId: authStateController.profileImageID!);
bucketId: userProfileImageBucketId,
fileId: authStateController.profileImageID!,
);
} catch (e) {
Future<void> deleteUserProfilePicture() async {
final imageId = authStateController.profileImageID;
if (imageId == null || imageId.isEmpty) return;
try {
await storage.deleteFile(
bucketId: userProfileImageBucketId,
fileId: imageId,
);
} on AppwriteException catch (e) {
// Ignore "not found" to keep delete idempotent
if (e.code != 404) log(e.toString());
} catch (e) {
log(e.toString());
}
}
🤖 Prompt for AI Agents
In lib/controllers/delete_account_controller.dart around lines 31 to 37, the
code uses a null-assertion on profileImageID and will crash if the ID is
missing, and it doesn't treat Appwrite 404 deletes as success; update the method
to first guard against null (return/skip delete if profileImageID is null)
instead of using !, then call storage.deleteFile only when an ID exists, and in
the catch block handle Appwrite exceptions by ignoring status code 404 (treat as
already deleted) while rethrowing or logging other errors so deletion remains
idempotent.

Comment on lines 42 to 49
Future<void> deleteUsernamesCollectionDocument() async {
try {
await databases.deleteDocument(
databaseId: userDatabaseID,
collectionId: usernameCollectionID,
documentId: authStateController.userName!);
databaseId: userDatabaseID,
collectionId: usernameCollectionID,
documentId: authStateController.userName!,
);
} catch (e) {
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Same here: avoid null-assertions on username docId and make delete idempotent

Guard the value and ignore 404s.

-  Future<void> deleteUsernamesCollectionDocument() async {
-    try {
-      await databases.deleteDocument(
-        databaseId: userDatabaseID,
-        collectionId: usernameCollectionID,
-        documentId: authStateController.userName!,
-      );
-    } catch (e) {
-      log(e.toString());
-    }
-  }
+  Future<void> deleteUsernamesCollectionDocument() async {
+    final username = authStateController.userName;
+    if (username == null || username.isEmpty) return;
+    try {
+      await databases.deleteDocument(
+        databaseId: userDatabaseID,
+        collectionId: usernameCollectionID,
+        documentId: username,
+      );
+    } on AppwriteException catch (e) {
+      if (e.code != 404) log(e.toString());
+    } catch (e) {
+      log(e.toString());
+    }
+  }
📝 Committable suggestion

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

Suggested change
Future<void> deleteUsernamesCollectionDocument() async {
try {
await databases.deleteDocument(
databaseId: userDatabaseID,
collectionId: usernameCollectionID,
documentId: authStateController.userName!);
databaseId: userDatabaseID,
collectionId: usernameCollectionID,
documentId: authStateController.userName!,
);
} catch (e) {
Future<void> deleteUsernamesCollectionDocument() async {
final username = authStateController.userName;
if (username == null || username.isEmpty) return;
try {
await databases.deleteDocument(
databaseId: userDatabaseID,
collectionId: usernameCollectionID,
documentId: username,
);
} on AppwriteException catch (e) {
if (e.code != 404) log(e.toString());
} catch (e) {
log(e.toString());
}
}
🤖 Prompt for AI Agents
In lib/controllers/delete_account_controller.dart around lines 42 to 49, the
code currently uses a null-assertion for authStateController.userName and always
attempts deletion; change it to guard the username first by returning early if
userName is null or empty, then attempt the delete inside the try; in the catch,
detect a not-found error (e.g. AppwriteException with statusCode 404 or the
provider's specific 404 indicator) and ignore it to make the operation
idempotent, otherwise rethrow or log the error.

Comment on lines 54 to 61
Future<void> deleteUsersCollectionDocument() async {
try {
await databases.deleteDocument(
databaseId: userDatabaseID,
collectionId: usersCollectionID,
documentId: authStateController.uid!);
databaseId: userDatabaseID,
collectionId: usersCollectionID,
documentId: authStateController.uid!,
);
} catch (e) {
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Same fix for users collection delete

Prevent NPE on uid and handle 404.

-  Future<void> deleteUsersCollectionDocument() async {
-    try {
-      await databases.deleteDocument(
-        databaseId: userDatabaseID,
-        collectionId: usersCollectionID,
-        documentId: authStateController.uid!,
-      );
-    } catch (e) {
-      log(e.toString());
-    }
-  }
+  Future<void> deleteUsersCollectionDocument() async {
+    final uid = authStateController.uid;
+    if (uid == null || uid.isEmpty) return;
+    try {
+      await databases.deleteDocument(
+        databaseId: userDatabaseID,
+        collectionId: usersCollectionID,
+        documentId: uid,
+      );
+    } on AppwriteException catch (e) {
+      if (e.code != 404) log(e.toString());
+    } catch (e) {
+      log(e.toString());
+    }
+  }
📝 Committable suggestion

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

Suggested change
Future<void> deleteUsersCollectionDocument() async {
try {
await databases.deleteDocument(
databaseId: userDatabaseID,
collectionId: usersCollectionID,
documentId: authStateController.uid!);
databaseId: userDatabaseID,
collectionId: usersCollectionID,
documentId: authStateController.uid!,
);
} catch (e) {
Future<void> deleteUsersCollectionDocument() async {
final uid = authStateController.uid;
if (uid == null || uid.isEmpty) return;
try {
await databases.deleteDocument(
databaseId: userDatabaseID,
collectionId: usersCollectionID,
documentId: uid,
);
} on AppwriteException catch (e) {
if (e.code != 404) log(e.toString());
} catch (e) {
log(e.toString());
}
}
🤖 Prompt for AI Agents
In lib/controllers/delete_account_controller.dart around lines 54 to 61, the
deleteUsersCollectionDocument method can throw a null pointer when
authStateController.uid is null and currently doesn't treat a "document not
found" (404) response gracefully; first check that authStateController.uid is
not null (return or bail out with a controlled error) before calling
databases.deleteDocument, and in the catch block detect the Appwrite/HTTP
404-not-found error (or the specific exception type/message your client throws)
and ignore it (or log as info) while rethrowing or logging other exceptions so
only non-404 errors propagate.

Comment on lines +45 to +47
final exploreStoryController = Get.put<ExploreStoryController>(
ExploreStoryController(),
);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Controller lifecycle: avoid field-level Get.put in State

Repeated navigation can leak or clash instances. Either inject via a Binding + Get.find or manage create/delete in initState/dispose.

-  final exploreStoryController = Get.put<ExploreStoryController>(
-    ExploreStoryController(),
-  );
+  late final ExploreStoryController exploreStoryController =
+      Get.isRegistered<ExploreStoryController>()
+          ? Get.find<ExploreStoryController>()
+          : Get.put(ExploreStoryController());

Additionally, consider cleanup in dispose:

@override
void dispose() {
  if (Get.isRegistered<ExploreStoryController>()) {
    // Only delete if you own the instance; otherwise rely on Binding scope.
    // Get.delete<ExploreStoryController>();
  }
  super.dispose();
}
🤖 Prompt for AI Agents
In lib/views/screens/explore_screen.dart around lines 45 to 47, the controller
is instantiated at field level with Get.put which can leak or clash instances on
repeated navigation; move the Get.put call into the State lifecycle (initState)
or use a Binding and retrieve with Get.find instead, and add cleanup in dispose
to conditionally delete the controller only if this widget created/owns it
(e.g., check Get.isRegistered and call Get.delete when appropriate) so the
controller is properly managed across navigation.

Comment on lines +132 to +134
final UpcomingRoomsController upcomingRoomsController = Get.put(
UpcomingRoomsController(),
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Don’t re-register controllers; use Get.find to avoid duplicate bindings.

Multiple Get.put calls for the same type can throw/override instances.

Apply:

-  final UpcomingRoomsController upcomingRoomsController = Get.put(
-    UpcomingRoomsController(),
-  );
+  final UpcomingRoomsController upcomingRoomsController =
+      Get.find<UpcomingRoomsController>();
-  final RoomsController roomsController = Get.put(RoomsController());
+  final RoomsController roomsController = Get.find<RoomsController>();

Also applies to: 160-160

🤖 Prompt for AI Agents
lib/views/screens/home_screen.dart lines 132-134 (and similar at line 160):
currently the code re-registers the UpcomingRoomsController with Get.put which
can create duplicate/overriding instances; change these to use
Get.find<UpcomingRoomsController>() instead of Get.put(...) and remove the
duplicate Get.put call at line 160; ensure the controller is registered once in
your app bindings (e.g., in a Binding or at app start) so Get.find will succeed.

Comment on lines +49 to +51
itemCount: pairChatController.usersList.length,
shrinkWrap: true,
itemBuilder: (context, index) {
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Top-level ListView: avoid shrinkWrap for performance

shrinkWrap: true forces extra layout passes and hurts scroll performance for a primary list.

-                itemCount: pairChatController.usersList.length,
-                shrinkWrap: true,
+                itemCount: pairChatController.usersList.length,
🤖 Prompt for AI Agents
In lib/views/screens/pair_chat_users_screen.dart around lines 49 to 51, the
ListView is using shrinkWrap: true which forces extra layout passes and degrades
scroll performance; remove the shrinkWrap property (or set it to false) and
ensure the ListView is given a bounded height by wrapping it with an Expanded or
Flexible when placed inside a Column (or otherwise place it as the primary
scrollable in the widget tree) so it can perform efficient, single-pass layout.

Comment on lines +52 to +76
return ListTile(
onTap: () async {
await pairChatController.pairWithSelectedUser(
pairChatController.usersList[index],
);
},
title: Text(pairChatController.usersList[index].userName!),
subtitle: Text(pairChatController.usersList[index].name!),

leading: CircleAvatar(
backgroundImage: NetworkImage(
pairChatController
.usersList[index].profileImageUrl!,
),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.star,
color: Colors.amber,
),
Text(pairChatController
.usersList[index].userRating!
.toStringAsFixed(1)),
],
),
// ...other fields
);
},
leading: CircleAvatar(
backgroundImage: NetworkImage(
pairChatController.usersList[index].profileImageUrl!,
),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.star, color: Colors.amber),
Text(
pairChatController.usersList[index].userRating!
.toStringAsFixed(1),
),
],
),
));
// ...other fields
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Null-safety: remove bang-operators and add image/rating fallbacks

The current code force-unwraps user fields and can crash on missing data. Add safe defaults and a placeholder avatar.

-                itemBuilder: (context, index) {
-                  return ListTile(
+                itemBuilder: (context, index) {
+                  final user = pairChatController.usersList[index];
+                  final imageUrl = user.profileImageUrl;
+                  final rating = (user.userRating ?? 0).toDouble();
+                  return ListTile(
                     onTap: () async {
-                      await pairChatController.pairWithSelectedUser(
-                        pairChatController.usersList[index],
-                      );
+                      await pairChatController.pairWithSelectedUser(user);
                     },
-                    title: Text(pairChatController.usersList[index].userName!),
-                    subtitle: Text(pairChatController.usersList[index].name!),
+                    title: Text(user.userName ?? ''),
+                    subtitle: Text(user.name ?? ''),
                     leading: CircleAvatar(
-                      backgroundImage: NetworkImage(
-                        pairChatController.usersList[index].profileImageUrl!,
-                      ),
+                      backgroundImage: (imageUrl != null && imageUrl.isNotEmpty)
+                          ? NetworkImage(imageUrl)
+                          : null,
+                      child: (imageUrl == null || imageUrl.isEmpty)
+                          ? const Icon(Icons.person)
+                          : null,
                     ),
                     trailing: Row(
                       mainAxisSize: MainAxisSize.min,
                       children: [
                         Icon(Icons.star, color: Colors.amber),
-                        Text(
-                          pairChatController.usersList[index].userRating!
-                              .toStringAsFixed(1),
-                        ),
+                        SizedBox(width: 4),
+                        Text(rating.toStringAsFixed(1)),
                       ],
                     ),
                     // ...other fields
                   );
                 },
📝 Committable suggestion

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

Suggested change
return ListTile(
onTap: () async {
await pairChatController.pairWithSelectedUser(
pairChatController.usersList[index],
);
},
title: Text(pairChatController.usersList[index].userName!),
subtitle: Text(pairChatController.usersList[index].name!),
leading: CircleAvatar(
backgroundImage: NetworkImage(
pairChatController
.usersList[index].profileImageUrl!,
),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.star,
color: Colors.amber,
),
Text(pairChatController
.usersList[index].userRating!
.toStringAsFixed(1)),
],
),
// ...other fields
);
},
leading: CircleAvatar(
backgroundImage: NetworkImage(
pairChatController.usersList[index].profileImageUrl!,
),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.star, color: Colors.amber),
Text(
pairChatController.usersList[index].userRating!
.toStringAsFixed(1),
),
],
),
));
// ...other fields
itemBuilder: (context, index) {
final user = pairChatController.usersList[index];
final imageUrl = user.profileImageUrl;
final rating = (user.userRating ?? 0).toDouble();
return ListTile(
onTap: () async {
await pairChatController.pairWithSelectedUser(user);
},
title: Text(user.userName ?? ''),
subtitle: Text(user.name ?? ''),
leading: CircleAvatar(
backgroundImage: (imageUrl != null && imageUrl.isNotEmpty)
? NetworkImage(imageUrl)
: null,
child: (imageUrl == null || imageUrl.isEmpty)
? const Icon(Icons.person)
: null,
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.star, color: Colors.amber),
SizedBox(width: 4),
Text(rating.toStringAsFixed(1)),
],
),
// ...other fields
);
},
🤖 Prompt for AI Agents
In lib/views/screens/pair_chat_users_screen.dart around lines 52 to 76, remove
the null-bang operators on user fields and replace them with null-safe
fallbacks: use pairChatController.usersList[index].userName ?? 'Unknown' and
.name ?? '' for title/subtitle, provide a placeholder avatar when
profileImageUrl is null/empty (e.g. use an AssetImage or initials in
CircleAvatar when profileImageUrl?.isNotEmpty != true), and guard rating by
using (pairChatController.usersList[index].userRating ?? 0.0).toStringAsFixed(1)
so toStringAsFixed is only called on a non-null double; ensure onTap still
passes the user object but defensively handle a null user in
pairWithSelectedUser if necessary.

Comment on lines +81 to +83
| Login Screen (Forest) | Home Screen (Time) | Create Room Screen (Time) |
| :----------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------- |
| <img src= "https://github.com/user-attachments/assets/e76147b1-0e51-4852-8198-06bbc975b25c" width="260" height="auto" /> | <img src="https://github.com/user-attachments/assets/ad62eecb-b621-4c31-a01c-001ff5462b28" width="250" height="auto" /> | <img src="https://github.com/user-attachments/assets/31ce6e73-8dca-4e2d-8f48-c22480fa1332" width="250" height="auto" /> |
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix missing alt text for images (MD045).

Add alt attributes for accessibility and to satisfy markdownlint.

-| <img src= "https://github.com/user-attachments/assets/e76147b1-0e51-4852-8198-06bbc975b25c" width="260" height="auto" /> | <img src="https://github.com/user-attachments/assets/ad62eecb-b621-4c31-a01c-001ff5462b28" width="250" height="auto" /> | <img src="https://github.com/user-attachments/assets/31ce6e73-8dca-4e2d-8f48-c22480fa1332" width="250" height="auto" /> |
+| <img alt="Login Screen (Forest)" src="https://github.com/user-attachments/assets/e76147b1-0e51-4852-8198-06bbc975b25c" width="260" height="auto" /> | <img alt="Home Screen (Time)" src="https://github.com/user-attachments/assets/ad62eecb-b621-4c31-a01c-001ff5462b28" width="250" height="auto" /> | <img alt="Create Room Screen (Time)" src="https://github.com/user-attachments/assets/31ce6e73-8dca-4e2d-8f48-c22480fa1332" width="250" height="auto" /> |
📝 Committable suggestion

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

Suggested change
| Login Screen (Forest) | Home Screen (Time) | Create Room Screen (Time) |
| :----------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------- |
| <img src= "https://github.com/user-attachments/assets/e76147b1-0e51-4852-8198-06bbc975b25c" width="260" height="auto" /> | <img src="https://github.com/user-attachments/assets/ad62eecb-b621-4c31-a01c-001ff5462b28" width="250" height="auto" /> | <img src="https://github.com/user-attachments/assets/31ce6e73-8dca-4e2d-8f48-c22480fa1332" width="250" height="auto" /> |
| Login Screen (Forest) | Home Screen (Time) | Create Room Screen (Time) |
| :----------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------- |
| <img alt="Login Screen (Forest)" src="https://github.com/user-attachments/assets/e76147b1-0e51-4852-8198-06bbc975b25c" width="260" height="auto" /> | <img alt="Home Screen (Time)" src="https://github.com/user-attachments/assets/ad62eecb-b621-4c31-a01c-001ff5462b28" width="250" height="auto" /> | <img alt="Create Room Screen (Time)" src="https://github.com/user-attachments/assets/31ce6e73-8dca-4e2d-8f48-c22480fa1332" width="250" height="auto" /> |
🧰 Tools
🪛 markdownlint-cli2 (0.17.2)

83-83: Images should have alternate text (alt text)

(MD045, no-alt-text)


83-83: Images should have alternate text (alt text)

(MD045, no-alt-text)


83-83: Images should have alternate text (alt text)

(MD045, no-alt-text)

🤖 Prompt for AI Agents
In README.md around lines 81 to 83 the three HTML <img> tags are missing alt
attributes which triggers markdownlint MD045 and harms accessibility; add
descriptive alt attributes to each image tag (for example: alt="Login Screen
(Forest)", alt="Home Screen (Time)", alt="Create Room Screen (Time)") or other
concise, meaningful descriptions, ensuring each <img> tag includes alt="..." to
satisfy the linter and accessibility requirements.

Copy link

@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.

Review continued from previous batch...

Comment on lines +167 to +181
FlutterCallkitIncoming.onEvent.listen((CallEvent? event) {
if (event!.event == Event.actionCallAccept) {
log(event.body['extra'].toString());

friendCallingController.onAnswerCall(
Map<String, dynamic>.from(event.body['extra']),
);
}
if (event.event == Event.actionCallDecline) {
friendCallingController.onDeclinedCall(
Map<String, dynamic>.from(event.body['extra']),
);
FlutterCallkitIncoming.showMissCallNotification(CallKitParams());
}
});
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

CallKit event stream: null-safety, extra payload checks, and missed-call params.

  • event! can be null.
  • event.body['extra'] may be absent/not a map.
  • showMissCallNotification(CallKitParams()) without required fields is likely ineffective.
-      FlutterCallkitIncoming.onEvent.listen((CallEvent? event) {
-        if (event!.event == Event.actionCallAccept) {
-          log(event.body['extra'].toString());
-
-          friendCallingController.onAnswerCall(
-            Map<String, dynamic>.from(event.body['extra']),
-          );
-        }
-        if (event.event == Event.actionCallDecline) {
-          friendCallingController.onDeclinedCall(
-            Map<String, dynamic>.from(event.body['extra']),
-          );
-          FlutterCallkitIncoming.showMissCallNotification(CallKitParams());
-        }
-      });
+      _callkitSub = FlutterCallkitIncoming.onEvent.listen((CallEvent? event) async {
+        if (event == null) return;
+        final extra = event.body['extra'];
+        final extraMap = (extra is Map) ? Map<String, dynamic>.from(extra) : const <String, dynamic>{};
+        switch (event.event) {
+          case Event.actionCallAccept:
+            log('Call accept: $extraMap');
+            await friendCallingController.onAnswerCall(extraMap);
+            break;
+          case Event.actionCallDecline:
+            await friendCallingController.onDeclinedCall(extraMap);
+            // Populate missed-call notification params (id/handle/name at minimum).
+            if (extraMap['id'] != null && extraMap['handle'] != null && extraMap['nameCaller'] != null) {
+              await FlutterCallkitIncoming.showMissCallNotification(
+                CallKitParams(
+                  id: extraMap['id'],
+                  nameCaller: extraMap['nameCaller'],
+                  handle: extraMap['handle'],
+                  type: 0,
+                  extra: extraMap,
+                ),
+              );
+            }
+            break;
+          default:
+            break;
+        }
+      });

Follow-up:

  • Store _callkitSub as a StreamSubscription<CallEvent?> field and cancel it in onClose() to prevent leaks.

Verification ask (docs/API surface):


🌐 Web query:

What parameters are required by FlutterCallkitIncoming.showMissCallNotification and the structure of CallKitParams? Provide the latest stable plugin docs.

💡 Result:

Required parameter:

  • showMissCallNotification takes a single CallKitParams instance (must include id). [1]

CallKitParams (main fields — id is required; others optional):

  • id (String, UUID) — Required.
  • nameCaller (String)
  • appName (String)
  • avatar (String URL)
  • handle (String)
  • type (int: 0 audio / 1 video)
  • duration (int ms)
  • textAccept / textDecline (String)
  • extra (Map)
  • headers (Map)
  • missedCallNotification (NotificationParams): { showNotification (bool), isShowCallback (bool), subtitle (String), callbackText (String) }
  • callingNotification (NotificationParams): { showNotification, isShowCallback, subtitle, callbackText }
  • android (AndroidParams): several props including isCustomNotification, isCustomSmallExNotification, isShowLogo, logoUrl, ringtonePath, backgroundColor, backgroundUrl, actionColor, textColor, incomingCallNotificationChannelName, missedCallNotificationChannelName, isShowCallID, isShowFullLockedScreen, etc.
  • ios (IOSParams): props like iconName, handleType, supportsVideo, maximumCallGroups, maximumCallsPerCallGroup, audioSessionMode, audioSessionActive, audioSessionPreferredSampleRate, audioSessionPreferredIOBufferDuration, supportsDTMF, supportsHolding, supportsGrouping, supportsUngrouping, ringtonePath, etc.

Examples and defaults (e.g. missedCallNotification.defaults: subtitle "Missed Call", callbackText "Call back", showNotification true) are in the plugin docs. See plugin README for full, up-to-date field list and examples. [1][2]

Sources:
[1] GitHub README — flutter_callkit_incoming (params, missedCall/calling/Android/iOS sections).
[2] README variant / examples (showMissCallNotification usage).


Refactor CallKit event handling: add null-safety, validate extra, and include required id for missed-call notification

-    FlutterCallkitIncoming.onEvent.listen((CallEvent? event) {
-      if (event!.event == Event.actionCallAccept) {
-        log(event.body['extra'].toString());
-
-        friendCallingController.onAnswerCall(
-          Map<String, dynamic>.from(event.body['extra']),
-        );
-      }
-      if (event.event == Event.actionCallDecline) {
-        friendCallingController.onDeclinedCall(
-          Map<String, dynamic>.from(event.body['extra']),
-        );
-        FlutterCallkitIncoming.showMissCallNotification(CallKitParams());
-      }
-    });
+    _callkitSub = FlutterCallkitIncoming.onEvent.listen((CallEvent? event) async {
+      if (event == null) return;
+      final rawExtra = event.body['extra'];
+      final extraMap = (rawExtra is Map) ? Map<String, dynamic>.from(rawExtra) : <String, dynamic>{};
+      switch (event.event) {
+        case Event.actionCallAccept:
+          log('Call accepted: $extraMap');
+          await friendCallingController.onAnswerCall(extraMap);
+          break;
+        case Event.actionCallDecline:
+          await friendCallingController.onDeclinedCall(extraMap);
+          final id = extraMap['id'] as String?;
+          final handle = extraMap['handle'] as String?;
+          final nameCaller = extraMap['nameCaller'] as String?;
+          if (id != null && handle != null && nameCaller != null) {
+            await FlutterCallkitIncoming.showMissCallNotification(
+              CallKitParams(
+                id: id,
+                nameCaller: nameCaller,
+                handle: handle,
+                type: 0,
+                extra: extraMap,
+              ),
+            );
+          }
+          break;
+        default:
+          break;
+      }
+    });

• Declare _callkitSub as a StreamSubscription<CallEvent?> field and cancel it in onClose() to avoid leaks.

📝 Committable suggestion

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

Suggested change
FlutterCallkitIncoming.onEvent.listen((CallEvent? event) {
if (event!.event == Event.actionCallAccept) {
log(event.body['extra'].toString());
friendCallingController.onAnswerCall(
Map<String, dynamic>.from(event.body['extra']),
);
}
if (event.event == Event.actionCallDecline) {
friendCallingController.onDeclinedCall(
Map<String, dynamic>.from(event.body['extra']),
);
FlutterCallkitIncoming.showMissCallNotification(CallKitParams());
}
});
_callkitSub = FlutterCallkitIncoming.onEvent.listen((CallEvent? event) async {
if (event == null) return;
final rawExtra = event.body['extra'];
final extraMap = (rawExtra is Map) ? Map<String, dynamic>.from(rawExtra) : <String, dynamic>{};
switch (event.event) {
case Event.actionCallAccept:
log('Call accepted: $extraMap');
await friendCallingController.onAnswerCall(extraMap);
break;
case Event.actionCallDecline:
await friendCallingController.onDeclinedCall(extraMap);
final id = extraMap['id'] as String?;
final handle = extraMap['handle'] as String?;
final nameCaller = extraMap['nameCaller'] as String?;
if (id != null && handle != null && nameCaller != null) {
await FlutterCallkitIncoming.showMissCallNotification(
CallKitParams(
id: id,
nameCaller: nameCaller,
handle: handle,
type: 0,
extra: extraMap,
),
);
}
break;
default:
break;
}
});
🤖 Prompt for AI Agents
In lib/controllers/auth_state_controller.dart around lines 167 to 181, the
CallKit event handler needs null-safety, validation of event.body['extra'], and
proper subscription lifecycle management: store the listener in a field named
_callkitSub of type StreamSubscription<CallEvent?>, check event for null before
accessing event.event, ensure event.body and event.body['extra'] are non-null
and are a Map<String, dynamic> (or safely convert) before calling
friendCallingController methods, and when calling
FlutterCallkitIncoming.showMissCallNotification provide a CallKitParams instance
that includes the required id (e.g., pull id from the validated extra map) to
avoid missing-id errors; finally cancel _callkitSub in onClose() to prevent
leaks.

Comment on lines +269 to 288
List<Document> subscribedUpcomingRooms = await databases
.listDocuments(
databaseId: upcomingRoomsDatabaseId,
collectionId: subscribedUserCollectionId,
queries: [
Query.equal("userID", [uid]),
],
)
.then((value) => value.documents);
for (var subscription in subscribedUpcomingRooms) {
List<dynamic> registrationTokens =
subscription.data['registrationTokens'];
registrationTokens.add(fcmToken!);
databases.updateDocument(
databaseId: upcomingRoomsDatabaseId,
collectionId: subscribedUserCollectionId,
documentId: subscription.$id,
data: {"registrationTokens": registrationTokens});
databaseId: upcomingRoomsDatabaseId,
collectionId: subscribedUserCollectionId,
documentId: subscription.$id,
data: {"registrationTokens": registrationTokens},
);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Await updates and ensure FCM token uniqueness to prevent duplicates.

  • registrationTokens.add(fcmToken!) can add duplicates.
  • databases.updateDocument(...) is not awaited; failures will be swallowed.
-    List<Document> subscribedUpcomingRooms = await databases
+    final List<Document> subscribedUpcomingRooms = await databases
         .listDocuments(
           databaseId: upcomingRoomsDatabaseId,
           collectionId: subscribedUserCollectionId,
           queries: [
             Query.equal("userID", [uid]),
           ],
         )
         .then((value) => value.documents);
-    for (var subscription in subscribedUpcomingRooms) {
-      List<dynamic> registrationTokens =
-          subscription.data['registrationTokens'];
-      registrationTokens.add(fcmToken!);
-      databases.updateDocument(
+    final futures = <Future<Document>>[];
+    for (final subscription in subscribedUpcomingRooms) {
+      final tokens = List<String>.from(subscription.data['registrationTokens'] ?? const <String>[]);
+      final token = fcmToken;
+      if (token != null && !tokens.contains(token)) {
+        tokens.add(token);
+      }
+      futures.add(databases.updateDocument(
         databaseId: upcomingRoomsDatabaseId,
         collectionId: subscribedUserCollectionId,
         documentId: subscription.$id,
-        data: {"registrationTokens": registrationTokens},
-      );
+        data: {"registrationTokens": tokens},
+      ));
     }
+    await Future.wait(futures);
📝 Committable suggestion

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

Suggested change
List<Document> subscribedUpcomingRooms = await databases
.listDocuments(
databaseId: upcomingRoomsDatabaseId,
collectionId: subscribedUserCollectionId,
queries: [
Query.equal("userID", [uid]),
],
)
.then((value) => value.documents);
for (var subscription in subscribedUpcomingRooms) {
List<dynamic> registrationTokens =
subscription.data['registrationTokens'];
registrationTokens.add(fcmToken!);
databases.updateDocument(
databaseId: upcomingRoomsDatabaseId,
collectionId: subscribedUserCollectionId,
documentId: subscription.$id,
data: {"registrationTokens": registrationTokens});
databaseId: upcomingRoomsDatabaseId,
collectionId: subscribedUserCollectionId,
documentId: subscription.$id,
data: {"registrationTokens": registrationTokens},
);
}
final List<Document> subscribedUpcomingRooms = await databases
.listDocuments(
databaseId: upcomingRoomsDatabaseId,
collectionId: subscribedUserCollectionId,
queries: [
Query.equal("userID", [uid]),
],
)
.then((value) => value.documents);
final futures = <Future<Document>>[];
for (final subscription in subscribedUpcomingRooms) {
final tokens = List<String>.from(subscription.data['registrationTokens'] ?? const <String>[]);
final token = fcmToken;
if (token != null && !tokens.contains(token)) {
tokens.add(token);
}
futures.add(databases.updateDocument(
databaseId: upcomingRoomsDatabaseId,
collectionId: subscribedUserCollectionId,
documentId: subscription.$id,
data: {"registrationTokens": tokens},
));
}
await Future.wait(futures);
🤖 Prompt for AI Agents
In lib/controllers/auth_state_controller.dart around lines 269 to 288, the code
unconditionally adds fcmToken to subscription.data['registrationTokens'] which
can create duplicates and calls databases.updateDocument without awaiting it;
fix by normalizing/initializing registrationTokens as a mutable List<String> (or
Set) and only add fcmToken if it's non-null and not already present, then await
the databases.updateDocument call and handle/propagate possible errors (e.g.,
try/catch or return the Future) so update failures are not swallowed.

Comment on lines 301 to 309
List<dynamic> creatorFcmTokens = upcomingRoom.data['creator_fcm_tokens'];
creatorFcmTokens.add(fcmToken!);
databases.updateDocument(
databaseId: upcomingRoomsDatabaseId,
collectionId: upcomingRoomsCollectionId,
documentId: upcomingRoom.$id,
data: {"creator_fcm_tokens": creatorFcmTokens});
databaseId: upcomingRoomsDatabaseId,
collectionId: upcomingRoomsCollectionId,
documentId: upcomingRoom.$id,
data: {"creator_fcm_tokens": creatorFcmTokens},
);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Same as above for creator tokens: unique and awaited.

-    for (var upcomingRoom in createdUpcomingRooms) {
-      List<dynamic> creatorFcmTokens = upcomingRoom.data['creator_fcm_tokens'];
-      creatorFcmTokens.add(fcmToken!);
-      databases.updateDocument(
+    final creatorFutures = <Future<Document>>[];
+    for (final upcomingRoom in createdUpcomingRooms) {
+      final tokens = List<String>.from(upcomingRoom.data['creator_fcm_tokens'] ?? const <String>[]);
+      final token = fcmToken;
+      if (token != null && !tokens.contains(token)) {
+        tokens.add(token);
+      }
+      creatorFutures.add(databases.updateDocument(
         databaseId: upcomingRoomsDatabaseId,
         collectionId: upcomingRoomsCollectionId,
         documentId: upcomingRoom.$id,
-        data: {"creator_fcm_tokens": creatorFcmTokens},
-      );
+        data: {"creator_fcm_tokens": tokens},
+      ));
     }
+    await Future.wait(creatorFutures);
📝 Committable suggestion

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

Suggested change
List<dynamic> creatorFcmTokens = upcomingRoom.data['creator_fcm_tokens'];
creatorFcmTokens.add(fcmToken!);
databases.updateDocument(
databaseId: upcomingRoomsDatabaseId,
collectionId: upcomingRoomsCollectionId,
documentId: upcomingRoom.$id,
data: {"creator_fcm_tokens": creatorFcmTokens});
databaseId: upcomingRoomsDatabaseId,
collectionId: upcomingRoomsCollectionId,
documentId: upcomingRoom.$id,
data: {"creator_fcm_tokens": creatorFcmTokens},
);
}
// Batch up all creator-token updates so we can await them together
final creatorFutures = <Future<Document>>[];
for (final upcomingRoom in createdUpcomingRooms) {
// Safely copy existing tokens (or start with empty list) and ensure they’re Strings
final tokens = List<String>.from(
upcomingRoom.data['creator_fcm_tokens'] ?? const <String>[],
);
final token = fcmToken;
// Only add if non-null and not already present
if (token != null && !tokens.contains(token)) {
tokens.add(token);
}
// Queue the update call
creatorFutures.add(databases.updateDocument(
databaseId: upcomingRoomsDatabaseId,
collectionId: upcomingRoomsCollectionId,
documentId: upcomingRoom.$id,
data: {"creator_fcm_tokens": tokens},
));
}
// Await all updates to complete
await Future.wait(creatorFutures);
🤖 Prompt for AI Agents
In lib/controllers/auth_state_controller.dart around lines 301 to 309, the code
appends the creator FCM token without deduplicating and calls updateDocument
without awaiting it; updateDocument should be awaited and the token list should
be made unique before saving. Retrieve creatorFcmTokens safely (handle null),
add the fcmToken only if non-null, dedupe the list (e.g., by converting to a Set
and back to List or checking contains before adding), then await
databases.updateDocument(...) with the updated unique list in the
"creator_fcm_tokens" field.

Comment on lines +21 to +23
TextEditingController confirmPasswordController = TextEditingController(
text: "",
);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Dispose TextEditingControllers to prevent leaks.

Add onClose and dispose controllers.

 class AuthenticationController extends GetxController {
@@
   TextEditingController passwordController = TextEditingController(text: "");
   TextEditingController confirmPasswordController = TextEditingController(
     text: "",
   );
@@
   late GlobalKey<FormState> registrationFormKey;
+
+  @override
+  void onClose() {
+    emailController.dispose();
+    passwordController.dispose();
+    confirmPasswordController.dispose();
+    super.onClose();
+  }
📝 Committable suggestion

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

Suggested change
TextEditingController confirmPasswordController = TextEditingController(
text: "",
);
class AuthenticationController extends GetxController {
TextEditingController emailController = TextEditingController(text: "");
TextEditingController passwordController = TextEditingController(text: "");
TextEditingController confirmPasswordController = TextEditingController(
text: "",
);
late GlobalKey<FormState> registrationFormKey;
@override
void onClose() {
emailController.dispose();
passwordController.dispose();
confirmPasswordController.dispose();
super.onClose();
}
// ...other methods and logic
}
🤖 Prompt for AI Agents
In lib/controllers/authentication_controller.dart around lines 21-23, the
TextEditingController(s) (e.g., confirmPasswordController) are created but not
disposed; override the controller's onClose method and call dispose() on each
TextEditingController field to prevent leaks (e.g.,
confirmPasswordController.dispose()), then call super.onClose(); ensure every
TextEditingController in this file is disposed there.

Comment on lines +49 to +61
.friendCallModel
.value!
.callerProfileImageUrl,
controller.friendCallModel.value!.callerName,
),
SizedBox(height: UiSizes.height_20),
_buildUserInfoRow(
controller
.friendCallModel
.value!
.recieverProfileImageUrl,
controller.friendCallModel.value!.recieverName,
),
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Null-safety: guard friendCallModel before dereferencing.

Multiple value! usages can crash if model hasn’t loaded. Render a loader/fallback until ready.

-                    Column(
-                      mainAxisAlignment: MainAxisAlignment.center,
-                      children: [
-                        _buildUserInfoRow(
-                          controller
-                              .friendCallModel
-                              .value!
-                              .callerProfileImageUrl,
-                          controller.friendCallModel.value!.callerName,
-                        ),
-                        SizedBox(height: UiSizes.height_20),
-                        _buildUserInfoRow(
-                          controller
-                              .friendCallModel
-                              .value!
-                              .recieverProfileImageUrl,
-                          controller.friendCallModel.value!.recieverName,
-                        ),
-                      ],
-                    ),
+                    Obx(() {
+                      final call = controller.friendCallModel.value;
+                      if (call == null) {
+                        return const Center(child: CircularProgressIndicator());
+                      }
+                      return Column(
+                        mainAxisAlignment: MainAxisAlignment.center,
+                        children: [
+                          _buildUserInfoRow(
+                            call.callerProfileImageUrl,
+                            call.callerName,
+                          ),
+                          SizedBox(height: UiSizes.height_20),
+                          _buildUserInfoRow(
+                            call.recieverProfileImageUrl,
+                            call.recieverName,
+                          ),
+                        ],
+                      );
+                    }),
📝 Committable suggestion

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

Suggested change
.friendCallModel
.value!
.callerProfileImageUrl,
controller.friendCallModel.value!.callerName,
),
SizedBox(height: UiSizes.height_20),
_buildUserInfoRow(
controller
.friendCallModel
.value!
.recieverProfileImageUrl,
controller.friendCallModel.value!.recieverName,
),
Obx(() {
final call = controller.friendCallModel.value;
if (call == null) {
return const Center(child: CircularProgressIndicator());
}
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildUserInfoRow(
call.callerProfileImageUrl,
call.callerName,
),
SizedBox(height: UiSizes.height_20),
_buildUserInfoRow(
call.recieverProfileImageUrl,
call.recieverName,
),
],
);
}),
🤖 Prompt for AI Agents
In lib/controllers/friend_call_screen.dart around lines 49 to 61, the code
dereferences controller.friendCallModel.value! multiple times which can crash if
the model hasn't loaded; update the widget to first check whether
controller.friendCallModel.value (or friendCallModel itself) is null and render
a loader/fallback (e.g., CircularProgressIndicator or placeholder widgets) while
null, otherwise render the existing _buildUserInfoRow calls using the non-null
value; implement this check as an early conditional (or conditional operator)
around these lines so no value! dereferences occur until the model is confirmed
non-null.

Comment on lines +149 to +156
return GetBuilder<SingleRoomController>(
init: SingleRoomController(appwriteRoom: widget.room),
builder: (controller) => ParticipantBlock(
participant: controller.participants[index].value,
controller: controller,
),
);
},
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Do not re-instantiate SingleRoomController inside each item builder.

GetBuilder(init: …) creates extra controllers, causes state duplication and leaks. Reuse the instance from initState.

-                  return GetBuilder<SingleRoomController>(
-                    init: SingleRoomController(appwriteRoom: widget.room),
+                  return GetBuilder<SingleRoomController>(
                     builder: (controller) => ParticipantBlock(
📝 Committable suggestion

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

Suggested change
return GetBuilder<SingleRoomController>(
init: SingleRoomController(appwriteRoom: widget.room),
builder: (controller) => ParticipantBlock(
participant: controller.participants[index].value,
controller: controller,
),
);
},
return GetBuilder<SingleRoomController>(
builder: (controller) => ParticipantBlock(
participant: controller.participants[index].value,
controller: controller,
),
);
🤖 Prompt for AI Agents
In lib/views/screens/room_screen.dart around lines 149 to 156, you're
re-instantiating SingleRoomController inside each item builder via
GetBuilder(init: …), which creates duplicate controllers and leaks state;
instead, create and store a single SingleRoomController instance in the State
(e.g., in initState use final controller =
Get.put(SingleRoomController(appwriteRoom: widget.room)) or assign
Get.find<SingleRoomController>() to a field), then remove the init: parameter
from the GetBuilder in the item builder and reference the stored controller (or
let GetBuilder find the existing instance). Ensure the item builder uses the
single controller instance (controller.participants[index].value) and do not
call new controller construction there.

Comment on lines +190 to +217
init: SingleRoomController(appwriteRoom: widget.room),
builder: (controller) {
return ElevatedButton.icon(
onPressed: () async {
await _deleteRoomDialog(
controller.appwriteRoom.isUserAdmin
? AppLocalizations.of(context)!.delete
: AppLocalizations.of(context)!.leave,
() async {
if (controller.appwriteRoom.isUserAdmin) {
await controller.deleteRoom();
} else {
await controller.leaveRoom();
}
},
);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color.fromARGB(255, 241, 108, 98),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
icon: const Icon(Icons.exit_to_app),
label: Text(AppLocalizations.of(context)!.leaveButton),
);
});
),
icon: const Icon(Icons.exit_to_app),
label: Text(AppLocalizations.of(context)!.leaveButton),
);
},
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Avoid creating a new controller in every GetBuilder (Leave button).

Same issue as above; remove init: … and reuse the provided instance.

-    return GetBuilder<SingleRoomController>(
-      init: SingleRoomController(appwriteRoom: widget.room),
+    return GetBuilder<SingleRoomController>(
       builder: (controller) {
📝 Committable suggestion

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

Suggested change
init: SingleRoomController(appwriteRoom: widget.room),
builder: (controller) {
return ElevatedButton.icon(
onPressed: () async {
await _deleteRoomDialog(
controller.appwriteRoom.isUserAdmin
? AppLocalizations.of(context)!.delete
: AppLocalizations.of(context)!.leave,
() async {
if (controller.appwriteRoom.isUserAdmin) {
await controller.deleteRoom();
} else {
await controller.leaveRoom();
}
},
);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color.fromARGB(255, 241, 108, 98),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
icon: const Icon(Icons.exit_to_app),
label: Text(AppLocalizations.of(context)!.leaveButton),
);
});
),
icon: const Icon(Icons.exit_to_app),
label: Text(AppLocalizations.of(context)!.leaveButton),
);
},
);
return GetBuilder<SingleRoomController>(
builder: (controller) {
return ElevatedButton.icon(
onPressed: () async {
await _deleteRoomDialog(
controller.appwriteRoom.isUserAdmin
? AppLocalizations.of(context)!.delete
: AppLocalizations.of(context)!.leave,
() async {
if (controller.appwriteRoom.isUserAdmin) {
await controller.deleteRoom();
} else {
await controller.leaveRoom();
}
},
);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color.fromARGB(255, 241, 108, 98),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
),
icon: const Icon(Icons.exit_to_app),
label: Text(AppLocalizations.of(context)!.leaveButton),
);
},
);
🤖 Prompt for AI Agents
In lib/views/screens/room_screen.dart around lines 190 to 217, the GetBuilder
for the Leave button is creating a new SingleRoomController via the init
parameter which instantiates a fresh controller each build; remove the init:
SingleRoomController(...) argument so the GetBuilder reuses the existing
controller instance (obtained from the parent scope or Get.find), and keep the
builder callback unchanged to use the provided controller for delete/leave
actions.

Comment on lines +222 to +252
init: SingleRoomController(appwriteRoom: widget.room),
builder: (controller) {
final bool hasRequestedToBeSpeaker =
controller.me.value.hasRequestedToBeSpeaker;

return FloatingActionButton(
onPressed: () {
if (hasRequestedToBeSpeaker) {
controller.unRaiseHand();
} else {
controller.raiseHand();
}
},
backgroundColor: hasRequestedToBeSpeaker
? Theme.of(context).colorScheme.primary
return FloatingActionButton(
onPressed: () {
if (hasRequestedToBeSpeaker) {
controller.unRaiseHand();
} else {
controller.raiseHand();
}
},
backgroundColor: hasRequestedToBeSpeaker
? Theme.of(context).colorScheme.primary
: Theme.of(context).brightness == Brightness.light
? Colors.white
: Colors.black54,
child: Icon(
hasRequestedToBeSpeaker
? Icons.back_hand
: Icons.back_hand_outlined,
color: hasRequestedToBeSpeaker
? Colors.black
: Theme.of(context).brightness == Brightness.light
? Colors.white
: Colors.black54,
child: Icon(
hasRequestedToBeSpeaker
? Icons.back_hand
: Icons.back_hand_outlined,
color: hasRequestedToBeSpeaker
? Colors.black
: Theme.of(context).brightness == Brightness.light
? Colors.black
: Colors.white54,
),
);
});
? Colors.black
: Colors.white54,
),
);
},
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Avoid re-initializing the controller in Raise Hand button.

Remove init:; also consider icon color contrast on dark primary.

-    return GetBuilder<SingleRoomController>(
-      init: SingleRoomController(appwriteRoom: widget.room),
+    return GetBuilder<SingleRoomController>(
       builder: (controller) {
📝 Committable suggestion

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

Suggested change
init: SingleRoomController(appwriteRoom: widget.room),
builder: (controller) {
final bool hasRequestedToBeSpeaker =
controller.me.value.hasRequestedToBeSpeaker;
return FloatingActionButton(
onPressed: () {
if (hasRequestedToBeSpeaker) {
controller.unRaiseHand();
} else {
controller.raiseHand();
}
},
backgroundColor: hasRequestedToBeSpeaker
? Theme.of(context).colorScheme.primary
return FloatingActionButton(
onPressed: () {
if (hasRequestedToBeSpeaker) {
controller.unRaiseHand();
} else {
controller.raiseHand();
}
},
backgroundColor: hasRequestedToBeSpeaker
? Theme.of(context).colorScheme.primary
: Theme.of(context).brightness == Brightness.light
? Colors.white
: Colors.black54,
child: Icon(
hasRequestedToBeSpeaker
? Icons.back_hand
: Icons.back_hand_outlined,
color: hasRequestedToBeSpeaker
? Colors.black
: Theme.of(context).brightness == Brightness.light
? Colors.white
: Colors.black54,
child: Icon(
hasRequestedToBeSpeaker
? Icons.back_hand
: Icons.back_hand_outlined,
color: hasRequestedToBeSpeaker
? Colors.black
: Theme.of(context).brightness == Brightness.light
? Colors.black
: Colors.white54,
),
);
});
? Colors.black
: Colors.white54,
),
);
},
);
return GetBuilder<SingleRoomController>(
builder: (controller) {
final bool hasRequestedToBeSpeaker =
controller.me.value.hasRequestedToBeSpeaker;
return FloatingActionButton(
onPressed: () {
if (hasRequestedToBeSpeaker) {
controller.unRaiseHand();
} else {
controller.raiseHand();
}
},
backgroundColor: hasRequestedToBeSpeaker
? Theme.of(context).colorScheme.primary
: Theme.of(context).brightness == Brightness.light
? Colors.white
: Colors.black54,
child: Icon(
hasRequestedToBeSpeaker
? Icons.back_hand
: Icons.back_hand_outlined,
color: hasRequestedToBeSpeaker
? Colors.black
: Theme.of(context).brightness == Brightness.light
? Colors.black
: Colors.white54,
),
);
},
);

Comment on lines 256 to +276
return GetBuilder<SingleRoomController>(
init: SingleRoomController(appwriteRoom: widget.room),
builder: (controller) {
final bool isMicOn = controller.me.value.isMicOn;
final bool isSpeaker = controller.me.value.isSpeaker;
init: SingleRoomController(appwriteRoom: widget.room),
builder: (controller) {
final bool isMicOn = controller.me.value.isMicOn;
final bool isSpeaker = controller.me.value.isSpeaker;

return FloatingActionButton(
onPressed: () {
if (isSpeaker) {
if (isMicOn) {
controller.turnOffMic();
} else {
controller.turnOnMic();
}
return FloatingActionButton(
onPressed: () {
if (isSpeaker) {
if (isMicOn) {
controller.turnOffMic();
} else {
controller.turnOnMic();
}
},
backgroundColor: isMicOn ? Colors.lightGreen : Colors.redAccent,
child: Icon(
isMicOn ? Icons.mic : Icons.mic_off,
color: Colors.black,
),
);
});
}
},
backgroundColor: isMicOn ? Colors.lightGreen : Colors.redAccent,
child: Icon(isMicOn ? Icons.mic : Icons.mic_off, color: Colors.black),
);
},
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Avoid re-initializing the controller in Mic button.

Same fix as above.

-    return GetBuilder<SingleRoomController>(
-      init: SingleRoomController(appwriteRoom: widget.room),
+    return GetBuilder<SingleRoomController>(
       builder: (controller) {
📝 Committable suggestion

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

Suggested change
return GetBuilder<SingleRoomController>(
init: SingleRoomController(appwriteRoom: widget.room),
builder: (controller) {
final bool isMicOn = controller.me.value.isMicOn;
final bool isSpeaker = controller.me.value.isSpeaker;
init: SingleRoomController(appwriteRoom: widget.room),
builder: (controller) {
final bool isMicOn = controller.me.value.isMicOn;
final bool isSpeaker = controller.me.value.isSpeaker;
return FloatingActionButton(
onPressed: () {
if (isSpeaker) {
if (isMicOn) {
controller.turnOffMic();
} else {
controller.turnOnMic();
}
return FloatingActionButton(
onPressed: () {
if (isSpeaker) {
if (isMicOn) {
controller.turnOffMic();
} else {
controller.turnOnMic();
}
},
backgroundColor: isMicOn ? Colors.lightGreen : Colors.redAccent,
child: Icon(
isMicOn ? Icons.mic : Icons.mic_off,
color: Colors.black,
),
);
});
}
},
backgroundColor: isMicOn ? Colors.lightGreen : Colors.redAccent,
child: Icon(isMicOn ? Icons.mic : Icons.mic_off, color: Colors.black),
);
},
);
return GetBuilder<SingleRoomController>(
builder: (controller) {
final bool isMicOn = controller.me.value.isMicOn;
final bool isSpeaker = controller.me.value.isSpeaker;
return FloatingActionButton(
onPressed: () {
if (isSpeaker) {
if (isMicOn) {
controller.turnOffMic();
} else {
controller.turnOnMic();
}
}
},
backgroundColor: isMicOn ? Colors.lightGreen : Colors.redAccent,
child: Icon(isMicOn ? Icons.mic : Icons.mic_off, color: Colors.black),
);
},
);
🤖 Prompt for AI Agents
In lib/views/screens/room_screen.dart around lines 256 to 276, the GetBuilder is
re-initializing SingleRoomController via the init parameter which overrides the
existing controller; remove the init: SingleRoomController(appwriteRoom:
widget.room) argument so the GetBuilder uses the already created controller (or
ensure the controller is created once at a higher level), leaving the builder
as-is to access the provided controller; if you must construct the controller
here instead, use Get.put(...) at a single initialization point rather than
passing init into every GetBuilder.

Comment on lines 280 to +291
return GetBuilder<SingleRoomController>(
init: SingleRoomController(appwriteRoom: widget.room),
builder: (controller) {
return FloatingActionButton(
onPressed: () {
controller.openChatSheet();
},
backgroundColor: Colors.redAccent,
child: Icon(
Icons.chat,
color: Colors.black,
),
);
});
init: SingleRoomController(appwriteRoom: widget.room),
builder: (controller) {
return FloatingActionButton(
onPressed: () {
controller.openChatSheet();
},
backgroundColor: Colors.redAccent,
child: Icon(Icons.chat, color: Colors.black),
);
},
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Avoid re-initializing the controller in Chat button.

Same fix as above.

-    return GetBuilder<SingleRoomController>(
-      init: SingleRoomController(appwriteRoom: widget.room),
+    return GetBuilder<SingleRoomController>(
       builder: (controller) {
📝 Committable suggestion

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

Suggested change
return GetBuilder<SingleRoomController>(
init: SingleRoomController(appwriteRoom: widget.room),
builder: (controller) {
return FloatingActionButton(
onPressed: () {
controller.openChatSheet();
},
backgroundColor: Colors.redAccent,
child: Icon(
Icons.chat,
color: Colors.black,
),
);
});
init: SingleRoomController(appwriteRoom: widget.room),
builder: (controller) {
return FloatingActionButton(
onPressed: () {
controller.openChatSheet();
},
backgroundColor: Colors.redAccent,
child: Icon(Icons.chat, color: Colors.black),
);
},
);
return GetBuilder<SingleRoomController>(
builder: (controller) {
return FloatingActionButton(
onPressed: () {
controller.openChatSheet();
},
backgroundColor: Colors.redAccent,
child: Icon(Icons.chat, color: Colors.black),
);
},
);
🤖 Prompt for AI Agents
In lib/views/screens/room_screen.dart around lines 280 to 291, the GetBuilder is
re-initializing SingleRoomController for the chat FAB which causes a new
controller instance to be created instead of reusing the existing one; remove
the init parameter and reference the existing controller (e.g., rely on
Get.find<SingleRoomController>() or ensure the controller is provided by a
parent binding) so the FAB uses the same controller instance and calls
controller.openChatSheet() on that existing controller.

Copy link

@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.

Review continued from previous batch...

Comment on lines +64 to +93
await databases.createDocument(
databaseId: masterDatabaseId,
collectionId: friendCallsCollectionId,
documentId: callModel.docId,
data: callModel.toJson(),
);

final notificationData = {
"recieverFCMToken": recieverFCMToken,
"data": {
"caller_name": callerName,
"caller_username": callerUsername,
"caller_profile_image_url": callerProfileImageUrl,
"caller_uid": callerUid,

"call_id": callModel.docId,
"type": "incoming_call",
"extra": jsonEncode(callModel.toJson()),
"livekit_room_id": callModel.livekitRoomId,
},
};
await functions.createExecution(
functionId: startFriendCallFunctionID,
body: jsonEncode(notificationData),
);
friendCallModel.value = callModel;
listenToCallChanges();

Get.toNamed(AppRoutes.ringingScreen);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Wrap startCall in try/catch and clean up on failure.

Prevents dangling “waiting” docs if the function invocation fails; surfaces UX feedback.

-    await databases.createDocument(
-      databaseId: masterDatabaseId,
-      collectionId: friendCallsCollectionId,
-      documentId: callModel.docId,
-      data: callModel.toJson(),
-    );
-    ...
-    await functions.createExecution(
-      functionId: startFriendCallFunctionID,
-      body: jsonEncode(notificationData),
-    );
-    friendCallModel.value = callModel;
-    listenToCallChanges();
-    Get.toNamed(AppRoutes.ringingScreen);
+    try {
+      await databases.createDocument(
+        databaseId: masterDatabaseId,
+        collectionId: friendCallsCollectionId,
+        documentId: callModel.docId,
+        data: callModel.toJson(),
+      );
+      await functions.createExecution(
+        functionId: startFriendCallFunctionID,
+        body: jsonEncode(notificationData),
+      );
+      friendCallModel.value = callModel;
+      listenToCallChanges();
+      Get.toNamed(AppRoutes.ringingScreen);
+    } catch (e, st) {
+      log('startCall failed: $e', stackTrace: st);
+      // best-effort cleanup
+      try {
+        await databases.deleteDocument(
+          databaseId: masterDatabaseId,
+          collectionId: friendCallsCollectionId,
+          documentId: callModel.docId,
+        );
+      } catch (_) {}
+      if (!Get.testMode) {
+        customSnackbar('Call', 'Failed to start the call', LogType.error);
+      }
+    }
📝 Committable suggestion

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

Suggested change
await databases.createDocument(
databaseId: masterDatabaseId,
collectionId: friendCallsCollectionId,
documentId: callModel.docId,
data: callModel.toJson(),
);
final notificationData = {
"recieverFCMToken": recieverFCMToken,
"data": {
"caller_name": callerName,
"caller_username": callerUsername,
"caller_profile_image_url": callerProfileImageUrl,
"caller_uid": callerUid,
"call_id": callModel.docId,
"type": "incoming_call",
"extra": jsonEncode(callModel.toJson()),
"livekit_room_id": callModel.livekitRoomId,
},
};
await functions.createExecution(
functionId: startFriendCallFunctionID,
body: jsonEncode(notificationData),
);
friendCallModel.value = callModel;
listenToCallChanges();
Get.toNamed(AppRoutes.ringingScreen);
}
try {
await databases.createDocument(
databaseId: masterDatabaseId,
collectionId: friendCallsCollectionId,
documentId: callModel.docId,
data: callModel.toJson(),
);
final notificationData = {
"recieverFCMToken": recieverFCMToken,
"data": {
"caller_name": callerName,
"caller_username": callerUsername,
"caller_profile_image_url": callerProfileImageUrl,
"caller_uid": callerUid,
"call_id": callModel.docId,
"type": "incoming_call",
"extra": jsonEncode(callModel.toJson()),
"livekit_room_id": callModel.livekitRoomId,
},
};
await functions.createExecution(
functionId: startFriendCallFunctionID,
body: jsonEncode(notificationData),
);
friendCallModel.value = callModel;
listenToCallChanges();
Get.toNamed(AppRoutes.ringingScreen);
} catch (e, st) {
log('startCall failed: $e', stackTrace: st);
// best-effort cleanup
try {
await databases.deleteDocument(
databaseId: masterDatabaseId,
collectionId: friendCallsCollectionId,
documentId: callModel.docId,
);
} catch (_) {}
if (!Get.testMode) {
customSnackbar('Call', 'Failed to start the call', LogType.error);
}
}

Comment on lines +101 to +121
static Future<void> onCallRecieved(RemoteMessage message) async {
log(message.data['extra']);
final params = CallKitParams(
id: message.data['call_id'],
nameCaller: message.data['caller_name'],
avatar: message.data['caller_profile_image_url'],
handle: message.data['caller_username'],
type: 0, // 0 = audio, 1 = video
duration: 30000, // ringing timeout
extra: {
"docData": jsonDecode(message.data['extra']),
"livekit_room_id": message.data['livekit_room_id'],
"call_id": message.data['call_id'],
},
appName: "Resonate",
android: AndroidParams(isShowFullLockedScreen: true),
);
if (!Get.testMode) {
await FlutterCallkitIncoming.showCallkitIncoming(params);
}
}
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Remove PII-heavy log and confirm CallKit “duration” units.

Logging the entire extra payload can leak PII to logs. Also verify whether duration expects ms or seconds.

-    log(message.data['extra']);
+    // Avoid logging full payloads with PII in production.

🌐 Web query:

In flutter_callkit_incoming, what units does CallKitParams.duration use on Android and iOS (milliseconds or seconds)?

💡 Result:

Short answer: milliseconds on both platforms — the docs say "duration (second)" but the example/default uses 30000 (i.e. 30,000 ms), so pass milliseconds. [1][2]

Sources:

  • README showing duration field described as "(second)" and example with duration: 30000. [1]
  • Pub package example also using duration: 30000. [2]

[1] https://github.com/hiennguyen92/flutter_callkit_incoming
[2] https://pub.dev/packages/flutter_callkit_incoming


Remove PII logging and confirm duration units
Replace the log(message.data['extra']); call to avoid leaking PII in production. CallKitParams.duration uses milliseconds on both Android and iOS, so duration: 30000 is already correct.

-    log(message.data['extra']);
+    // Avoid logging full payloads with PII in production.
📝 Committable suggestion

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

Suggested change
static Future<void> onCallRecieved(RemoteMessage message) async {
log(message.data['extra']);
final params = CallKitParams(
id: message.data['call_id'],
nameCaller: message.data['caller_name'],
avatar: message.data['caller_profile_image_url'],
handle: message.data['caller_username'],
type: 0, // 0 = audio, 1 = video
duration: 30000, // ringing timeout
extra: {
"docData": jsonDecode(message.data['extra']),
"livekit_room_id": message.data['livekit_room_id'],
"call_id": message.data['call_id'],
},
appName: "Resonate",
android: AndroidParams(isShowFullLockedScreen: true),
);
if (!Get.testMode) {
await FlutterCallkitIncoming.showCallkitIncoming(params);
}
}
static Future<void> onCallRecieved(RemoteMessage message) async {
// Avoid logging full payloads with PII in production.
final params = CallKitParams(
id: message.data['call_id'],
nameCaller: message.data['caller_name'],
avatar: message.data['caller_profile_image_url'],
handle: message.data['caller_username'],
type: 0, // 0 = audio, 1 = video
duration: 30000, // ringing timeout
extra: {
"docData": jsonDecode(message.data['extra']),
"livekit_room_id": message.data['livekit_room_id'],
"call_id": message.data['call_id'],
},
appName: "Resonate",
android: AndroidParams(isShowFullLockedScreen: true),
);
if (!Get.testMode) {
await FlutterCallkitIncoming.showCallkitIncoming(params);
}
}
🤖 Prompt for AI Agents
In lib/controllers/friend_calling_controller.dart around lines 101 to 121,
remove the direct call to log(message.data['extra']) to avoid leaking PII;
instead either delete the line or wrap any logging in a non-production guard
(e.g., if (Get.testMode || kDebugMode) { log(...sanitizedData...); }) and ensure
only non-identifying, sanitized fields are logged; do not change the
CallKitParams.duration value because it is already in milliseconds (30000) and
is correct for both Android and iOS.

Comment on lines +195 to +243
void listenToCallChanges() async {
String channel =
'databases.$masterDatabaseId.collections.$friendCallsCollectionId.documents.${friendCallModel.value!.docId}';
callSubscription = realtime.subscribe([channel]);
callSubscription?.stream.listen((data) async {
if (data.payload.isNotEmpty) {
if (data.events.first.endsWith('.update')) {
if (data.payload['callStatus'] == FriendCallStatus.connected.name) {
friendCallModel.value = friendCallModel.value!.copyWith(
callStatus: FriendCallStatus.connected,
);
if (!Get.testMode) {
await joinCall(
friendCallModel.value!.livekitRoomId,
friendCallModel.value!.callerUid,
);
}
}
if (data.payload['callStatus'] == FriendCallStatus.ended.name) {
if (!Get.testMode) {
FlutterCallkitIncoming.endAllCalls();
}
friendCallModel.value = friendCallModel.value!.copyWith(
callStatus: FriendCallStatus.ended,
);
await Get.delete<LiveKitController>(force: true);
Get.offNamedUntil(AppRoutes.tabview, (route) => false);
}
if (data.payload['callStatus'] == FriendCallStatus.declined.name) {
await Get.delete<LiveKitController>(force: true);
if (!Get.testMode) {
FlutterCallkitIncoming.endAllCalls();
}
friendCallModel.value = friendCallModel.value!.copyWith(
callStatus: FriendCallStatus.declined,
);
customSnackbar(
AppLocalizations.of(Get.context!)!.callDeclined,
AppLocalizations.of(
Get.context!,
)!.callDeclinedTo(friendCallModel.value!.recieverName),
LogType.info,
);
Get.offNamedUntil(AppRoutes.tabview, (route) => false);
}
}
}
});
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Fix event filtering, prevent double-join, and unsubscribe on terminal states.

  • Use events.any(...) instead of first.
  • Only the caller should auto-join on “connected”; the receiver joins in onAnswerCall.
  • Close subscription on ended/declined.
   callSubscription = realtime.subscribe([channel]);
   callSubscription?.stream.listen((data) async {
     if (data.payload.isNotEmpty) {
-        if (data.events.first.endsWith('.update')) {
+        if (data.events.any((e) => e.endsWith('.update'))) {
+          final currentUid = Get.find<AuthStateController>().uid;
           if (data.payload['callStatus'] == FriendCallStatus.connected.name) {
             friendCallModel.value = friendCallModel.value!.copyWith(
               callStatus: FriendCallStatus.connected,
             );
-            if (!Get.testMode) {
-              await joinCall(
-                friendCallModel.value!.livekitRoomId,
-                friendCallModel.value!.callerUid,
-              );
-            }
+            // Only caller auto-joins here. Receiver joins via onAnswerCall.
+            if (!Get.testMode &&
+                currentUid == friendCallModel.value!.callerUid) {
+              await joinCall(
+                friendCallModel.value!.livekitRoomId,
+                currentUid,
+              );
+            }
           }
           if (data.payload['callStatus'] == FriendCallStatus.ended.name) {
             if (!Get.testMode) {
-              FlutterCallkitIncoming.endAllCalls();
+              await FlutterCallkitIncoming.endAllCalls();
             }
             friendCallModel.value = friendCallModel.value!.copyWith(
               callStatus: FriendCallStatus.ended,
             );
             await Get.delete<LiveKitController>(force: true);
+            await callSubscription?.close();
+            callSubscription = null;
             Get.offNamedUntil(AppRoutes.tabview, (route) => false);
           }
           if (data.payload['callStatus'] == FriendCallStatus.declined.name) {
             await Get.delete<LiveKitController>(force: true);
             if (!Get.testMode) {
-              FlutterCallkitIncoming.endAllCalls();
+              await FlutterCallkitIncoming.endAllCalls();
             }
             friendCallModel.value = friendCallModel.value!.copyWith(
               callStatus: FriendCallStatus.declined,
             );
             customSnackbar(
               AppLocalizations.of(Get.context!)!.callDeclined,
               AppLocalizations.of(
                 Get.context!,
               )!.callDeclinedTo(friendCallModel.value!.recieverName),
               LogType.info,
             );
+            await callSubscription?.close();
+            callSubscription = null;
             Get.offNamedUntil(AppRoutes.tabview, (route) => false);
           }
         }
       }
     });
📝 Committable suggestion

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

Suggested change
void listenToCallChanges() async {
String channel =
'databases.$masterDatabaseId.collections.$friendCallsCollectionId.documents.${friendCallModel.value!.docId}';
callSubscription = realtime.subscribe([channel]);
callSubscription?.stream.listen((data) async {
if (data.payload.isNotEmpty) {
if (data.events.first.endsWith('.update')) {
if (data.payload['callStatus'] == FriendCallStatus.connected.name) {
friendCallModel.value = friendCallModel.value!.copyWith(
callStatus: FriendCallStatus.connected,
);
if (!Get.testMode) {
await joinCall(
friendCallModel.value!.livekitRoomId,
friendCallModel.value!.callerUid,
);
}
}
if (data.payload['callStatus'] == FriendCallStatus.ended.name) {
if (!Get.testMode) {
FlutterCallkitIncoming.endAllCalls();
}
friendCallModel.value = friendCallModel.value!.copyWith(
callStatus: FriendCallStatus.ended,
);
await Get.delete<LiveKitController>(force: true);
Get.offNamedUntil(AppRoutes.tabview, (route) => false);
}
if (data.payload['callStatus'] == FriendCallStatus.declined.name) {
await Get.delete<LiveKitController>(force: true);
if (!Get.testMode) {
FlutterCallkitIncoming.endAllCalls();
}
friendCallModel.value = friendCallModel.value!.copyWith(
callStatus: FriendCallStatus.declined,
);
customSnackbar(
AppLocalizations.of(Get.context!)!.callDeclined,
AppLocalizations.of(
Get.context!,
)!.callDeclinedTo(friendCallModel.value!.recieverName),
LogType.info,
);
Get.offNamedUntil(AppRoutes.tabview, (route) => false);
}
}
}
});
}
void listenToCallChanges() async {
String channel =
'databases.$masterDatabaseId.collections.$friendCallsCollectionId.documents.${friendCallModel.value!.docId}';
callSubscription = realtime.subscribe([channel]);
callSubscription?.stream.listen((data) async {
if (data.payload.isNotEmpty) {
if (data.events.any((e) => e.endsWith('.update'))) {
final currentUid = Get.find<AuthStateController>().uid;
if (data.payload['callStatus'] == FriendCallStatus.connected.name) {
friendCallModel.value = friendCallModel.value!.copyWith(
callStatus: FriendCallStatus.connected,
);
// Only caller auto-joins here. Receiver joins via onAnswerCall.
if (!Get.testMode &&
currentUid == friendCallModel.value!.callerUid) {
await joinCall(
friendCallModel.value!.livekitRoomId,
currentUid,
);
}
}
if (data.payload['callStatus'] == FriendCallStatus.ended.name) {
if (!Get.testMode) {
await FlutterCallkitIncoming.endAllCalls();
}
friendCallModel.value = friendCallModel.value!.copyWith(
callStatus: FriendCallStatus.ended,
);
await Get.delete<LiveKitController>(force: true);
await callSubscription?.close();
callSubscription = null;
Get.offNamedUntil(AppRoutes.tabview, (route) => false);
}
if (data.payload['callStatus'] == FriendCallStatus.declined.name) {
await Get.delete<LiveKitController>(force: true);
if (!Get.testMode) {
await FlutterCallkitIncoming.endAllCalls();
}
friendCallModel.value = friendCallModel.value!.copyWith(
callStatus: FriendCallStatus.declined,
);
customSnackbar(
AppLocalizations.of(Get.context!)!.callDeclined,
AppLocalizations.of(
Get.context!,
)!.callDeclinedTo(friendCallModel.value!.recieverName),
LogType.info,
);
await callSubscription?.close();
callSubscription = null;
Get.offNamedUntil(AppRoutes.tabview, (route) => false);
}
}
}
});
}
🤖 Prompt for AI Agents
In lib/controllers/friend_calling_controller.dart around lines 195 to 243,
change the event check to use data.events.any((e) => e.endsWith('.update'))
instead of data.events.first, only trigger automatic join for the caller (i.e.
verify current user's uid equals friendCallModel.value!.callerUid before calling
joinCall to avoid double-join), and when callStatus becomes ended or declined
cancel/close the realtime subscription (callSubscription?.cancel() or
realtime.unsubscribe(...) as appropriate) before performing cleanup and
navigation so the handler is not invoked after terminal states.

Comment on lines +245 to +259
void toggleMic() async {
isMicOn.value = !isMicOn.value;
if (!Get.testMode) {
await Get.find<LiveKitController>().liveKitRoom.localParticipant
?.setMicrophoneEnabled(isMicOn.value);
}
}

void toggleLoudSpeaker() {
isLoudSpeakerOn.value = !isLoudSpeakerOn.value;
if (!Get.testMode) {
Hardware.instance.setSpeakerphoneOn(isLoudSpeakerOn.value);
}
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add onClose to release resources.

Guarantees cleanup even if the controller is disposed mid-call.

   void toggleLoudSpeaker() {
@@
   }
 }
+
+@override
+void onClose() {
+  callSubscription?.close();
+  callSubscription = null;
+  super.onClose();
+}

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

🤖 Prompt for AI Agents
In lib/controllers/friend_calling_controller.dart around lines 245-259, add an
onClose override to release audio resources when the controller is disposed:
implement onClose() that sets isMicOn and isLoudSpeakerOn to false, calls
Get.find<LiveKitController>().liveKitRoom.localParticipant?.setMicrophoneEnabled(false)
when not in test mode, calls Hardware.instance.setSpeakerphoneOn(false) when not
in test mode, disposes or cancels any local subscriptions/controllers related to
the call, and then calls super.onClose(); ensure all calls are safe
(null-checks) and await asynchronous teardown where appropriate.

Comment on lines +18 to 24
@JsonKey(name: "\$id", includeToJson: false) String get docId;@JsonKey(name: "followerUserId") String get uid;@JsonKey(name: "followerUsername") String get username;@JsonKey(name: "followerProfileImageUrl") String get profileImageUrl;@JsonKey(name: "followerName") String get name;@JsonKey(name: 'followerFCMToken') String get fcmToken;@JsonKey(name: "followingUserId") String? get followingUserId;@JsonKey(name: 'followerRating', fromJson: toDouble) double? get followerRating;
/// Create a copy of FollowerUserModel
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$FollowerUserModelCopyWith<FollowerUserModel> get copyWith => _$FollowerUserModelCopyWithImpl<FollowerUserModel>(this as FollowerUserModel, _$identity);

Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Confirm nullable-but-required followerRating intent

The model now treats followerRating as double? but the constructor marks it required. That’s legal (caller must pass it, possibly null) but unusual and easy to misuse.

Consider either:

  • Make it non-nullable with a default:
    • In source (not generated): @JsonKey(name: 'followerRating', fromJson: toDouble) @Default(0.0) double followerRating,
  • Or keep it nullable and drop required so JSON absence is fine:
    • In source: @JsonKey(name: 'followerRating', fromJson: toDouble) double? followerRating,

Check current call sites/JSON usage before changing:

Note: Update the source .dart and regenerate; don’t edit this file.


🏁 Script executed:

#!/bin/bash
# Where followerRating is constructed/used
rg -nP '\bfollowerRating\b' -g 'lib/**/*.dart' -C3

Length of output: 18866


Make followerRating non-nullable with a default
Change in lib/models/follower_user_model.dart:

@JsonKey(name: 'followerRating', fromJson: toDouble)
@Default(0.0)
double followerRating,

– remove ? and the custom required double? so it’s always non-null; then regenerate.

🤖 Prompt for AI Agents
In lib/models/follower_user_model.freezed.dart around lines 18–24 the generated
getter for followerRating is nullable, but the intention is to make it
non-nullable with a default; update the source model file
lib/models/follower_user_model.dart to declare followerRating as non-nullable
with @Default(0.0) and remove the nullable/required double? signature, ensure
the @JsonKey(fromJson: toDouble) remains, then run the code generator (e.g.,
flutter pub run build_runner build --delete-conflicting-outputs) to regenerate
follower_user_model.freezed.dart so the generated getter is non-nullable and
uses the default value; also adjust any constructors/usages that expected a
nullable followerRating.

Comment on lines +201 to 211
Icon(Icons.star, color: Colors.amber),
Padding(
padding: const EdgeInsets.only(left: 5),
child: Text(widget.isCreatorProfile == null
? (authController.ratingTotal /
authController.ratingCount)
.toStringAsFixed(1)
: widget.creator!.userRating!.toStringAsFixed(1)),
child: Text(
widget.isCreatorProfile == null
? (authController.ratingTotal /
authController.ratingCount)
.toStringAsFixed(1)
: widget.creator!.userRating!.toStringAsFixed(1),
),
),
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Guard against divide-by-zero in rating.

If ratingCount is 0 this will crash/NaN.

-                        child: Text(
-                          widget.isCreatorProfile == null
-                              ? (authController.ratingTotal /
-                                        authController.ratingCount)
-                                    .toStringAsFixed(1)
-                              : widget.creator!.userRating!.toStringAsFixed(1),
-                        ),
+                        child: Text(
+                          widget.isCreatorProfile == null
+                              ? ((authController.ratingCount > 0
+                                      ? authController.ratingTotal /
+                                          authController.ratingCount
+                                      : 0.0)
+                                  .toStringAsFixed(1))
+                              : ((widget.creator!.userRating ?? 0.0)
+                                  .toStringAsFixed(1)),
+                        ),
📝 Committable suggestion

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

Suggested change
Icon(Icons.star, color: Colors.amber),
Padding(
padding: const EdgeInsets.only(left: 5),
child: Text(widget.isCreatorProfile == null
? (authController.ratingTotal /
authController.ratingCount)
.toStringAsFixed(1)
: widget.creator!.userRating!.toStringAsFixed(1)),
child: Text(
widget.isCreatorProfile == null
? (authController.ratingTotal /
authController.ratingCount)
.toStringAsFixed(1)
: widget.creator!.userRating!.toStringAsFixed(1),
),
),
Icon(Icons.star, color: Colors.amber),
Padding(
padding: const EdgeInsets.only(left: 5),
child: Text(
widget.isCreatorProfile == null
? ((authController.ratingCount > 0
? authController.ratingTotal /
authController.ratingCount
: 0.0)
.toStringAsFixed(1))
: ((widget.creator!.userRating ?? 0.0)
.toStringAsFixed(1)),
),
),
🤖 Prompt for AI Agents
In lib/views/screens/profile_screen.dart around lines 201 to 211, the expression
(authController.ratingTotal / authController.ratingCount).toStringAsFixed(1) can
divide by zero when ratingCount is 0; instead guard before dividing: if
ratingCount > 0 compute and format the division, otherwise use a safe fallback
(e.g. 0.0 or a placeholder) and still call toStringAsFixed(1) as appropriate;
also similarly null-check widget.creator!.userRating before formatting to avoid
null errors.

@M4dhav M4dhav merged commit 83bb7f6 into AOSSIE-Org:dev Sep 6, 2025
3 checks passed
@github-actions
Copy link
Contributor

github-actions bot commented Sep 6, 2025

PR Closed - Thank You, @M4dhav!

  • If this PR was merged: Congratulations! Your contribution is now part of the project. 🚀
  • If this PR was closed without merging: Don’t worry! You can always improve it and submit again. 💪

We appreciate your effort and look forward to more contributions from you! 🤝

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request gsoc Part of Google Summer of Code season

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement Ability to add and call friends directly

2 participants