-
Notifications
You must be signed in to change notification settings - Fork 347
Deploy v2.5 of Resonate App #564
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Feat: Added metadata to all keys and removed duplicate translations
fix: Updated the deprecated code and removed unused code
feat: updated the UX issue
- Added complete translation for Gujarati (gu) - Added app_gu.arb and app_localizations_gu.dart - Updated supportedLocales in main.dart - Added Gujarati to iOS CFBundleLocalizations
feat: add Gujarati translation support
Add Live Chapters Feature
|
🎉 Welcome @M4dhav!
We appreciate your contribution! 🚀 |
WalkthroughAdds Live Chapters and reporting/moderation features, Gujarati localization, new models/controllers/widgets/screens, recording & transcription integrations, reportedUsers room filtering, API/RoomService extensions, dependency updates, and multiple i18n key changes. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Creator
participant StoryScreen
participant StartDialog as StartLiveChapterDialog
participant LiveChapterCtrl as LiveChapterController
participant Appwrite
participant RoomService
participant ApiService
participant LiveKit
Creator->>StartDialog: fill title/desc, start
StartDialog->>LiveChapterCtrl: startLiveChapter(...)
LiveChapterCtrl->>Appwrite: create liveChapter doc
LiveChapterCtrl->>RoomService: createLiveChapterRoom(appwriteId, adminUid)
RoomService->>ApiService: createLiveChapterRoom(...)
ApiService-->>RoomService: return token,url
RoomService->>LiveKit: joinLiveKitRoom(url, token, isLiveChapter: true)
LiveChapterCtrl->>Appwrite: create attendees doc
LiveChapterCtrl->>StoryScreen: navigate to LiveChapterScreen
sequenceDiagram
autonumber
actor Listener
participant LiveTile as LiveChapterListTile
participant LiveChapterCtrl as LiveChapterController
participant Appwrite
participant RoomService
participant ApiService
participant LiveKit
Listener->>LiveTile: tap to join
LiveTile->>LiveChapterCtrl: joinLiveChapter(liveChapterId)
LiveChapterCtrl->>Appwrite: update attendees
LiveChapterCtrl->>RoomService: joinLiveChapterRoom(roomId, userId)
RoomService->>ApiService: get token,url
ApiService-->>RoomService: token,url
RoomService->>LiveKit: joinLiveKitRoom(url, token, isLiveChapter: true)
LiveChapterCtrl->>LiveTile: navigate to LiveChapterScreen
sequenceDiagram
autonumber
actor User
participant ParticipantMenu
participant SingleRoomCtrl
participant ReportWidget
participant AuthCtrl
participant Appwrite
User->>ParticipantMenu: select "Report Participant"
ParticipantMenu->>SingleRoomCtrl: reportParticipant(participant)
SingleRoomCtrl->>ReportWidget: open dialog
User->>ReportWidget: select type + submit
ReportWidget->>AuthCtrl: build UserReportModel
ReportWidget->>Appwrite: createDocument(userReportsCollection)
Appwrite-->>ReportWidget: success
ReportWidget-->>SingleRoomCtrl: result true
SingleRoomCtrl->>Appwrite: update room.reportedUsers, kick participant
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120+ minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches🧪 Generate unit tests (beta)
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 21
🧹 Nitpick comments (19)
lib/views/widgets/chapter_player.dart (1)
25-30: LGTM! Consider simplifying the alpha calculation.The conversion from
withOpacitytowithAlphais correct and preserves the intended visual behavior. The alpha values are computed accurately (204 and 76 for progress < 0.75, 255 otherwise).However, the
~/ 1operation is redundant—it performs integer division by 1, which is effectively a floor operation. Consider simplifying:chapter.tintColor.withAlpha( - (progress < 0.75 ? 0.8 : 1) * 255 ~/ 1, + ((progress < 0.75 ? 0.8 : 1) * 255).toInt(), ), chapter.tintColor.withAlpha( - (progress < 0.75 ? 0.3 : 1) * 255 ~/ 1, + ((progress < 0.75 ? 0.3 : 1) * 255).toInt(), ),lib/views/screens/tabview_screen.dart (1)
175-175: Consider structured logging for production error handling.While replacing
logis an improvement for debugging, consider using a structured logging framework (e.g.,loggerpackage) for production environments. This would provide better log levels, formatting, and filtering capabilities.Example with the
loggerpackage:final logger = Logger(); // In catch block: logger.error('Room creation error', error: e, stackTrace: stackTrace);lib/controllers/auth_state_controller.dart (1)
239-242: Consider making the user blocking threshold configurable.The hard-coded threshold of 5 reports for blocking users is a magic number that could benefit from being a named constant or configuration value. This would improve maintainability and allow easier threshold adjustments without code changes.
Consider extracting to a constant:
static const int USER_BLOCK_THRESHOLD = 5; // Later in isUserLoggedIn: if (reportsCount > USER_BLOCK_THRESHOLD) { Get.offNamed(AppRoutes.userBlockedScreen); return; }Additionally, please verify:
- Is a threshold of 5 reports appropriate for your moderation policy?
- Should users receive warnings before being blocked?
- Is there an appeal or review process for blocked users?
lib/controllers/about_app_screen_controller.dart (1)
41-41: Leverage Upgrader caching and surface init-phase errors
checkForUpdatein onInit is asynchronous and runs only once via the singleton—no repeated init/performance impact.- Defaults to
clearSettings = true, which resets Upgrader’s saved state and bypassesdurationUntilAlertAgain; setclearSettings: falsefor init or manual checks to respect the retry interval and reduce network calls.- Init-phase failures are only logged; manual checks show a snackbar on
checkFailed. Add UI feedback for errors during the automatic init check if needed.lib/views/widgets/participant_block.dart (1)
84-89: Report participant action added consistently.The "Report Participant" menu item is correctly added across all role scenarios (admin, moderator) with proper localization and controller integration.
Consider using a distinct icon for the report action (e.g.,
Icons.flagorIcons.report) instead ofIcons.remove_circle_outlineto better differentiate reporting from removal/kick actions. Currently, all menu items share the same icon viamakeMenuItems.Example refinement to
makeMenuItems:List<FocusedMenuItem> makeMenuItems( List<FocusedMenuItemData> items, Brightness currentBrightness, ) { return items .map( (item) => FocusedMenuItem( title: Text( item.textContent, style: TextStyle(fontSize: UiSizes.size_14), ), trailingIcon: Icon( item.textContent.contains('Report') ? Icons.flag : Icons.remove_circle_outline, color: item.textContent.contains('Report') ? Colors.orange : Colors.red, size: UiSizes.size_18, ), onPressed: item.action, backgroundColor: currentBrightness == Brightness.light ? Colors.white : Colors.black, ), ) .toList(); }Or extend
FocusedMenuItemDatato include optional icon/color parameters for better flexibility.Also applies to: 107-112, 133-138
lib/views/screens/user_blocked_screen.dart (1)
12-24: Center the SVG image to prevent stretching.The Column uses
crossAxisAlignment: CrossAxisAlignment.stretch, which will stretch the SVG horizontally to fill the screen width. Since the SVG has a fixed square size (200×200), this stretching may distort the image.Wrap the SvgPicture in a Center widget or use
crossAxisAlignment: CrossAxisAlignment.center:body: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, + crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ SvgPicture.asset(lib/services/api_service.dart (2)
130-132: Inconsistent error handling withdeleteLiveChapterRoom.This method catches
AppwriteExceptionspecifically, whiledeleteLiveChapterRoom(line 160) uses a genericcatch. For consistency and to avoid missed exceptions, both methods should use the same error-handling strategy.Apply generic error handling to both methods:
- } on AppwriteException catch (error) { + } catch (error) { throw Exception('ERROR $error'); }
105-133: Preserve stack traces when re-throwing errors.Wrapping errors with
Exception('ERROR $error')discards the original stack trace, making debugging harder. Consider re-throwing the original exception or using a custom exception that preserves the cause.If you must wrap, preserve the stack trace:
- } catch (error) { - throw Exception('ERROR $error'); + } catch (error, stackTrace) { + Error.throwWithStackTrace( + Exception('ERROR $error'), + stackTrace, + ); }Or simply re-throw:
} catch (error) { - throw Exception('ERROR $error'); + rethrow; }Note: This same pattern appears in
deleteLiveChapterRoom(lines 135-163) and should be addressed there as well.lib/models/user_report_model.dart (1)
7-7: Fix typo in comment."recieving" should be "receiving".
-//This model is only supposed to be used when sending reports to the database, to use while recieving modify the fromJson method for reportedUser (Relationship attribute) +//This model is only supposed to be used when sending reports to the database, to use while receiving modify the fromJson method for reportedUser (Relationship attribute)lib/views/widgets/live_chapter_header.dart (1)
18-30: Use textTheme and overflow to avoid layout breaks on long titles.Prefer Theme.of(context).textTheme styles and add overflow/softWrap where appropriate for chapterName/description to prevent overflow and improve theming consistency.
Apply this minimal change:
- Text( - chapterName, - style: TextStyle( - fontSize: UiSizes.size_20, - ), - ), + Text( + chapterName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontSize: UiSizes.size_20, + ), + ), ... - Text( - chapterDescription, - style: TextStyle(color: Colors.grey, fontSize: UiSizes.size_14), - ), + Text( + chapterDescription, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Colors.grey, + fontSize: UiSizes.size_14, + ), + ),lib/views/widgets/live_chapter_list_tile.dart (1)
13-16: Align surface/background colors for contrast.Using colorScheme.secondary as background with onSurface text may reduce contrast. Prefer secondaryContainer with onSecondaryContainer, or keep surface background with onSurface text.
- decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: Theme.of(context).colorScheme.secondary, - ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Theme.of(context).colorScheme.secondaryContainer, + ),- style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context).colorScheme.onSurface, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onSecondaryContainer,Also applies to: 29-31, 41-41, 47-48
lib/views/widgets/live_chapter_attendee_block.dart (1)
9-14: Prefer VoidCallback over Function for actions.Use typedef VoidCallback for stricter typing and better tooling.
-class FocusedMenuItemData { - final String textContent; - final Function action; +class FocusedMenuItemData { + final String textContent; + final VoidCallback action;lib/models/live_chapter_attendees_model.dart (1)
10-13: Avoid reusing "users" JSON key for both input and output.
Rename theuserIdswrite key to"userIds"(or add custom (de)serialization) so that read‐onlyusersand write‐onlyuserIdsmap to distinct JSON fields.- @JsonKey(includeFromJson: false, name: "users", includeToJson: true) + @JsonKey(includeFromJson: false, name: "userIds", includeToJson: true) List<String>? userIds,lib/services/room_service.dart (1)
125-148: Consider extracting shared logic betweencreateRoomandcreateLiveChapterRoom.The methods
createRoom(lines 91-123) andcreateLiveChapterRoom(lines 125-148) share significant logic for handling LiveKit socket URL replacement and secure storage operations. This duplication could lead to maintenance issues if the logic needs to change.Consider extracting the shared logic into a helper method:
static Future<Map<String, String>> _storeLiveKitCredentials({ required String livekitToken, required String livekitSocketUrl, }) async { final processedUrl = livekitSocketUrl == "wss://host.docker.internal:7880" ? localhostLivekitEndpoint : livekitSocketUrl; const storage = FlutterSecureStorage(); await storage.write(key: "createdRoomAdminToken", value: livekitToken); await storage.write(key: "createdRoomLivekitUrl", value: processedUrl); return {'token': livekitToken, 'url': processedUrl}; }Then use it in both methods to reduce duplication.
lib/views/screens/live_chapter_screen.dart (2)
109-152: Review thedouble.maxFiniteheight constraint.Line 111 sets
height: double.maxFinitefor the GridView container. This could cause layout issues if the parent doesn't properly constrain the height, potentially leading to overflow errors or unexpected rendering.Consider using
Expandedor calculating a more reasonable fixed/relative height:Obx(() { - return SizedBox( - height: double.maxFinite, - width: 400, - child: GridView.builder( + return Expanded( + child: SizedBox( + width: 400, + child: GridView.builder( + shrinkWrap: true, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(Or if you need a fixed height, calculate it based on screen dimensions:
height: MediaQuery.of(context).size.height * 0.6,
23-34: Consider extracting the dialog to a reusable utility.The
_deleteRoomDialogmethod is well-structured but could be useful in other contexts. If similar confirmation dialogs are needed elsewhere in the app, consider extracting this to a shared utility class.lib/controllers/livekit_controller.dart (1)
81-87: Document the reasoning for disabling dynacast, adaptiveStream, and simulcast.The room options explicitly disable several LiveKit features (dynacast, adaptiveStream, simulcast) without explanation. These options can significantly impact performance and bandwidth usage.
Add a comment explaining why these features are disabled:
liveKitRoom = Room( roomOptions: const RoomOptions( + // Disabled for live chapter recording to ensure consistent quality + // and avoid adaptive stream switching during transcription dynacast: false, adaptiveStream: false, defaultVideoPublishOptions: VideoPublishOptions(simulcast: false),If these should only be disabled for live chapters, consider making them conditional:
roomOptions: RoomOptions( dynacast: !isLiveChapter, adaptiveStream: !isLiveChapter, defaultVideoPublishOptions: VideoPublishOptions( simulcast: !isLiveChapter, ), ),lib/controllers/live_chapter_controller.dart (2)
239-277: Simplify error handling inendLiveChapter.The nested try-catch blocks (lines 244-257 inside the outer try starting at line 242) make the error flow difficult to follow. The inner catch logs but continues execution, while the outer catch logs and dismisses the loading dialog. This could lead to inconsistent state if the inner operations fail but transcription succeeds.
Consider refactoring to handle errors more explicitly:
Future<void> endLiveChapter() async { loadingDialog(Get.context!); try { Get.find<LiveKitController>().isRecording.value = false; // Clean up database documents await _cleanupLiveChapterDocuments(); // Transcribe and delete room final transcripController = WhisperTranscriptionController(); final String lyrics = await transcripController.transcribeChapter( liveChapterModel.value!.livekitRoomId, ); await RoomService.deleteLiveChapterRoom( roomId: liveChapterModel.value!.livekitRoomId, ); await liveChapterAttendeesSubscription?.close(); await Get.delete<LiveKitController>(force: true); Get.back(); await Future.delayed(const Duration(milliseconds: 500)); Get.to(() => VerifyChapterDetailsScreen(lyricsString: lyrics)); } catch (e) { log("Error ending live chapter: ${e.toString()}"); Get.back(); // Dismiss loading dialog // Consider showing user-facing error rethrow; } } Future<void> _cleanupLiveChapterDocuments() async { try { await databases.deleteDocument( databaseId: storyDatabaseId, collectionId: liveChaptersCollectionId, documentId: liveChapterModel.value!.id, ); await databases.deleteDocument( databaseId: userDatabaseID, collectionId: liveChapterAttendeesCollectionId, documentId: liveChapterModel.value!.id, ); } catch (e) { log('Warning: Failed to cleanup live chapter documents: ${e.toString()}'); // Continue execution even if cleanup fails } }
71-136: Consider separating notification logic from room creation.The
startLiveChaptermethod mixes room creation, database operations, and notification sending (lines 114-129). This violates the single responsibility principle and makes testing more difficult.Consider extracting the notification logic into a separate method:
Future<void> _sendLiveChapterNotification( String storyName, LiveChapterModel liveChapterData, ) async { if (authStateController.followerDocuments.isEmpty) return; log('Sending notification for live chapter'); var body = json.encode({ 'creatorId': authStateController.uid, 'payload': { 'title': 'Live Chapter Starting!', 'body': "${authStateController.displayName} is starting a Live Chapter in $storyName: ${liveChapterData.chapterTitle}. Tune In!", }, }); var results = await functions.createExecution( functionId: sendStoryNotificationFunctionID, body: body.toString(), ); log('Notification status: ${results.status}'); }Then call it from
startLiveChapter:await _sendLiveChapterNotification(storyName, liveChapterData);This makes the code more testable and maintainable.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pubspec.lockis excluded by!**/*.lock
📒 Files selected for processing (61)
ios/Runner/Info.plist(1 hunks)l10n.yaml(1 hunks)lib/controllers/about_app_screen_controller.dart(1 hunks)lib/controllers/auth_state_controller.dart(4 hunks)lib/controllers/create_room_controller.dart(1 hunks)lib/controllers/explore_story_controller.dart(3 hunks)lib/controllers/live_chapter_controller.dart(1 hunks)lib/controllers/livekit_controller.dart(3 hunks)lib/controllers/rooms_controller.dart(2 hunks)lib/controllers/single_room_controller.dart(5 hunks)lib/controllers/whisper_transcription_controller.dart(1 hunks)lib/l10n/app_en.arb(25 hunks)lib/l10n/app_gu.arb(1 hunks)lib/l10n/app_hi.arb(23 hunks)lib/l10n/app_localizations.dart(5 hunks)lib/l10n/app_localizations_en.dart(4 hunks)lib/l10n/app_localizations_gu.dart(1 hunks)lib/l10n/app_localizations_hi.dart(2 hunks)lib/main.dart(1 hunks)lib/models/appwrite_room.dart(2 hunks)lib/models/live_chapter_attendees_model.dart(1 hunks)lib/models/live_chapter_attendees_model.freezed.dart(1 hunks)lib/models/live_chapter_attendees_model.g.dart(1 hunks)lib/models/live_chapter_model.dart(1 hunks)lib/models/live_chapter_model.freezed.dart(1 hunks)lib/models/live_chapter_model.g.dart(1 hunks)lib/models/story.dart(3 hunks)lib/models/user_report_model.dart(1 hunks)lib/models/user_report_model.freezed.dart(1 hunks)lib/models/user_report_model.g.dart(1 hunks)lib/routes/app_pages.dart(3 hunks)lib/routes/app_routes.dart(1 hunks)lib/services/api_service.dart(1 hunks)lib/services/room_service.dart(2 hunks)lib/themes/theme_controller.dart(0 hunks)lib/utils/constants.dart(3 hunks)lib/utils/enums/report_type_enum.dart(1 hunks)lib/views/screens/about_app_screen.dart(2 hunks)lib/views/screens/add_chapter_screen.dart(1 hunks)lib/views/screens/chapter_play_screen.dart(2 hunks)lib/views/screens/edit_profile_screen.dart(3 hunks)lib/views/screens/email_verification_screen.dart(4 hunks)lib/views/screens/home_screen.dart(4 hunks)lib/views/screens/live_chapter_screen.dart(1 hunks)lib/views/screens/story_screen.dart(4 hunks)lib/views/screens/tabview_screen.dart(3 hunks)lib/views/screens/user_blocked_screen.dart(1 hunks)lib/views/screens/verify_chapter_details_screen.dart(1 hunks)lib/views/widgets/chapter_player.dart(1 hunks)lib/views/widgets/live_chapter_attendee_block.dart(1 hunks)lib/views/widgets/live_chapter_header.dart(1 hunks)lib/views/widgets/live_chapter_list_tile.dart(1 hunks)lib/views/widgets/live_room_tile.dart(1 hunks)lib/views/widgets/participant_block.dart(3 hunks)lib/views/widgets/report_widget.dart(1 hunks)lib/views/widgets/start_live_chapter_dialog.dart(1 hunks)project_structure.txt(10 hunks)pubspec.yaml(2 hunks)test/controllers/auth_state_controller_test.dart(1 hunks)test/controllers/friends_controller_test.dart(0 hunks)untranslated.txt(1 hunks)
💤 Files with no reviewable changes (2)
- lib/themes/theme_controller.dart
- test/controllers/friends_controller_test.dart
🧰 Additional context used
🪛 RuboCop (1.81.1)
lib/l10n/app_gu.arb
[warning] 611-611: Duplicated key in hash literal.
(Lint/DuplicateHashKey)
[warning] 612-612: Duplicated key in hash literal.
(Lint/DuplicateHashKey)
[warning] 613-613: Duplicated key in hash literal.
(Lint/DuplicateHashKey)
[warning] 614-614: Duplicated key in hash literal.
(Lint/DuplicateHashKey)
[warning] 615-615: Duplicated key in hash literal.
(Lint/DuplicateHashKey)
[warning] 616-616: Duplicated key in hash literal.
(Lint/DuplicateHashKey)
[warning] 617-617: Duplicated key in hash literal.
(Lint/DuplicateHashKey)
[warning] 618-618: Duplicated key in hash literal.
(Lint/DuplicateHashKey)
[warning] 619-619: Duplicated key in hash literal.
(Lint/DuplicateHashKey)
[warning] 620-620: Duplicated key in hash literal.
(Lint/DuplicateHashKey)
[warning] 621-621: Duplicated key in hash literal.
(Lint/DuplicateHashKey)
[warning] 622-622: Duplicated key in hash literal.
(Lint/DuplicateHashKey)
[warning] 623-623: Duplicated key in hash literal.
(Lint/DuplicateHashKey)
[warning] 624-624: Duplicated key in hash literal.
(Lint/DuplicateHashKey)
[warning] 625-625: Duplicated key in hash literal.
(Lint/DuplicateHashKey)
[warning] 626-626: Duplicated key in hash literal.
(Lint/DuplicateHashKey)
[warning] 627-627: Duplicated key in hash literal.
(Lint/DuplicateHashKey)
[warning] 628-628: Duplicated key in hash literal.
(Lint/DuplicateHashKey)
[warning] 629-629: Duplicated key in hash literal.
(Lint/DuplicateHashKey)
[warning] 630-630: Duplicated key in hash literal.
(Lint/DuplicateHashKey)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Run Software Tests
🔇 Additional comments (36)
lib/views/screens/tabview_screen.dart (1)
1-1: LGTM! Appropriate import for logging.Adding
dart:developerenables the use of thelogfunction, which is more suitable for debugging and production logging thanuntranslated.txt (1)
1-23: LGTM!The untranslated.txt file correctly tracks 20 Gujarati localization keys that remain untranslated. This is expected when adding a new locale and provides a clear roadmap for completing the Gujarati translation effort.
lib/views/screens/add_chapter_screen.dart (1)
169-169: LGTM!The localization key update from
pushNewChapterstonewChaptersis correctly applied and aligns with the broader localization refactoring in this PR.ios/Runner/Info.plist (1)
11-11: LGTM!The addition of Gujarati ("gu") to the CFBundleLocalizations array correctly configures iOS to recognize and support Gujarati localization throughout the app.
pubspec.yaml (2)
20-20: LGTM!The version bump from 2.0.0+5 to 2.5.0+6 appropriately reflects the significant new features being introduced (Live Chapters and Host Moderation Controls) and follows semantic versioning conventions.
95-98: Dependencies up to date with no known vulnerabilities. flutter_webrtc 1.2.0, path_provider 2.1.5, whisper_flutter_new 1.0.1, and ffmpeg_kit_flutter_new 3.2.0 match the latest pub.dev releases and have no reported security advisories.lib/controllers/auth_state_controller.dart (3)
54-54: LGTM!The addition of the
reportsCountfield properly extends the user profile data model to support the new reporting and moderation features.
221-222: LGTM!The
reportsCountinitialization correctly reads the length of theuserReportsarray from the user data document, with proper null-safety handling (defaulting to 0 if the array is null).
111-119: LGTM!Removing the unused
NotificationSettingsassignment frommessaging.requestPermission(...)is appropriate, as the result was never referenced. The permission request itself is still properly invoked.test/controllers/auth_state_controller_test.dart (1)
233-233: LGTM!The test assertion correctly verifies that
reportsCountis initialized to 0 when the user data document contains nouserReportsarray, aligning with the null-safety implementation in the controller.lib/main.dart (1)
78-78: LGTM: Gujarati locale support added.The addition of Gujarati (
gu) to supported locales is implemented correctly and aligns with the broader localization expansion in this PR.lib/routes/app_routes.dart (1)
35-36: LGTM: New routes added for expanded features.The new route constants for
userBlockedScreenandliveChapterScreenfollow the existing naming convention and support the reporting and live chapter features introduced in this release.lib/models/appwrite_room.dart (1)
13-13: LGTM: Model extended for reporting feature.The addition of the
reportedUsersfield correctly extends the room model to support the new user reporting functionality. The field is properly marked as required and late final, ensuring initialization at construction time.Also applies to: 25-25
lib/utils/enums/report_type_enum.dart (1)
1-18: LGTM: Well-defined report type enum.The
ReportTypeEnumprovides a comprehensive set of report categories with proper JSON serialization support via@JsonValueannotations. The enum values align with common moderation scenarios.lib/views/widgets/live_room_tile.dart (1)
53-61: LGTM: Correct migration to SharePlus API.The migration from the deprecated
Share.share()toSharePlus.instance.share(ShareParams(...))follows best practices and uses localized text for a better user experience.Based on learnings
lib/views/screens/home_screen.dart (1)
346-346: LGTM: Dummy data updated for new model field.The dummy
AppwriteRoominstances are correctly updated with the newreportedUsersfield, initialized to empty lists as appropriate for test data.Also applies to: 362-362, 378-378, 394-394
lib/views/screens/chapter_play_screen.dart (1)
182-182: LGTM: Localization key simplified.
aboutkey present in all ARB files.lib/views/screens/email_verification_screen.dart (2)
107-107: LGTM! Localization key updated correctly.The localization key change from
verificationCompletetoverificationCompleteMessagemaintains consistency with the updated localization structure.
156-161: LGTM! Localization keys updated correctly.The localization key changes from
otpMismatchErrortootpMismatchare applied consistently in both the snackbar and accessibility announcement.lib/utils/constants.dart (3)
6-6: Verify the hardcoded IP address change.The default
baseDomainchanged from'192.168.29.24'to'10.12.78.30'. Both are private IP addresses. Ensure this change is intentional and won't break existing development environments. Consider documenting the expected value or using environment configuration.
52-55: LGTM! New collection IDs added.The new constants for user reports, live chapter attendees, and live chapters collections are properly defined and follow the existing naming convention.
81-82: LGTM! Live chapter function IDs added.The new function IDs for creating and deleting live chapter rooms are properly defined and align with the API service changes.
lib/models/user_report_model.dart (1)
9-21: LGTM! Freezed model properly structured.The model follows Freezed best practices with required/optional fields, proper factory constructors, and JSON serialization support.
lib/models/live_chapter_model.g.dart (1)
9-34: LGTM! Generated serialization code is correct.The generated code properly handles:
- Reading the Appwrite document ID from
json['$id'](line 21)- Excluding
idfrom serialization (write-only concern)- Excluding
attendeesfrom JSON (runtime-only field)This aligns with typical Appwrite response structure patterns.
lib/controllers/single_room_controller.dart (3)
106-110: LGTM! Good defensive programming.The mic safety guard automatically turns off the microphone when a user loses speaker privileges while their mic is on. This prevents audio leakage and enforces permission boundaries.
218-218: LGTM! Proper cleanup of realtime subscription.Explicitly closing the subscription when leaving the room prevents resource leaks and ensures clean teardown.
173-178: LGTM! Localized user removal notification.The removal notification now uses localized strings for better user experience across languages.
lib/models/live_chapter_attendees_model.g.dart (1)
18-23: Asymmetric JSON key configuration is intentional The model usesincludeToJson=falseonusersandincludeFromJson=false, name:"users"onuserIdsto read raw user objects and write only IDs. Generated serializers are correct.Likely an incorrect or invalid review comment.
lib/models/live_chapter_model.dart (1)
7-25: Model and JSON annotations look good.Fields and JsonKey usage (id, attendees) align with typical Appwrite patterns.
lib/views/widgets/report_widget.dart (1)
95-116: Verify RadioListTile wiring (groupValue/onChanged).RadioListTile usually needs explicit type and onChanged/groupValue to be interactive. Here they’re omitted inside RadioGroup; ensure RadioGroup injects them.
If not, update as:
- return RadioListTile( - value: ReportTypeEnum.values.elementAt(index), + return RadioListTile<ReportTypeEnum>( + value: ReportTypeEnum.values.elementAt(index), + groupValue: _selectedReportType, + onChanged: (v) => setState(() => _selectedReportType = v), title: Text( AppLocalizations.of(context)!.reportType( ReportTypeEnum.values.elementAt(index).name, ), ), );Also applies to: 104-111
lib/views/screens/story_screen.dart (2)
350-397: Verify the conditional indexing logic for live chapters.The conditional itemCount increment and index offset logic (when
liveChapter != null, the first item is the live chapter tile, and subsequent items are offset by-1) is correct but could be fragile if the list structure changes. Ensure that:
- The live chapter tile is always rendered first when present
- Regular chapters are correctly indexed after the offset
- Edge cases (empty chapters list with live chapter, single chapter with live chapter) behave as expected
Consider adding a comment explaining the indexing logic:
itemCount: widget.story.liveChapter != null ? widget.story.chapters.length + 1 : widget.story.chapters.length, itemBuilder: (context, index) { - log(index.toString()); - log(widget.story.chapters.length.toString()); + // When a live chapter exists, render it as the first item (index 0) + // and offset subsequent chapter indices by -1 if (index == 0 &&
403-449: Good addition of dual action buttons for story creators.The UI enhancement to provide separate "Add Chapter" and "Start Live Chapter" buttons improves the user experience by making both actions easily accessible.
lib/views/screens/live_chapter_screen.dart (1)
209-230: Clarify the recording button behavior.The recording button allows starting a recording but blocks stopping it with a snackbar message (lines 214-219). This asymmetric behavior could confuse users. Consider either:
- Disabling the button entirely when recording is active (if stopping should only happen server-side or via a different mechanism)
- Allowing users to stop recordings if the "cannot stop" restriction is temporary or conditional
Please clarify the intended behavior: should users be able to stop recordings from this UI, or is the recording lifecycle managed elsewhere?
lib/controllers/livekit_controller.dart (1)
21-23: Good addition of live chapter recording infrastructure.The new fields (
isLiveChapter,mediaRecorder,isRecording) cleanly integrate recording capabilities into the LiveKit controller, with proper reactive state management usingRxBool.lib/controllers/live_chapter_controller.dart (1)
42-69: Good use of realtime subscriptions for attendee updates.The realtime subscription implementation cleanly handles attendee updates and room deletion events. The automatic cleanup and navigation on delete (lines 57-66) provides a good user experience.
lib/l10n/app_localizations.dart (1)
2171-2272: Align reportType keys with enum values.Please ensure the strings handled here (
harassment,abuse,spam,impersonation,illegal,selfharm,misuse,other) match exactly whatReportTypeEnumemits (e.g., via.name). If the enum uses different casing or identifiers, the UI will fall back to theotherbranch.
| supported-locales: | ||
| - en | ||
| - hi |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing Gujarati locale in supported-locales configuration.
The PR introduces comprehensive Gujarati localization support (AppLocalizationsGu, app_gu.arb, Info.plist "gu" entry, untranslated.txt "gu" key), but the l10n.yaml configuration only lists "en" and "hi" in supported-locales. This inconsistency will prevent Flutter's localization system from properly generating and resolving Gujarati translations.
Apply this diff to add Gujarati locale support:
supported-locales:
- en
- hi
+ - gu🤖 Prompt for AI Agents
In l10n.yaml around lines 4 to 6, the supported-locales list only contains "en"
and "hi" but the PR added Gujarati resources; update the supported-locales block
to include "gu" (e.g. add a new list item "- gu"), save the file and re-run the
Flutter localization generation (flutter pub get / flutter gen-l10n) so Gujarati
is picked up.
| final attendeesDocument = await databases.getDocument( | ||
| databaseId: userDatabaseID, | ||
| collectionId: liveChapterAttendeesCollectionId, | ||
| documentId: liveStoryDocuments.first.data['\$id'], | ||
| ); | ||
|
|
||
| final attendeesModel = LiveChapterAttendeesModel.fromJson( | ||
| attendeesDocument.data, | ||
| ); | ||
| final liveChapterModel = LiveChapterModel.fromJson( | ||
| liveStoryDocuments.first.data, | ||
| ).copyWith(attendees: attendeesModel); | ||
|
|
||
| return liveChapterModel; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use the document ID when loading attendees.
liveStoryDocuments.first.data['\$id'] is always null—$id lives on the Document object, not inside data. As written, the follow-up getDocument call will request documentId: null and throw. Use liveStoryDocuments.first.$id (and consider guarding for missing attendees).
- final attendeesDocument = await databases.getDocument(
+ final attendeesDocument = await databases.getDocument(
databaseId: userDatabaseID,
collectionId: liveChapterAttendeesCollectionId,
- documentId: liveStoryDocuments.first.data['\$id'],
+ documentId: liveStoryDocuments.first.$id,
);📝 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.
| final attendeesDocument = await databases.getDocument( | |
| databaseId: userDatabaseID, | |
| collectionId: liveChapterAttendeesCollectionId, | |
| documentId: liveStoryDocuments.first.data['\$id'], | |
| ); | |
| final attendeesModel = LiveChapterAttendeesModel.fromJson( | |
| attendeesDocument.data, | |
| ); | |
| final liveChapterModel = LiveChapterModel.fromJson( | |
| liveStoryDocuments.first.data, | |
| ).copyWith(attendees: attendeesModel); | |
| return liveChapterModel; | |
| } | |
| final attendeesDocument = await databases.getDocument( | |
| databaseId: userDatabaseID, | |
| collectionId: liveChapterAttendeesCollectionId, | |
| documentId: liveStoryDocuments.first.$id, | |
| ); | |
| final attendeesModel = LiveChapterAttendeesModel.fromJson( | |
| attendeesDocument.data, | |
| ); | |
| final liveChapterModel = LiveChapterModel.fromJson( | |
| liveStoryDocuments.first.data, | |
| ).copyWith(attendees: attendeesModel); | |
| return liveChapterModel; | |
| } |
🤖 Prompt for AI Agents
In lib/controllers/explore_story_controller.dart around lines 691 to 705, the
code uses liveStoryDocuments.first.data['$id'] which is null because $id is a
property of the Document, not inside data; update the getDocument call to use
liveStoryDocuments.first.$id for documentId and add a null-check/early return or
throw if that $id is missing, and also guard the attendees lookup by checking
whether the attendees document exists (handle not-found or null result) before
constructing LiveChapterAttendeesModel so you don't pass null into fromJson.
| Future<void> joinLiveChapter( | ||
| String roomId, | ||
|
|
||
| LiveChapterModel liveChapterData, | ||
| ) async { | ||
| try { | ||
| final newAttendeesModel = liveChapterData.attendees!.copyWith( | ||
| userIds: [ | ||
| ...liveChapterData.attendees!.users.map( | ||
| (element) => element["\$id"] as String, | ||
| ), | ||
| authStateController.uid!, | ||
| ], | ||
| users: [ | ||
| ...liveChapterData.attendees!.users, | ||
| { | ||
| "\$id": authStateController.uid!, | ||
| "name": authStateController.displayName, | ||
| "profileImageUrl": authStateController.profileImageUrl, | ||
| }, | ||
| ], | ||
| ); | ||
| log(newAttendeesModel.toJson().toString()); | ||
|
|
||
| await databases.updateDocument( | ||
| databaseId: userDatabaseID, | ||
| collectionId: liveChapterAttendeesCollectionId, | ||
| documentId: roomId, | ||
| data: newAttendeesModel.toJson(), | ||
| ); | ||
|
|
||
| liveChapterModel.value = liveChapterData.copyWith( | ||
| attendees: newAttendeesModel, | ||
| ); | ||
| await RoomService.joinLiveChapterRoom( | ||
| roomId: roomId, | ||
| userId: authStateController.uid!, | ||
| ); | ||
| listenForAttendeesAdded(); | ||
| Get.toNamed(AppRoutes.liveChapterScreen); | ||
| } catch (e) { | ||
| log(e.toString()); | ||
| rethrow; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid force unwrapping and improve error handling in joinLiveChapter.
Line 169 (within the copyWith call on line 144-159) implicitly assumes that liveChapterData.attendees is non-null when accessing its properties. If attendees is null, this will throw a runtime exception. Additionally, the error handling at lines 178-181 only logs and rethrows, which may not provide enough context for debugging.
Consider adding null checks and improving error messages:
Future<void> joinLiveChapter(
String roomId,
-
LiveChapterModel liveChapterData,
) async {
try {
+ if (liveChapterData.attendees == null) {
+ throw Exception('Cannot join live chapter: attendees data is missing');
+ }
+
final newAttendeesModel = liveChapterData.attendees!.copyWith(And improve the error handling:
} catch (e) {
- log(e.toString());
- rethrow;
+ log('Error joining live chapter $roomId: ${e.toString()}');
+ // Consider showing a user-facing error message
+ rethrow;
}📝 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> joinLiveChapter( | |
| String roomId, | |
| LiveChapterModel liveChapterData, | |
| ) async { | |
| try { | |
| final newAttendeesModel = liveChapterData.attendees!.copyWith( | |
| userIds: [ | |
| ...liveChapterData.attendees!.users.map( | |
| (element) => element["\$id"] as String, | |
| ), | |
| authStateController.uid!, | |
| ], | |
| users: [ | |
| ...liveChapterData.attendees!.users, | |
| { | |
| "\$id": authStateController.uid!, | |
| "name": authStateController.displayName, | |
| "profileImageUrl": authStateController.profileImageUrl, | |
| }, | |
| ], | |
| ); | |
| log(newAttendeesModel.toJson().toString()); | |
| await databases.updateDocument( | |
| databaseId: userDatabaseID, | |
| collectionId: liveChapterAttendeesCollectionId, | |
| documentId: roomId, | |
| data: newAttendeesModel.toJson(), | |
| ); | |
| liveChapterModel.value = liveChapterData.copyWith( | |
| attendees: newAttendeesModel, | |
| ); | |
| await RoomService.joinLiveChapterRoom( | |
| roomId: roomId, | |
| userId: authStateController.uid!, | |
| ); | |
| listenForAttendeesAdded(); | |
| Get.toNamed(AppRoutes.liveChapterScreen); | |
| } catch (e) { | |
| log(e.toString()); | |
| rethrow; | |
| } | |
| } | |
| Future<void> joinLiveChapter( | |
| String roomId, | |
| LiveChapterModel liveChapterData, | |
| ) async { | |
| try { | |
| if (liveChapterData.attendees == null) { | |
| throw Exception('Cannot join live chapter: attendees data is missing'); | |
| } | |
| final newAttendeesModel = liveChapterData.attendees!.copyWith( | |
| userIds: [ | |
| ...liveChapterData.attendees!.users.map( | |
| (element) => element["\$id"] as String, | |
| ), | |
| authStateController.uid!, | |
| ], | |
| users: [ | |
| ...liveChapterData.attendees!.users, | |
| { | |
| "\$id": authStateController.uid!, | |
| "name": authStateController.displayName, | |
| "profileImageUrl": authStateController.profileImageUrl, | |
| }, | |
| ], | |
| ); | |
| log(newAttendeesModel.toJson().toString()); | |
| await databases.updateDocument( | |
| databaseId: userDatabaseID, | |
| collectionId: liveChapterAttendeesCollectionId, | |
| documentId: roomId, | |
| data: newAttendeesModel.toJson(), | |
| ); | |
| liveChapterModel.value = liveChapterData.copyWith( | |
| attendees: newAttendeesModel, | |
| ); | |
| await RoomService.joinLiveChapterRoom( | |
| roomId: roomId, | |
| userId: authStateController.uid!, | |
| ); | |
| listenForAttendeesAdded(); | |
| Get.toNamed(AppRoutes.liveChapterScreen); | |
| } catch (e) { | |
| log('Error joining live chapter $roomId: ${e.toString()}'); | |
| // Consider showing a user-facing error message | |
| rethrow; | |
| } | |
| } |
🤖 Prompt for AI Agents
In lib/controllers/live_chapter_controller.dart around lines 138 to 182, avoid
force-unwrapping liveChapterData.attendees and authStateController fields; first
null-check attendees and if null build a new AttendeesModel with the current
user as the sole attendee (or return with a clear error), validate
authStateController.uid/displayName/profileImageUrl before use and fail early
with a descriptive exception if missing, update the databases.updateDocument
payload accordingly, and move or guard
RoomService.joinLiveChapterRoom/listenForAttendeesAdded so they only run after a
successful DB update; also improve the catch block to log a contextual error
message including operation details and stacktrace (or wrap and rethrow a more
descriptive exception) rather than only rethrowing the raw error.
| Future<Chapter> createChapterFromLiveChapter( | ||
| String coverImgPath, | ||
| String audioFilePath, | ||
| String lyricsString, | ||
| ) async { | ||
| final track = io.File(audioFilePath); | ||
| final metadata = readMetadata(track); | ||
| int playDuration = metadata.duration?.inMilliseconds ?? 0; | ||
|
|
||
| Color primaryColor; | ||
|
|
||
| if (!coverImgPath.contains('http')) { | ||
| ColorScheme imageColorScheme = await ColorScheme.fromImageProvider( | ||
| provider: FileImage(io.File(coverImgPath)), | ||
| ); | ||
|
|
||
| primaryColor = imageColorScheme.primary; | ||
| } else { | ||
| primaryColor = const Color(0xffcbc6c6); | ||
| } | ||
|
|
||
| // coverImageUrl and audioFileUrl recieve paths while the chapter creation process | ||
| // as cannot push files to storage to get URL unless user is final on creating a story | ||
|
|
||
| return Chapter( | ||
| liveChapterModel.value!.id, | ||
| liveChapterModel.value!.chapterTitle, | ||
| coverImgPath, | ||
| liveChapterModel.value!.chapterDescription, | ||
| lyricsString, | ||
| audioFilePath, | ||
| playDuration, | ||
| primaryColor, | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add error handling for metadata reading.
Line 285 calls readMetadata(track) without a try-catch block. If the audio file is corrupted, missing, or in an unsupported format, this will throw an exception and crash the app.
Apply this diff to add error handling:
Future<Chapter> createChapterFromLiveChapter(
String coverImgPath,
String audioFilePath,
String lyricsString,
) async {
+ try {
final track = io.File(audioFilePath);
final metadata = readMetadata(track);
int playDuration = metadata.duration?.inMilliseconds ?? 0;
Color primaryColor;
if (!coverImgPath.contains('http')) {
ColorScheme imageColorScheme = await ColorScheme.fromImageProvider(
provider: FileImage(io.File(coverImgPath)),
);
primaryColor = imageColorScheme.primary;
} else {
primaryColor = const Color(0xffcbc6c6);
}
return Chapter(
liveChapterModel.value!.id,
liveChapterModel.value!.chapterTitle,
coverImgPath,
liveChapterModel.value!.chapterDescription,
lyricsString,
audioFilePath,
playDuration,
primaryColor,
);
+ } catch (e) {
+ log('Error creating chapter from live chapter: ${e.toString()}');
+ rethrow;
+ }
}📝 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<Chapter> createChapterFromLiveChapter( | |
| String coverImgPath, | |
| String audioFilePath, | |
| String lyricsString, | |
| ) async { | |
| final track = io.File(audioFilePath); | |
| final metadata = readMetadata(track); | |
| int playDuration = metadata.duration?.inMilliseconds ?? 0; | |
| Color primaryColor; | |
| if (!coverImgPath.contains('http')) { | |
| ColorScheme imageColorScheme = await ColorScheme.fromImageProvider( | |
| provider: FileImage(io.File(coverImgPath)), | |
| ); | |
| primaryColor = imageColorScheme.primary; | |
| } else { | |
| primaryColor = const Color(0xffcbc6c6); | |
| } | |
| // coverImageUrl and audioFileUrl recieve paths while the chapter creation process | |
| // as cannot push files to storage to get URL unless user is final on creating a story | |
| return Chapter( | |
| liveChapterModel.value!.id, | |
| liveChapterModel.value!.chapterTitle, | |
| coverImgPath, | |
| liveChapterModel.value!.chapterDescription, | |
| lyricsString, | |
| audioFilePath, | |
| playDuration, | |
| primaryColor, | |
| ); | |
| } | |
| Future<Chapter> createChapterFromLiveChapter( | |
| String coverImgPath, | |
| String audioFilePath, | |
| String lyricsString, | |
| ) async { | |
| try { | |
| final track = io.File(audioFilePath); | |
| final metadata = readMetadata(track); | |
| int playDuration = metadata.duration?.inMilliseconds ?? 0; | |
| Color primaryColor; | |
| if (!coverImgPath.contains('http')) { | |
| ColorScheme imageColorScheme = await ColorScheme.fromImageProvider( | |
| provider: FileImage(io.File(coverImgPath)), | |
| ); | |
| primaryColor = imageColorScheme.primary; | |
| } else { | |
| primaryColor = const Color(0xffcbc6c6); | |
| } | |
| return Chapter( | |
| liveChapterModel.value!.id, | |
| liveChapterModel.value!.chapterTitle, | |
| coverImgPath, | |
| liveChapterModel.value!.chapterDescription, | |
| lyricsString, | |
| audioFilePath, | |
| playDuration, | |
| primaryColor, | |
| ); | |
| } catch (e) { | |
| log('Error creating chapter from live chapter: ${e.toString()}'); | |
| rethrow; | |
| } | |
| } |
🤖 Prompt for AI Agents
In lib/controllers/live_chapter_controller.dart around lines 279 to 313, the
call to readMetadata(track) is unprotected and can throw for
corrupted/missing/unsupported audio files; wrap the readMetadata call in a
try-catch, on error set metadata to null (or a minimal fallback) and set
playDuration to 0, optionally log the exception (or surface a user-friendly
error) but do not let it crash the method — then proceed to compute primaryColor
and return the Chapter using the safe playDuration value.
| void listenToRecordingStateChanges() async { | ||
| final storagePath = await getApplicationDocumentsDirectory(); | ||
| isRecording.listen((state) async { | ||
| if (state) { | ||
| log("Starting recording..."); | ||
|
|
||
| log('Local track published: ${storagePath.path}'); | ||
| await mediaRecorder.start( | ||
| '${storagePath.path}/recordings/${liveKitRoom.name}.mp4', | ||
| audioChannel: RecorderAudioChannel.INPUT, | ||
| ); | ||
| } else { | ||
| await mediaRecorder.stop(); | ||
| log( | ||
| 'Recording stopped, file saved to: $storagePath/${liveKitRoom.name}.mp4', | ||
| ); | ||
| } | ||
| }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ensure the recordings directory exists before writing.
The recording path on line 52 writes to ${storagePath.path}/recordings/${liveKitRoom.name}.mp4, but the code doesn't verify that the recordings subdirectory exists. This could cause the mediaRecorder.start() call to fail if the directory hasn't been created.
Apply this diff to ensure the directory exists:
void listenToRecordingStateChanges() async {
final storagePath = await getApplicationDocumentsDirectory();
+ final recordingsDir = Directory('${storagePath.path}/recordings');
+ if (!await recordingsDir.exists()) {
+ await recordingsDir.create(recursive: true);
+ }
+
isRecording.listen((state) async {
if (state) {
log("Starting recording...");Also add the import:
import 'dart:developer';
import 'dart:async';
+import 'dart:io';
import 'package:flutter_webrtc/flutter_webrtc.dart';📝 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 listenToRecordingStateChanges() async { | |
| final storagePath = await getApplicationDocumentsDirectory(); | |
| isRecording.listen((state) async { | |
| if (state) { | |
| log("Starting recording..."); | |
| log('Local track published: ${storagePath.path}'); | |
| await mediaRecorder.start( | |
| '${storagePath.path}/recordings/${liveKitRoom.name}.mp4', | |
| audioChannel: RecorderAudioChannel.INPUT, | |
| ); | |
| } else { | |
| await mediaRecorder.stop(); | |
| log( | |
| 'Recording stopped, file saved to: $storagePath/${liveKitRoom.name}.mp4', | |
| ); | |
| } | |
| }); | |
| } | |
| // Add this import | |
| import 'dart:developer'; | |
| import 'dart:async'; | |
| import 'dart:io'; | |
| import 'package:flutter_webrtc/flutter_webrtc.dart'; | |
| void listenToRecordingStateChanges() async { | |
| final storagePath = await getApplicationDocumentsDirectory(); | |
| final recordingsDir = Directory('${storagePath.path}/recordings'); | |
| if (!await recordingsDir.exists()) { | |
| await recordingsDir.create(recursive: true); | |
| } | |
| isRecording.listen((state) async { | |
| if (state) { | |
| log("Starting recording..."); | |
| log('Local track published: ${storagePath.path}'); | |
| await mediaRecorder.start( | |
| '${storagePath.path}/recordings/${liveKitRoom.name}.mp4', | |
| audioChannel: RecorderAudioChannel.INPUT, | |
| ); | |
| } else { | |
| await mediaRecorder.stop(); | |
| log( | |
| 'Recording stopped, file saved to: $storagePath/${liveKitRoom.name}.mp4', | |
| ); | |
| } | |
| }); | |
| } |
🤖 Prompt for AI Agents
In lib/controllers/livekit_controller.dart around lines 44 to 62, the code
writes to ${storagePath.path}/recordings/${liveKitRoom.name}.mp4 without
ensuring the recordings subdirectory exists; before calling
mediaRecorder.start(), create the recordings directory (e.g., use
Directory('${storagePath.path}/recordings') and call create(recursive: true) or
createSync(recursive: true) and handle/await errors), then proceed to start
recording; also add the required import for dart:io at the top of the file.
| style: Theme.of(context).textTheme.bodyMedium!.copyWith( | ||
| color: Theme.of(context).colorScheme.onSurface, | ||
| fontWeight: FontWeight.w500, | ||
| fontSize: 17, | ||
| fontStyle: FontStyle.normal, | ||
| fontFamily: 'Inter', | ||
| ), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove hardcoded 'Inter' or include the font asset.
'Inter' isn't present in assets (project_structure.txt shows only Montserrat). This causes platform fallback and inconsistent metrics.
- style: Theme.of(context).textTheme.bodyMedium!.copyWith(
+ style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w500,
fontSize: 17,
fontStyle: FontStyle.normal,
- fontFamily: 'Inter',
),- style: Theme.of(context).textTheme.bodyMedium!.copyWith(
+ style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 12,
fontStyle: FontStyle.normal,
- fontFamily: 'Inter',
),Also applies to: 41-45
🤖 Prompt for AI Agents
In lib/views/widgets/live_chapter_list_tile.dart around lines 29-35 (and
similarly lines 41-45), the TextStyle hardcodes fontFamily: 'Inter' while the
project only includes Montserrat, causing platform fallbacks and inconsistent
metrics; either remove the fontFamily property so the app uses the configured
Theme typography (or set it to
Theme.of(context).textTheme.bodyMedium!.fontFamily) OR add Inter as a bundled
font in pubspec.yaml and include the font files under assets/fonts and run
flutter pub get; update the TextStyle to reference the now-available font only
after the asset is added.
| class _ReportWidgetState extends State<ReportWidget> { | ||
| ReportTypeEnum? _selectedReportType; | ||
| TextEditingController reportTextController = TextEditingController(); | ||
| final AuthStateController authStateController = | ||
| Get.find<AuthStateController>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dispose the TextEditingController to avoid leaks.
Add dispose() to clean up the controller.
class _ReportWidgetState extends State<ReportWidget> {
ReportTypeEnum? _selectedReportType;
TextEditingController reportTextController = TextEditingController();
final AuthStateController authStateController =
Get.find<AuthStateController>();
+ @override
+ void dispose() {
+ reportTextController.dispose();
+ super.dispose();
+ }🤖 Prompt for AI Agents
In lib/views/widgets/report_widget.dart around lines 27 to 31, the
TextEditingController reportTextController is created but never disposed which
can leak resources; add an override of dispose() in the _ReportWidgetState that
calls reportTextController.dispose() and then super.dispose() to properly clean
up the controller when the widget is removed.
| final UserReportModel report = UserReportModel( | ||
| reporterUid: authStateController.uid!, | ||
| reportedUid: widget.participantId, | ||
| reportType: _selectedReportType!, | ||
| reportText: reportTextController.text.isEmpty | ||
| ? null | ||
| : reportTextController.text, | ||
| reportedUser: widget.participantId, | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wrong field assigned: reportedUser should be the name, not the id.
Currently both reportedUid and reportedUser use the id.
- final UserReportModel report = UserReportModel(
+ final UserReportModel report = UserReportModel(
reporterUid: authStateController.uid!,
reportedUid: widget.participantId,
reportType: _selectedReportType!,
reportText: reportTextController.text.isEmpty
? null
: reportTextController.text,
- reportedUser: widget.participantId,
+ reportedUser: widget.participantName,
);🤖 Prompt for AI Agents
In lib/views/widgets/report_widget.dart around lines 43 to 51, the reportedUser
field is incorrectly assigned the participant ID; change it to use the
participant's name property from the widget (e.g., widget.participantName or the
correct display-name field on the widget) instead of widget.participantId, and
add a safe fallback or null check if the name can be null/empty.
| Get.back(result: true); | ||
| await Future.delayed(Duration(milliseconds: 500)); | ||
| customSnackbar( | ||
| AppLocalizations.of(Get.context!)!.success, | ||
| AppLocalizations.of(Get.context!)!.reportSubmitted, | ||
| LogType.success, | ||
| ); | ||
|
|
||
| return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use local context (and show snackbar before popping) to avoid null/disposed context.
Get.context! may be null after Get.back; also avoid delayed snackbar after pop.
- Get.back(result: true);
- await Future.delayed(Duration(milliseconds: 500));
- customSnackbar(
- AppLocalizations.of(Get.context!)!.success,
- AppLocalizations.of(Get.context!)!.reportSubmitted,
- LogType.success,
- );
+ customSnackbar(
+ AppLocalizations.of(context)!.success,
+ AppLocalizations.of(context)!.reportSubmitted,
+ LogType.success,
+ );
+ if (mounted) Get.back(result: true);- customSnackbar(
- AppLocalizations.of(Get.context!)!.error,
- '${AppLocalizations.of(Get.context!)!.reportFailed}: $e',
- LogType.error,
- );
+ customSnackbar(
+ AppLocalizations.of(context)!.error,
+ '${AppLocalizations.of(context)!.reportFailed}: $e',
+ LogType.error,
+ );Also applies to: 70-73
🤖 Prompt for AI Agents
In lib/views/widgets/report_widget.dart around lines 58-66 (also applies to
70-73): the code uses Get.context! and calls Get.back before showing a snackbar
(with a delayed Future), which can make context null/disposed; instead retrieve
and store a local BuildContext (e.g., final ctx = Get.context or the widget's
context) before doing any navigation, call customSnackbar immediately using that
saved context (not Get.context!), and then call Get.back(); remove the arbitrary
Future.delayed and ensure snackbar is shown prior to popping so no null/disposed
context is used.
| await Get.put( | ||
| LiveChapterController(), | ||
| ).startLiveChapter( | ||
| ID.unique(), | ||
| chapterName, | ||
| chapterDescription, | ||
| story.storyId, | ||
| story.title, | ||
| ); | ||
| } catch (e) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid re-registering LiveChapterController on every tap.
Calling Get.put(LiveChapterController()) each time the Start button is pressed re-registers the dependency; on the second launch it throws Instance "LiveChapterController" has already been created. Use the existing instance when registered.
- await Get.put(
- LiveChapterController(),
- ).startLiveChapter(
+ final controller = Get.isRegistered<LiveChapterController>()
+ ? Get.find<LiveChapterController>()
+ : Get.put(LiveChapterController());
+ await controller.startLiveChapter(📝 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 Get.put( | |
| LiveChapterController(), | |
| ).startLiveChapter( | |
| ID.unique(), | |
| chapterName, | |
| chapterDescription, | |
| story.storyId, | |
| story.title, | |
| ); | |
| } catch (e) { | |
| final controller = Get.isRegistered<LiveChapterController>() | |
| ? Get.find<LiveChapterController>() | |
| : Get.put(LiveChapterController()); | |
| await controller.startLiveChapter( | |
| ID.unique(), | |
| chapterName, | |
| chapterDescription, | |
| story.storyId, | |
| story.title, | |
| ); | |
| } catch (e) { |
🤖 Prompt for AI Agents
In lib/views/widgets/start_live_chapter_dialog.dart around lines 66 to 75, the
code calls Get.put(LiveChapterController()) on every tap which re-registers the
controller and causes an "already been created" error; instead obtain the
existing instance (or register only if not present). Replace the direct Get.put
call with either Get.find<LiveChapterController>() to fetch the existing
controller, or guard with if (!Get.isRegistered<LiveChapterController>())
Get.put(LiveChapterController()) then use Get.find<LiveChapterController>() to
call startLiveChapter, ensuring you use the typed generic to avoid
re-registration.
various bugfixes
|
✅ PR Closed - Thank You, @M4dhav!
We appreciate your effort and look forward to more contributions from you! 🤝 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
♻️ Duplicate comments (2)
lib/views/screens/live_chapter_screen.dart (1)
243-261: Remove the redundant block in_buildMicButton.There’s still a stray
{ ... }scope wrapping the logic inside theObx. Please drop it to keep the function clean.lib/l10n/app_localizations_gu.dart (1)
1080-1082: Remove escaped dollar signs in username interpolations.The escaped dollar signs (
\$) will render as literal "$" characters in the UI (e.g., "$john_doe"). Use normal Dart string interpolation without the backslash.Apply this diff:
String friendRequestAcceptedTo(String username) { - return 'તમે હવે \$$username સાથે મિત્ર છો.'; + return 'તમે હવે $username સાથે મિત્ર છો.'; } String friendRequestDeclinedTo(String username) { - return 'તમે \$$username ની મિત્ર વિનંતી નકારી છે.'; + return 'તમે $username ની મિત્ર વિનંતી નકારી છે.'; } String callDeclinedTo(String username) { - return 'યુઝર \$$username એ કૉલ નકારી છે.'; + return 'યુઝર $username એ કૉલ નકારી છે.'; }Also applies to: 1088-1090, 1099-1101
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
lib/l10n/app_en.arb(25 hunks)lib/l10n/app_hi.arb(23 hunks)lib/l10n/app_localizations.dart(5 hunks)lib/l10n/app_localizations_en.dart(4 hunks)lib/l10n/app_localizations_gu.dart(1 hunks)lib/l10n/app_localizations_hi.dart(2 hunks)lib/views/screens/live_chapter_screen.dart(1 hunks)lib/views/screens/profile_screen.dart(2 hunks)lib/views/widgets/start_live_chapter_dialog.dart(1 hunks)untranslated.txt(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- lib/views/widgets/start_live_chapter_dialog.dart
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Run Software Tests
🔇 Additional comments (3)
lib/l10n/app_localizations.dart (2)
9-9: LGTM! Gujarati localization properly integrated.The Gujarati locale has been correctly wired into the localization system:
- Import added for
AppLocalizationsGu'gu'added tosupportedLocaleslist- Delegate's
isSupportedupdated to recognize'gu'- Lookup logic extended to instantiate
AppLocalizationsGufor Gujarati localeAlso applies to: 99-99, 2291-2291, 2302-2303
103-2278: LGTM! Comprehensive API expansion with proper documentation.The public API surface has been significantly expanded with new localization keys for reporting, live chapters, and moderation features. All new entries include proper doc comments explaining their purpose and usage.
lib/l10n/app_en.arb (1)
2-1636: LGTM! Comprehensive localization metadata.The English ARB file has been properly expanded with:
- Detailed descriptions for all localization keys
- Proper placeholder definitions with types and examples
- Consistent formatting and structure
- New keys for reporting and live chapter features
The metadata will improve code generation and provide clear documentation for translators.
| String get alert => 'Alert'; | ||
|
|
||
| @override | ||
| String get removedFromRoom => | ||
| 'You have been reported or removed from the room'; | ||
|
|
||
| @override | ||
| String reportType(String type) { | ||
| String _temp0 = intl.Intl.selectLogic(type, { | ||
| 'harassment': 'Harassment / Hate Speech', | ||
| 'abuse': 'Abusive content / Violence', | ||
| 'spam': 'Spam / Scams / Fraud', | ||
| 'impersonation': 'Impersonation / Fake Accounts', | ||
| 'illegal': 'Illegal Activities', | ||
| 'selfharm': 'Self-harm / Suicide / Mental health', | ||
| 'misuse': 'Misuse of platform', | ||
| 'other': 'Other', | ||
| }); | ||
| return '$_temp0'; | ||
| } | ||
|
|
||
| @override | ||
| String get userBlockedFromResonate => | ||
| 'You have received multiple reports from users and you have been blocked from using Resonate. Please contact AOSSIE if you believe this is a mistake.'; | ||
|
|
||
| @override | ||
| String get reportParticipant => 'Report Participant'; | ||
|
|
||
| @override | ||
| String get selectReportType => 'Please select a report type'; | ||
|
|
||
| @override | ||
| String get reportSubmitted => 'Report Submitted Successfully'; | ||
|
|
||
| @override | ||
| String get reportFailed => 'Report Submission Failed'; | ||
|
|
||
| @override | ||
| String get additionalDetailsOptional => 'Additional details (optional)'; | ||
|
|
||
| @override | ||
| String get submitReport => 'Submit Report'; | ||
|
|
||
| @override | ||
| String get actionBlocked => 'Action Blocked'; | ||
|
|
||
| @override | ||
| String get cannotStopRecording => | ||
| 'You cannot stop the recording manually, the recording will be stopped when the room is closed.'; | ||
|
|
||
| @override | ||
| String get liveChapter => 'Live Chapter'; | ||
|
|
||
| @override | ||
| String get viewOrEditLyrics => 'View or Edit Lyrics'; | ||
|
|
||
| @override | ||
| String get close => 'Close'; | ||
|
|
||
| @override | ||
| String get verifyChapterDetails => 'Verify Chapter Details'; | ||
|
|
||
| @override | ||
| String get author => 'Author'; | ||
|
|
||
| @override | ||
| String get startLiveChapter => 'Start a Live Chapter'; | ||
|
|
||
| @override | ||
| String get fillAllFields => 'Please fill in all required fields'; | ||
|
|
||
| @override | ||
| String get noRecordingError => | ||
| 'You have not recorded anything for the chapter. Please record a chapter before exiting the room'; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Translate the new keys to Gujarati.
The new reporting and live chapter strings (lines 1170-1244) are in English instead of Gujarati. This creates an inconsistent experience for Gujarati-speaking users.
Please translate these keys to Gujarati to maintain consistency with the rest of the file:
alert,removedFromRoom,reportType,userBlockedFromResonatereportParticipant,selectReportType,reportSubmitted,reportFailedadditionalDetailsOptional,submitReport,actionBlockedcannotStopRecording,liveChapter,viewOrEditLyrics,closeverifyChapterDetails,author,startLiveChapter,fillAllFields,noRecordingError
| @override | ||
| String get alert => 'अलर्ट'; | ||
|
|
||
| @override | ||
| String get removedFromRoom => | ||
| 'आपको कमरे से रिपोर्ट किया गया है या हटा दिया गया है'; | ||
|
|
||
| @override | ||
| String reportType(String type) { | ||
| String _temp0 = intl.Intl.selectLogic(type, { | ||
| 'harassment': 'हिंसा / नफरत भरा भाषण', | ||
| 'abuse': 'दुरुपयोग सामग्री / हिंसा', | ||
| 'spam': 'स्पैम / धोखाधड़ी', | ||
| 'impersonation': 'नकली खाते', | ||
| 'illegal': 'गैरकानूनी गतिविधियाँ', | ||
| 'selfharm': 'आत्म-हानि / आत्महत्या / मानसिक स्वास्थ्य', | ||
| 'misuse': 'प्लेटफ़ॉर्म का दुरुपयोग', | ||
| 'other': 'अन्य', | ||
| }); | ||
| return '$_temp0'; | ||
| } | ||
|
|
||
| @override | ||
| String get userBlockedFromResonate => | ||
| 'आपको कई उपयोगकर्ताओं द्वारा रिपोर्ट किया गया है और आपको रेज़ोनेट का उपयोग करने से ब्लॉक कर दिया गया है। यदि आपको लगता है कि यह गलती है, तो कृपया AOSSIE से संपर्क करें।'; | ||
|
|
||
| @override | ||
| String get reportParticipant => 'प्रतिभागी की रिपोर्ट करें'; | ||
|
|
||
| @override | ||
| String get selectReportType => 'रिपोर्ट का प्रकार चुनें'; | ||
|
|
||
| @override | ||
| String get reportSubmitted => 'रिपोर्ट सफलतापूर्वक सबमिट की गई'; | ||
|
|
||
| @override | ||
| String get reportFailed => 'रिपोर्ट सबमिशन फेल'; | ||
|
|
||
| @override | ||
| String get additionalDetailsOptional => 'अतिरिक्त विवरण (वैकल्पिक)'; | ||
|
|
||
| @override | ||
| String get submitReport => 'रिपोर्ट सबमिट करें'; | ||
|
|
||
| @override | ||
| String get actionBlocked => 'कार्रवाई अवरुद्ध'; | ||
|
|
||
| @override | ||
| String get cannotStopRecording => | ||
| 'आप रिकॉर्डिंग को मैन्युअल रूप से रोक नहीं सकते, रिकॉर्डिंग तब रोकी जाएगी जब कमरा बंद होगा।'; | ||
|
|
||
| @override | ||
| String get liveChapter => 'लाइव चैप्टर'; | ||
|
|
||
| @override | ||
| String get viewOrEditLyrics => 'गीत देखें या संपादित करें'; | ||
|
|
||
| @override | ||
| String get close => 'बंद करें'; | ||
|
|
||
| @override | ||
| String get verifyChapterDetails => 'चैप्टर विवरण सत्यापित करें'; | ||
|
|
||
| @override | ||
| String get author => 'लेखक'; | ||
|
|
||
| @override | ||
| String get startLiveChapter => 'लाइव चैप्टर शुरू करें'; | ||
|
|
||
| @override | ||
| String get fillAllFields => 'कृपया सभी आवश्यक फ़ील्ड भरें'; | ||
|
|
||
| @override | ||
| String get noRecordingError => | ||
| 'आपके पास कोई रिकॉर्डिंग नहीं है। लाइव चैप्टर रूम से बाहर निकलने के लिए, कृपया पहले रिकॉर्डिंग शुरू करें।'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Translate the new keys to Hindi.
The new reporting and live chapter strings (lines 1173-1247) are currently in English instead of Hindi. This creates an inconsistent experience for Hindi-speaking users.
All other strings in this file are properly translated to Hindi. Please translate these new keys to maintain consistency:
alert,removedFromRoom,reportType,userBlockedFromResonatereportParticipant,selectReportType,reportSubmitted,reportFailedadditionalDetailsOptional,submitReport,actionBlockedcannotStopRecording,liveChapter,viewOrEditLyrics,closeverifyChapterDetails,author,startLiveChapter,fillAllFields,noRecordingError
🤖 Prompt for AI Agents
In lib/l10n/app_localizations_hi.dart around lines 1173 to 1247 the new
reporting and live-chapter strings are still in English; replace them with Hindi
translations so the file remains consistent. Update the simple getters (alert,
removedFromRoom, userBlockedFromResonate, reportParticipant, selectReportType,
reportSubmitted, reportFailed, additionalDetailsOptional, submitReport,
actionBlocked, cannotStopRecording, liveChapter, viewOrEditLyrics, close,
verifyChapterDetails, author, startLiveChapter, fillAllFields, noRecordingError)
to proper Hindi text and update the reportType(String type) selectLogic map
values to Hindi equivalents for each key (harassment, abuse, spam,
impersonation, illegal, selfharm, misuse, other); ensure punctuation and
pluralization match surrounding entries and run l10n tests/build to verify no
syntax or Intl issues.
| try { | ||
| await friendsController.removeFriend(friendModel); | ||
| customSnackbar( | ||
| AppLocalizations.of( | ||
| context, | ||
| )!.friendRequestCancelled, | ||
| AppLocalizations.of( | ||
| context, | ||
| )!.friendRequestCancelledTo( | ||
| widget.creator!.name!, | ||
| ), | ||
| LogType.info, | ||
| ); | ||
| } catch (e) { | ||
| log(e.toString()); | ||
| } | ||
| customSnackbar( | ||
| AppLocalizations.of( | ||
| context, | ||
| )!.friendRequestCancelled, | ||
| AppLocalizations.of( | ||
| context, | ||
| )!.friendRequestCancelledTo(widget.creator!.name!), | ||
| LogType.info, | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do not show a success snackbar when the removal fails.
If friendsController.removeFriend throws, we still display friendRequestCancelled and mislead the user. Handle the failure path explicitly (e.g., surface an error toast and return early) instead of always announcing success.
🤖 Prompt for AI Agents
In lib/views/screens/profile_screen.dart around lines 420 to 434, the try/catch
currently logs removal errors but always shows the success snackbar; change the
flow so that if friendsController.removeFriend throws you surface an error
snackbar (or toast) with a clear message and return early without showing the
success message, and only show the friendRequestCancelled success snackbar when
removeFriend completes successfully; ensure the catch block awaits/handles the
error display and exits the surrounding block so success is not displayed on
failure.
Description
This PR merges previous changes into main and deploys v2.5 of Resonate which include the new features of Improved Host Moderation Controls and Live Chapters with automated transcriptions.
Type of change
Please delete options that are not relevant.
How Has This Been Tested?
Software tests are passing and app features are verified to be working
Checklist:
Maintainer Checklist
Summary by CodeRabbit
New Features
Localization
Enhancements
Tests