Skip to content
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
fff3951
TW-2766: Displayed audio player if user goes to list of chats or to a…
nqhhdev Dec 10, 2025
89795de
fixup! fixup! TW-2766: Displayed audio player if user goes to list of…
nqhhdev Dec 19, 2025
9bd409c
TW-2796: Implement autoplay for voice messages and enhance audio even…
nqhhdev Dec 23, 2025
e8b6891
fixup! TW-2796: Implement autoplay for voice messages and enhance aud…
nqhhdev Jan 6, 2026
2fa6daa
fixup! fixup! TW-2796: Implement autoplay for voice messages and enha…
nqhhdev Jan 6, 2026
f7a7530
fixup! fixup! fixup! TW-2796: Implement autoplay for voice messages a…
nqhhdev Jan 12, 2026
b0b98e5
fixup! fixup! fixup! fixup! TW-2796: Implement autoplay for voice mes…
nqhhdev Jan 12, 2026
e838060
fixup! fixup! fixup! fixup! fixup! TW-2796: Implement autoplay for vo…
nqhhdev Jan 12, 2026
9832fe6
fixup! fixup! fixup! fixup! fixup! fixup! TW-2796: Implement autoplay…
nqhhdev Jan 12, 2026
242258e
fixup! fixup! fixup! fixup! fixup! fixup! fixup! TW-2796: Implement a…
nqhhdev Jan 12, 2026
4f02ae7
fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! TW-2796: Impl…
nqhhdev Jan 15, 2026
ca05e5d
fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! TW-279…
nqhhdev Jan 15, 2026
3efdf7e
fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup!…
nqhhdev Jan 16, 2026
58e4ce7
fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup!…
nqhhdev Jan 16, 2026
bbd89a8
fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup!…
nqhhdev Jan 19, 2026
8d24e67
fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup!…
nqhhdev Jan 19, 2026
a6b07d6
fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup!…
nqhhdev Jan 19, 2026
2338f7f
fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup!…
nqhhdev Jan 19, 2026
a18d44b
fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup!…
nqhhdev Jan 22, 2026
5a38912
fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup!…
nqhhdev Jan 22, 2026
cb5a40f
fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup!…
nqhhdev Jan 22, 2026
b7a6e5b
fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup!…
nqhhdev Feb 9, 2026
2b6e3d5
fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup!…
nqhhdev Feb 9, 2026
d4dd228
fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup!…
nqhhdev Feb 10, 2026
f67fcd5
fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup!…
nqhhdev Feb 10, 2026
ff145e6
fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup!…
nqhhdev Feb 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 9 additions & 17 deletions lib/pages/chat/chat.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3120,18 +3120,8 @@ class ChatController extends State<Chat>
);
}

void disposeAudioPlayer() {
if (PlatformInfos.isMobile) {
return;
}
disposeAudioMixin();
matrix?.audioPlayer.stop();
matrix?.audioPlayer.clearAudioSources();
matrix?.voiceMessageEvent.value = null;
}

