diff --git a/apps/design_system_gallery/lib/components/message_composer/message_composer.dart b/apps/design_system_gallery/lib/components/message_composer/message_composer.dart index e74dccc..07fed36 100644 --- a/apps/design_system_gallery/lib/components/message_composer/message_composer.dart +++ b/apps/design_system_gallery/lib/components/message_composer/message_composer.dart @@ -7,22 +7,30 @@ import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; // Playground // ============================================================================= +final emptyVoiceRecordingCallback = VoiceRecordingCallback( + onLongPressStart: () {}, + onLongPressCancel: () {}, + onLongPressEnd: (_) {}, + onLongPressMoveUpdate: (_) {}, +); + @widgetbook.UseCase( name: 'Playground', - type: StreamBaseMessageComposer, + type: StreamCoreMessageComposer, path: '[Components]/Message Composer', ) Widget buildStreamMessageComposerPlayground(BuildContext context) { final textEditingController = TextEditingController(); return Center( - child: StreamBaseMessageComposer( + child: StreamCoreMessageComposer( controller: textEditingController, isFloating: false, - inputTrailing: StreamMessageComposerInputTrailing( + inputTrailing: StreamCoreMessageComposerInputTrailing( controller: textEditingController, onSendPressed: () {}, - onMicrophonePressed: () {}, + voiceRecordingCallback: emptyVoiceRecordingCallback, + buttonState: StreamMessageComposerInputTrailingState.microphone, ), ), ); @@ -34,7 +42,7 @@ Widget buildStreamMessageComposerPlayground(BuildContext context) { @widgetbook.UseCase( name: 'Real-world Example', - type: StreamBaseMessageComposer, + type: StreamCoreMessageComposer, path: '[Components]/Message Composer', ) Widget buildStreamMessageComposerExample(BuildContext context) { @@ -126,13 +134,14 @@ Widget buildStreamMessageComposerExample(BuildContext context) { left: 0, right: 0, bottom: 0, - child: StreamBaseMessageComposer( + child: StreamCoreMessageComposer( controller: textEditingController, isFloating: true, - inputTrailing: StreamMessageComposerInputTrailing( + inputTrailing: StreamCoreMessageComposerInputTrailing( controller: textEditingController, onSendPressed: () {}, - onMicrophonePressed: () {}, + voiceRecordingCallback: emptyVoiceRecordingCallback, + buttonState: StreamMessageComposerInputTrailingState.microphone, ), ), ), @@ -160,13 +169,14 @@ Widget buildStreamMessageComposerExample(BuildContext context) { ), ), // Non-floating composer - StreamBaseMessageComposer( + StreamCoreMessageComposer( controller: textEditingController, isFloating: false, - inputTrailing: StreamMessageComposerInputTrailing( + inputTrailing: StreamCoreMessageComposerInputTrailing( controller: textEditingController, onSendPressed: () {}, - onMicrophonePressed: () {}, + voiceRecordingCallback: emptyVoiceRecordingCallback, + buttonState: StreamMessageComposerInputTrailingState.microphone, ), ), ], diff --git a/apps/design_system_gallery/lib/components/message_composer/message_composer_attachment_link_preview.dart b/apps/design_system_gallery/lib/components/message_composer/message_composer_attachment_link_preview.dart index 20add47..25d0f9a 100644 --- a/apps/design_system_gallery/lib/components/message_composer/message_composer_attachment_link_preview.dart +++ b/apps/design_system_gallery/lib/components/message_composer/message_composer_attachment_link_preview.dart @@ -9,7 +9,7 @@ import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; @widgetbook.UseCase( name: 'Playground', - type: MessageComposerAttachmentLinkPreview, + type: MessageComposerLinkPreviewAttachment, path: '[Components]/Message Composer', ) Widget buildMessageComposerAttachmentLinkPreviewPlayground(BuildContext context) { @@ -42,7 +42,7 @@ Widget buildMessageComposerAttachmentLinkPreviewPlayground(BuildContext context) return Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 360), - child: MessageComposerAttachmentLinkPreview( + child: MessageComposerLinkPreviewAttachment( title: title.isEmpty ? null : title, subtitle: subtitle.isEmpty ? null : subtitle, url: url.isEmpty ? null : url, diff --git a/apps/design_system_gallery/lib/components/message_composer/message_composer_attachment_media_file.dart b/apps/design_system_gallery/lib/components/message_composer/message_composer_attachment_media_file.dart index 5698d92..bb79ecb 100644 --- a/apps/design_system_gallery/lib/components/message_composer/message_composer_attachment_media_file.dart +++ b/apps/design_system_gallery/lib/components/message_composer/message_composer_attachment_media_file.dart @@ -8,12 +8,12 @@ import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; @widgetbook.UseCase( name: 'Playground', - type: MessageComposerAttachmentMediaFile, + type: MessageComposerMediaFileAttachment, path: '[Components]/Message Composer', ) Widget buildMessageComposerAttachmentMediaFilePlayground(BuildContext context) { return Center( - child: MessageComposerAttachmentMediaFile( + child: MessageComposerMediaFileAttachment.image( image: const AssetImage('assets/attachment_image.png'), onRemovePressed: () {}, ), diff --git a/apps/design_system_gallery/lib/components/message_composer/message_composer_attachment_reply.dart b/apps/design_system_gallery/lib/components/message_composer/message_composer_attachment_reply.dart index fe4db18..2ea43b0 100644 --- a/apps/design_system_gallery/lib/components/message_composer/message_composer_attachment_reply.dart +++ b/apps/design_system_gallery/lib/components/message_composer/message_composer_attachment_reply.dart @@ -9,7 +9,7 @@ import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; @widgetbook.UseCase( name: 'Playground', - type: MessageComposerAttachmentReply, + type: MessageComposerReplyAttachment, path: '[Components]/Message Composer', ) Widget buildMessageComposerAttachmentReplyPlayground(BuildContext context) { @@ -24,7 +24,7 @@ Widget buildMessageComposerAttachmentReplyPlayground(BuildContext context) { return Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 360), - child: MessageComposerAttachmentReply( + child: MessageComposerReplyAttachment( title: 'Reply to John Doe', subtitle: 'We had a great time during our holiday.', image: const AssetImage('assets/attachment_image.png'), diff --git a/packages/stream_core_flutter/lib/src/components.dart b/packages/stream_core_flutter/lib/src/components.dart index 511b62e..94c1ef5 100644 --- a/packages/stream_core_flutter/lib/src/components.dart +++ b/packages/stream_core_flutter/lib/src/components.dart @@ -4,6 +4,7 @@ export 'components/avatar/stream_avatar.dart' hide DefaultStreamAvatar; export 'components/avatar/stream_avatar_group.dart' hide DefaultStreamAvatarGroup; export 'components/avatar/stream_avatar_stack.dart' hide DefaultStreamAvatarStack; export 'components/badge/stream_badge_count.dart' hide DefaultStreamBadgeCount; +export 'components/badge/stream_media_badge.dart'; export 'components/badge/stream_online_indicator.dart' hide DefaultStreamOnlineIndicator; export 'components/buttons/stream_button.dart' hide DefaultStreamButton; export 'components/buttons/stream_emoji_button.dart' hide DefaultStreamEmojiButton; @@ -11,6 +12,7 @@ export 'components/common/stream_checkbox.dart' hide DefaultStreamCheckbox; export 'components/common/stream_progress_bar.dart' hide DefaultStreamProgressBar; export 'components/context_menu/stream_context_menu.dart'; export 'components/context_menu/stream_context_menu_item.dart' hide DefaultStreamContextMenuItem; +export 'components/controls/stream_remove_control.dart'; export 'components/message_composer.dart'; export 'components/reaction/picker/stream_reaction_picker_sheet.dart'; diff --git a/packages/stream_core_flutter/lib/src/components/badge/media_badge.dart b/packages/stream_core_flutter/lib/src/components/badge/stream_media_badge.dart similarity index 62% rename from packages/stream_core_flutter/lib/src/components/badge/media_badge.dart rename to packages/stream_core_flutter/lib/src/components/badge/stream_media_badge.dart index 62f47f7..b522606 100644 --- a/packages/stream_core_flutter/lib/src/components/badge/media_badge.dart +++ b/packages/stream_core_flutter/lib/src/components/badge/stream_media_badge.dart @@ -2,12 +2,8 @@ import 'package:flutter/widgets.dart'; import '../../../stream_core_flutter.dart'; -class MediaBadge extends StatelessWidget { - const MediaBadge({ - super.key, - required this.type, - required this.duration, - }); +class StreamMediaBadge extends StatelessWidget { + const StreamMediaBadge({super.key, required this.type, this.duration}); final MediaBadgeType type; final Duration? duration; @@ -17,13 +13,14 @@ class MediaBadge extends StatelessWidget { return Container( decoration: BoxDecoration( color: context.streamColorScheme.backgroundInverse, - shape: BoxShape.circle, + borderRadius: BorderRadius.all(context.streamRadius.max), ), padding: EdgeInsets.symmetric( horizontal: context.streamSpacing.xs, vertical: context.streamSpacing.xxs, ), - child: Column( + child: Row( + mainAxisSize: MainAxisSize.min, children: [ Icon( switch (type) { @@ -31,10 +28,14 @@ class MediaBadge extends StatelessWidget { MediaBadgeType.audio => context.streamIcons.microphoneSolid, }, size: 12, - color: context.streamColorScheme.textPrimary, + color: context.streamColorScheme.textOnDark, ), - if (duration case final duration?) Text(duration.toReadableString()), + if (duration case final duration?) + Text( + duration.toReadableString(), + style: context.streamTextTheme.numericMd.copyWith(color: context.streamColorScheme.textOnDark), + ), ], ), ); @@ -53,7 +54,4 @@ extension on Duration { } } -enum MediaBadgeType { - video, - audio, -} +enum MediaBadgeType { video, audio } diff --git a/packages/stream_core_flutter/lib/src/components/controls/remove_control.dart b/packages/stream_core_flutter/lib/src/components/controls/stream_remove_control.dart similarity index 90% rename from packages/stream_core_flutter/lib/src/components/controls/remove_control.dart rename to packages/stream_core_flutter/lib/src/components/controls/stream_remove_control.dart index ca32bd5..58a60cd 100644 --- a/packages/stream_core_flutter/lib/src/components/controls/remove_control.dart +++ b/packages/stream_core_flutter/lib/src/components/controls/stream_remove_control.dart @@ -2,8 +2,8 @@ import 'package:flutter/widgets.dart'; import '../../../stream_core_flutter.dart'; -class RemoveControl extends StatelessWidget { - const RemoveControl({ +class StreamRemoveControl extends StatelessWidget { + const StreamRemoveControl({ super.key, required this.onPressed, }); diff --git a/packages/stream_core_flutter/lib/src/components/message_composer.dart b/packages/stream_core_flutter/lib/src/components/message_composer.dart index 998a193..5cf1e94 100644 --- a/packages/stream_core_flutter/lib/src/components/message_composer.dart +++ b/packages/stream_core_flutter/lib/src/components/message_composer.dart @@ -1,6 +1,7 @@ -export 'message_composer/attachment/message_composer_attachment_link_preview.dart'; -export 'message_composer/attachment/message_composer_attachment_media_file.dart'; -export 'message_composer/attachment/message_composer_attachment_reply.dart'; +export 'message_composer/attachment/message_composer_file_attachment.dart'; +export 'message_composer/attachment/message_composer_link_preview_attachment.dart'; +export 'message_composer/attachment/message_composer_media_file_attachment.dart'; +export 'message_composer/attachment/message_composer_reply_attachment.dart'; export 'message_composer/message_composer.dart'; export 'message_composer/message_composer_input.dart'; export 'message_composer/message_composer_input_trailing.dart'; diff --git a/packages/stream_core_flutter/lib/src/components/message_composer/attachment/message_composer_attachment_media_file.dart b/packages/stream_core_flutter/lib/src/components/message_composer/attachment/message_composer_attachment_media_file.dart deleted file mode 100644 index 9c4a577..0000000 --- a/packages/stream_core_flutter/lib/src/components/message_composer/attachment/message_composer_attachment_media_file.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:flutter/widgets.dart'; - -import '../../../../stream_core_flutter.dart'; -import '../../controls/remove_control.dart'; - -class MessageComposerAttachmentMediaFile extends StatelessWidget { - const MessageComposerAttachmentMediaFile({super.key, required this.image, required this.onRemovePressed}); - - final ImageProvider image; - final VoidCallback onRemovePressed; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 80, - width: 80, - child: Stack( - children: [ - Padding( - padding: EdgeInsets.all(context.streamSpacing.xxs), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.all(context.streamRadius.lg), - border: Border.all( - color: context.streamColorScheme.borderDefault.withAlpha(25), - ), - image: DecorationImage(image: image, fit: BoxFit.cover), - ), - ), - ), - Align( - alignment: Alignment.topRight, - child: RemoveControl(onPressed: onRemovePressed), - ), - ], - ), - ); - } -} diff --git a/packages/stream_core_flutter/lib/src/components/message_composer/attachment/message_composer_file_attachment.dart b/packages/stream_core_flutter/lib/src/components/message_composer/attachment/message_composer_file_attachment.dart new file mode 100644 index 0000000..f5f2ef0 --- /dev/null +++ b/packages/stream_core_flutter/lib/src/components/message_composer/attachment/message_composer_file_attachment.dart @@ -0,0 +1,71 @@ +import 'package:flutter/widgets.dart'; + +import '../../../../stream_core_flutter.dart'; + +class MessageComposerFileAttachment extends StatelessWidget { + const MessageComposerFileAttachment({ + super.key, + this.title, + this.fileTypeIcon, + this.subtitle, + this.onRemovePressed, + }); + + final StreamFileTypeIcon? fileTypeIcon; + final String? title; + final Widget? subtitle; + final VoidCallback? onRemovePressed; + + @override + Widget build(BuildContext context) { + final textColor = context.streamColorScheme.textPrimary; + + final titleStyle = context.streamTextTheme.captionEmphasis.copyWith(color: textColor); + + final spacing = context.streamSpacing; + return Stack( + children: [ + Container( + margin: EdgeInsets.all(spacing.xxs), + padding: EdgeInsets.fromLTRB(spacing.md, spacing.md, spacing.sm, spacing.md), + foregroundDecoration: BoxDecoration( + borderRadius: BorderRadius.all(context.streamRadius.lg), + border: Border.all( + color: context.streamColorScheme.borderDefault, + ), + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.all(context.streamRadius.lg), + ), + child: Row( + children: [ + ?fileTypeIcon, + SizedBox(width: spacing.xs), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (title case final title?) + Text( + title, + style: titleStyle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ?subtitle, + ], + ), + ), + ], + ), + ), + if (onRemovePressed case final VoidCallback onRemovePressed?) + Align( + alignment: Alignment.topRight, + child: StreamRemoveControl(onPressed: onRemovePressed), + ), + ], + ); + } +} diff --git a/packages/stream_core_flutter/lib/src/components/message_composer/attachment/message_composer_attachment_link_preview.dart b/packages/stream_core_flutter/lib/src/components/message_composer/attachment/message_composer_link_preview_attachment.dart similarity index 93% rename from packages/stream_core_flutter/lib/src/components/message_composer/attachment/message_composer_attachment_link_preview.dart rename to packages/stream_core_flutter/lib/src/components/message_composer/attachment/message_composer_link_preview_attachment.dart index 29652bd..cde9dbc 100644 --- a/packages/stream_core_flutter/lib/src/components/message_composer/attachment/message_composer_attachment_link_preview.dart +++ b/packages/stream_core_flutter/lib/src/components/message_composer/attachment/message_composer_link_preview_attachment.dart @@ -1,10 +1,9 @@ import 'package:flutter/widgets.dart'; import '../../../../stream_core_flutter.dart'; -import '../../controls/remove_control.dart'; -class MessageComposerAttachmentLinkPreview extends StatelessWidget { - const MessageComposerAttachmentLinkPreview({ +class MessageComposerLinkPreviewAttachment extends StatelessWidget { + const MessageComposerLinkPreviewAttachment({ super.key, this.title, this.subtitle, @@ -95,7 +94,7 @@ class MessageComposerAttachmentLinkPreview extends StatelessWidget { if (onRemovePressed case final VoidCallback onRemovePressed?) Align( alignment: Alignment.topRight, - child: RemoveControl(onPressed: onRemovePressed), + child: StreamRemoveControl(onPressed: onRemovePressed), ), ], ); diff --git a/packages/stream_core_flutter/lib/src/components/message_composer/attachment/message_composer_media_file_attachment.dart b/packages/stream_core_flutter/lib/src/components/message_composer/attachment/message_composer_media_file_attachment.dart new file mode 100644 index 0000000..8ea5756 --- /dev/null +++ b/packages/stream_core_flutter/lib/src/components/message_composer/attachment/message_composer_media_file_attachment.dart @@ -0,0 +1,71 @@ +import 'package:flutter/widgets.dart'; + +import '../../../../stream_core_flutter.dart'; + +class MessageComposerMediaFileAttachment extends StatelessWidget { + const MessageComposerMediaFileAttachment({ + super.key, + required this.child, + this.onRemovePressed, + this.mediaBadge, + }); + + MessageComposerMediaFileAttachment.image({ + super.key, + required ImageProvider image, + required this.onRemovePressed, + ImageFrameBuilder? frameBuilder, + ImageErrorWidgetBuilder? errorBuilder, + this.mediaBadge, + }) : child = Image( + image: image, + frameBuilder: frameBuilder, + errorBuilder: errorBuilder, + fit: BoxFit.cover, + ); + + final Widget child; + final Widget? mediaBadge; + final VoidCallback? onRemovePressed; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 80, + width: 80, + child: Stack( + children: [ + Padding( + padding: EdgeInsets.all(context.streamSpacing.xxs), + child: Container( + clipBehavior: Clip.antiAlias, + foregroundDecoration: BoxDecoration( + borderRadius: BorderRadius.all(context.streamRadius.lg), + border: Border.all( + color: context.streamColorScheme.borderDefault.withAlpha(25), + ), + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.all(context.streamRadius.lg), + ), + child: child, + ), + ), + if (onRemovePressed case final VoidCallback onRemovePressed?) + Align( + alignment: Alignment.topRight, + child: StreamRemoveControl(onPressed: onRemovePressed), + ), + if (mediaBadge case final Widget mediaBadge?) + Align( + alignment: Alignment.bottomLeft, + child: Padding( + padding: EdgeInsets.all(context.streamSpacing.xs), + child: mediaBadge, + ), + ), + ], + ), + ); + } +} diff --git a/packages/stream_core_flutter/lib/src/components/message_composer/attachment/message_composer_attachment_reply.dart b/packages/stream_core_flutter/lib/src/components/message_composer/attachment/message_composer_reply_attachment.dart similarity index 94% rename from packages/stream_core_flutter/lib/src/components/message_composer/attachment/message_composer_attachment_reply.dart rename to packages/stream_core_flutter/lib/src/components/message_composer/attachment/message_composer_reply_attachment.dart index c2dba45..3a298b7 100644 --- a/packages/stream_core_flutter/lib/src/components/message_composer/attachment/message_composer_attachment_reply.dart +++ b/packages/stream_core_flutter/lib/src/components/message_composer/attachment/message_composer_reply_attachment.dart @@ -2,10 +2,9 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import '../../../../stream_core_flutter.dart'; -import '../../controls/remove_control.dart'; -class MessageComposerAttachmentReply extends StatelessWidget { - const MessageComposerAttachmentReply({ +class MessageComposerReplyAttachment extends StatelessWidget { + const MessageComposerReplyAttachment({ super.key, required this.title, required this.subtitle, @@ -96,7 +95,7 @@ class MessageComposerAttachmentReply extends StatelessWidget { if (onRemovePressed case final VoidCallback onRemovePressed?) Align( alignment: Alignment.topRight, - child: RemoveControl(onPressed: onRemovePressed), + child: StreamRemoveControl(onPressed: onRemovePressed), ), ], ); diff --git a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer.dart b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer.dart index d7d38f4..4ee9f8a 100644 --- a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer.dart +++ b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer.dart @@ -5,8 +5,8 @@ import 'package:flutter/widgets.dart'; import '../../../stream_core_flutter.dart'; -class StreamBaseMessageComposer extends StatefulWidget { - const StreamBaseMessageComposer({ +class StreamCoreMessageComposer extends StatefulWidget { + const StreamCoreMessageComposer({ super.key, required this.controller, required this.isFloating, @@ -16,6 +16,7 @@ class StreamBaseMessageComposer extends StatefulWidget { this.composerTrailing, this.inputLeading, this.inputTrailing, + this.inputBody, this.inputHeader, }); @@ -28,13 +29,14 @@ class StreamBaseMessageComposer extends StatefulWidget { final Widget? composerTrailing; final Widget? inputLeading; final Widget? inputTrailing; + final Widget? inputBody; final Widget? inputHeader; @override - State createState() => _StreamBaseMessageComposerState(); + State createState() => _StreamCoreMessageComposerState(); } -class _StreamBaseMessageComposerState extends State { +class _StreamCoreMessageComposerState extends State { late TextEditingController _controller; @override @@ -44,7 +46,7 @@ class _StreamBaseMessageComposerState extends State { } @override - void didUpdateWidget(StreamBaseMessageComposer oldWidget) { + void didUpdateWidget(StreamCoreMessageComposer oldWidget) { super.didUpdateWidget(oldWidget); if (widget.controller != oldWidget.controller) { _disposeController(oldWidget); @@ -62,7 +64,7 @@ class _StreamBaseMessageComposerState extends State { _controller = widget.controller ?? TextEditingController(); } - void _disposeController(StreamBaseMessageComposer widget) { + void _disposeController(StreamCoreMessageComposer widget) { if (widget.controller == null) { _controller.dispose(); } @@ -98,6 +100,7 @@ class _StreamBaseMessageComposerState extends State { isFloating: widget.isFloating, inputLeading: widget.inputLeading, inputTrailing: widget.inputTrailing, + inputBody: widget.inputBody, inputHeader: widget.inputHeader, focusNode: widget.focusNode, ), diff --git a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input.dart b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input.dart index 7fb96cb..989f9c6 100644 --- a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input.dart +++ b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input.dart @@ -10,6 +10,7 @@ class StreamMessageComposerInput extends StatelessWidget { this.isFloating = false, this.inputLeading, this.inputTrailing, + this.inputBody, this.inputHeader, this.focusNode, }); @@ -19,18 +20,23 @@ class StreamMessageComposerInput extends StatelessWidget { final bool isFloating; final Widget? inputLeading; final Widget? inputTrailing; + final Widget? inputBody; final Widget? inputHeader; final FocusNode? focusNode; @override Widget build(BuildContext context) { - return DecoratedBox( - decoration: BoxDecoration( - color: context.streamColorScheme.backgroundElevation1, + return Container( + clipBehavior: Clip.antiAlias, + foregroundDecoration: BoxDecoration( borderRadius: BorderRadius.all(context.streamRadius.xxxl), border: Border.all( color: context.streamColorScheme.borderDefault, ), + ), + decoration: BoxDecoration( + color: context.streamColorScheme.backgroundElevation1, + borderRadius: BorderRadius.all(context.streamRadius.xxxl), boxShadow: isFloating ? context.streamBoxShadow.elevation3 : null, ), child: Column( @@ -42,11 +48,13 @@ class StreamMessageComposerInput extends StatelessWidget { children: [ ?inputLeading, Expanded( - child: _MessageComposerInputField( - controller: controller, - placeholder: placeholder, - focusNode: focusNode, - ), + child: + inputBody ?? + _MessageComposerInputField( + controller: controller, + placeholder: placeholder, + focusNode: focusNode, + ), ), ?inputTrailing, ], diff --git a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_trailing.dart b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_trailing.dart index c8caed3..6f7cb77 100644 --- a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_trailing.dart +++ b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_trailing.dart @@ -3,70 +3,99 @@ import 'package:flutter/widgets.dart'; import '../../../stream_core_flutter.dart'; -class StreamMessageComposerInputTrailing extends StatefulWidget { - const StreamMessageComposerInputTrailing({ +enum StreamMessageComposerInputTrailingState { + send, + microphone, + voiceRecordingActive, +} + +class StreamCoreMessageComposerInputTrailing extends StatelessWidget { + const StreamCoreMessageComposerInputTrailing({ super.key, required this.controller, required this.onSendPressed, - required this.onMicrophonePressed, + required this.voiceRecordingCallback, + this.buttonState = StreamMessageComposerInputTrailingState.send, }); final TextEditingController controller; final VoidCallback onSendPressed; - final VoidCallback? onMicrophonePressed; - - @override - State createState() => _StreamMessageComposerInputTrailingState(); -} - -class _StreamMessageComposerInputTrailingState extends State { - var _hasText = false; - - @override - void initState() { - super.initState(); - widget.controller.addListener(_onInputTextChanged); - _hasText = widget.controller.text.isNotEmpty; - } - - @override - void didUpdateWidget(StreamMessageComposerInputTrailing oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.controller != oldWidget.controller) { - oldWidget.controller.removeListener(_onInputTextChanged); - widget.controller.addListener(_onInputTextChanged); - } - } - - void _onInputTextChanged() { - final hasText = widget.controller.text.isNotEmpty; - if (_hasText != hasText) { - setState(() => _hasText = hasText); - } - } + final VoiceRecordingCallback? voiceRecordingCallback; + final StreamMessageComposerInputTrailingState buttonState; @override Widget build(BuildContext context) { - // TODO: Implement the trailing component - - if (_hasText || widget.onMicrophonePressed == null) { + if (buttonState == StreamMessageComposerInputTrailingState.send || voiceRecordingCallback == null) { return StreamButton.icon( key: _messageComposerInputTrailingSendKey, icon: context.streamIcons.paperPlane, size: StreamButtonSize.small, - onTap: widget.onSendPressed, + onTap: onSendPressed, ); } - return StreamButton.icon( + return StreamVoiceRecordingButton( + voiceRecordingCallback: voiceRecordingCallback!, + isRecording: buttonState == StreamMessageComposerInputTrailingState.voiceRecordingActive, + ); + } +} + +class StreamVoiceRecordingButton extends StatelessWidget { + const StreamVoiceRecordingButton({ + super.key, + required this.voiceRecordingCallback, + required this.isRecording, + }); + + final VoiceRecordingCallback voiceRecordingCallback; + final bool isRecording; + + @override + Widget build(BuildContext context) { + return GestureDetector( key: _messageComposerInputTrailingMicrophoneKey, - icon: context.streamIcons.microphone, - type: StreamButtonType.ghost, - style: StreamButtonStyle.secondary, - size: StreamButtonSize.small, - onTap: widget.onMicrophonePressed, + onLongPress: voiceRecordingCallback.onLongPressStart, + onLongPressCancel: voiceRecordingCallback.onLongPressCancel, + onLongPressEnd: voiceRecordingCallback.onLongPressEnd, + onLongPressMoveUpdate: voiceRecordingCallback.onLongPressMoveUpdate, + behavior: HitTestBehavior.translucent, + child: StreamButtonTheme( + data: StreamButtonThemeData( + secondary: StreamButtonTypeStyle( + ghost: StreamButtonThemeStyle( + backgroundColor: isRecording + ? WidgetStateProperty.all( + context.streamColorScheme.statePressed, + ) + : null, + ), + ), + ), + child: StreamButton.icon( + icon: context.streamIcons.microphone, + type: StreamButtonType.ghost, + style: StreamButtonStyle.secondary, + size: StreamButtonSize.small, + onTap: () {}, + ), + ), ); } } +class VoiceRecordingCallback { + VoiceRecordingCallback({ + required this.onLongPressStart, + required this.onLongPressCancel, + required this.onLongPressEnd, + this.onLongPressMoveUpdate, + }); + + final VoidCallback onLongPressStart; + final VoidCallback onLongPressCancel; + final GestureLongPressEndCallback onLongPressEnd; + final GestureLongPressMoveUpdateCallback? onLongPressMoveUpdate; +} + final _messageComposerInputTrailingSendKey = UniqueKey(); final _messageComposerInputTrailingMicrophoneKey = UniqueKey(); diff --git a/packages/stream_core_flutter/test/components/message_composer/message_composer_attachment_link_preview_golden_test.dart b/packages/stream_core_flutter/test/components/message_composer/message_composer_attachment_link_preview_golden_test.dart index 1440132..96cf2f6 100644 --- a/packages/stream_core_flutter/test/components/message_composer/message_composer_attachment_link_preview_golden_test.dart +++ b/packages/stream_core_flutter/test/components/message_composer/message_composer_attachment_link_preview_golden_test.dart @@ -18,7 +18,7 @@ void main() { GoldenTestScenario( name: 'full_no_remove', child: _buildLinkPreviewInTheme( - const MessageComposerAttachmentLinkPreview( + const MessageComposerLinkPreviewAttachment( title: 'Getting started with Stream', subtitle: 'Build in-app messaging with our flexible SDKs.', url: 'https://getstream.io/chat/docs/', @@ -29,7 +29,7 @@ void main() { GoldenTestScenario( name: 'full_with_remove', child: _buildLinkPreviewInTheme( - MessageComposerAttachmentLinkPreview( + MessageComposerLinkPreviewAttachment( title: 'Getting started with Stream', subtitle: 'Build in-app messaging with our flexible SDKs.', url: 'https://getstream.io/chat/docs/', @@ -40,7 +40,7 @@ void main() { GoldenTestScenario( name: 'full_with_image_no_remove', child: _buildLinkPreviewInTheme( - MessageComposerAttachmentLinkPreview( + MessageComposerLinkPreviewAttachment( title: 'Getting started with Stream', subtitle: 'Build in-app messaging with our flexible SDKs.', url: 'https://getstream.io/chat/docs/', @@ -52,7 +52,7 @@ void main() { GoldenTestScenario( name: 'full_with_image_with_remove', child: _buildLinkPreviewInTheme( - MessageComposerAttachmentLinkPreview( + MessageComposerLinkPreviewAttachment( title: 'Getting started with Stream', subtitle: 'Build in-app messaging with our flexible SDKs.', url: 'https://getstream.io/chat/docs/', @@ -64,7 +64,7 @@ void main() { GoldenTestScenario( name: 'url_only_no_remove', child: _buildLinkPreviewInTheme( - const MessageComposerAttachmentLinkPreview( + const MessageComposerLinkPreviewAttachment( title: null, subtitle: null, url: 'https://getstream.io/', @@ -75,7 +75,7 @@ void main() { GoldenTestScenario( name: 'url_only_with_remove', child: _buildLinkPreviewInTheme( - MessageComposerAttachmentLinkPreview( + MessageComposerLinkPreviewAttachment( title: null, subtitle: null, url: 'https://getstream.io/', @@ -96,7 +96,7 @@ void main() { GoldenTestScenario( name: 'full_no_remove', child: _buildLinkPreviewInTheme( - const MessageComposerAttachmentLinkPreview( + const MessageComposerLinkPreviewAttachment( title: 'Getting started with Stream', subtitle: 'Build in-app messaging with our flexible SDKs.', url: 'https://getstream.io/chat/docs/', @@ -108,7 +108,7 @@ void main() { GoldenTestScenario( name: 'full_with_remove', child: _buildLinkPreviewInTheme( - MessageComposerAttachmentLinkPreview( + MessageComposerLinkPreviewAttachment( title: 'Getting started with Stream', subtitle: 'Build in-app messaging with our flexible SDKs.', url: 'https://getstream.io/chat/docs/', @@ -120,7 +120,7 @@ void main() { GoldenTestScenario( name: 'full_with_image_no_remove', child: _buildLinkPreviewInTheme( - MessageComposerAttachmentLinkPreview( + MessageComposerLinkPreviewAttachment( title: 'Getting started with Stream', subtitle: 'Build in-app messaging with our flexible SDKs.', url: 'https://getstream.io/chat/docs/', @@ -133,7 +133,7 @@ void main() { GoldenTestScenario( name: 'full_with_image_with_remove', child: _buildLinkPreviewInTheme( - MessageComposerAttachmentLinkPreview( + MessageComposerLinkPreviewAttachment( title: 'Getting started with Stream', subtitle: 'Build in-app messaging with our flexible SDKs.', url: 'https://getstream.io/chat/docs/', @@ -146,7 +146,7 @@ void main() { GoldenTestScenario( name: 'url_only_no_remove', child: _buildLinkPreviewInTheme( - const MessageComposerAttachmentLinkPreview( + const MessageComposerLinkPreviewAttachment( title: null, subtitle: null, url: 'https://getstream.io/', @@ -158,7 +158,7 @@ void main() { GoldenTestScenario( name: 'url_only_with_remove', child: _buildLinkPreviewInTheme( - MessageComposerAttachmentLinkPreview( + MessageComposerLinkPreviewAttachment( title: null, subtitle: null, url: 'https://getstream.io/', diff --git a/packages/stream_core_flutter/test/components/message_composer/message_composer_attachment_reply_golden_test.dart b/packages/stream_core_flutter/test/components/message_composer/message_composer_attachment_reply_golden_test.dart index 6dc694d..5a76147 100644 --- a/packages/stream_core_flutter/test/components/message_composer/message_composer_attachment_reply_golden_test.dart +++ b/packages/stream_core_flutter/test/components/message_composer/message_composer_attachment_reply_golden_test.dart @@ -17,7 +17,7 @@ void main() { GoldenTestScenario( name: '${style.name}_no_remove', child: _buildReplyInTheme( - MessageComposerAttachmentReply( + MessageComposerReplyAttachment( title: 'Reply to John Doe', subtitle: 'We had a great time during our holiday.', onRemovePressed: null, @@ -29,7 +29,7 @@ void main() { GoldenTestScenario( name: '${style.name}_with_remove', child: _buildReplyInTheme( - MessageComposerAttachmentReply( + MessageComposerReplyAttachment( title: 'Reply to John Doe', subtitle: 'We had a great time during our holiday.', onRemovePressed: () {}, @@ -51,7 +51,7 @@ void main() { GoldenTestScenario( name: '${style.name}_no_remove', child: _buildReplyInTheme( - MessageComposerAttachmentReply( + MessageComposerReplyAttachment( title: 'Reply to John Doe', subtitle: 'We had a great time during our holiday.', onRemovePressed: null, @@ -64,7 +64,7 @@ void main() { GoldenTestScenario( name: '${style.name}_with_remove', child: _buildReplyInTheme( - MessageComposerAttachmentReply( + MessageComposerReplyAttachment( title: 'Reply to John Doe', subtitle: 'We had a great time during our holiday.', onRemovePressed: () {}, @@ -100,7 +100,7 @@ void main() { textColor: Colors.white, ), ), - child: MessageComposerAttachmentReply( + child: MessageComposerReplyAttachment( title: 'Reply to John Doe', subtitle: 'We had a great time during our holiday.', onRemovePressed: null,