Implement Friends and Direct calling functionality#524
Implement Friends and Direct calling functionality#524M4dhav merged 6 commits intoAOSSIE-Org:devfrom
Conversation
|
🎉 Welcome @M4dhav!
We appreciate your contribution! 🚀 |
WalkthroughIntroduces 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
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Possibly related PRs
Poem
✨ Finishing Touches🧪 Generate unit tests
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. Comment |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
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:onDisposeis not a GetX lifecycle hook; subscription won’t be cancelledThis method is never called by GetX, causing a leak. Use
onClose()and callsuper.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
liveKitRoomandlisteneron reconnect. Withlate 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()andcreateListener()occur beforeconnect(). 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 IDWhen 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.delayedratingCount 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 resetPasswordresetPassword 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 BuildContextComplements 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
onCloseto 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 flavorlib/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 missingwithValuesextension onColor
withValuesis invoked across multiple widgets (e.g. settings_screen.dart, ringing_screen.dart, friend_call_screen.dart) but nowithValuesextension or helper is defined—add aColor.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 inputAvoid
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 andindexWherecan 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 overwriteupcomingRoomScrollController.valuewith a newScrollControllerwithout disposing the old one. Call.dispose()on the existing controller before assignment (or overrideonClose()inUpcommingRoomsControllerto 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, + ),
|
|
||
| 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" |
There was a problem hiding this comment.
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.
| 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.
| Future<void> createRoom( | ||
| String name, | ||
| String description, | ||
| List<String> tags, | ||
| bool fromCreateScreen, | ||
| ) async { |
There was a problem hiding this comment.
🛠️ 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.
| Future<void> deleteUserProfilePicture() async { | ||
| try { | ||
| await storage.deleteFile( | ||
| bucketId: userProfileImageBucketId, | ||
| fileId: authStateController.profileImageID!); | ||
| bucketId: userProfileImageBucketId, | ||
| fileId: authStateController.profileImageID!, | ||
| ); | ||
| } catch (e) { |
There was a problem hiding this comment.
🛠️ 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.
| 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.
| Future<void> deleteUsernamesCollectionDocument() async { | ||
| try { | ||
| await databases.deleteDocument( | ||
| databaseId: userDatabaseID, | ||
| collectionId: usernameCollectionID, | ||
| documentId: authStateController.userName!); | ||
| databaseId: userDatabaseID, | ||
| collectionId: usernameCollectionID, | ||
| documentId: authStateController.userName!, | ||
| ); | ||
| } catch (e) { |
There was a problem hiding this comment.
🛠️ 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.
| 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.
| Future<void> deleteUsersCollectionDocument() async { | ||
| try { | ||
| await databases.deleteDocument( | ||
| databaseId: userDatabaseID, | ||
| collectionId: usersCollectionID, | ||
| documentId: authStateController.uid!); | ||
| databaseId: userDatabaseID, | ||
| collectionId: usersCollectionID, | ||
| documentId: authStateController.uid!, | ||
| ); | ||
| } catch (e) { |
There was a problem hiding this comment.
🛠️ 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.
| 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.
| final exploreStoryController = Get.put<ExploreStoryController>( | ||
| ExploreStoryController(), | ||
| ); |
There was a problem hiding this comment.
🛠️ 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.
| final UpcomingRoomsController upcomingRoomsController = Get.put( | ||
| UpcomingRoomsController(), | ||
| ); |
There was a problem hiding this comment.
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.
| itemCount: pairChatController.usersList.length, | ||
| shrinkWrap: true, | ||
| itemBuilder: (context, index) { |
There was a problem hiding this comment.
🛠️ 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.
| 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 |
There was a problem hiding this comment.
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.
| 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.
| | 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" /> | |
There was a problem hiding this comment.
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.
| | 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.
| 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()); | ||
| } | ||
| }); |
There was a problem hiding this comment.
💡 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
_callkitSubas a StreamSubscription<CallEvent?> field and cancel it inonClose()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.
| 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.
| 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}, | ||
| ); | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
| 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.
| 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}, | ||
| ); | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
| 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.
| TextEditingController confirmPasswordController = TextEditingController( | ||
| text: "", | ||
| ); |
There was a problem hiding this comment.
🛠️ 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.
| 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.
| .friendCallModel | ||
| .value! | ||
| .callerProfileImageUrl, | ||
| controller.friendCallModel.value!.callerName, | ||
| ), | ||
| SizedBox(height: UiSizes.height_20), | ||
| _buildUserInfoRow( | ||
| controller | ||
| .friendCallModel | ||
| .value! | ||
| .recieverProfileImageUrl, | ||
| controller.friendCallModel.value!.recieverName, | ||
| ), |
There was a problem hiding this comment.
🛠️ 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.
| .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.
| return GetBuilder<SingleRoomController>( | ||
| init: SingleRoomController(appwriteRoom: widget.room), | ||
| builder: (controller) => ParticipantBlock( | ||
| participant: controller.participants[index].value, | ||
| controller: controller, | ||
| ), | ||
| ); | ||
| }, |
There was a problem hiding this comment.
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.
| 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.
| 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), | ||
| ); | ||
| }, | ||
| ); |
There was a problem hiding this comment.
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.
| 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.
| 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, | ||
| ), | ||
| ); | ||
| }, | ||
| ); |
There was a problem hiding this comment.
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.
| 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, | |
| ), | |
| ); | |
| }, | |
| ); |
| 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), | ||
| ); | ||
| }, | ||
| ); |
There was a problem hiding this comment.
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.
| 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.
| 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), | ||
| ); | ||
| }, | ||
| ); |
There was a problem hiding this comment.
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.
| 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.
| 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); | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
| 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); | |
| } | |
| } |
| 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); | ||
| } | ||
| } |
There was a problem hiding this comment.
💡 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.
| 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.
| 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); | ||
| } | ||
| } | ||
| } | ||
| }); | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
| 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.
| 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); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
| @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); | ||
|
|
There was a problem hiding this comment.
💡 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,
- In source (not generated):
- Or keep it nullable and drop
requiredso JSON absence is fine:- In source:
@JsonKey(name: 'followerRating', fromJson: toDouble) double? followerRating,
- In source:
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' -C3Length 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.
| 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), | ||
| ), | ||
| ), |
There was a problem hiding this comment.
🛠️ 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.
| 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.
|
✅ PR Closed - Thank You, @M4dhav!
We appreciate your effort and look forward to more contributions from you! 🤝 |
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.
How Has This Been Tested?
All Automated tests were run and passed and functionality was manually tested.
Checklist:
Maintainer Checklist
Summary by CodeRabbit