Skip to content

TW-2796: Implement autoplay for voice messages and enhance audio even#2797

Open
nqhhdev wants to merge 26 commits intomainfrom
TW-2796-Implement-autoplay-for-voice-messages
Open

TW-2796: Implement autoplay for voice messages and enhance audio even#2797
nqhhdev wants to merge 26 commits intomainfrom
TW-2796-Implement-autoplay-for-voice-messages

Conversation

@nqhhdev
Copy link
Member

@nqhhdev nqhhdev commented Dec 23, 2025

Ticket

Related issue

Resolved

Attach screenshots or videos demonstrating the changes

  • Web:
  • Android:
  • IOS:
Simulator.Screen.Recording.-.iPhone.Air.-.2025-12-23.at.16.58.38.mov
Simulator.Screen.Recording.-.iPhone.17.-.2026-01-12.at.14.03.35.mov

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Enhanced null-safety guards in audio playback to prevent crashes and improve stability.
    • Improved audio resource cleanup and disposal, reducing memory leaks.
    • Better error handling for audio downloads and playback failures.
  • Improvements

    • Refined waveform calculation for more accurate visual representation during audio playback.
    • Optimized audio event filtering and sequencing for better playback experience.

@coderabbitai
Copy link

coderabbitai bot commented Dec 23, 2025

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR refactors audio playback handling across the chat application by introducing two new mixins (AudioMixin and EventFilterMixin) that centralize audio state management, event filtering, and lifecycle operations. The changes migrate audio player initialization and disposal logic from individual widgets to a mixin-based architecture, add platform-specific null-safety guards, and implement caching and auto-play capabilities. Chat pages now use AudioMixin for cross-platform audio management while event filtering logic is extracted into a reusable mixin for media and audio event handling.

Possibly related PRs

Suggested reviewers

  • dab246
  • hoangdat
  • tddang-linagora
🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Description check ⚠️ Warning Description is largely incomplete; it only includes ticket number and resolved section with video attachments, but is missing Root cause, Solution, Impact description, Test recommendations, and Pre-merge sections. Fill in missing required sections: add a detailed Solution explaining the changes (AudioMixin, EventFilterMixin, refactored widgets), Impact description, Test recommendations, and Pre-merge checklist items.
Title check ❓ Inconclusive Title is partially related to the changeset; it mentions autoplay for voice messages but is truncated and incomplete, missing context about audio event handling enhancements. Complete the title by appending the cut-off text: 'TW-2796: Implement autoplay for voice messages and enhance audio event handling' to provide full clarity on all major changes.
✅ Passed checks (1 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch TW-2796-Implement-autoplay-for-voice-messages

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


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

❤️ Share

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

@nqhhdev
Copy link
Member Author

nqhhdev commented Dec 23, 2025

@coderabbitai pls help me review

@coderabbitai
Copy link

coderabbitai bot commented Dec 23, 2025

@nqhhdev I'll review the changes in this PR for you. Let me analyze the implementation of autoplay for voice messages and the audio event enhancements.

✅ Actions performed

Review triggered.

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

@github-actions
Copy link
Contributor

This PR has been deployed to https://linagora.github.io/twake-on-matrix/2797

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
lib/pages/chat/events/audio_message/audio_player_widget.dart (1)

128-135: Remove the unused handleOggAudioFileIniOS method.

This method is not called anywhere in the codebase. Similar audio file handling for iOS exists in MatrixState.handleOggAudioFileIniOS and ChatAudioPlayerState._handleOggAudioFileIniOS, which are actively used.

🧹 Nitpick comments (5)
lib/pages/chat/chat_audio_player_widget.dart (1)

250-260: Consider adding mounted check before showing SnackBar in play error handler.

The onError callback at line 250 shows a SnackBar, but unlike the download error handler (line 210), there's no context.mounted check. If playback fails after the widget is disposed, this could cause an exception.

🔎 Proposed fix
     matrix?.audioPlayer?.play().onError((e, s) {
       Logs().e('Could not play audio file', e, s);
+      if (!context.mounted) return;
       ScaffoldMessenger.of(context).showSnackBar(
         SnackBar(
           content: Text(
             e?.toLocalizedString(context) ??
                 L10n.of(context)!.couldNotPlayAudioFile,
           ),
         ),
       );
     });
lib/presentation/mixins/event_filter_mixin.dart (1)

520-534: Clarify the double-reversal logic in getAudioEventsUpToClicked.

The method reverses audioEvents twice (line 524 and 533), which could be confusing. If the input is in chronological order (oldest to newest), the comment states it returns events "from clicked to newest," but the double-reversal might produce unexpected results.

Consider simplifying or adding a clarifying comment about the expected input/output ordering.

   List<Event> getAudioEventsUpToClicked(
     List<Event> audioEvents,
     Event clickedEvent,
   ) {
-    final clickedIndex = audioEvents.reversed.toList().indexWhere(
+    // audioEvents is in chronological order (oldest first)
+    // We want to find clicked event and return it + all newer events
+    final reversedEvents = audioEvents.reversed.toList();
+    final clickedIndex = reversedEvents.indexWhere(
           (event) => event.eventId == clickedEvent.eventId,
         );
 
     if (clickedIndex == -1) {
       return [];
     }
 
-    // Return from clicked event onwards for sequential playback
-    return audioEvents.reversed.toList().sublist(clickedIndex);
+    // Return from clicked event onwards (newest to oldest for playback queue)
+    return reversedEvents.sublist(clickedIndex);
   }
lib/widgets/matrix.dart (3)

1201-1201: Unnecessary null fallback for MatrixFile.bytes.

Based on learnings, MatrixFile.bytes is non-nullable (Uint8List) in matrix-dart-sdk v4.1.0+. The ?? [] fallback is defensive but unnecessary.

-        await file.writeAsBytes(matrixFile.bytes ?? []);
+        await file.writeAsBytes(matrixFile.bytes);

1237-1247: Add mounted check before showing SnackBar in play error handler.

Unlike the download error handler (line 1212), the play error callback doesn't check if context is still mounted. This could cause an exception if playback fails after navigation.

🔎 Proposed fix
     audioPlayer?.play().onError((e, s) {
       Logs().e('Could not play audio file', e, s);
+      if (!context.mounted) return;
       ScaffoldMessenger.of(context).showSnackBar(
         SnackBar(
           content: Text(
             e?.toLocalizedString(context) ??
                 L10n.of(context)!.couldNotPlayAudioFile,
           ),
         ),
       );
     });

1250-1257: Duplicate helper method across files.

handleOggAudioFileIniOS is now duplicated in MatrixState (here), ChatAudioPlayerWidget, and AudioPlayerState. Consider consolidating this into a shared utility or the AudioMixin to avoid code duplication.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 62fb56d and 7af62cc.

📒 Files selected for processing (5)
  • lib/pages/chat/chat.dart
  • lib/pages/chat/chat_audio_player_widget.dart
  • lib/pages/chat/events/audio_message/audio_player_widget.dart
  • lib/presentation/mixins/event_filter_mixin.dart
  • lib/widgets/matrix.dart
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2025-12-12T07:13:07.752Z
Learnt from: tddang-linagora
Repo: linagora/twake-on-matrix PR: 2770
File: lib/pages/chat/chat.dart:623-670
Timestamp: 2025-12-12T07:13:07.752Z
Learning: In `lib/pages/chat/chat.dart`, when `_processReadMarker()` encounters a null or empty timeline, the design choice is to drop the pending read marker and wait for the next visibility event to trigger a fresh `setReadMarker()` call, rather than retrying. This is intentional behavior.

Applied to files:

  • lib/pages/chat/chat.dart
📚 Learning: 2025-12-10T03:56:52.820Z
Learnt from: nqhhdev
Repo: linagora/twake-on-matrix PR: 2767
File: lib/pages/chat/chat_audio_player_widget.dart:264-301
Timestamp: 2025-12-10T03:56:52.820Z
Learning: In the audio player widget (lib/pages/chat/chat_audio_player_widget.dart), the supported playback speeds are 0.5x, 1.0x, 1.5x, and 2.0x. The 0.75x speed is not supported.

Applied to files:

  • lib/pages/chat/chat.dart
  • lib/widgets/matrix.dart
  • lib/pages/chat/events/audio_message/audio_player_widget.dart
  • lib/pages/chat/chat_audio_player_widget.dart
📚 Learning: 2025-12-09T06:19:59.615Z
Learnt from: tddang-linagora
Repo: linagora/twake-on-matrix PR: 2761
File: lib/pages/contacts_tab/widgets/sliver_invite_friend_button.dart:49-57
Timestamp: 2025-12-09T06:19:59.615Z
Learning: For the Twake on Matrix Flutter app (fluffychat), desktop platforms (macOS, Linux, Windows) are not supported. During reviews, assume only web and mobile targets and avoid introducing desktop-specific builds, dependencies, or platform checks. This guideline applies across Dart source files in the repository.

Applied to files:

  • lib/pages/chat/chat.dart
  • lib/widgets/matrix.dart
  • lib/pages/chat/events/audio_message/audio_player_widget.dart
  • lib/presentation/mixins/event_filter_mixin.dart
  • lib/pages/chat/chat_audio_player_widget.dart
📚 Learning: 2025-12-22T06:44:09.742Z
Learnt from: tddang-linagora
Repo: linagora/twake-on-matrix PR: 2644
File: lib/pages/chat/events/audio_message/audio_player_widget.dart:134-134
Timestamp: 2025-12-22T06:44:09.742Z
Learning: When using MatrixFile from matrix-dart-sdk (v4.1.0+), MatrixFile.bytes is non-nullable (Uint8List). Ensure code handles non-null bytes and rely on methods like downloadAndDecryptAttachment() that guarantee non-null returned bytes. Do not assume MatrixFile.bytes may be null; add null-safety checks only where the API guarantees non-null yield, and avoid nullable casts unless upgrading the SDK version. This guideline applies to Dart files across the codebase that interact with MatrixFile.

Applied to files:

  • lib/pages/chat/chat.dart
  • lib/widgets/matrix.dart
  • lib/pages/chat/events/audio_message/audio_player_widget.dart
  • lib/presentation/mixins/event_filter_mixin.dart
  • lib/pages/chat/chat_audio_player_widget.dart
📚 Learning: 2025-12-10T05:00:02.734Z
Learnt from: chibenwa
Repo: linagora/twake-on-matrix PR: 2711
File: lib/domain/model/user_info/user_info_visibility.dart:8-38
Timestamp: 2025-12-10T05:00:02.734Z
Learning: In the fluffychat codebase, domain models (in lib/domain/model/) should remain UI-agnostic and not contain UI-related methods that depend on BuildContext or L10n. UI presentation logic for domain enums should be implemented as extensions in the presentation layer (e.g., lib/presentation/extensions/).

Applied to files:

  • lib/widgets/matrix.dart
  • lib/pages/chat/events/audio_message/audio_player_widget.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). (2)
  • GitHub Check: Deploy preview versions on pull requests
  • GitHub Check: Widget test
🔇 Additional comments (13)
lib/pages/chat/chat.dart (2)

3135-3138: LGTM! Proper null-safety guards for audio player operations.

The optional chaining (?.) on audioPlayer aligns with the nullable AudioPlayer? type introduced in MatrixState. This prevents null dereference when disposing audio resources.


3141-3157: Review the platform-specific conditional logic.

The logic now skips resetting voiceMessageEvent and currentAudioStatus on mobile platforms. This appears intentional for the new queue-based autoplay feature, but verify this doesn't leave stale state when users navigate away from chat on mobile.

Additionally, the nested conditionals at lines 3154-3156 are good defensive programming—avoiding unnecessary state updates when already in notDownloaded status.

lib/pages/chat/chat_audio_player_widget.dart (3)

34-37: LGTM! Null guard for matrix instance.

Early return with an empty widget prevents null pointer issues when the MatrixState is unavailable.


56-61: LGTM! Null-safe stream access with appropriate fallbacks.

Using optional chaining with fallback streams ensures the StreamGroup.merge doesn't fail when audioPlayer is null.


208-219: Improved error handling with user feedback.

Good change from rethrowing to showing a SnackBar and resetting state. The mounted check at line 210 prevents accessing context after widget disposal. The early return at line 218 ensures the method exits cleanly on error.

lib/presentation/mixins/event_filter_mixin.dart (2)

1-25: Well-documented mixin with comprehensive event filtering utilities.

The mixin provides a clean abstraction for event filtering, decryption, and pagination. The documentation with usage examples is helpful for maintainability.


121-143: LGTM! Robust decryption with graceful error handling.

The decryption logic correctly checks for encryption support, handles each event independently, and logs failures without breaking the entire batch. This is resilient design for encrypted room scenarios.

lib/widgets/matrix.dart (2)

97-101: LGTM! Audio player now nullable with queue support.

Making audioPlayer nullable and adding voiceMessageEvents supports the new queue-based autoplay feature. This is a good architectural change for sequential voice message playback.


1141-1181: Queue-based audio playback with proper state management.

The setupAudioPlayerAutoDispose method now manages a queue of voice messages, removing completed events and automatically playing the next one. The cleanup logic when the queue is empty (lines 1168-1173) properly disposes resources.

lib/pages/chat/events/audio_message/audio_player_widget.dart (4)

50-51: LGTM! EventFilterMixin integration.

Adding EventFilterMixin enables the widget to use the centralized event filtering and audio initialization utilities.


91-114: LGTM! Null-safe audio player operations.

The optional chaining on audioPlayer properties (isAtEndPosition, playing) and methods (stop, dispose, pause, play) aligns with the nullable AudioPlayer? type and prevents null dereference.


117-125: Clean delegation to EventFilterMixin and MatrixState.

The audio initialization now uses initAudioEventsUpToClicked from the mixin to fetch pending audio events, then delegates playback to matrix.autoPlayAudio. This is a good separation of concerns.


180-198: Improved disposal with async cleanup and post-frame callback.

The disposal now:

  1. Only cleans up if this widget's event is currently playing (line 183)
  2. Uses async cleanup with error handling (lines 186-190)
  3. Defers state updates to post-frame callback to avoid widget tree lock issues (lines 194-197)

This is a robust pattern for avoiding common Flutter disposal issues.

@nqhhdev nqhhdev changed the title TW-2796: Implement autoplay for voice messages and enhance audio even… TW-2796: Implement autoplay for voice messages and enhance audio even Dec 29, 2025
Base automatically changed from TW-2766-Player-should-still-be-displayed-if-user-goes-to-list-of-chats-or-to-another-chat to main January 5, 2026 07:23
@nqhhdev nqhhdev force-pushed the TW-2796-Implement-autoplay-for-voice-messages branch from 7af62cc to 40b2237 Compare January 6, 2026 03:44
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (2)
lib/pages/chat/chat_audio_player_widget.dart (1)

250-260: Missing mounted check in play error handler.

The onError callback for play() accesses context to show a SnackBar, but there's no context.mounted check. If the widget is disposed before the error occurs, this could throw a FlutterError.

🔎 Proposed fix
   matrix?.audioPlayer?.play().onError((e, s) {
     Logs().e('Could not play audio file', e, s);
+    if (!context.mounted) return;
     ScaffoldMessenger.of(context).showSnackBar(
       SnackBar(
         content: Text(
           e?.toLocalizedString(context) ??
               L10n.of(context)!.couldNotPlayAudioFile,
         ),
       ),
     );
   });
lib/pages/chat/events/audio_message/audio_player_widget.dart (1)

180-202: Fix resource disposal race condition in dispose method.

The dispose() method schedules async stop() and dispose() operations without awaiting them and without capturing the current audio player reference. If a new audio message is selected before the async operations complete, matrix.audioPlayer will be reassigned to a new player instance. The old widget's .then() callback will then inadvertently dispose the new player instead of the old one, causing resource conflicts or premature disposal of the active player.

Store a reference to the current player before scheduling async operations, or use await (as done in matrix.dart line 1169) to ensure completion before the widget fully disposes:

if (matrix.voiceMessageEvent.value?.eventId == widget.event.eventId) {
  final playerToDispose = matrix.audioPlayer;
  if (playerToDispose != null) {
    await playerToDispose.stop();
    await playerToDispose.dispose();
  }
  
  WidgetsBinding.instance.addPostFrameCallback((_) {
    if (matrix.audioPlayer == playerToDispose) {
      matrix.currentAudioStatus.value = AudioPlayerStatus.notDownloaded;
      matrix.voiceMessageEvent.value = null;
    }
  });
}
🤖 Fix all issues with AI Agents
In @lib/presentation/mixins/event_filter_mixin.dart:
- Around line 533-548: The getAudioEventsUpToClicked function currently reverses
audioEvents (reversedEvents) and returns reversedEvents.sublist(clickedIndex),
producing a backwards list; instead operate on the original audioEvents: find
clickedIndex by searching audioEvents for event.eventId == clickedEvent.eventId
and return audioEvents.sublist(clickedIndex) so the returned sequence is forward
chronological (e.g., [c, d, e]); remove the reversedEvents usage and adjust
variable references accordingly.

In @lib/widgets/matrix.dart:
- Around line 1237-1247: The onError callback for audioPlayer?.play() uses
ScaffoldMessenger.of(context).showSnackBar without verifying the State is still
mounted; update the closure in the play error handler to check the State's
mounted flag before touching context (e.g., if (!mounted) return;) so you only
call ScaffoldMessenger.of(context).showSnackBar when mounted, preserving the
existing Logs().e and the error message construction
(e?.toLocalizedString(context) ?? L10n.of(context)!.couldNotPlayAudioFile)
inside that guarded block.
- Around line 1196-1201: Avoid the force-unwrap and the redundant
null-coalescing: check that currentEvent.attachmentOrThumbnailMxcUrl() is
non-null before using it to build fileName (use a fallback name like
matrixFile.name or a timestamp if null) and remove the unnecessary "?? []" on
matrixFile.bytes since bytes is non-nullable; update the code around fileName
and the File(...) + writeAsBytes(...) call to use the safe fileName and call
writeAsBytes(matrixFile.bytes) directly.
🧹 Nitpick comments (3)
lib/pages/chat/chat_audio_player_widget.dart (2)

34-42: Consider extracting defaults to avoid recreation on every build.

The defaultAudioStatus and defaultEvent ValueNotifiers are created on every build call, even when matrix is not null. Consider moving these to class-level static constants or restructuring to avoid unnecessary object creation.

🔎 Suggested refactor
+class ChatAudioPlayerWidget extends StatelessWidget {
+  static final _defaultAudioStatus = ValueNotifier<AudioPlayerStatus>(
+    AudioPlayerStatus.notDownloaded,
+  );
+  static final _defaultEvent = ValueNotifier<Event?>(null);
+
   final MatrixState? matrix;
   // ...
 
   @override
   Widget build(BuildContext context) {
     if (matrix == null) {
       return const SizedBox.shrink();
     }
-    final defaultAudioStatus = ValueNotifier<AudioPlayerStatus>(
-      AudioPlayerStatus.notDownloaded,
-    );
-    final defaultEvent = ValueNotifier<Event?>(null);
 
     return ValueListenableBuilder(
-      valueListenable: matrix?.currentAudioStatus ?? defaultAudioStatus,
+      valueListenable: matrix?.currentAudioStatus ?? _defaultAudioStatus,

194-199: Unnecessary null check on matrixFile.bytes.

Based on learnings, MatrixFile.bytes is non-nullable (Uint8List) in matrix-dart-sdk v4.1.0+. The downloadAndDecryptAttachment() method guarantees non-null bytes. The null/empty check on bytes at line 196 is defensive but inconsistent with the SDK contract.

🔎 Suggested refactor
-        final bytes = matrixFile?.bytes;
-
-        if (bytes == null || bytes.isEmpty) {
-          throw Exception('Downloaded file has no content');
-        }
-        await file.writeAsBytes(bytes);
+        await file.writeAsBytes(matrixFile!.bytes);
lib/presentation/mixins/event_filter_mixin.dart (1)

369-383: Minor optimization: avoid repeated list reversals.

The reversed.toList() is called on both backwardEvents and forwardEvents for filtering. Consider reversing once and reusing.

🔎 Suggested refactor
+        final backwardEventsReversed = backwardEvents.reversed.toList();
+        final forwardEventsReversed = forwardEvents.reversed.toList();
+
         initialFilteredEvents = [
           ...(messageTypes != null
               ? filterEventsByTypes(
-                  backwardEvents.reversed.toList(),
+                  backwardEventsReversed,
                   messageTypes,
                 )
-              : filterMediaEvents(backwardEvents.reversed.toList())),
+              : filterMediaEvents(backwardEventsReversed)),
           ...initialFilteredEvents,
           ...(messageTypes != null
               ? filterEventsByTypes(
-                  forwardEvents.reversed.toList(),
+                  forwardEventsReversed,
                   messageTypes,
                 )
-              : filterMediaEvents(forwardEvents.reversed.toList())),
+              : filterMediaEvents(forwardEventsReversed)),
         ];
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7af62cc and 40b2237.

📒 Files selected for processing (5)
  • lib/pages/chat/chat.dart
  • lib/pages/chat/chat_audio_player_widget.dart
  • lib/pages/chat/events/audio_message/audio_player_widget.dart
  • lib/presentation/mixins/event_filter_mixin.dart
  • lib/widgets/matrix.dart
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2025-12-09T06:19:59.615Z
Learnt from: tddang-linagora
Repo: linagora/twake-on-matrix PR: 2761
File: lib/pages/contacts_tab/widgets/sliver_invite_friend_button.dart:49-57
Timestamp: 2025-12-09T06:19:59.615Z
Learning: For the Twake on Matrix Flutter app (fluffychat), desktop platforms (macOS, Linux, Windows) are not supported. During reviews, assume only web and mobile targets and avoid introducing desktop-specific builds, dependencies, or platform checks. This guideline applies across Dart source files in the repository.

Applied to files:

  • lib/presentation/mixins/event_filter_mixin.dart
  • lib/pages/chat/events/audio_message/audio_player_widget.dart
  • lib/pages/chat/chat_audio_player_widget.dart
  • lib/pages/chat/chat.dart
  • lib/widgets/matrix.dart
📚 Learning: 2025-12-22T06:44:09.742Z
Learnt from: tddang-linagora
Repo: linagora/twake-on-matrix PR: 2644
File: lib/pages/chat/events/audio_message/audio_player_widget.dart:134-134
Timestamp: 2025-12-22T06:44:09.742Z
Learning: When using MatrixFile from matrix-dart-sdk (v4.1.0+), MatrixFile.bytes is non-nullable (Uint8List). Ensure code handles non-null bytes and rely on methods like downloadAndDecryptAttachment() that guarantee non-null returned bytes. Do not assume MatrixFile.bytes may be null; add null-safety checks only where the API guarantees non-null yield, and avoid nullable casts unless upgrading the SDK version. This guideline applies to Dart files across the codebase that interact with MatrixFile.

Applied to files:

  • lib/presentation/mixins/event_filter_mixin.dart
  • lib/pages/chat/events/audio_message/audio_player_widget.dart
  • lib/pages/chat/chat_audio_player_widget.dart
  • lib/pages/chat/chat.dart
  • lib/widgets/matrix.dart
📚 Learning: 2025-12-10T03:56:52.820Z
Learnt from: nqhhdev
Repo: linagora/twake-on-matrix PR: 2767
File: lib/pages/chat/chat_audio_player_widget.dart:264-301
Timestamp: 2025-12-10T03:56:52.820Z
Learning: In the audio player widget (lib/pages/chat/chat_audio_player_widget.dart), the supported playback speeds are 0.5x, 1.0x, 1.5x, and 2.0x. The 0.75x speed is not supported.

Applied to files:

  • lib/pages/chat/events/audio_message/audio_player_widget.dart
  • lib/pages/chat/chat_audio_player_widget.dart
  • lib/widgets/matrix.dart
📚 Learning: 2025-12-10T05:00:02.734Z
Learnt from: chibenwa
Repo: linagora/twake-on-matrix PR: 2711
File: lib/domain/model/user_info/user_info_visibility.dart:8-38
Timestamp: 2025-12-10T05:00:02.734Z
Learning: In the fluffychat codebase, domain models (in lib/domain/model/) should remain UI-agnostic and not contain UI-related methods that depend on BuildContext or L10n. UI presentation logic for domain enums should be implemented as extensions in the presentation layer (e.g., lib/presentation/extensions/).

Applied to files:

  • lib/pages/chat/events/audio_message/audio_player_widget.dart
  • lib/widgets/matrix.dart
📚 Learning: 2025-12-12T07:13:07.752Z
Learnt from: tddang-linagora
Repo: linagora/twake-on-matrix PR: 2770
File: lib/pages/chat/chat.dart:623-670
Timestamp: 2025-12-12T07:13:07.752Z
Learning: In `lib/pages/chat/chat.dart`, when `_processReadMarker()` encounters a null or empty timeline, the design choice is to drop the pending read marker and wait for the next visibility event to trigger a fresh `setReadMarker()` call, rather than retrying. This is intentional behavior.

Applied to files:

  • lib/pages/chat/chat.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). (3)
  • GitHub Check: Deploy preview versions on pull requests
  • GitHub Check: Analyze code
  • GitHub Check: Widget test
🔇 Additional comments (5)
lib/pages/chat/chat.dart (1)

3128-3157: Approve audio player lifecycle management.

The null-safety guards and platform-specific behavior are well implemented:

  • Mobile correctly preserves playing audio across navigation
  • Web correctly resets state when entering a new chat
  • Optional chaining prevents NPE on nullable audioPlayer

The logic at lines 3148-3156 could be slightly more concise, but the explicit null checks are clear and safe.

lib/pages/chat/events/audio_message/audio_player_widget.dart (1)

117-125: Approve the refactored audio initialization flow.

The new approach cleanly delegates event fetching and queue management to EventFilterMixin and MatrixState:

  1. initAudioEventsUpToClicked fetches the audio playlist
  2. voiceMessageEvents stores the queue
  3. autoPlayAudio handles playback orchestration

This separation of concerns improves maintainability.

lib/presentation/mixins/event_filter_mixin.dart (1)

247-261: Token matching issue has been addressed.

The positional indexing approach correctly handles Future.wait results:

  • When both tokens are provided: result[0] = backward, result[1] = forward
  • When only one token is provided: result[0] is that single response

This resolves the previously identified issue with incorrect token matching.

lib/widgets/matrix.dart (2)

97-101: Approve nullable audioPlayer and queue-based design.

Making audioPlayer nullable and adding voiceMessageEvents as a queue enables proper lifecycle management:

  • Audio player is only instantiated when needed
  • Queue supports autoplay of consecutive voice messages
  • Disposal is safe with null checks

1144-1178: Approve queue-based autoplay progression.

The setupAudioPlayerAutoDispose method correctly:

  1. Removes the completed message from the queue
  2. Cleans up when the queue is empty
  3. Triggers autoplay for the next message

The state management is consistent with the new nullable audioPlayer pattern.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

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

⚠️ Outside diff range comments (1)
lib/pages/chat/chat_audio_player_widget.dart (1)

196-198: Remove redundant null check on non-nullable bytes.

MatrixFile.bytes is a non-nullable Uint8List per the Matrix SDK API, making the bytes == null check always false. The isEmpty check remains valid.

🔎 Proposed fix
-        if (bytes == null || bytes.isEmpty) {
+        if (bytes.isEmpty) {
           throw Exception('Downloaded file has no content');
         }

Based on learnings, MatrixFile.bytes is non-nullable in matrix-dart-sdk v4.1.0+.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e83f78e and 37fb831.

📒 Files selected for processing (2)
  • lib/pages/chat/chat_audio_player_widget.dart
  • lib/presentation/mixins/event_filter_mixin.dart
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-12-10T03:56:52.820Z
Learnt from: nqhhdev
Repo: linagora/twake-on-matrix PR: 2767
File: lib/pages/chat/chat_audio_player_widget.dart:264-301
Timestamp: 2025-12-10T03:56:52.820Z
Learning: In the audio player widget (lib/pages/chat/chat_audio_player_widget.dart), the supported playback speeds are 0.5x, 1.0x, 1.5x, and 2.0x. The 0.75x speed is not supported.

Applied to files:

  • lib/pages/chat/chat_audio_player_widget.dart
📚 Learning: 2025-12-09T06:19:59.615Z
Learnt from: tddang-linagora
Repo: linagora/twake-on-matrix PR: 2761
File: lib/pages/contacts_tab/widgets/sliver_invite_friend_button.dart:49-57
Timestamp: 2025-12-09T06:19:59.615Z
Learning: For the Twake on Matrix Flutter app (fluffychat), desktop platforms (macOS, Linux, Windows) are not supported. During reviews, assume only web and mobile targets and avoid introducing desktop-specific builds, dependencies, or platform checks. This guideline applies across Dart source files in the repository.

Applied to files:

  • lib/pages/chat/chat_audio_player_widget.dart
  • lib/presentation/mixins/event_filter_mixin.dart
📚 Learning: 2025-12-22T06:44:09.742Z
Learnt from: tddang-linagora
Repo: linagora/twake-on-matrix PR: 2644
File: lib/pages/chat/events/audio_message/audio_player_widget.dart:134-134
Timestamp: 2025-12-22T06:44:09.742Z
Learning: When using MatrixFile from matrix-dart-sdk (v4.1.0+), MatrixFile.bytes is non-nullable (Uint8List). Ensure code handles non-null bytes and rely on methods like downloadAndDecryptAttachment() that guarantee non-null returned bytes. Do not assume MatrixFile.bytes may be null; add null-safety checks only where the API guarantees non-null yield, and avoid nullable casts unless upgrading the SDK version. This guideline applies to Dart files across the codebase that interact with MatrixFile.

Applied to files:

  • lib/pages/chat/chat_audio_player_widget.dart
  • lib/presentation/mixins/event_filter_mixin.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). (3)
  • GitHub Check: Deploy preview versions on pull requests
  • GitHub Check: Widget test
  • GitHub Check: Analyze code
🔇 Additional comments (7)
lib/pages/chat/chat_audio_player_widget.dart (3)

32-35: LGTM: Static defaults prevent unnecessary rebuilds.

The static default notifiers provide safe fallback values when matrix is null, avoiding widget rebuilds and null-reference issues in ValueListenableBuilder widgets.


39-42: LGTM: Appropriate null guard.

Returning an empty widget when matrix is unavailable prevents null-reference errors and is the correct defensive pattern for optional dependencies.


209-219: LGTM: Proper async error handling with mounted guards.

The error handling correctly logs the exception, guards against unmounted context before showing UI feedback, and recovers gracefully by resetting state.

lib/presentation/mixins/event_filter_mixin.dart (4)

247-261: LGTM: Correct positional indexing for pagination responses.

The implementation correctly uses positional indexing to match responses, as Future.wait preserves input order. The logic properly handles all token combinations with appropriate bounds checking.


30-110: LGTM: Filter methods are clear and correctly implemented.

The event filtering methods follow a consistent pattern and provide comprehensive coverage of Matrix message types. The implementations are straightforward and appropriate.


121-143: LGTM: Robust decryption with proper error handling.

The method correctly handles encrypted events with parallel decryption via Future.wait, gracefully falls back to original events on failure, and logs errors appropriately.


270-398: LGTM: Comprehensive media initialization with smart expansion.

The method correctly orchestrates initial load, decryption, filtering, and automatic expansion to reach the target event count. The reversal of paginated chunks (lines 369-370) properly handles the Matrix API's ordering conventions, ensuring final events are in chronological order.

@nqhhdev
Copy link
Member Author

nqhhdev commented Jan 6, 2026

if (bytes.isEmpty) {

@coderabbitai about the Outside diff range comments (1):

  • final bytes = matrixFile?.bytes; with type Uint8List? bytes can be null, should check null and empty

@coderabbitai
Copy link

coderabbitai bot commented Jan 6, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for the code location with "final bytes = matrixFile?.bytes"
rg -n "final bytes = matrixFile\?\.bytes" --type dart -C 5

Length of output: 1012


@nqhhdev You're absolutely correct, and I'm pleased to see the code already implements the proper checks!

At line 194-198 in lib/pages/chat/chat_audio_player_widget.dart:

final bytes = matrixFile?.bytes;

if (bytes == null || bytes.isEmpty) {
  throw Exception('Downloaded file has no content');
}

This implementation is correct because:

  1. matrixFile?.bytes produces Uint8List? - Even though MatrixFile.bytes is a non-nullable Uint8List field in matrix-dart-sdk 4.1.0+, the optional chaining operator ?. makes the result nullable (returns null if matrixFile itself is null)

  2. Both checks are necessary:

    • bytes == null - handles the case when matrixFile is null
    • bytes.isEmpty - handles the case when the file was downloaded but contains no data

This defensive approach ensures robustness when dealing with downloaded audio files. The current implementation properly guards against both scenarios before attempting to write the bytes to a file.

    (\(\
    ( -.-)
    o_(")(")
   [Thoughtful rabbit]

🧠 Learnings used
Learnt from: tddang-linagora
Repo: linagora/twake-on-matrix PR: 2644
File: lib/pages/chat/events/audio_message/audio_player_widget.dart:134-134
Timestamp: 2025-12-22T06:44:19.329Z
Learning: In the upstream matrix-dart-sdk (version 4.1.0+), MatrixFile.bytes is a non-nullable field (Uint8List), not Uint8List?. Methods like downloadAndDecryptAttachment() guarantee non-null bytes in the returned MatrixFile.

Copy link
Collaborator

@tddang-linagora tddang-linagora left a comment

Choose a reason for hiding this comment

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

  • Make sure audio player is paused and list playing is cleared when logged out and switch account

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In @lib/widgets/matrix.dart:
- Around line 1408-1411: The dispose method is missing a call to dispose the
ValueNotifier voiceMessageEvents; add voiceMessageEvents.dispose() alongside
voiceMessageEvent.dispose() (and before super.dispose()) to ensure both
notifiers are cleaned up and avoid leaking listeners, keeping the existing
audioPlayer cancellation/disposal sequence intact.
- Around line 1241-1245: The code calls audioPlayer?.setFilePath(file.path)
without awaiting it, which can cause a race when play() is invoked; update the
if branch to await the Future returned by AudioPlayer.setFilePath (i.e., use
await audioPlayer?.setFilePath(file.path)) so the file is fully loaded before
playback, mirroring the existing await used for
audioPlayer?.setAudioSource(MatrixFileAudioSource(matrixFile)).
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 37fb831 and 50f38a5.

📒 Files selected for processing (1)
  • lib/widgets/matrix.dart
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2025-12-22T06:44:09.742Z
Learnt from: tddang-linagora
Repo: linagora/twake-on-matrix PR: 2644
File: lib/pages/chat/events/audio_message/audio_player_widget.dart:134-134
Timestamp: 2025-12-22T06:44:09.742Z
Learning: When using MatrixFile from matrix-dart-sdk (v4.1.0+), MatrixFile.bytes is non-nullable (Uint8List). Ensure code handles non-null bytes and rely on methods like downloadAndDecryptAttachment() that guarantee non-null returned bytes. Do not assume MatrixFile.bytes may be null; add null-safety checks only where the API guarantees non-null yield, and avoid nullable casts unless upgrading the SDK version. This guideline applies to Dart files across the codebase that interact with MatrixFile.

Applied to files:

  • lib/widgets/matrix.dart
📚 Learning: 2026-01-09T07:54:23.212Z
Learnt from: nqhhdev
Repo: linagora/twake-on-matrix PR: 2818
File: lib/utils/matrix_sdk_extensions/event_extension.dart:427-438
Timestamp: 2026-01-09T07:54:23.212Z
Learning: In lib/utils/matrix_sdk_extensions/event_extension.dart, the method isMediaAndFilesWithCaption() intentionally returns true for ALL reply events (not just media replies), using the logic: (media/file with caption) OR (any reply event). This is the desired behavior for the reply-with-media feature in PR #2818, allowing caption input for all replies.

Applied to files:

  • lib/widgets/matrix.dart
📚 Learning: 2025-12-10T05:00:02.734Z
Learnt from: chibenwa
Repo: linagora/twake-on-matrix PR: 2711
File: lib/domain/model/user_info/user_info_visibility.dart:8-38
Timestamp: 2025-12-10T05:00:02.734Z
Learning: In the fluffychat codebase, domain models (in lib/domain/model/) should remain UI-agnostic and not contain UI-related methods that depend on BuildContext or L10n. UI presentation logic for domain enums should be implemented as extensions in the presentation layer (e.g., lib/presentation/extensions/).

Applied to files:

  • lib/widgets/matrix.dart
📚 Learning: 2025-12-10T03:56:52.820Z
Learnt from: nqhhdev
Repo: linagora/twake-on-matrix PR: 2767
File: lib/pages/chat/chat_audio_player_widget.dart:264-301
Timestamp: 2025-12-10T03:56:52.820Z
Learning: In the audio player widget (lib/pages/chat/chat_audio_player_widget.dart), the supported playback speeds are 0.5x, 1.0x, 1.5x, and 2.0x. The 0.75x speed is not supported.

Applied to files:

  • lib/widgets/matrix.dart
📚 Learning: 2025-12-09T06:19:59.615Z
Learnt from: tddang-linagora
Repo: linagora/twake-on-matrix PR: 2761
File: lib/pages/contacts_tab/widgets/sliver_invite_friend_button.dart:49-57
Timestamp: 2025-12-09T06:19:59.615Z
Learning: For the Twake on Matrix Flutter app (fluffychat), desktop platforms (macOS, Linux, Windows) are not supported. During reviews, assume only web and mobile targets and avoid introducing desktop-specific builds, dependencies, or platform checks. This guideline applies across Dart source files in the repository.

Applied to files:

  • lib/widgets/matrix.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). (3)
  • GitHub Check: Deploy preview versions on pull requests
  • GitHub Check: Analyze code
  • GitHub Check: Widget test
🔇 Additional comments (10)
lib/widgets/matrix.dart (10)

13-22: LGTM!

The new imports for audio_play_extension.dart, opus_caf_converter_dart.dart, and path_provider.dart are appropriate for the audio autoplay and iOS OGG conversion features.


97-101: LGTM!

Making audioPlayer nullable aligns with on-demand initialization, and voiceMessageEvents properly supports the queue-based autoplay feature.


170-171: LGTM!

Calling cleanupAudioPlayer() when switching accounts ensures audio playback is properly stopped before transitioning to a different user session.


1147-1187: LGTM!

The auto-dispose logic correctly handles:

  • Canceling previous subscriptions before setting up new ones
  • Filtering out completed events from the queue
  • Disposing the player when the queue is empty
  • Automatically playing the next queued message

The sequence of operations (filter before nulling voiceMessageEvent.value) ensures the completed event is properly identified and removed.


1223-1234: LGTM!

Proper context.mounted checks before accessing ScaffoldMessenger in async callbacks. This prevents "looking up a deactivated widget's ancestor" errors.


1264-1276: LGTM!

The iOS OGG-to-CAF conversion handles errors gracefully by returning null and letting the caller fall back to the original file. Caching converted files by checking existence is a good optimization.


1366-1385: LGTM!

The cleanup method properly resets all audio state, including pausing, stopping, disposing the player, clearing the queue, and resetting the status notifier. The try-catch ensures cleanup failures don't crash the app.


1111-1112: LGTM!

Cleanup during logout ensures audio playback is properly stopped when the user logs out.


526-527: LGTM!

Consistent cleanup call when handling logout with multiple accounts.


1198-1211: No changes needed. The code at line 1211 correctly uses matrixFile.bytes ?? [] because MatrixFile.bytes is nullable in this repository's forked matrix-dart-sdk (twake-supported-0.22.6). This is consistent with defensive null handling patterns used throughout the codebase (e.g., story_page.dart:212, image_viewer_view.dart:139, mxc_image.dart:207).

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
lib/widgets/matrix.dart (2)

1237-1248: Potential resource leak if autoPlayAudio is called while a player is active.

Line 1238 creates a new AudioPlayer() instance without checking if audioPlayer is already non-null and active. If autoPlayAudio is called externally while audio is playing (not through the queue mechanism), the existing player reference would be overwritten without disposal, leaking resources.

Consider disposing the existing player before creating a new one:

♻️ Proposed fix
     // Initialize audio player before use
+    await audioPlayer?.stop();
+    await audioPlayer?.dispose();
     audioPlayer = AudioPlayer();
     voiceMessageEvent.value = currentEvent;

1209-1211: Consider validating downloaded bytes before writing to file.

If matrixFile.bytes is empty (which could happen in edge cases), writing an empty file would lead to playback failures. Based on learnings, MatrixFile.bytes should be non-nullable in matrix-dart-sdk v4.1.0+, making ?? [] potentially masking an unexpected state.

Consider adding validation similar to other parts of the codebase:

♻️ Proposed fix
         file = File('${tempDir.path}/${fileName}_${matrixFile.name}');

+        if (matrixFile.bytes.isEmpty) {
+          throw Exception('Downloaded file has no content');
+        }
         await file.writeAsBytes(matrixFile.bytes ?? []);
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 50f38a5 and af793a3.

📒 Files selected for processing (1)
  • lib/widgets/matrix.dart
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: nqhhdev
Repo: linagora/twake-on-matrix PR: 2767
File: lib/pages/chat/chat_audio_player_widget.dart:264-301
Timestamp: 2025-12-10T03:56:52.820Z
Learning: In the audio player widget (lib/pages/chat/chat_audio_player_widget.dart), the supported playback speeds are 0.5x, 1.0x, 1.5x, and 2.0x. The 0.75x speed is not supported.
📚 Learning: 2025-12-22T06:44:09.742Z
Learnt from: tddang-linagora
Repo: linagora/twake-on-matrix PR: 2644
File: lib/pages/chat/events/audio_message/audio_player_widget.dart:134-134
Timestamp: 2025-12-22T06:44:09.742Z
Learning: When using MatrixFile from matrix-dart-sdk (v4.1.0+), MatrixFile.bytes is non-nullable (Uint8List). Ensure code handles non-null bytes and rely on methods like downloadAndDecryptAttachment() that guarantee non-null returned bytes. Do not assume MatrixFile.bytes may be null; add null-safety checks only where the API guarantees non-null yield, and avoid nullable casts unless upgrading the SDK version. This guideline applies to Dart files across the codebase that interact with MatrixFile.

Applied to files:

  • lib/widgets/matrix.dart
📚 Learning: 2026-01-09T07:54:23.212Z
Learnt from: nqhhdev
Repo: linagora/twake-on-matrix PR: 2818
File: lib/utils/matrix_sdk_extensions/event_extension.dart:427-438
Timestamp: 2026-01-09T07:54:23.212Z
Learning: In lib/utils/matrix_sdk_extensions/event_extension.dart, the method isMediaAndFilesWithCaption() intentionally returns true for ALL reply events (not just media replies), using the logic: (media/file with caption) OR (any reply event). This is the desired behavior for the reply-with-media feature in PR #2818, allowing caption input for all replies.

Applied to files:

  • lib/widgets/matrix.dart
📚 Learning: 2025-12-10T05:00:02.734Z
Learnt from: chibenwa
Repo: linagora/twake-on-matrix PR: 2711
File: lib/domain/model/user_info/user_info_visibility.dart:8-38
Timestamp: 2025-12-10T05:00:02.734Z
Learning: In the fluffychat codebase, domain models (in lib/domain/model/) should remain UI-agnostic and not contain UI-related methods that depend on BuildContext or L10n. UI presentation logic for domain enums should be implemented as extensions in the presentation layer (e.g., lib/presentation/extensions/).

Applied to files:

  • lib/widgets/matrix.dart
📚 Learning: 2025-12-10T03:56:52.820Z
Learnt from: nqhhdev
Repo: linagora/twake-on-matrix PR: 2767
File: lib/pages/chat/chat_audio_player_widget.dart:264-301
Timestamp: 2025-12-10T03:56:52.820Z
Learning: In the audio player widget (lib/pages/chat/chat_audio_player_widget.dart), the supported playback speeds are 0.5x, 1.0x, 1.5x, and 2.0x. The 0.75x speed is not supported.

Applied to files:

  • lib/widgets/matrix.dart
📚 Learning: 2025-12-09T06:19:59.615Z
Learnt from: tddang-linagora
Repo: linagora/twake-on-matrix PR: 2761
File: lib/pages/contacts_tab/widgets/sliver_invite_friend_button.dart:49-57
Timestamp: 2025-12-09T06:19:59.615Z
Learning: For the Twake on Matrix Flutter app (fluffychat), desktop platforms (macOS, Linux, Windows) are not supported. During reviews, assume only web and mobile targets and avoid introducing desktop-specific builds, dependencies, or platform checks. This guideline applies across Dart source files in the repository.

Applied to files:

  • lib/widgets/matrix.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). (2)
  • GitHub Check: Analyze code
  • GitHub Check: Widget test
🔇 Additional comments (9)
lib/widgets/matrix.dart (9)

13-22: LGTM!

The new imports appropriately support the audio playback enhancements: audio_play_extension.dart for audio extensions, opus_caf_converter_dart for iOS OGG-to-CAF conversion, and path_provider for temporary file storage.


97-101: LGTM!

Making audioPlayer nullable enables proper lazy initialization and lifecycle management. The voiceMessageEvents queue supports the autoplay feature by tracking pending voice messages.


170-171: LGTM!

Properly cleaning up audio resources during account switching prevents orphaned playback and ensures a clean state for the new account context.


526-527: LGTM!

Consistent cleanup during logout ensures audio resources are released when user signs out.


1111-1112: LGTM!

Cleanup on last logout completes the audio resource management across all logout scenarios.


1147-1187: LGTM!

The refactored setupAudioPlayerAutoDispose properly implements queue-based autoplay:

  • Removes completed messages from the queue
  • Disposes and nullifies the player when the queue is empty
  • Auto-advances to the next message when available

The optional chaining on the nullable audioPlayer ensures null-safety throughout the listener.


1366-1385: LGTM!

Comprehensive cleanup method that properly:

  • Stops and disposes the audio player
  • Cancels the state subscription
  • Resets all voice message state
  • Handles errors gracefully with try-catch

The sequential pause()stop()dispose() pattern is defensive and ensures clean shutdown regardless of player state.


1408-1411: LGTM!

The dispose method properly cleans up audio resources with null-safe optional chaining on audioPlayer and disposes both ValueNotifier instances (voiceMessageEvents and voiceMessageEvent).


1264-1276: LGTM!

The iOS OGG-to-CAF conversion includes sensible caching by checking file existence before converting. Returning null on error allows graceful fallback to the original file. The synchronous OpusCaf().convertOpusToCaf() call is acceptable for occasional audio message conversions.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @lib/widgets/matrix.dart:
- Around line 1271-1283: handleOggAudioFileIniOS currently returns the target
File immediately after calling OpusCaf().convertOpusToCaf(...) which can fail
silently; update the method to await the conversion call (if it returns a
Future) and then verify convertedFile.exists() after conversion, returning the
File only if the file actually exists and returning null otherwise; apply the
same post-conversion existence check to the other two similar methods in
chat_audio_player_widget.dart and audio_player_widget.dart and log a
warning/error when the converted file is missing.
🧹 Nitpick comments (2)
lib/widgets/matrix.dart (2)

1174-1178: Missing subscription cancellation when disposing player in auto-dispose handler.

When the queue is empty and the player is disposed, the _audioPlayerStateSubscription should also be cancelled to prevent potential issues with the orphaned subscription.

♻️ Suggested fix
        // Check if there are more messages to play
        if (voiceMessageEvents.value.isEmpty) {
+         _audioPlayerStateSubscription?.cancel();
+         _audioPlayerStateSubscription = null;
          await audioPlayer?.stop();
          await audioPlayer?.dispose();
          audioPlayer = null;
          return;
        }

1211-1215: Inconsistent null-safety handling for matrixFile.bytes.

Line 1211 checks matrixFile.bytes?.isEmpty == true but line 1215 uses matrixFile.bytes ?? []. If bytes were null (which per the SDK v4.1.0+ it shouldn't be), the empty check would pass (evaluates to null == truefalse) and then an empty file would be written.

Per the retrieved learnings, MatrixFile.bytes is non-nullable in matrix-dart-sdk v4.1.0+, so the defensive null checks can be simplified:

♻️ Suggested simplification
-        if (matrixFile.bytes?.isEmpty == true) {
+        if (matrixFile.bytes.isEmpty) {
           throw Exception('Downloaded file has no content');
         }

-        await file.writeAsBytes(matrixFile.bytes ?? []);
+        await file.writeAsBytes(matrixFile.bytes);

Based on learnings, MatrixFile.bytes is non-nullable in matrix-dart-sdk v4.1.0+.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between af793a3 and 9227c7b.

📒 Files selected for processing (1)
  • lib/widgets/matrix.dart
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: nqhhdev
Repo: linagora/twake-on-matrix PR: 2818
File: lib/utils/matrix_sdk_extensions/event_extension.dart:427-438
Timestamp: 2026-01-09T07:54:23.212Z
Learning: In lib/utils/matrix_sdk_extensions/event_extension.dart, the method isMediaAndFilesWithCaption() intentionally returns true for ALL reply events (not just media replies), using the logic: (media/file with caption) OR (any reply event). This is the desired behavior for the reply-with-media feature in PR #2818, allowing caption input for all replies.
Learnt from: nqhhdev
Repo: linagora/twake-on-matrix PR: 2767
File: lib/pages/chat/chat_audio_player_widget.dart:264-301
Timestamp: 2025-12-10T03:56:52.820Z
Learning: In the audio player widget (lib/pages/chat/chat_audio_player_widget.dart), the supported playback speeds are 0.5x, 1.0x, 1.5x, and 2.0x. The 0.75x speed is not supported.
📚 Learning: 2025-12-22T06:44:09.742Z
Learnt from: tddang-linagora
Repo: linagora/twake-on-matrix PR: 2644
File: lib/pages/chat/events/audio_message/audio_player_widget.dart:134-134
Timestamp: 2025-12-22T06:44:09.742Z
Learning: When using MatrixFile from matrix-dart-sdk (v4.1.0+), MatrixFile.bytes is non-nullable (Uint8List). Ensure code handles non-null bytes and rely on methods like downloadAndDecryptAttachment() that guarantee non-null returned bytes. Do not assume MatrixFile.bytes may be null; add null-safety checks only where the API guarantees non-null yield, and avoid nullable casts unless upgrading the SDK version. This guideline applies to Dart files across the codebase that interact with MatrixFile.

Applied to files:

  • lib/widgets/matrix.dart
📚 Learning: 2026-01-09T07:54:23.212Z
Learnt from: nqhhdev
Repo: linagora/twake-on-matrix PR: 2818
File: lib/utils/matrix_sdk_extensions/event_extension.dart:427-438
Timestamp: 2026-01-09T07:54:23.212Z
Learning: In lib/utils/matrix_sdk_extensions/event_extension.dart, the method isMediaAndFilesWithCaption() intentionally returns true for ALL reply events (not just media replies), using the logic: (media/file with caption) OR (any reply event). This is the desired behavior for the reply-with-media feature in PR #2818, allowing caption input for all replies.

Applied to files:

  • lib/widgets/matrix.dart
📚 Learning: 2025-12-10T05:00:02.734Z
Learnt from: chibenwa
Repo: linagora/twake-on-matrix PR: 2711
File: lib/domain/model/user_info/user_info_visibility.dart:8-38
Timestamp: 2025-12-10T05:00:02.734Z
Learning: In the fluffychat codebase, domain models (in lib/domain/model/) should remain UI-agnostic and not contain UI-related methods that depend on BuildContext or L10n. UI presentation logic for domain enums should be implemented as extensions in the presentation layer (e.g., lib/presentation/extensions/).

Applied to files:

  • lib/widgets/matrix.dart
📚 Learning: 2025-12-10T03:56:52.820Z
Learnt from: nqhhdev
Repo: linagora/twake-on-matrix PR: 2767
File: lib/pages/chat/chat_audio_player_widget.dart:264-301
Timestamp: 2025-12-10T03:56:52.820Z
Learning: In the audio player widget (lib/pages/chat/chat_audio_player_widget.dart), the supported playback speeds are 0.5x, 1.0x, 1.5x, and 2.0x. The 0.75x speed is not supported.

Applied to files:

  • lib/widgets/matrix.dart
📚 Learning: 2025-12-09T06:19:59.615Z
Learnt from: tddang-linagora
Repo: linagora/twake-on-matrix PR: 2761
File: lib/pages/contacts_tab/widgets/sliver_invite_friend_button.dart:49-57
Timestamp: 2025-12-09T06:19:59.615Z
Learning: For the Twake on Matrix Flutter app (fluffychat), desktop platforms (macOS, Linux, Windows) are not supported. During reviews, assume only web and mobile targets and avoid introducing desktop-specific builds, dependencies, or platform checks. This guideline applies across Dart source files in the repository.

Applied to files:

  • lib/widgets/matrix.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). (3)
  • GitHub Check: Deploy preview versions on pull requests
  • GitHub Check: Analyze code
  • GitHub Check: Widget test
🔇 Additional comments (7)
lib/widgets/matrix.dart (7)

97-101: LGTM!

The nullable audioPlayer allows proper lazy initialization and cleanup. The voiceMessageEvents ValueNotifier provides the queue needed for autoplay functionality.


170-171: LGTM!

Properly awaiting audio cleanup when switching accounts prevents audio from continuing to play under the wrong context.


526-527: LGTM!

Audio cleanup is properly added to the multi-account logout flow.


1111-1112: LGTM!

Audio cleanup is properly added to the last logout flow.


1373-1392: LGTM!

Comprehensive cleanup method that properly handles all audio-related state. The try-catch ensures cleanup errors don't propagate, and the sequential pause→stop→dispose pattern ensures graceful shutdown.


1416-1418: LGTM!

The nullable audioPlayer is properly disposed with null-safe access, and the new voiceMessageEvents ValueNotifier is correctly disposed alongside other ValueNotifiers.


1257-1268: Missing context.mounted check in onError callback.

The onError callback accesses context to show a snackbar, but unlike the catch block (line 1229), it doesn't guard against an unmounted context before accessing ScaffoldMessenger.of(context).

🐛 Proposed fix
     audioPlayer?.play().onError((e, s) {
       Logs().e('Could not play audio file', e, s);
       if (!context.mounted) return;
       ScaffoldMessenger.of(context).showSnackBar(
         SnackBar(
           content: Text(
             e?.toLocalizedString(context) ??
                 L10n.of(context)!.couldNotPlayAudioFile,
           ),
         ),
       );
     });

Likely an incorrect or invalid review comment.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @lib/widgets/matrix.dart:
- Around line 1259-1270: When audioPlayer?.play() fails the code logs an error
and shows a SnackBar but does not reset audio playback state, which can leave
the queue stuck; update the play error handler (the onError callback for
audioPlayer?.play()) to reset or clear voiceMessageEvent and set
currentAudioStatus to an idle/none state and then trigger the same
queue-advancement logic used by setupAudioPlayerAutoDispose (so it behaves like
ProcessingState.completed), ensuring any UI/state listeners are updated and the
next item can start.
🧹 Nitpick comments (2)
lib/widgets/matrix.dart (2)

1194-1248: Redundant voiceMessageEvent assignment.

voiceMessageEvent.value is set to currentEvent at line 1194, then identically again at line 1248. The second assignment is unnecessary.

Suggested fix
-    audioPlayer = AudioPlayer();
-    voiceMessageEvent.value = currentEvent;
+    audioPlayer = AudioPlayer();

1273-1292: Consider moving OpusCaf().convertOpusToCaf() to a background isolate.

The convertOpusToCaf() call is synchronous and blocks the UI thread. While this is acceptable for typical voice messages (which are short), larger audio files could cause noticeable jank on iOS. Consider using compute() or an isolate to run the conversion off the main thread if performance issues arise.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9227c7b and 8226290.

📒 Files selected for processing (1)
  • lib/widgets/matrix.dart
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: nqhhdev
Repo: linagora/twake-on-matrix PR: 2767
File: lib/pages/chat/chat_audio_player_widget.dart:264-301
Timestamp: 2025-12-10T03:56:52.820Z
Learning: In the audio player widget (lib/pages/chat/chat_audio_player_widget.dart), the supported playback speeds are 0.5x, 1.0x, 1.5x, and 2.0x. The 0.75x speed is not supported.
📚 Learning: 2025-12-22T06:44:09.742Z
Learnt from: tddang-linagora
Repo: linagora/twake-on-matrix PR: 2644
File: lib/pages/chat/events/audio_message/audio_player_widget.dart:134-134
Timestamp: 2025-12-22T06:44:09.742Z
Learning: When using MatrixFile from matrix-dart-sdk (v4.1.0+), MatrixFile.bytes is non-nullable (Uint8List). Ensure code handles non-null bytes and rely on methods like downloadAndDecryptAttachment() that guarantee non-null returned bytes. Do not assume MatrixFile.bytes may be null; add null-safety checks only where the API guarantees non-null yield, and avoid nullable casts unless upgrading the SDK version. This guideline applies to Dart files across the codebase that interact with MatrixFile.

Applied to files:

  • lib/widgets/matrix.dart
📚 Learning: 2026-01-09T07:54:23.212Z
Learnt from: nqhhdev
Repo: linagora/twake-on-matrix PR: 2818
File: lib/utils/matrix_sdk_extensions/event_extension.dart:427-438
Timestamp: 2026-01-09T07:54:23.212Z
Learning: In lib/utils/matrix_sdk_extensions/event_extension.dart, the method isMediaAndFilesWithCaption() intentionally returns true for ALL reply events (not just media replies), using the logic: (media/file with caption) OR (any reply event). This is the desired behavior for the reply-with-media feature in PR #2818, allowing caption input for all replies.

Applied to files:

  • lib/widgets/matrix.dart
📚 Learning: 2025-12-10T05:00:02.734Z
Learnt from: chibenwa
Repo: linagora/twake-on-matrix PR: 2711
File: lib/domain/model/user_info/user_info_visibility.dart:8-38
Timestamp: 2025-12-10T05:00:02.734Z
Learning: In the fluffychat codebase, domain models (in lib/domain/model/) should remain UI-agnostic and not contain UI-related methods that depend on BuildContext or L10n. UI presentation logic for domain enums should be implemented as extensions in the presentation layer (e.g., lib/presentation/extensions/).

Applied to files:

  • lib/widgets/matrix.dart
📚 Learning: 2025-12-10T03:56:52.820Z
Learnt from: nqhhdev
Repo: linagora/twake-on-matrix PR: 2767
File: lib/pages/chat/chat_audio_player_widget.dart:264-301
Timestamp: 2025-12-10T03:56:52.820Z
Learning: In the audio player widget (lib/pages/chat/chat_audio_player_widget.dart), the supported playback speeds are 0.5x, 1.0x, 1.5x, and 2.0x. The 0.75x speed is not supported.

Applied to files:

  • lib/widgets/matrix.dart
📚 Learning: 2025-12-09T06:19:59.615Z
Learnt from: tddang-linagora
Repo: linagora/twake-on-matrix PR: 2761
File: lib/pages/contacts_tab/widgets/sliver_invite_friend_button.dart:49-57
Timestamp: 2025-12-09T06:19:59.615Z
Learning: For the Twake on Matrix Flutter app (fluffychat), desktop platforms (macOS, Linux, Windows) are not supported. During reviews, assume only web and mobile targets and avoid introducing desktop-specific builds, dependencies, or platform checks. This guideline applies across Dart source files in the repository.

Applied to files:

  • lib/widgets/matrix.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). (3)
  • GitHub Check: Deploy preview versions on pull requests
  • GitHub Check: Analyze code
  • GitHub Check: Widget test
🔇 Additional comments (5)
lib/widgets/matrix.dart (5)

97-101: LGTM - Nullable audio player and queue state.

Making audioPlayer nullable enables explicit lifecycle management, and voiceMessageEvents properly supports the queue-based autoplay feature.


170-171: LGTM - Proper audio cleanup on account switch.

Cleaning up the audio player before switching accounts prevents orphaned playback state.


1147-1189: LGTM - Queue-based playback management.

The logic correctly manages the voice message queue by removing completed items and advancing to the next, with proper cleanup when the queue empties.


1382-1401: LGTM - Comprehensive audio cleanup.

The method properly resets all audio-related state and handles potential errors during cleanup.


1425-1427: LGTM - Proper resource disposal.

The nullable audio player and new voiceMessageEvents ValueNotifier are properly disposed.

/// - [clickedEvent]: The event that was clicked
///
/// Returns a list of audio events from the clicked position onwards.
List<Event> getAudioEventsUpToClicked(
Copy link
Member

Choose a reason for hiding this comment

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

test this function

Room room,
) {
return [
...(eventContext.eventsAfter?.reversed.toList() ?? []),
Copy link
Member

Choose a reason for hiding this comment

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

maybe you duplicated revert events list. Here and also in getAudioEventsUpToClicked

Copy link
Member Author

Choose a reason for hiding this comment

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

Should keep it

Copy link
Member

Choose a reason for hiding this comment

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

but why? 2 times of reverse?

});
}

Future<File?> handleOggAudioFileIniOS(File file) async {
Copy link
Member

Choose a reason for hiding this comment

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

this function be duplicated in chat_audio_player_widget & audio_player_widget & here


if (Platform.isIOS &&
matrixFile.mimeType.toLowerCase() == 'audio/ogg') {
final oggAudioFileIniOS = await handleOggAudioFileIniOS(file);
Copy link
Member

Choose a reason for hiding this comment

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

how about throwing or catch oggAudioFileIniOS == null to show error to user?

///
/// Returns a new list containing only events with messageType of
/// [MessageTypes.Image].
List<Event> filterImageEvents(List<Event> events) {
Copy link
Member

Choose a reason for hiding this comment

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

not in use?

///
/// Returns a new list containing only events with messageType of
/// [MessageTypes.Video].
List<Event> filterVideoEvents(List<Event> events) {
Copy link
Member

Choose a reason for hiding this comment

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

idem

///
/// Returns a new list containing only events with messageType of
/// [MessageTypes.Audio].
List<Event> filterAudioEvents(List<Event> events) {
Copy link
Member

Choose a reason for hiding this comment

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

idem

Comment on lines +1175 to +1179
_audioPlayerStateSubscription?.cancel();
_audioPlayerStateSubscription = null;
await audioPlayer?.stop();
await audioPlayer?.dispose();
audioPlayer = null;
Copy link
Member

Choose a reason for hiding this comment

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

It happens only in ProcessingState.completed, how about in other state?

final fileName = Uri.encodeComponent(
mxcUrl.pathSegments.last,
);
file = File('${tempDir.path}/${fileName}_${matrixFile.name}');
Copy link
Member

Choose a reason for hiding this comment

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

when you delete this temp file?

final fileName = Uri.encodeComponent(
mxcUrl.pathSegments.last,
);
file = File('${tempDir.path}/${fileName}_${matrixFile.name}');
Copy link
Member

Choose a reason for hiding this comment

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

how about also using cache memory -> cache file -> downloading?

… fixup! fixup! fixup! TW-2796: Implement autoplay for voice messages and enhance audio event handling
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

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

⚠️ Outside diff range comments (1)
lib/presentation/mixins/audio_mixin.dart (1)

246-259: Stream listeners accumulate and cause StateError on multi-chunk reads.

On each iteration of the while loop, new listeners are added to reader.onLoad and reader.onError without cancelling previous ones. When the FileReader fires onLoad for chunk N, all N accumulated listeners execute—causing earlier listeners to call complete() on already-completed Completers, which throws StateError.

Compare with createMatrixAudioFileFromWebFile (lines 332–345) which correctly cancels listeners after use.

🐛 Proposed fix
   while (offset < file.size) {
     final end =
         (offset + chunkSize) > file.size ? file.size : offset + chunkSize;
     final blob = file.slice(offset, end);

     final completer = Completer<Uint8List>();

-    reader.onLoad.listen((_) {
+    final loadListener = reader.onLoad.listen((_) {
       final result = reader.result;
       if (result is Uint8List) {
         completer.complete(result);
       } else if (result is ByteBuffer) {
         completer.complete(result.asUint8List());
       } else {
         completer.completeError('Unexpected result type');
       }
     });

-    reader.onError.listen((event) {
+    final errorListener = reader.onError.listen((event) {
       completer.completeError('FileReader failed: ${reader.error}');
     });

     reader.readAsArrayBuffer(blob);

-    yield await completer.future.timeout(
-      const Duration(seconds: 30),
-      onTimeout: () => throw TimeoutException('Chunk reading timed out'),
-    );
+    try {
+      yield await completer.future.timeout(
+        const Duration(seconds: 30),
+        onTimeout: () => throw TimeoutException('Chunk reading timed out'),
+      );
+    } finally {
+      await loadListener.cancel();
+      await errorListener.cancel();
+    }

     offset = end;
   }

… fixup! fixup! fixup! fixup! TW-2796: Implement autoplay for voice messages and enhance audio event handling
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@lib/presentation/mixins/audio_mixin.dart`:
- Around line 568-636: The autoPlayAudio flow can update currentAudioStatus and
caches after a newer call has changed voiceMessageEvent; to fix, after any async
download/IO work (just before setting currentAudioStatus.value =
AudioPlayerStatus.downloaded and before writing to disk/memory or doing iOS
conversion) check if voiceMessageEvent.value?.eventId == currentEvent.eventId
and if not, abort/return early so the stale task does not overwrite state or
caches; update references in autoPlayAudio (use voiceMessageEvent,
currentAudioStatus, _audioMemoryCache, and file/matrixFile logic) to perform
this guard right after the try block completes and also before any final file
writes or status mutations.

… fixup! fixup! fixup! fixup! fixup! TW-2796: Implement autoplay for voice messages and enhance audio event handling
… fixup! fixup! fixup! fixup! fixup! fixup! TW-2796: Implement autoplay for voice messages and enhance audio event handling
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

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

⚠️ Outside diff range comments (1)
lib/pages/chat/events/audio_message/audio_player_widget.dart (1)

171-194: Accessing widget after dispose() is unsafe.

The addPostFrameCallback accesses widget.event.eventId after dispose() has returned. At that point, the widget is unmounted and accessing widget is undefined behavior in Flutter. Capture the event ID before scheduling the callback.

🐛 Proposed fix
 `@override`
 void dispose() {
   if (!PlatformInfos.isMobile) {
+    final currentEventId = widget.event.eventId;
     WidgetsBinding.instance.addPostFrameCallback((_) async {
       // Only dispose if this event is currently playing
-      if (matrix.voiceMessageEvent.value?.eventId == widget.event.eventId) {
+      if (matrix.voiceMessageEvent.value?.eventId == currentEventId) {
         // Stop and dispose audio player asynchronously to avoid blocking
         // dispose
         final playerToDispose = matrix.audioPlayer;
         if (playerToDispose != null) {
           await playerToDispose.stop();
           await playerToDispose.dispose();
         }

         // Schedule value updates for after the current frame to avoid
         // setState() during widget tree lock
         matrix.currentAudioStatus.value = AudioPlayerStatus.notDownloaded;
         matrix.voiceMessageEvent.value = null;
       }
     });
   }

   super.dispose();
 }
🧹 Nitpick comments (1)
lib/pages/chat/events/audio_message/audio_player_widget.dart (1)

117-126: Consider awaiting or explicitly ignoring autoPlayAudio.

matrix.autoPlayAudio is called without await while it's called with await elsewhere in the codebase (e.g., in audio_mixin.dart). Though the method has internal error handling via try-catch, the inconsistency suggests this fire-and-forget pattern may be unintentional. If fire-and-forget is desired, use unawaited() from dart:async to make the intent explicit and suppress lint warnings. Otherwise, consider awaiting it for consistency.

♻️ Suggested change if fire-and-forget is intentional
     matrix.voiceMessageEvents.value = audioPending.events;

-    matrix.autoPlayAudio(currentEvent: widget.event, context: context);
+    unawaited(matrix.autoPlayAudio(currentEvent: widget.event, context: context));
   }

… fixup! fixup! fixup! fixup! fixup! fixup! fixup! TW-2796: Implement autoplay for voice messages and enhance audio event handling
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
lib/pages/chat/events/audio_message/audio_player_widget.dart (1)

89-125: Add error handling for initAudioEventsUpToClicked to prevent inconsistent state.
The initAudioEventsUpToClicked call lacks error handling and can leave the UI in an inconsistent state if it fails. While autoPlayAudio has built-in error handling, wrapping the initialization block ensures proper state reset and error feedback if the async operation fails.

🔧 Suggested guard
-    final audioPending = await initAudioEventsUpToClicked(
-      client: matrix.client,
-      room: widget.event.room,
-      clickedEvent: widget.event,
-    );
-
-    matrix.voiceMessageEvents.value = audioPending.events;
-
-    await matrix.autoPlayAudio(currentEvent: widget.event, context: context);
+    try {
+      final audioPending = await initAudioEventsUpToClicked(
+        client: matrix.client,
+        room: widget.event.room,
+        clickedEvent: widget.event,
+      );
+
+      matrix.voiceMessageEvents.value = audioPending.events;
+
+      await matrix.autoPlayAudio(currentEvent: widget.event, context: context);
+    } catch (e, s) {
+      Logs().e('Could not initialize audio playback', e, s);
+      matrix.voiceMessageEvent.value = null;
+      matrix.currentAudioStatus.value = AudioPlayerStatus.notDownloaded;
+      ScaffoldMessenger.of(context).showSnackBar(
+        SnackBar(
+          content: Text(
+            e.toLocalizedString(context) ??
+                L10n.of(context)!.couldNotPlayAudioFile,
+          ),
+        ),
+      );
+    }
🤖 Fix all issues with AI agents
In `@lib/pages/chat/events/audio_message/audio_player_widget.dart`:
- Around line 227-232: The wavePosition calculation passes
_calculatedWaveform.length as maxWaves which can be zero; guard this by using
AudioPlayerStyle.maxWaveCount(context) when _calculatedWaveform.isEmpty so
calculateWaveCountAuto never receives 0. Update the call that computes
wavePosition (the expression using calculateWaveCountAuto with minWaves and
maxWaves) to compute maxWaves = _calculatedWaveform.isNotEmpty ?
_calculatedWaveform.length : AudioPlayerStyle.maxWaveCount(context) and pass
that value into calculateWaveCountAuto; keep minWaves as
AudioPlayerStyle.minWaveCount and leave the rest of the calculation unchanged.
🧹 Nitpick comments (1)
lib/pages/chat/events/audio_message/audio_player_widget.dart (1)

133-163: Avoid resetting global audio status from every widget init.
matrix.currentAudioStatus is shared; setting it unconditionally in each widget’s initState can clobber the currently playing event’s state. Consider only updating when this widget is the active event (or when no event is active).

♻️ Suggested guard
     WidgetsBinding.instance.addPostFrameCallback((_) {
-      matrix.currentAudioStatus.value = AudioPlayerStatus.notDownloaded;
+      final isCurrentEvent =
+          matrix.voiceMessageEvent.value?.eventId == widget.event.eventId;
+      if (matrix.voiceMessageEvent.value == null || isCurrentEvent) {
+        matrix.currentAudioStatus.value = AudioPlayerStatus.notDownloaded;
+      }
       final durationInt = widget.event.content
           .tryGetMap<String, dynamic>('org.matrix.msc1767.audio')
           ?.tryGet<int>('duration');
@@
       if (_calculatedWaveform.isEmpty) {
         _calculatedWaveform.addAll(waveFromHeight);
-        matrix.currentAudioStatus.value = AudioPlayerStatus.downloaded;
+        if (matrix.voiceMessageEvent.value == null || isCurrentEvent) {
+          matrix.currentAudioStatus.value = AudioPlayerStatus.downloaded;
+        }
       }

… fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! TW-2796: Implement autoplay for voice messages and enhance audio event handling
///
/// This mixin manages audio playback state, auto-play queue, and cleanup.
/// It should be used with State classes that need audio playback capabilities.
mixin AudioPlayerMixin<T extends StatefulWidget> on State<T> {
Copy link
Member

Choose a reason for hiding this comment

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

not in use

widget.matrix?.setupAudioPlayerAutoDispose(context: context);

matrix?.audioPlayer.play().onError((e, s) {
widget.matrix?.audioPlayer?.play().onError((e, s) {
Copy link
Member

Choose a reason for hiding this comment

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

why not use try/catch instead of onError?

// Act
final result = audioMixin.calculateWaveForm(
eventWaveForm: waveform,
waveCount: 6,
Copy link
Member

Choose a reason for hiding this comment

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

we tried to generate to fill all the gap in message bubble

}

// Memory cache for audio data
final Map<String, Uint8List> _audioMemoryCache = {};
Copy link
Member

Choose a reason for hiding this comment

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

  • Cache grows unbounded during session
  • Moreover, we still need to re-download recent play audio.

How about implement LRU cache for audio?

@@ -89,19 +86,21 @@ class AudioPlayerState extends State<AudioPlayerWidget>
ScaffoldMessenger.of(matrix.context).clearMaterialBanners();
});
if (matrix.voiceMessageEvent.value?.eventId == widget.event.eventId) {
Copy link
Member

Choose a reason for hiding this comment

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

Maybe race condition for this, in case user click to play voice message rapidly and download take time?

… fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! TW-2796: Implement autoplay for voice messages and enhance audio event handling
… fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! TW-2796: Implement autoplay for voice messages and enhance audio event handling
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@lib/pages/chat/events/audio_message/audio_player_style.dart`:
- Around line 16-26: The comment is stale and the calculation can produce a
negative wave count: update the comment in maxWaveCount to accurately list the
terms being subtracted (remove or add the mentioned safetyMargin to match the
code), then guard against negative availableWidth by clamping it to zero before
dividing (i.e., compute availableWidth using
MessageStyle.messageBubbleWidth(context) minus paddingLeft, paddingRight,
playButtonWidth, spacingAfterButton, (and optional safetyMargin if you add it to
the calculation), then if availableWidth < 0 set it to 0 before computing
availableWidth ~/ maxWaveWidth) so the returned wave count is never negative;
reference MessageStyle.messageBubbleWidth, paddingLeft, paddingRight,
playButtonWidth, spacingAfterButton, safetyMargin, availableWidth, and
maxWaveWidth.

In `@lib/presentation/mixins/audio_mixin.dart`:
- Around line 770-780: Wrap the call to
audioPlayer?.setAudioSource(MatrixFileAudioSource(matrixFile)) in a try-catch
inside the block that initializes the audio player (the code that creates
audioPlayer, sets voiceMessageEvent.value, and calls
setupAudioPlayerAutoDispose). On catch, log the error, ensure the audioPlayer is
disposed/cleaned up (call dispose/stop as needed), reset voiceMessageEvent.value
to null or previous state, and return/abort further playback setup so the
exception does not propagate; keep setupAudioPlayerAutoDispose only after a
successful setAudioSource.
- Around line 547-569: putInAudioCache currently stores audio blobs even when
they're below the minimum size that getFromAudioCache rejects, wasting cache
slots; add an early size guard in putInAudioCache that mirrors getFromAudioCache
(e.g. if data.length < 1024) and simply return without modifying
_audioMemoryCache, _audioCacheAccessOrder, or _currentCacheMemoryBytes when the
blob is too small; also emit a debug/log like Logs().d indicating the small blob
was ignored so behavior matches getFromAudioCache and prevents evicting valid
entries.

In `@test/presentation/mixins/audio_mixin_test.dart`:
- Around line 672-683: The test comment is incorrect because currently
getFromAudioCache performs the size validation; update putInAudioCache to
perform the same size check (reject and do not store entries smaller than 1024
bytes) so the rejection happens at insertion time, and then adjust the test
comment in audio_mixin_test.dart to state that putInAudioCache rejects too-small
data; specifically modify the putInAudioCache implementation to check the byte
length threshold (1024) and return/skip caching when below that threshold, and
keep getFromAudioCache's existing defensive check as-is.
- Around line 685-702: The test is using a timing workaround instead of awaiting
the async cleanup; call and await the Future returned by cleanupAudioPlayer()
(the method cleanupAudioPlayer) before asserting cache state, then remove the
Future.delayed block so that after awaiting cleanupAudioPlayer you directly
verify via getFromAudioCache('event_1'), getFromAudioCache('event_2'), and
getFromAudioCache('event_3') that putInAudioCache entries were cleared.
🧹 Nitpick comments (3)
lib/presentation/mixins/audio_mixin.dart (2)

721-735: iOS OGG→CAF conversion result is not stored back in memory cache.

After the OGG-to-CAF conversion on iOS (lines 722–735), the converted audioBytes replace the original, but the in-memory cache (populated at line 694 or 717) still holds the raw OGG bytes. On the next play of the same event, the cache hit returns OGG data, triggering conversion again every time, negating the cache benefit on iOS.

Consider updating the memory cache with the converted bytes after a successful conversion:

          if (oggAudioFileIniOS != null) {
            audioBytes = await oggAudioFileIniOS.readAsBytes();
            mimeType = 'audio/x-caf';
+           putInAudioCache(currentEvent.eventId, audioBytes);
            Logs().d('AudioMixin: Conversion successful');

498-533: LRU cache uses List.remove for access-order tracking — O(n) per access.

_audioCacheAccessOrder is a List<String>. Each getFromAudioCache call performs _audioCacheAccessOrder.remove(eventId) (line 528) which is O(n), and putInAudioCache also calls remove (line 550). With up to 20 cached items this is acceptable, but using a LinkedHashMap would provide O(1) access-order updates naturally if the cache grows.

Not urgent given the small cap of 20, just noting for future consideration.

lib/presentation/mixins/event_filter_mixin.dart (1)

355-380: getAudioEventsUpToClicked docstring is misleading about expected input order.

The docstring (line 351) states the input should be "in chronological order," but the actual input from initMediaEvents (via buildInitialEventList) is reverse-chronological. The method internally reverses the input (line 359) to compensate. The timestamp filter at lines 375–377 acts as an additional safety net.

The logic works correctly given the actual reverse-chronological input, but the docstring should reflect reality to avoid confusion for future maintainers.

Comment on lines 16 to 26
static int maxWaveCount(BuildContext context) {
return (MessageStyle.messageBubbleWidth(context) - maxBodyContentWidth) ~/
maxWaveWidth;
// Calculate available width for waveform:
// messageBubbleWidth - padding - playButton - spacing - safetyMargin
final availableWidth = MessageStyle.messageBubbleWidth(context) -
paddingLeft -
paddingRight -
playButtonWidth -
spacingAfterButton;

return availableWidth ~/ maxWaveWidth;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let me examine the file to see the current state of the maxWaveCount method
cat -n lib/pages/chat/events/audio_message/audio_player_style.dart | head -35

Repository: linagora/twake-on-matrix

Length of output: 1468


🏁 Script executed:

# Search for minWaveCount and maxWaveCount usage
rg -n 'maxWaveCount|minWaveCount' --type=dart -C3

Repository: linagora/twake-on-matrix

Length of output: 3697


🏁 Script executed:

# Look for the audio_player_style.dart file content entirely to understand context
wc -l lib/pages/chat/events/audio_message/audio_player_style.dart

Repository: linagora/twake-on-matrix

Length of output: 130


Fix stale comment and guard against negative availableWidth.

Two issues:

  1. The comment on line 18 mentions safetyMargin, but the calculation doesn't subtract any safety margin.
  2. If messageBubbleWidth is too small (e.g., on a very narrow viewport), availableWidth becomes negative and ~/ maxWaveWidth returns a negative wave count, which is semantically invalid for a UI component.
Suggested fix
   static int maxWaveCount(BuildContext context) {
-    // Calculate available width for waveform:
-    // messageBubbleWidth - padding - playButton - spacing - safetyMargin
+    // Calculate available width for waveform:
+    // messageBubbleWidth - padding - playButton - spacing
     final availableWidth = MessageStyle.messageBubbleWidth(context) -
         paddingLeft -
         paddingRight -
         playButtonWidth -
         spacingAfterButton;

-    return availableWidth ~/ maxWaveWidth;
+    return (availableWidth ~/ maxWaveWidth).clamp(minWaveCount, double.infinity).toInt();
   }
🤖 Prompt for AI Agents
In `@lib/pages/chat/events/audio_message/audio_player_style.dart` around lines 16
- 26, The comment is stale and the calculation can produce a negative wave
count: update the comment in maxWaveCount to accurately list the terms being
subtracted (remove or add the mentioned safetyMargin to match the code), then
guard against negative availableWidth by clamping it to zero before dividing
(i.e., compute availableWidth using MessageStyle.messageBubbleWidth(context)
minus paddingLeft, paddingRight, playButtonWidth, spacingAfterButton, (and
optional safetyMargin if you add it to the calculation), then if availableWidth
< 0 set it to 0 before computing availableWidth ~/ maxWaveWidth) so the returned
wave count is never negative; reference MessageStyle.messageBubbleWidth,
paddingLeft, paddingRight, playButtonWidth, spacingAfterButton, safetyMargin,
availableWidth, and maxWaveWidth.

Comment on lines +547 to +569
void putInAudioCache(String eventId, Uint8List data) {
if (_audioMemoryCache.containsKey(eventId)) {
_currentCacheMemoryBytes -= _audioMemoryCache[eventId]!.length;
_audioCacheAccessOrder.remove(eventId);
}

while ((_audioMemoryCache.length >= _maxCachedAudioItems ||
_currentCacheMemoryBytes + data.length > _maxCacheMemoryBytes) &&
_audioCacheAccessOrder.isNotEmpty) {
_evictOldestAudioFromCache();
}

_audioMemoryCache[eventId] = data;
_audioCacheAccessOrder.add(eventId);
_currentCacheMemoryBytes += data.length;

Logs().d(
'AudioMixin: Cached $eventId (${data.length} bytes). '
'Cache: ${_audioMemoryCache.length}/$_maxCachedAudioItems items, '
'${_formatCacheBytes(_currentCacheMemoryBytes)}/'
'${_formatCacheBytes(_maxCacheMemoryBytes)}',
);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

putInAudioCache accepts entries below the minimum valid size that getFromAudioCache rejects.

getFromAudioCache (line 518) rejects entries smaller than 1024 bytes and removes them. But putInAudioCache stores them unconditionally, wasting a cache slot and potentially evicting a valid entry. Add the same size guard at the start of putInAudioCache.

Suggested fix
   `@visibleForTesting`
   void putInAudioCache(String eventId, Uint8List data) {
+    const minValidAudioSize = 1024;
+    if (data.length < minValidAudioSize) {
+      Logs().w(
+        'AudioMixin: Rejecting cache entry for $eventId '
+        '(${data.length} bytes < $minValidAudioSize minimum)',
+      );
+      return;
+    }
+
     if (_audioMemoryCache.containsKey(eventId)) {
       _currentCacheMemoryBytes -= _audioMemoryCache[eventId]!.length;
       _audioCacheAccessOrder.remove(eventId);
     }
🤖 Prompt for AI Agents
In `@lib/presentation/mixins/audio_mixin.dart` around lines 547 - 569,
putInAudioCache currently stores audio blobs even when they're below the minimum
size that getFromAudioCache rejects, wasting cache slots; add an early size
guard in putInAudioCache that mirrors getFromAudioCache (e.g. if data.length <
1024) and simply return without modifying _audioMemoryCache,
_audioCacheAccessOrder, or _currentCacheMemoryBytes when the blob is too small;
also emit a debug/log like Logs().d indicating the small blob was ignored so
behavior matches getFromAudioCache and prevents evicting valid entries.

Comment on lines +770 to +780
// Initialize audio player
await audioPlayer?.stop();
await audioPlayer?.dispose();

audioPlayer = AudioPlayer();
voiceMessageEvent.value = currentEvent;

await audioPlayer?.setAudioSource(MatrixFileAudioSource(matrixFile));

// Set up auto-dispose listener
setupAudioPlayerAutoDispose(context: context);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

setAudioSource is not wrapped in a try-catch — invalid audio data will crash.

Lines 771–777 dispose the old player and set a new audio source, but this section is outside the try-catch blocks above (download, lines 662–768) and below (play, lines 782–796). If setAudioSource throws (e.g., corrupt or unsupported audio), the exception propagates unhandled.

Suggested fix
     // Initialize audio player
     await audioPlayer?.stop();
     await audioPlayer?.dispose();
 
     audioPlayer = AudioPlayer();
     voiceMessageEvent.value = currentEvent;
 
-    await audioPlayer?.setAudioSource(MatrixFileAudioSource(matrixFile));
-
-    // Set up auto-dispose listener
-    setupAudioPlayerAutoDispose(context: context);
-
     try {
+      await audioPlayer?.setAudioSource(MatrixFileAudioSource(matrixFile));
+
+      // Set up auto-dispose listener
+      setupAudioPlayerAutoDispose(context: context);
+
       await audioPlayer?.play();
     } catch (e, s) {
       Logs().e('Could not play audio file', e, s);
       voiceMessageEvent.value = null;
       currentAudioStatus.value = AudioPlayerStatus.notDownloaded;
       if (!context.mounted) return;
       ScaffoldMessenger.of(context).showSnackBar(
         SnackBar(
           content: Text(
             e.toLocalizedString(context),
           ),
         ),
       );
     }
🤖 Prompt for AI Agents
In `@lib/presentation/mixins/audio_mixin.dart` around lines 770 - 780, Wrap the
call to audioPlayer?.setAudioSource(MatrixFileAudioSource(matrixFile)) in a
try-catch inside the block that initializes the audio player (the code that
creates audioPlayer, sets voiceMessageEvent.value, and calls
setupAudioPlayerAutoDispose). On catch, log the error, ensure the audioPlayer is
disposed/cleaned up (call dispose/stop as needed), reset voiceMessageEvent.value
to null or previous state, and return/abort further playback setup so the
exception does not propagate; keep setupAudioPlayerAutoDispose only after a
successful setAudioSource.

Comment on lines +685 to +702
test('should clear all cache data on cleanup', () {
// Arrange
final data = Uint8List.fromList(List.generate(2048, (i) => i % 256));
audioMixin.putInAudioCache('event_1', data);
audioMixin.putInAudioCache('event_2', data);
audioMixin.putInAudioCache('event_3', data);

// Act
audioMixin.cleanupAudioPlayer();

// Give async cleanup time to complete
return Future.delayed(const Duration(milliseconds: 100), () {
// Assert - All cache should be cleared
expect(audioMixin.getFromAudioCache('event_1'), null);
expect(audioMixin.getFromAudioCache('event_2'), null);
expect(audioMixin.getFromAudioCache('event_3'), null);
});
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Await cleanupAudioPlayer() instead of relying on Future.delayed.

cleanupAudioPlayer() returns a Future<void>, but line 693 discards it. The test then uses Future.delayed(100ms) as a timing-based workaround, which is inherently flaky.

Suggested fix
-    test('should clear all cache data on cleanup', () {
+    test('should clear all cache data on cleanup', () async {
       // Arrange
       final data = Uint8List.fromList(List.generate(2048, (i) => i % 256));
       audioMixin.putInAudioCache('event_1', data);
       audioMixin.putInAudioCache('event_2', data);
       audioMixin.putInAudioCache('event_3', data);

       // Act
-      audioMixin.cleanupAudioPlayer();
+      await audioMixin.cleanupAudioPlayer();

-      // Give async cleanup time to complete
-      return Future.delayed(const Duration(milliseconds: 100), () {
-        // Assert - All cache should be cleared
-        expect(audioMixin.getFromAudioCache('event_1'), null);
-        expect(audioMixin.getFromAudioCache('event_2'), null);
-        expect(audioMixin.getFromAudioCache('event_3'), null);
-      });
+      // Assert - All cache should be cleared
+      expect(audioMixin.getFromAudioCache('event_1'), null);
+      expect(audioMixin.getFromAudioCache('event_2'), null);
+      expect(audioMixin.getFromAudioCache('event_3'), null);
     });
📝 Committable suggestion

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

Suggested change
test('should clear all cache data on cleanup', () {
// Arrange
final data = Uint8List.fromList(List.generate(2048, (i) => i % 256));
audioMixin.putInAudioCache('event_1', data);
audioMixin.putInAudioCache('event_2', data);
audioMixin.putInAudioCache('event_3', data);
// Act
audioMixin.cleanupAudioPlayer();
// Give async cleanup time to complete
return Future.delayed(const Duration(milliseconds: 100), () {
// Assert - All cache should be cleared
expect(audioMixin.getFromAudioCache('event_1'), null);
expect(audioMixin.getFromAudioCache('event_2'), null);
expect(audioMixin.getFromAudioCache('event_3'), null);
});
});
test('should clear all cache data on cleanup', () async {
// Arrange
final data = Uint8List.fromList(List.generate(2048, (i) => i % 256));
audioMixin.putInAudioCache('event_1', data);
audioMixin.putInAudioCache('event_2', data);
audioMixin.putInAudioCache('event_3', data);
// Act
await audioMixin.cleanupAudioPlayer();
// Assert - All cache should be cleared
expect(audioMixin.getFromAudioCache('event_1'), null);
expect(audioMixin.getFromAudioCache('event_2'), null);
expect(audioMixin.getFromAudioCache('event_3'), null);
});
🤖 Prompt for AI Agents
In `@test/presentation/mixins/audio_mixin_test.dart` around lines 685 - 702, The
test is using a timing workaround instead of awaiting the async cleanup; call
and await the Future returned by cleanupAudioPlayer() (the method
cleanupAudioPlayer) before asserting cache state, then remove the Future.delayed
block so that after awaiting cleanupAudioPlayer you directly verify via
getFromAudioCache('event_1'), getFromAudioCache('event_2'), and
getFromAudioCache('event_3') that putInAudioCache entries were cleared.

… fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! TW-2796: Implement autoplay for voice messages and enhance audio event handling
… fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! TW-2796: Implement autoplay for voice messages and enhance audio event handling
Copy link
Collaborator

@tddang-linagora tddang-linagora left a comment

Choose a reason for hiding this comment

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

  • When select another audio record when the previous one is still loading, user has to wait for the previous one to download
ap-fast-select.mov
  • There's no loading indicator when user tap an audio record
ap-loading-missing.mov
  • If the audio is cached, why does it take so long to play the same audio record again?
ap-not-cached.mov
  • The progresses in the audio wave and in pinned audio player are desynced
ap-progresses-desync.mov

Comment on lines +41 to +42
class _ChatAudioPlayerWidgetState extends State<ChatAudioPlayerWidget>
with AudioMixin {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can AudioMixin be used with StatelessWidget? Why does ChatAudioPlayerWidget need to be StatefulWidget

Copy link
Member Author

Choose a reason for hiding this comment

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

but it's mixing in AudioMixin which has mutable fields. This violates Flutter's immutability requirement.

@nqhhdev
Copy link
Member Author

nqhhdev commented Feb 11, 2026

ap-not-cached.mov

  • The progresses in the audio wave and in pinned audio player are desynced
Simulator.Screen.Recording.-.iPhone.17.-.2026-02-11.at.15.36.07.mov

… fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! TW-2796: Implement autoplay for voice messages and enhance audio event handling
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants