diff --git a/apps/design_system_gallery/lib/app/gallery_app.directories.g.dart b/apps/design_system_gallery/lib/app/gallery_app.directories.g.dart index e6c4db4..07c0532 100644 --- a/apps/design_system_gallery/lib/app/gallery_app.directories.g.dart +++ b/apps/design_system_gallery/lib/app/gallery_app.directories.g.dart @@ -96,8 +96,6 @@ import 'package:design_system_gallery/components/message/stream_message_replies. as _design_system_gallery_components_message_stream_message_replies; import 'package:design_system_gallery/components/message/stream_message_text.dart' as _design_system_gallery_components_message_stream_message_text; -import 'package:design_system_gallery/components/message_composer/message_composer.dart' - as _design_system_gallery_components_message_composer_message_composer; import 'package:design_system_gallery/components/message_composer/message_composer_attachment_edit_message.dart' as _design_system_gallery_components_message_composer_message_composer_attachment_edit_message; import 'package:design_system_gallery/components/message_composer/message_composer_attachment_link_preview.dart' @@ -999,23 +997,6 @@ final directories = <_widgetbook.WidgetbookNode>[ _widgetbook.WidgetbookFolder( name: 'Message Composer', children: [ - _widgetbook.WidgetbookComponent( - name: 'StreamCoreMessageComposer', - useCases: [ - _widgetbook.WidgetbookUseCase( - name: 'Playground', - builder: - _design_system_gallery_components_message_composer_message_composer - .buildStreamMessageComposerPlayground, - ), - _widgetbook.WidgetbookUseCase( - name: 'Real-world Example', - builder: - _design_system_gallery_components_message_composer_message_composer - .buildStreamMessageComposerExample, - ), - ], - ), _widgetbook.WidgetbookComponent( name: 'StreamMessageComposerEditMessageAttachment', useCases: [ 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 deleted file mode 100644 index 7cfb4e8..0000000 --- a/apps/design_system_gallery/lib/components/message_composer/message_composer.dart +++ /dev/null @@ -1,216 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_core_flutter/stream_core_flutter.dart'; -import 'package:widgetbook/widgetbook.dart'; -import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; - -// ============================================================================= -// Playground -// ============================================================================= - -final emptyVoiceRecordingCallback = VoiceRecordingCallback( - onLongPressStart: () {}, - onLongPressCancel: () {}, - onLongPressEnd: (_) {}, - onLongPressMoveUpdate: (_) {}, -); - -@widgetbook.UseCase( - name: 'Playground', - type: StreamCoreMessageComposer, - path: '[Components]/Message Composer', -) -Widget buildStreamMessageComposerPlayground(BuildContext context) { - final textEditingController = TextEditingController(); - - return Center( - child: StreamCoreMessageComposer( - controller: textEditingController, - isFloating: false, - inputTrailing: StreamCoreMessageComposerInputTrailing( - controller: textEditingController, - onSendPressed: () {}, - voiceRecordingCallback: emptyVoiceRecordingCallback, - buttonState: StreamMessageComposerInputTrailingState.microphone, - ), - ), - ); -} - -// ============================================================================= -// Real-world Example -// ============================================================================= - -@widgetbook.UseCase( - name: 'Real-world Example', - type: StreamCoreMessageComposer, - path: '[Components]/Message Composer', -) -Widget buildStreamMessageComposerExample(BuildContext context) { - final theme = StreamTheme.of(context); - final colorScheme = theme.colorScheme; - final textTheme = theme.textTheme; - - final isFloating = context.knobs.boolean( - label: 'Floating', - description: 'When true, the composer has no background or border.', - ); - - // Sample messages for scrollable list - const messages = [ - (message: 'Hey! How are you doing today?', isMe: false), - (message: "I'm doing great, thanks for asking!", isMe: true), - (message: 'Did you see the new design updates?', isMe: false), - (message: 'Yes! They look amazing. Great work on the color scheme.', isMe: true), - (message: 'Thanks! We spent a lot of time on the details.', isMe: false), - (message: 'It really shows. The typography is much cleaner now.', isMe: true), - (message: 'Glad you like it! Any feedback?', isMe: false), - (message: 'Maybe we could add more spacing in some areas?', isMe: true), - (message: "Good point, I'll look into that.", isMe: false), - (message: 'Perfect! Let me know if you need any help.', isMe: true), - (message: 'Should be finished by tomorrow.', isMe: false), - (message: 'Great! Thanks for the update.', isMe: true), - (message: "No problem! You're welcome.", isMe: false), - (message: 'I need to go now. See you later!', isMe: false), - (message: 'Bye! Take care.', isMe: true), - (message: 'Thanks! You too!', isMe: false), - (message: 'See you soon!', isMe: true), - (message: 'Bye!', isMe: false), - (message: 'See you soon!', isMe: true), - ]; - - final textEditingController = TextEditingController(); - - return Scaffold( - appBar: AppBar( - title: Row( - children: [ - StreamAvatar( - size: StreamAvatarSize.sm, - placeholder: (context) => const Text('JD'), - ), - const SizedBox(width: 12), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'John Doe', - style: textTheme.bodyEmphasis.copyWith( - color: colorScheme.textPrimary, - ), - ), - Text( - 'Online', - style: textTheme.captionDefault.copyWith( - color: colorScheme.accentSuccess, - ), - ), - ], - ), - ], - ), - ), - body: isFloating - ? Stack( - children: [ - // Scrollable messages area (with bottom padding for composer) - ListView.builder( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 250), - itemCount: messages.length, - itemBuilder: (context, index) { - final msg = messages[index]; - return Padding( - padding: EdgeInsets.only( - bottom: index < messages.length - 1 ? 8 : 0, - ), - child: _MessageBubble( - message: msg.message, - isMe: msg.isMe, - ), - ); - }, - ), - // Floating composer at bottom - Positioned( - left: 0, - right: 0, - bottom: 0, - child: StreamCoreMessageComposer( - controller: textEditingController, - isFloating: true, - inputTrailing: StreamCoreMessageComposerInputTrailing( - controller: textEditingController, - onSendPressed: () {}, - voiceRecordingCallback: emptyVoiceRecordingCallback, - buttonState: StreamMessageComposerInputTrailingState.microphone, - ), - ), - ), - ], - ) - : Column( - children: [ - // Scrollable messages area - Expanded( - child: ListView.builder( - padding: const EdgeInsets.all(16), - itemCount: messages.length, - itemBuilder: (context, index) { - final msg = messages[index]; - return Padding( - padding: EdgeInsets.only( - bottom: index < messages.length - 1 ? 8 : 0, - ), - child: _MessageBubble( - message: msg.message, - isMe: msg.isMe, - ), - ); - }, - ), - ), - // Non-floating composer - StreamCoreMessageComposer( - controller: textEditingController, - isFloating: false, - inputTrailing: StreamCoreMessageComposerInputTrailing( - controller: textEditingController, - onSendPressed: () {}, - voiceRecordingCallback: emptyVoiceRecordingCallback, - buttonState: StreamMessageComposerInputTrailingState.microphone, - ), - ), - ], - ), - ); -} - -class _MessageBubble extends StatelessWidget { - const _MessageBubble({ - required this.message, - required this.isMe, - }); - - final String message; - final bool isMe; - - @override - Widget build(BuildContext context) { - Widget child = Align( - alignment: isMe ? Alignment.centerRight : Alignment.centerLeft, - child: StreamMessageBubble( - child: Text(message), - ), - ); - - if (isMe) { - child = StreamMessageLayout( - data: const StreamMessageLayoutData( - alignment: StreamMessageAlignment.end, - ), - child: child, - ); - } - - return child; - } -} diff --git a/packages/stream_core_flutter/CHANGELOG.md b/packages/stream_core_flutter/CHANGELOG.md index 612bcb7..77238a7 100644 --- a/packages/stream_core_flutter/CHANGELOG.md +++ b/packages/stream_core_flutter/CHANGELOG.md @@ -1,5 +1,9 @@ ## Upcoming +### 🛑 Breaking / Removals + +- Removed `StreamCoreMessageComposer`, `StreamMessageComposerInput`, `StreamMessageComposerInputField`, `StreamCoreMessageComposerInputTrailing`, `StreamVoiceRecordingButton`, `VoiceRecordingCallback`, `StreamMessageComposerInputTrailingState`, and `InputThemeDefaults`. These composer-flow widgets now live entirely in `stream_chat_flutter`. The attachment widgets (`StreamMessageComposerAttachment` and variants) remain in this package. + ### ✨ Features - Added `StreamJumpToUnreadButton` component and `StreamJumpToUnreadButtonTheme`. diff --git a/packages/stream_core_flutter/lib/src/components.dart b/packages/stream_core_flutter/lib/src/components.dart index 6edf46a..1804d81 100644 --- a/packages/stream_core_flutter/lib/src/components.dart +++ b/packages/stream_core_flutter/lib/src/components.dart @@ -48,7 +48,20 @@ export 'components/message/stream_message_content.dart' hide DefaultStreamMessag export 'components/message/stream_message_metadata.dart' hide DefaultStreamMessageMetadata; export 'components/message/stream_message_replies.dart' hide DefaultStreamMessageReplies; export 'components/message/stream_message_text.dart' hide DefaultStreamMessageText; -export 'components/message_composer.dart'; +export 'components/message_composer/attachment/stream_message_composer_attachment.dart' + hide DefaultStreamMessageComposerAttachment; +export 'components/message_composer/attachment/stream_message_composer_edit_message_attachment.dart' + hide DefaultStreamMessageComposerEditMessageAttachment; +export 'components/message_composer/attachment/stream_message_composer_file_attachment.dart' + hide DefaultStreamMessageComposerFileAttachment; +export 'components/message_composer/attachment/stream_message_composer_link_preview_attachment.dart' + hide DefaultStreamMessageComposerLinkPreviewAttachment; +export 'components/message_composer/attachment/stream_message_composer_media_attachment.dart' + hide DefaultStreamMessageComposerMediaAttachment; +export 'components/message_composer/attachment/stream_message_composer_reply_attachment.dart' + hide DefaultStreamMessageComposerReplyAttachment; +export 'components/message_composer/attachment/stream_message_composer_unsupported_attachment.dart' + hide DefaultStreamMessageComposerUnsupportedAttachment; export 'components/message_layout/stream_message_alignment.dart'; export 'components/message_layout/stream_message_channel_kind.dart'; export 'components/message_layout/stream_message_content_kind.dart'; diff --git a/packages/stream_core_flutter/lib/src/components/message_composer.dart b/packages/stream_core_flutter/lib/src/components/message_composer.dart deleted file mode 100644 index e85a1ec..0000000 --- a/packages/stream_core_flutter/lib/src/components/message_composer.dart +++ /dev/null @@ -1,17 +0,0 @@ -export 'message_composer/attachment/stream_message_composer_attachment.dart' - hide DefaultStreamMessageComposerAttachment; -export 'message_composer/attachment/stream_message_composer_edit_message_attachment.dart' - hide DefaultStreamMessageComposerEditMessageAttachment; -export 'message_composer/attachment/stream_message_composer_file_attachment.dart' - hide DefaultStreamMessageComposerFileAttachment; -export 'message_composer/attachment/stream_message_composer_link_preview_attachment.dart' - hide DefaultStreamMessageComposerLinkPreviewAttachment; -export 'message_composer/attachment/stream_message_composer_media_attachment.dart' - hide DefaultStreamMessageComposerMediaAttachment; -export 'message_composer/attachment/stream_message_composer_reply_attachment.dart' - hide DefaultStreamMessageComposerReplyAttachment; -export 'message_composer/attachment/stream_message_composer_unsupported_attachment.dart' - hide DefaultStreamMessageComposerUnsupportedAttachment; -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/message_composer.dart b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer.dart deleted file mode 100644 index c725233..0000000 --- a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer.dart +++ /dev/null @@ -1,127 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; - -import '../../../stream_core_flutter.dart'; - -class StreamCoreMessageComposer extends StatefulWidget { - const StreamCoreMessageComposer({ - super.key, - required this.controller, - required this.isFloating, - this.placeholder, - this.focusNode, - this.composerLeading, - this.composerTrailing, - this.inputLeading, - this.inputTrailing, - this.inputBody, - this.inputHeader, - }); - - final TextEditingController? controller; - final bool isFloating; - final String? placeholder; - final FocusNode? focusNode; - - final Widget? composerLeading; - final Widget? composerTrailing; - final Widget? inputLeading; - final Widget? inputTrailing; - final Widget? inputBody; - final Widget? inputHeader; - - @override - State createState() => _StreamCoreMessageComposerState(); -} - -class _StreamCoreMessageComposerState extends State { - late TextEditingController _controller; - - @override - void initState() { - super.initState(); - _initController(); - } - - @override - void didUpdateWidget(StreamCoreMessageComposer oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.controller != oldWidget.controller) { - _disposeController(oldWidget); - _initController(); - } - } - - @override - void dispose() { - _disposeController(widget); - super.dispose(); - } - - void _initController() { - _controller = widget.controller ?? TextEditingController(); - } - - void _disposeController(StreamCoreMessageComposer widget) { - if (widget.controller == null) { - _controller.dispose(); - } - } - - @override - Widget build(BuildContext context) { - final spacing = context.streamSpacing; - - return Container( - padding: EdgeInsets.only(top: spacing.md), - decoration: widget.isFloating - ? null - : BoxDecoration( - border: Border( - top: BorderSide(color: context.streamColorScheme.borderDefault), - ), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - SizedBox(width: spacing.md), - ?widget.composerLeading, - Expanded( - child: StreamMessageComposerInput( - controller: _controller, - placeholder: widget.placeholder, - isFloating: widget.isFloating, - inputLeading: widget.inputLeading, - inputTrailing: widget.inputTrailing, - inputBody: widget.inputBody, - inputHeader: widget.inputHeader, - focusNode: widget.focusNode, - ), - ), - ?widget.composerTrailing, - SizedBox(width: spacing.md), - ], - ), - ); - } -} - -class MessageData {} - -class InputThemeDefaults { - InputThemeDefaults({required this.context}) - : _colorScheme = context.streamColorScheme, - _textTheme = context.streamTextTheme; - - final BuildContext context; - final StreamColorScheme _colorScheme; - final StreamTextTheme _textTheme; - - StreamTextInputStyle get style => StreamTextInputStyle( - cursorWidth: 2, - cursorColor: _colorScheme.accentPrimary, - cursorErrorColor: _colorScheme.accentError, - textStyle: _textTheme.bodyDefault.copyWith(color: _colorScheme.textPrimary), - hintStyle: _textTheme.bodyDefault.copyWith(color: _colorScheme.textTertiary), - ); -} 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 deleted file mode 100644 index edfae20..0000000 --- a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input.dart +++ /dev/null @@ -1,179 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../../../stream_core_flutter.dart'; - -/// A widget that represents the message composer input area. -/// This usually contains the input field and the send or microphone button. -class StreamMessageComposerInput extends StatelessWidget { - const StreamMessageComposerInput({ - super.key, - required this.controller, - this.placeholder, - this.isFloating = false, - this.inputLeading, - this.inputTrailing, - this.inputBody, - this.inputHeader, - this.focusNode, - }); - - final TextEditingController controller; - final String? placeholder; - final bool isFloating; - final Widget? inputLeading; - final Widget? inputTrailing; - final Widget? inputBody; - final Widget? inputHeader; - final FocusNode? focusNode; - - @override - Widget build(BuildContext context) { - 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( - mainAxisSize: MainAxisSize.min, - children: [ - ?inputHeader, - Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - ?inputLeading, - Expanded( - child: - inputBody ?? - StreamMessageComposerInputField( - controller: controller, - placeholder: placeholder, - focusNode: focusNode, - ), - ), - ?inputTrailing, - ], - ), - ], - ), - ); - } -} - -/// A widget that represents the actual text input field for the message composer. -class StreamMessageComposerInputField extends StatelessWidget { - const StreamMessageComposerInputField({ - super.key, - required this.controller, - this.placeholder, - this.focusNode, - this.command, - this.onDismissCommand, - this.textInputAction, - this.keyboardType, - this.textCapitalization = TextCapitalization.sentences, - this.autofocus = false, - this.autocorrect = true, - }); - - /// The controller for the text field. - final TextEditingController controller; - - /// The placeholder text shown when the field is empty. - final String? placeholder; - - /// The focus node for the text field. - final FocusNode? focusNode; - - /// The active command label displayed as a chip. - final String? command; - - /// Called when the user dismisses the command chip. - final VoidCallback? onDismissCommand; - - /// The type of action button to use for the keyboard. - final TextInputAction? textInputAction; - - /// The type of keyboard to use for editing the text. - final TextInputType? keyboardType; - - /// {@macro flutter.widgets.editableText.textCapitalization} - final TextCapitalization textCapitalization; - - /// Whether the text field should be focused initially. - final bool autofocus; - - /// Whether to enable autocorrect. - final bool autocorrect; - - @override - Widget build(BuildContext context) { - final spacing = context.streamSpacing; - - final inputStyle = context.streamTextInputTheme.style; - final inputDefaults = InputThemeDefaults(context: context).style; - - final effectiveStyle = inputStyle?.textStyle ?? inputDefaults.textStyle; - final effectiveHintStyle = inputStyle?.hintStyle ?? inputDefaults.hintStyle; - final effectiveCursorColor = inputStyle?.cursorColor ?? inputDefaults.cursorColor; - final effectiveCursorWidth = inputStyle?.cursorWidth ?? inputDefaults.cursorWidth ?? 2.0; - final effectiveCursorHeight = inputStyle?.cursorHeight ?? inputDefaults.cursorHeight; - final effectiveCursorRadius = inputStyle?.cursorRadius ?? inputDefaults.cursorRadius; - - return ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 124), - child: Padding( - padding: .all(spacing.sm), - child: Row( - mainAxisSize: .min, - spacing: spacing.xxs, - crossAxisAlignment: .end, - children: [ - if (command case final command?) - StreamCommandChip( - label: command, - onDismiss: onDismissCommand, - ), - Expanded( - child: TextField( - controller: controller, - focusNode: focusNode, - textInputAction: textInputAction, - keyboardType: keyboardType, - textCapitalization: textCapitalization, - autofocus: autofocus, - autocorrect: autocorrect, - style: effectiveStyle, - cursorColor: effectiveCursorColor, - cursorWidth: effectiveCursorWidth, - cursorHeight: effectiveCursorHeight, - cursorRadius: effectiveCursorRadius, - maxLines: null, - decoration: InputDecoration( - isCollapsed: true, - filled: false, - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - disabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - focusedErrorBorder: InputBorder.none, - hintText: placeholder, - hintStyle: effectiveHintStyle, - contentPadding: .symmetric(horizontal: spacing.xxs, vertical: spacing.xxxs), - ), - ), - ), - ], - ), - ), - ); - } -} 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 deleted file mode 100644 index f98a031..0000000 --- a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_trailing.dart +++ /dev/null @@ -1,111 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; - -import '../../../stream_core_flutter.dart'; - -enum StreamMessageComposerInputTrailingState { - send, - edit, - command, - microphone, - voiceRecordingActive, -} - -class StreamCoreMessageComposerInputTrailing extends StatelessWidget { - const StreamCoreMessageComposerInputTrailing({ - super.key, - required this.controller, - required this.onSendPressed, - required this.voiceRecordingCallback, - this.buttonState = StreamMessageComposerInputTrailingState.send, - }); - - final TextEditingController controller; - final VoidCallback? onSendPressed; - final VoiceRecordingCallback? voiceRecordingCallback; - final StreamMessageComposerInputTrailingState buttonState; - - @override - Widget build(BuildContext context) { - if (buttonState == StreamMessageComposerInputTrailingState.send || - buttonState == StreamMessageComposerInputTrailingState.edit || - buttonState == StreamMessageComposerInputTrailingState.command || - voiceRecordingCallback == null) { - return StreamButton.icon( - key: _messageComposerInputTrailingSendKey, - icon: Icon( - buttonState == StreamMessageComposerInputTrailingState.edit || - buttonState == StreamMessageComposerInputTrailingState.command - ? context.streamIcons.checkmark - : context.streamIcons.send, - ), - size: StreamButtonSize.small, - onPressed: onSendPressed, - ); - } - 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, - 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.backgroundPressed, - ) - : null, - ), - ), - ), - child: StreamButton.icon( - icon: Icon(context.streamIcons.voice), - type: StreamButtonType.ghost, - style: StreamButtonStyle.secondary, - size: StreamButtonSize.small, - onPressed: () {}, - ), - ), - ); - } -} - -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();