void initAudioPlayer() {
if (matrix?.audioPlayer.playing == true) {
if (matrix?.audioPlayer?.playing == true) {
if (!PlatformInfos.isMobile) {
matrix?.audioPlayer
?..stop()
Expand All @@ -3140,12 +3130,14 @@ class ChatController extends State<Chat>
// On mobile, keep audio playing and return early
return;
}
if (matrix?.voiceMessageEvent != null) {
matrix?.voiceMessageEvent.value = null;
}
if (!PlatformInfos.isMobile) {
if (matrix?.voiceMessageEvent != null) {
matrix?.voiceMessageEvent.value = null;
}

if (matrix?.currentAudioStatus.value != AudioPlayerStatus.notDownloaded) {
matrix?.currentAudioStatus.value = AudioPlayerStatus.notDownloaded;
if (matrix?.currentAudioStatus.value != AudioPlayerStatus.notDownloaded) {
matrix?.currentAudioStatus.value = AudioPlayerStatus.notDownloaded;
}
}
}

Expand Down Expand Up @@ -3269,7 +3261,7 @@ class ChatController extends State<Chat>
showScrollDownButtonNotifier.dispose();
editEventNotifier.dispose();
focusHover.dispose();
disposeAudioPlayer();
disposeAudioMixin();
super.dispose();
}

Expand Down
116 changes: 67 additions & 49 deletions lib/pages/chat/chat_audio_player_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:fluffychat/resource/image_paths.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:fluffychat/utils/string_extension.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:fluffychat/presentation/mixins/audio_mixin.dart';
import 'package:fluffychat/widgets/twake_components/twake_icon_button.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
Expand All @@ -16,10 +17,9 @@ import 'package:fluffychat/generated/l10n/app_localizations.dart';
import 'package:just_audio/just_audio.dart';
import 'package:linagora_design_flutter/linagora_design_flutter.dart';
import 'package:matrix/matrix.dart';
import 'package:opus_caf_converter_dart/opus_caf_converter_dart.dart';
import 'package:path_provider/path_provider.dart';

class ChatAudioPlayerWidget extends StatelessWidget {
class ChatAudioPlayerWidget extends StatefulWidget {
final MatrixState? matrix;
final bool enableBorder;

Expand All @@ -29,29 +29,45 @@ class ChatAudioPlayerWidget extends StatelessWidget {
super.key,
});

static final _defaultAudioStatus = ValueNotifier<AudioPlayerStatus>(
AudioPlayerStatus.notDownloaded,
);
static final _defaultEvent = ValueNotifier<Event?>(null);

@override
State<ChatAudioPlayerWidget> createState() => _ChatAudioPlayerWidgetState();
}

class _ChatAudioPlayerWidgetState extends State<ChatAudioPlayerWidget>
with AudioMixin {
Comment on lines +41 to +42
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.

@override
Widget build(BuildContext context) {
final defaultAudioStatus = ValueNotifier<AudioPlayerStatus>(
AudioPlayerStatus.notDownloaded,
);
final defaultEvent = ValueNotifier<Event?>(null);
// Return empty if matrix is not available
if (widget.matrix == null) {
return const SizedBox.shrink();
}

return ValueListenableBuilder(
valueListenable: matrix?.currentAudioStatus ?? defaultAudioStatus,
valueListenable: widget.matrix?.currentAudioStatus ??
ChatAudioPlayerWidget._defaultAudioStatus,
builder: (context, status, _) {
return ValueListenableBuilder(
valueListenable: matrix?.voiceMessageEvent ?? defaultEvent,
valueListenable: widget.matrix?.voiceMessageEvent ??
ChatAudioPlayerWidget._defaultEvent,
builder: (context, hasEvent, _) {
if (hasEvent == null) {
return const SizedBox.shrink();
}
final audioPlayer = matrix?.audioPlayer;
final audioPlayer = widget.matrix?.audioPlayer;
return StreamBuilder<Object>(
stream: StreamGroup.merge([
matrix?.audioPlayer.positionStream.asBroadcastStream() ??
widget.matrix?.audioPlayer?.positionStream
.asBroadcastStream() ??
Stream.value(Duration.zero),
matrix?.audioPlayer.playerStateStream.asBroadcastStream() ??
widget.matrix?.audioPlayer?.playerStateStream
.asBroadcastStream() ??
Stream.value(Duration.zero),
matrix?.audioPlayer.speedStream.asBroadcastStream() ??
widget.matrix?.audioPlayer?.speedStream.asBroadcastStream() ??
Stream.value(Duration.zero),
]),
builder: (context, snapshot) {
Expand All @@ -67,7 +83,7 @@ class ChatAudioPlayerWidget extends StatelessWidget {
constraints: const BoxConstraints(maxHeight: 40),
decoration: BoxDecoration(
color: LinagoraSysColors.material().onPrimary,
border: enableBorder
border: widget.enableBorder
? Border(
top: BorderSide(
color: LinagoraStateLayer(
Expand Down Expand Up @@ -150,31 +166,22 @@ class ChatAudioPlayerWidget extends StatelessWidget {
}

Future<void> _handleCloseAudioPlayer() async {
matrix?.voiceMessageEvent.value = null;
matrix?.cancelAudioPlayerAutoDispose();
await matrix?.audioPlayer.stop();
await matrix?.audioPlayer.dispose();
matrix?.currentAudioStatus.value = AudioPlayerStatus.notDownloaded;
}

Future<File> _handleOggAudioFileIniOS(File file) async {
Logs().v('Convert ogg audio file for iOS...');
final convertedFile = File('${file.path}.caf');
if (await convertedFile.exists() == false) {
OpusCaf().convertOpusToCaf(file.path, convertedFile.path);
}
return convertedFile;
widget.matrix?.voiceMessageEvent.value = null;
widget.matrix?.cancelAudioPlayerAutoDispose();
await widget.matrix?.audioPlayer?.stop();
await widget.matrix?.audioPlayer?.dispose();
widget.matrix?.currentAudioStatus.value = AudioPlayerStatus.notDownloaded;
}

Future<void> _handlePlayAudioAgain(BuildContext context) async {
File? file;
MatrixFile? matrixFile;
await matrix?.audioPlayer.stop();
await matrix?.audioPlayer.dispose();
matrix?.currentAudioStatus.value = AudioPlayerStatus.notDownloaded;
final currentEvent = matrix?.voiceMessageEvent.value;
await widget.matrix?.audioPlayer?.stop();
await widget.matrix?.audioPlayer?.dispose();
widget.matrix?.currentAudioStatus.value = AudioPlayerStatus.notDownloaded;
final currentEvent = widget.matrix?.voiceMessageEvent.value;

matrix?.currentAudioStatus.value = AudioPlayerStatus.downloading;
widget.matrix?.currentAudioStatus.value = AudioPlayerStatus.downloading;

try {
matrixFile = await currentEvent?.downloadAndDecryptAttachment();
Expand All @@ -194,23 +201,31 @@ class ChatAudioPlayerWidget extends StatelessWidget {

if (Platform.isIOS &&
matrixFile?.mimeType.toLowerCase() == 'audio/ogg') {
file = await _handleOggAudioFileIniOS(file);
final converted = await handleOggAudioFileIniOS(file);
if (converted == null) {
throw Exception('OGG to CAF conversion failed');
}
file = converted;
}
}

matrix?.currentAudioStatus.value = AudioPlayerStatus.downloaded;
widget.matrix?.currentAudioStatus.value = AudioPlayerStatus.downloaded;
} catch (e, s) {
Logs().v('Could not download audio file', e, s);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(e.toLocalizedString(context)),
),
);
rethrow;
Logs().e('Could not download audio file', e, s);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(e.toLocalizedString(context)),
),
);
}
widget.matrix?.currentAudioStatus.value = AudioPlayerStatus.notDownloaded;
return;
}
if (!context.mounted) return;

if (matrix == null) {
if (widget.matrix == null) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.couldNotPlayAudioFile),
Expand All @@ -219,13 +234,13 @@ class ChatAudioPlayerWidget extends StatelessWidget {
return;
}

matrix!.audioPlayer = AudioPlayer();
widget.matrix!.audioPlayer = AudioPlayer();

if (file != null) {
await matrix?.audioPlayer.setFilePath(file.path);
await widget.matrix?.audioPlayer?.setFilePath(file.path);
} else if (matrixFile != null) {
await matrix?.audioPlayer
.setAudioSource(MatrixFileAudioSource(matrixFile));
await widget.matrix?.audioPlayer
?.setAudioSource(MatrixFileAudioSource(matrixFile));
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
Expand All @@ -236,10 +251,13 @@ class ChatAudioPlayerWidget extends StatelessWidget {
}

// Set up auto-dispose listener managed globally in MatrixState
matrix?.setupAudioPlayerAutoDispose();
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?

Logs().e('Could not play audio file', e, s);
widget.matrix?.voiceMessageEvent.value = null;
widget.matrix?.currentAudioStatus.value = AudioPlayerStatus.notDownloaded;
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
Expand All @@ -252,7 +270,7 @@ class ChatAudioPlayerWidget extends StatelessWidget {
}

Future<void> _handlePlayOrPauseAudioPlayer(BuildContext context) async {
final audioPlayer = matrix?.audioPlayer;
final audioPlayer = widget.matrix?.audioPlayer;
if (audioPlayer == null) return;
if (audioPlayer.isAtEndPosition) {
await _handlePlayAudioAgain(context);
Expand Down
Loading