Skip to content

Commit 27cbb03

Browse files
authored
feat(ui): voice recording attachment builder (#1907)
* feat(llc,ui): voice recording attachment builder * test: add coverage * fix: fix formatting * uncomment logic
1 parent ca92ee0 commit 27cbb03

24 files changed

+1573
-14
lines changed

packages/stream_chat/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## Unreleased
2+
3+
✅ Added
4+
- Added `voiceRecording` attachment type
5+
16
## 7.2.0-hotfix.1
27

38
- Version to keep in sync with the rest of the packages

packages/stream_chat/lib/src/core/models/attachment.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ mixin AttachmentType {
1717
static const giphy = 'giphy';
1818
static const video = 'video';
1919
static const audio = 'audio';
20+
static const voiceRecording = 'voiceRecording';
2021

2122
/// Application custom types.
2223
static const urlPreview = 'url_preview';

packages/stream_chat_flutter/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## Unreleased
2+
3+
✅ Added
4+
- Added `VoiceRecordingAttachmentBuilder`, for displaying voice recording attachments in the chat.
5+
16
## 7.2.0-hotfix.1
27

38
🔄 Changed

packages/stream_chat_flutter/example/ios/Runner.xcodeproj/project.pbxproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@
155155
97C146E61CF9000F007C117D /* Project object */ = {
156156
isa = PBXProject;
157157
attributes = {
158-
LastUpgradeCheck = 1300;
158+
LastUpgradeCheck = 1430;
159159
ORGANIZATIONNAME = "";
160160
TargetAttributes = {
161161
97C146ED1CF9000F007C117D = {
@@ -204,6 +204,7 @@
204204
files = (
205205
);
206206
inputPaths = (
207+
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
207208
);
208209
name = "Thin Binary";
209210
outputPaths = (

packages/stream_chat_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<Scheme
3-
LastUpgradeVersion = "1300"
3+
LastUpgradeVersion = "1430"
44
version = "1.3">
55
<BuildAction
66
parallelizeBuildables = "YES"

packages/stream_chat_flutter/lib/src/attachment/builder/attachment_widget_builder.dart

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,17 @@
1+
import 'package:collection/collection.dart';
12
import 'package:flutter/material.dart';
2-
import 'package:stream_chat_flutter/src/attachment/attachment.dart';
33
import 'package:stream_chat_flutter/src/attachment/thumbnail/media_attachment_thumbnail.dart';
4-
import 'package:stream_chat_flutter/src/stream_chat.dart';
5-
import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart';
6-
import 'package:stream_chat_flutter/src/utils/utils.dart';
7-
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
4+
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
85

96
part 'fallback_attachment_builder.dart';
10-
117
part 'file_attachment_builder.dart';
12-
138
part 'gallery_attachment_builder.dart';
14-
159
part 'giphy_attachment_builder.dart';
16-
1710
part 'image_attachment_builder.dart';
18-
1911
part 'mixed_attachment_builder.dart';
20-
2112
part 'url_attachment_builder.dart';
22-
2313
part 'video_attachment_builder.dart';
14+
part 'voice_recording_attachment_builder/voice_recording_attachment_builder.dart';
2415

2516
/// {@template streamAttachmentWidgetTapCallback}
2617
/// Signature for a function that's called when the user taps on an attachment.
@@ -120,6 +111,9 @@ abstract class StreamAttachmentWidgetBuilder {
120111
padding: padding,
121112
onAttachmentTap: onAttachmentTap,
122113
),
114+
115+
VoiceRecordingAttachmentBuilder(),
116+
123117
// We don't handle URL attachments if the message is a reply.
124118
if (message.quotedMessage == null)
125119
UrlAttachmentBuilder(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import 'dart:async';
2+
3+
import 'package:collection/collection.dart';
4+
import 'package:flutter/material.dart';
5+
import 'package:just_audio/just_audio.dart';
6+
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
7+
8+
/// {@template StreamVoiceRecordingListPlayer}
9+
/// Display many audios and displays a list of AudioPlayerMessage.
10+
/// {@endtemplate}
11+
class StreamVoiceRecordingListPlayer extends StatefulWidget {
12+
/// {@macro StreamVoiceRecordingListPlayer}
13+
const StreamVoiceRecordingListPlayer({
14+
super.key,
15+
required this.playList,
16+
this.attachmentBorderRadiusGeometry,
17+
this.constraints,
18+
});
19+
20+
/// List of audio attachments.
21+
final List<PlayListItem> playList;
22+
23+
/// The border radius of each audio.
24+
final BorderRadiusGeometry? attachmentBorderRadiusGeometry;
25+
26+
/// Constraints of audio attachments
27+
final BoxConstraints? constraints;
28+
29+
@override
30+
State<StreamVoiceRecordingListPlayer> createState() =>
31+
_StreamVoiceRecordingListPlayerState();
32+
}
33+
34+
class _StreamVoiceRecordingListPlayerState
35+
extends State<StreamVoiceRecordingListPlayer> {
36+
final _player = AudioPlayer();
37+
late StreamSubscription<PlayerState> _playerStateChangedSubscription;
38+
39+
Widget _createAudioPlayer(int index, PlayListItem item) {
40+
final url = item.assetUrl;
41+
Widget child;
42+
43+
if (url == null) {
44+
child = const StreamVoiceRecordingLoading();
45+
} else {
46+
child = StreamVoiceRecordingPlayer(
47+
player: _player,
48+
duration: item.duration,
49+
waveBars: item.waveForm,
50+
index: index,
51+
);
52+
}
53+
54+
final theme =
55+
StreamChatTheme.of(context).voiceRecordingTheme.listPlayerTheme;
56+
57+
return Container(
58+
margin: theme.margin,
59+
constraints: widget.constraints,
60+
decoration: BoxDecoration(
61+
color: theme.backgroundColor,
62+
border: Border.all(
63+
color: theme.borderColor!,
64+
),
65+
borderRadius:
66+
widget.attachmentBorderRadiusGeometry ?? theme.borderRadius,
67+
),
68+
child: child,
69+
);
70+
}
71+
72+
void _playerStateListener(PlayerState state) async {
73+
if (state.processingState == ProcessingState.completed) {
74+
await _player.stop();
75+
await _player.seek(Duration.zero, index: 0);
76+
}
77+
}
78+
79+
@override
80+
void initState() {
81+
super.initState();
82+
83+
_playerStateChangedSubscription =
84+
_player.playerStateStream.listen(_playerStateListener);
85+
}
86+
87+
@override
88+
void dispose() {
89+
super.dispose();
90+
91+
_playerStateChangedSubscription.cancel();
92+
_player.dispose();
93+
}
94+
95+
@override
96+
Widget build(BuildContext context) {
97+
final playList = widget.playList
98+
.where((attachment) => attachment.assetUrl != null)
99+
.map((attachment) => AudioSource.uri(Uri.parse(attachment.assetUrl!)))
100+
.toList();
101+
102+
final audioSource = ConcatenatingAudioSource(children: playList);
103+
104+
_player
105+
..setShuffleModeEnabled(false)
106+
..setLoopMode(LoopMode.off)
107+
..setAudioSource(audioSource, preload: false);
108+
109+
return Column(
110+
children: widget.playList.mapIndexed(_createAudioPlayer).toList(),
111+
);
112+
}
113+
}
114+
115+
/// {@template PlayListItem}
116+
/// Represents an audio attachment meta data.
117+
/// {@endtemplate}
118+
class PlayListItem {
119+
/// {@macro PlayListItem}
120+
const PlayListItem({
121+
this.assetUrl,
122+
required this.duration,
123+
required this.waveForm,
124+
});
125+
126+
/// The url of the audio.
127+
final String? assetUrl;
128+
129+
/// The duration of the audio.
130+
final Duration duration;
131+
132+
/// The wave form of the audio.
133+
final List<double> waveForm;
134+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
3+
4+
/// {@template StreamVoiceRecordingLoading}
5+
/// Loading widget for audio message. Use this when the url from the audio
6+
/// message is still not available. One use situation in when the audio is
7+
/// still being uploaded.
8+
/// {@endtemplate}
9+
class StreamVoiceRecordingLoading extends StatelessWidget {
10+
/// {@macro StreamVoiceRecordingLoading}
11+
const StreamVoiceRecordingLoading({super.key});
12+
13+
@override
14+
Widget build(BuildContext context) {
15+
final theme = StreamChatTheme.of(context).voiceRecordingTheme.loadingTheme;
16+
17+
return Padding(
18+
padding: theme.padding!,
19+
child: SizedBox(
20+
height: theme.size!.height,
21+
width: theme.size!.width,
22+
child: CircularProgressIndicator(
23+
strokeWidth: theme.strokeWidth!,
24+
color: theme.color,
25+
),
26+
),
27+
);
28+
}
29+
}

0 commit comments

Comments
 (0)