Skip to content

Commit

Permalink
feat(ui): voice recording attachment builder (#1907)
Browse files Browse the repository at this point in the history
* feat(llc,ui): voice recording attachment builder

* test: add coverage

* fix: fix formatting

* uncomment logic
  • Loading branch information
esarbanis authored May 10, 2024
1 parent ca92ee0 commit 27cbb03
Show file tree
Hide file tree
Showing 24 changed files with 1,573 additions and 14 deletions.
5 changes: 5 additions & 0 deletions packages/stream_chat/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## Unreleased

✅ Added
- Added `voiceRecording` attachment type

## 7.2.0-hotfix.1

- Version to keep in sync with the rest of the packages
Expand Down
1 change: 1 addition & 0 deletions packages/stream_chat/lib/src/core/models/attachment.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ mixin AttachmentType {
static const giphy = 'giphy';
static const video = 'video';
static const audio = 'audio';
static const voiceRecording = 'voiceRecording';

/// Application custom types.
static const urlPreview = 'url_preview';
Expand Down
5 changes: 5 additions & 0 deletions packages/stream_chat_flutter/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## Unreleased

✅ Added
- Added `VoiceRecordingAttachmentBuilder`, for displaying voice recording attachments in the chat.

## 7.2.0-hotfix.1

🔄 Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1300;
LastUpgradeCheck = 1430;
ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
Expand Down Expand Up @@ -204,6 +204,7 @@
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1300"
LastUpgradeVersion = "1430"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,17 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/attachment/attachment.dart';
import 'package:stream_chat_flutter/src/attachment/thumbnail/media_attachment_thumbnail.dart';
import 'package:stream_chat_flutter/src/stream_chat.dart';
import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart';
import 'package:stream_chat_flutter/src/utils/utils.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';

part 'fallback_attachment_builder.dart';

part 'file_attachment_builder.dart';

part 'gallery_attachment_builder.dart';

part 'giphy_attachment_builder.dart';

part 'image_attachment_builder.dart';

part 'mixed_attachment_builder.dart';

part 'url_attachment_builder.dart';

part 'video_attachment_builder.dart';
part 'voice_recording_attachment_builder/voice_recording_attachment_builder.dart';

/// {@template streamAttachmentWidgetTapCallback}
/// Signature for a function that's called when the user taps on an attachment.
Expand Down Expand Up @@ -120,6 +111,9 @@ abstract class StreamAttachmentWidgetBuilder {
padding: padding,
onAttachmentTap: onAttachmentTap,
),

VoiceRecordingAttachmentBuilder(),

// We don't handle URL attachments if the message is a reply.
if (message.quotedMessage == null)
UrlAttachmentBuilder(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import 'dart:async';

import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:just_audio/just_audio.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';

/// {@template StreamVoiceRecordingListPlayer}
/// Display many audios and displays a list of AudioPlayerMessage.
/// {@endtemplate}
class StreamVoiceRecordingListPlayer extends StatefulWidget {
/// {@macro StreamVoiceRecordingListPlayer}
const StreamVoiceRecordingListPlayer({
super.key,
required this.playList,
this.attachmentBorderRadiusGeometry,
this.constraints,
});

/// List of audio attachments.
final List<PlayListItem> playList;

/// The border radius of each audio.
final BorderRadiusGeometry? attachmentBorderRadiusGeometry;

/// Constraints of audio attachments
final BoxConstraints? constraints;

@override
State<StreamVoiceRecordingListPlayer> createState() =>
_StreamVoiceRecordingListPlayerState();
}

class _StreamVoiceRecordingListPlayerState
extends State<StreamVoiceRecordingListPlayer> {
final _player = AudioPlayer();
late StreamSubscription<PlayerState> _playerStateChangedSubscription;

Widget _createAudioPlayer(int index, PlayListItem item) {
final url = item.assetUrl;
Widget child;

if (url == null) {
child = const StreamVoiceRecordingLoading();
} else {
child = StreamVoiceRecordingPlayer(
player: _player,
duration: item.duration,
waveBars: item.waveForm,
index: index,
);
}

final theme =
StreamChatTheme.of(context).voiceRecordingTheme.listPlayerTheme;

return Container(
margin: theme.margin,
constraints: widget.constraints,
decoration: BoxDecoration(
color: theme.backgroundColor,
border: Border.all(
color: theme.borderColor!,
),
borderRadius:
widget.attachmentBorderRadiusGeometry ?? theme.borderRadius,
),
child: child,
);
}

void _playerStateListener(PlayerState state) async {
if (state.processingState == ProcessingState.completed) {
await _player.stop();
await _player.seek(Duration.zero, index: 0);
}
}

@override
void initState() {
super.initState();

_playerStateChangedSubscription =
_player.playerStateStream.listen(_playerStateListener);
}

@override
void dispose() {
super.dispose();

_playerStateChangedSubscription.cancel();
_player.dispose();
}

@override
Widget build(BuildContext context) {
final playList = widget.playList
.where((attachment) => attachment.assetUrl != null)
.map((attachment) => AudioSource.uri(Uri.parse(attachment.assetUrl!)))
.toList();

final audioSource = ConcatenatingAudioSource(children: playList);

_player
..setShuffleModeEnabled(false)
..setLoopMode(LoopMode.off)
..setAudioSource(audioSource, preload: false);

return Column(
children: widget.playList.mapIndexed(_createAudioPlayer).toList(),
);
}
}

/// {@template PlayListItem}
/// Represents an audio attachment meta data.
/// {@endtemplate}
class PlayListItem {
/// {@macro PlayListItem}
const PlayListItem({
this.assetUrl,
required this.duration,
required this.waveForm,
});

/// The url of the audio.
final String? assetUrl;

/// The duration of the audio.
final Duration duration;

/// The wave form of the audio.
final List<double> waveForm;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';

/// {@template StreamVoiceRecordingLoading}
/// Loading widget for audio message. Use this when the url from the audio
/// message is still not available. One use situation in when the audio is
/// still being uploaded.
/// {@endtemplate}
class StreamVoiceRecordingLoading extends StatelessWidget {
/// {@macro StreamVoiceRecordingLoading}
const StreamVoiceRecordingLoading({super.key});

@override
Widget build(BuildContext context) {
final theme = StreamChatTheme.of(context).voiceRecordingTheme.loadingTheme;

return Padding(
padding: theme.padding!,
child: SizedBox(
height: theme.size!.height,
width: theme.size!.width,
child: CircularProgressIndicator(
strokeWidth: theme.strokeWidth!,
color: theme.color,
),
),
);
}
}
Loading

0 comments on commit 27cbb03

Please sign in to comment.