Skip to content

Commit 40b2237

Browse files
committed
TW-2796: Implement autoplay for voice messages and enhance audio event handling
1 parent b28ed45 commit 40b2237

File tree

5 files changed

+781
-113
lines changed

5 files changed

+781
-113
lines changed

lib/pages/chat/chat.dart

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3130,13 +3130,13 @@ class ChatController extends State<Chat>
31303130
return;
31313131
}
31323132
disposeAudioMixin();
3133-
matrix?.audioPlayer.stop();
3134-
matrix?.audioPlayer.clearAudioSources();
3133+
matrix?.audioPlayer?.stop();
3134+
matrix?.audioPlayer?.clearAudioSources();
31353135
matrix?.voiceMessageEvent.value = null;
31363136
}
31373137

31383138
void initAudioPlayer() {
3139-
if (matrix?.audioPlayer.playing == true) {
3139+
if (matrix?.audioPlayer?.playing == true) {
31403140
if (!PlatformInfos.isMobile) {
31413141
matrix?.audioPlayer
31423142
?..stop()
@@ -3145,12 +3145,14 @@ class ChatController extends State<Chat>
31453145
// On mobile, keep audio playing and return early
31463146
return;
31473147
}
3148-
if (matrix?.voiceMessageEvent != null) {
3149-
matrix?.voiceMessageEvent.value = null;
3150-
}
3148+
if (!PlatformInfos.isMobile) {
3149+
if (matrix?.voiceMessageEvent != null) {
3150+
matrix?.voiceMessageEvent.value = null;
3151+
}
31513152

3152-
if (matrix?.currentAudioStatus.value != AudioPlayerStatus.notDownloaded) {
3153-
matrix?.currentAudioStatus.value = AudioPlayerStatus.notDownloaded;
3153+
if (matrix?.currentAudioStatus.value != AudioPlayerStatus.notDownloaded) {
3154+
matrix?.currentAudioStatus.value = AudioPlayerStatus.notDownloaded;
3155+
}
31543156
}
31553157
}
31563158

lib/pages/chat/chat_audio_player_widget.dart

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,16 @@ class ChatAudioPlayerWidget extends StatelessWidget {
3131

3232
@override
3333
Widget build(BuildContext context) {
34+
// Return empty if matrix is not available
35+
if (matrix == null) {
36+
return const SizedBox.shrink();
37+
}
38+
3439
final defaultAudioStatus = ValueNotifier<AudioPlayerStatus>(
3540
AudioPlayerStatus.notDownloaded,
3641
);
3742
final defaultEvent = ValueNotifier<Event?>(null);
43+
3844
return ValueListenableBuilder(
3945
valueListenable: matrix?.currentAudioStatus ?? defaultAudioStatus,
4046
builder: (context, status, _) {
@@ -47,11 +53,11 @@ class ChatAudioPlayerWidget extends StatelessWidget {
4753
final audioPlayer = matrix?.audioPlayer;
4854
return StreamBuilder<Object>(
4955
stream: StreamGroup.merge([
50-
matrix?.audioPlayer.positionStream.asBroadcastStream() ??
56+
matrix?.audioPlayer?.positionStream.asBroadcastStream() ??
5157
Stream.value(Duration.zero),
52-
matrix?.audioPlayer.playerStateStream.asBroadcastStream() ??
58+
matrix?.audioPlayer?.playerStateStream.asBroadcastStream() ??
5359
Stream.value(Duration.zero),
54-
matrix?.audioPlayer.speedStream.asBroadcastStream() ??
60+
matrix?.audioPlayer?.speedStream.asBroadcastStream() ??
5561
Stream.value(Duration.zero),
5662
]),
5763
builder: (context, snapshot) {
@@ -152,8 +158,8 @@ class ChatAudioPlayerWidget extends StatelessWidget {
152158
Future<void> _handleCloseAudioPlayer() async {
153159
matrix?.voiceMessageEvent.value = null;
154160
matrix?.cancelAudioPlayerAutoDispose();
155-
await matrix?.audioPlayer.stop();
156-
await matrix?.audioPlayer.dispose();
161+
await matrix?.audioPlayer?.stop();
162+
await matrix?.audioPlayer?.dispose();
157163
matrix?.currentAudioStatus.value = AudioPlayerStatus.notDownloaded;
158164
}
159165

@@ -169,8 +175,8 @@ class ChatAudioPlayerWidget extends StatelessWidget {
169175
Future<void> _handlePlayAudioAgain(BuildContext context) async {
170176
File? file;
171177
MatrixFile? matrixFile;
172-
await matrix?.audioPlayer.stop();
173-
await matrix?.audioPlayer.dispose();
178+
await matrix?.audioPlayer?.stop();
179+
await matrix?.audioPlayer?.dispose();
174180
matrix?.currentAudioStatus.value = AudioPlayerStatus.notDownloaded;
175181
final currentEvent = matrix?.voiceMessageEvent.value;
176182

@@ -200,13 +206,16 @@ class ChatAudioPlayerWidget extends StatelessWidget {
200206

201207
matrix?.currentAudioStatus.value = AudioPlayerStatus.downloaded;
202208
} catch (e, s) {
203-
Logs().v('Could not download audio file', e, s);
204-
ScaffoldMessenger.of(context).showSnackBar(
205-
SnackBar(
206-
content: Text(e.toLocalizedString(context)),
207-
),
208-
);
209-
rethrow;
209+
Logs().e('Could not download audio file', e, s);
210+
if (context.mounted) {
211+
ScaffoldMessenger.of(context).showSnackBar(
212+
SnackBar(
213+
content: Text(e.toLocalizedString(context)),
214+
),
215+
);
216+
}
217+
matrix?.currentAudioStatus.value = AudioPlayerStatus.notDownloaded;
218+
return;
210219
}
211220
if (!context.mounted) return;
212221

@@ -222,10 +231,10 @@ class ChatAudioPlayerWidget extends StatelessWidget {
222231
matrix!.audioPlayer = AudioPlayer();
223232

224233
if (file != null) {
225-
await matrix?.audioPlayer.setFilePath(file.path);
234+
await matrix?.audioPlayer?.setFilePath(file.path);
226235
} else if (matrixFile != null) {
227236
await matrix?.audioPlayer
228-
.setAudioSource(MatrixFileAudioSource(matrixFile));
237+
?.setAudioSource(MatrixFileAudioSource(matrixFile));
229238
} else {
230239
ScaffoldMessenger.of(context).showSnackBar(
231240
SnackBar(
@@ -238,7 +247,7 @@ class ChatAudioPlayerWidget extends StatelessWidget {
238247
// Set up auto-dispose listener managed globally in MatrixState
239248
matrix?.setupAudioPlayerAutoDispose();
240249

241-
matrix?.audioPlayer.play().onError((e, s) {
250+
matrix?.audioPlayer?.play().onError((e, s) {
242251
Logs().e('Could not play audio file', e, s);
243252
ScaffoldMessenger.of(context).showSnackBar(
244253
SnackBar(

lib/pages/chat/events/audio_message/audio_player_widget.dart

Lines changed: 30 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:fluffychat/pages/chat/events/audio_message/audio_player_style.da
66
import 'package:fluffychat/pages/chat/events/message/message_style.dart';
77
import 'package:fluffychat/pages/chat/seen_by_row.dart';
88
import 'package:fluffychat/presentation/mixins/audio_mixin.dart';
9+
import 'package:fluffychat/presentation/mixins/event_filter_mixin.dart';
910
import 'package:fluffychat/utils/matrix_sdk_extensions/download_file_extension.dart';
1011
import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart';
1112
import 'package:fluffychat/utils/platform_infos.dart';
@@ -16,14 +17,12 @@ import 'package:fluffychat/widgets/file_widget/file_tile_widget.dart';
1617
import 'package:fluffychat/widgets/file_widget/message_file_tile_style.dart';
1718
import 'package:fluffychat/widgets/matrix.dart';
1819
import 'package:fluffychat/widgets/twake_components/twake_icon_button.dart';
19-
import 'package:flutter/foundation.dart';
2020
import 'package:flutter/material.dart';
2121
import 'package:intl/intl.dart';
2222
import 'package:just_audio/just_audio.dart';
2323
import 'package:linagora_design_flutter/linagora_design_flutter.dart';
2424
import 'package:matrix/matrix.dart';
2525
import 'package:fluffychat/generated/l10n/app_localizations.dart';
26-
import 'package:path_provider/path_provider.dart';
2726

2827
import 'package:fluffychat/utils/localized_exception_extension.dart';
2928
import 'package:opus_caf_converter_dart/opus_caf_converter_dart.dart';
@@ -49,7 +48,7 @@ class AudioPlayerWidget extends StatefulWidget {
4948
enum AudioPlayerStatus { notDownloaded, downloading, downloaded }
5049

5150
class AudioPlayerState extends State<AudioPlayerWidget>
52-
with AudioMixin, AutomaticKeepAliveClientMixin {
51+
with AudioMixin, AutomaticKeepAliveClientMixin, EventFilterMixin {
5352
final List<double> _calculatedWaveform = [];
5453

5554
final ValueNotifier<Duration> _durationNotifier =
@@ -89,18 +88,18 @@ class AudioPlayerState extends State<AudioPlayerWidget>
8988
ScaffoldMessenger.of(matrix.context).clearMaterialBanners();
9089
});
9190
if (matrix.voiceMessageEvent.value?.eventId == widget.event.eventId) {
92-
if (matrix.audioPlayer.isAtEndPosition) {
91+
if (matrix.audioPlayer?.isAtEndPosition == true) {
9392
matrix.voiceMessageEvent.value = null;
94-
await matrix.audioPlayer.stop();
95-
await matrix.audioPlayer.dispose();
93+
await matrix.audioPlayer?.stop();
94+
await matrix.audioPlayer?.dispose();
9695
matrix.currentAudioStatus.value = AudioPlayerStatus.notDownloaded;
9796
await _onButtonTap();
9897
return;
9998
}
100-
if (matrix.audioPlayer.playing == true) {
101-
matrix.audioPlayer.pause();
99+
if (matrix.audioPlayer?.playing == true) {
100+
matrix.audioPlayer?.pause();
102101
} else {
103-
matrix.audioPlayer.play().onError((e, s) {
102+
matrix.audioPlayer?.play().onError((e, s) {
104103
Logs().e('Could not play audio file', e, s);
105104
ScaffoldMessenger.of(context).showSnackBar(
106105
SnackBar(
@@ -115,66 +114,15 @@ class AudioPlayerState extends State<AudioPlayerWidget>
115114
return;
116115
}
117116

118-
matrix.voiceMessageEvent.value = widget.event;
119-
await matrix.audioPlayer.stop();
120-
await matrix.audioPlayer.dispose();
121-
File? file;
122-
MatrixFile? matrixFile;
123-
124-
matrix.currentAudioStatus.value = AudioPlayerStatus.downloading;
125-
try {
126-
matrixFile = await widget.event.downloadAndDecryptAttachment();
127-
128-
if (!kIsWeb) {
129-
final tempDir = await getTemporaryDirectory();
130-
final fileName = Uri.encodeComponent(
131-
widget.event.attachmentOrThumbnailMxcUrl()!.pathSegments.last,
132-
);
133-
file = File('${tempDir.path}/${fileName}_${matrixFile.name}');
134-
135-
await file.writeAsBytes(matrixFile.bytes ?? []);
136-
137-
if (Platform.isIOS &&
138-
matrixFile.mimeType.toLowerCase() == 'audio/ogg') {
139-
file = await handleOggAudioFileIniOS(file);
140-
}
141-
}
142-
143-
matrix.currentAudioStatus.value = AudioPlayerStatus.downloaded;
144-
} catch (e, s) {
145-
Logs().v('Could not download audio file', e, s);
146-
ScaffoldMessenger.of(context).showSnackBar(
147-
SnackBar(
148-
content: Text(e.toLocalizedString(context)),
149-
),
150-
);
151-
rethrow;
152-
}
153-
if (matrix.voiceMessageEvent.value?.eventId != widget.event.eventId) return;
154-
matrix.audioPlayer = AudioPlayer();
155-
matrix.voiceMessageEvent.value = widget.event;
156-
157-
if (file != null) {
158-
matrix.audioPlayer.setFilePath(file.path);
159-
} else {
160-
await matrix.audioPlayer
161-
.setAudioSource(MatrixFileAudioSource(matrixFile));
162-
}
117+
final audioPending = await initAudioEventsUpToClicked(
118+
client: matrix.client,
119+
room: widget.event.room,
120+
clickedEvent: widget.event,
121+
);
163122

164-
// Set up auto-dispose listener managed globally in MatrixState
165-
matrix.setupAudioPlayerAutoDispose();
123+
matrix.voiceMessageEvents.value = audioPending.events;
166124

167-
matrix.audioPlayer.play().onError((e, s) {
168-
Logs().e('Could not play audio file', e, s);
169-
ScaffoldMessenger.of(context).showSnackBar(
170-
SnackBar(
171-
content: Text(
172-
e?.toLocalizedString(context) ??
173-
L10n.of(context)!.couldNotPlayAudioFile,
174-
),
175-
),
176-
);
177-
});
125+
matrix.autoPlayAudio(currentEvent: widget.event);
178126
}
179127

180128
Future<File> handleOggAudioFileIniOS(File file) async {
@@ -231,21 +179,23 @@ class AudioPlayerState extends State<AudioPlayerWidget>
231179
@override
232180
void dispose() {
233181
if (!PlatformInfos.isMobile) {
234-
// Stop and dispose audio player asynchronously to avoid blocking dispose
235-
matrix.audioPlayer.stop().then((_) {
236-
matrix.audioPlayer.dispose();
237-
}).catchError((error) {
238-
Logs().e('Error disposing audio player', error);
239-
});
240-
241-
// Schedule value updates for after the current frame to avoid
242-
// setState() during widget tree lock
243-
WidgetsBinding.instance.addPostFrameCallback((_) {
244-
if (matrix.voiceMessageEvent.value?.eventId == widget.event.eventId) {
182+
// Only dispose if this event is currently playing
183+
if (matrix.voiceMessageEvent.value?.eventId == widget.event.eventId) {
184+
// Stop and dispose audio player asynchronously to avoid blocking
185+
// dispose
186+
matrix.audioPlayer?.stop().then((_) {
187+
matrix.audioPlayer?.dispose();
188+
}).catchError((error) {
189+
Logs().e('Error disposing audio player', error);
190+
});
191+
192+
// Schedule value updates for after the current frame to avoid
193+
// setState() during widget tree lock
194+
WidgetsBinding.instance.addPostFrameCallback((_) {
245195
matrix.currentAudioStatus.value = AudioPlayerStatus.notDownloaded;
246196
matrix.voiceMessageEvent.value = null;
247-
}
248-
});
197+
});
198+
}
249199
}
250200

251201
super.dispose();

0 commit comments

Comments
 (0)