From db6a3985b7307ab6c816bf1c4adf1054c4349167 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Tue, 5 May 2026 11:32:15 +0200 Subject: [PATCH 01/13] rename message composer --- CLAUDE.md | 4 +- ...dart => stream_message_composer_test.dart} | 12 ++-- docs/docs_screenshots/test/src/mocks.dart | 2 +- .../voice_recording/voice_recording_test.dart | 12 ++-- migrations/redesign/message_composer.md | 58 ++++++++++--------- migrations/v10-migration.md | 12 ++-- packages/stream_chat_flutter/README.md | 2 +- .../stream_chat_flutter/example/lib/main.dart | 4 +- .../example/lib/split_view.dart | 2 +- .../example/lib/tutorial_part_1.dart | 4 +- .../example/lib/tutorial_part_2.dart | 2 +- .../example/lib/tutorial_part_3.dart | 2 +- .../example/lib/tutorial_part_4.dart | 4 +- .../example/lib/tutorial_part_5.dart | 2 +- .../example/lib/tutorial_part_6.dart | 4 +- .../message_composer/message_composer.dart | 2 +- ...er.dart => stream_chat_message_input.dart} | 30 +++++----- .../lib/src/keyboard_shortcuts/keysets.dart | 4 +- .../lib/src/localization/translations.dart | 14 ++--- .../stream_attachment_picker_controller.dart | 2 +- .../lib/src/message_input/enums.dart | 2 +- .../message_input_placeholder.dart | 6 +- ...nput.dart => stream_message_composer.dart} | 28 ++++----- .../message_list_view/message_list_view.dart | 2 +- .../lib/src/utils/typedefs.dart | 4 +- .../lib/stream_chat_flutter.dart | 2 +- .../src/message_input/message_input_test.dart | 46 +++++++-------- .../example/lib/add_new_lang.dart | 2 +- .../example/lib/main.dart | 2 +- .../example/lib/override_lang.dart | 2 +- sample_app/lib/pages/channel_page.dart | 2 +- sample_app/lib/pages/new_chat_screen.dart | 2 +- sample_app/lib/pages/thread_page.dart | 2 +- 33 files changed, 141 insertions(+), 139 deletions(-) rename docs/docs_screenshots/test/message_input/{stream_message_input_test.dart => stream_message_composer_test.dart} (94%) rename packages/stream_chat_flutter/lib/src/components/message_composer/{stream_chat_message_composer.dart => stream_chat_message_input.dart} (94%) rename packages/stream_chat_flutter/lib/src/message_input/{stream_message_input.dart => stream_message_composer.dart} (98%) diff --git a/CLAUDE.md b/CLAUDE.md index 7d2892bd2b..0c3b05414d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -91,13 +91,13 @@ Full UI package. Key architectural points: **Key UI components:** - `StreamChannelListView` + `StreamChannelListTile` — channel list using `StreamChannelListController` - `StreamMessageListView` — message list with floating date dividers, unread indicators, thread separators -- `StreamMessageInput` (legacy) / `StreamChatMessageComposer` (new design system) — message composition +- `StreamMessageComposer` (full-featured) / `StreamChatMessageInput` (new design system, UI-only) — message composition - `StreamMessageWidget` — renders individual messages with attachments, reactions, threads - Scroll views in `lib/src/scroll_view/` — generic paged scroll views for channels, threads, members, users, drafts, polls **New design system components** (`lib/src/components/`): - `StreamUserAvatar`, `StreamChannelAvatar`, `StreamUserAvatarGroup` — avatar components; these are chat-domain wrappers around the base components in `stream_core_flutter` -- `StreamChatMessageComposer` — new composer using `MessageComposerFactory` for custom layouts +- `StreamChatMessageInput` — new composer using `MessageComposerFactory` for custom layouts **Golden tests:** Use `alchemist` package. Platform goldens used locally, CI goldens used in CI (detected via `CI`/`GITHUB_ACTIONS` env vars). Goldens stored alongside tests in `goldens/` subdirectories. diff --git a/docs/docs_screenshots/test/message_input/stream_message_input_test.dart b/docs/docs_screenshots/test/message_input/stream_message_composer_test.dart similarity index 94% rename from docs/docs_screenshots/test/message_input/stream_message_input_test.dart rename to docs/docs_screenshots/test/message_input/stream_message_composer_test.dart index 875a3a7435..c74aa7f2c1 100644 --- a/docs/docs_screenshots/test/message_input/stream_message_input_test.dart +++ b/docs/docs_screenshots/test/message_input/stream_message_composer_test.dart @@ -12,7 +12,7 @@ import '../src/mocks.dart'; Widget _buildMessageInputScaffold({ required MockClient client, required MockChannel channel, - StreamMessageInput? messageInput, + StreamMessageComposer? messageInput, }) { return MaterialApp( theme: docsScreenshotsTheme(), @@ -28,7 +28,7 @@ Widget _buildMessageInputScaffold({ body: Column( children: [ Expanded(child: Container()), - messageInput ?? const StreamMessageInput(), + messageInput ?? const StreamMessageComposer(), ], ), ), @@ -46,7 +46,7 @@ void main() { goldenTest( 'default state', - fileName: 'stream_message_input_default', + fileName: 'stream_message_composer_default', constraints: const BoxConstraints.tightFor(width: 375, height: 100), builder: () { final client = MockClient(); @@ -125,7 +125,7 @@ void main() { body: Column( children: [ const Expanded(child: SizedBox()), - StreamMessageInput(messageInputController: controller), + StreamMessageComposer(messageInputController: controller), ], ), ), @@ -181,7 +181,7 @@ void main() { body: Column( children: [ Expanded(child: Container()), - const StreamMessageInput(), + const StreamMessageComposer(), ], ), ), @@ -218,7 +218,7 @@ void main() { return _buildMessageInputScaffold( client: client, channel: channel, - messageInput: StreamMessageInput(messageInputController: controller), + messageInput: StreamMessageComposer(messageInputController: controller), ); }, ); diff --git a/docs/docs_screenshots/test/src/mocks.dart b/docs/docs_screenshots/test/src/mocks.dart index 397120f1b2..1970657871 100644 --- a/docs/docs_screenshots/test/src/mocks.dart +++ b/docs/docs_screenshots/test/src/mocks.dart @@ -105,7 +105,7 @@ class MockChannelState extends Mock implements ChannelClientState { } } -/// Sets up a [MockChannel] with all stubs required by [StreamMessageInput]. +/// Sets up a [MockChannel] with all stubs required by [StreamMessageComposer]. void setupMockChannel({ required MockClient client, required MockClientState clientState, diff --git a/docs/docs_screenshots/test/voice_recording/voice_recording_test.dart b/docs/docs_screenshots/test/voice_recording/voice_recording_test.dart index 7e7b693141..ba927f3170 100644 --- a/docs/docs_screenshots/test/voice_recording/voice_recording_test.dart +++ b/docs/docs_screenshots/test/voice_recording/voice_recording_test.dart @@ -41,7 +41,7 @@ Widget _buildVoiceRecordingMessageInputScaffold({ body: Column( children: [ Expanded(child: Container()), - StreamMessageInput( + StreamMessageComposer( enableVoiceRecording: true, messageInputController: messageInputController, ), @@ -93,7 +93,7 @@ Widget _buildVoiceRecordingContextScaffold({ ], ), ), - const StreamMessageInput(enableVoiceRecording: true), + const StreamMessageComposer(enableVoiceRecording: true), ], ), ), @@ -103,11 +103,11 @@ Widget _buildVoiceRecordingContextScaffold({ } /// Scaffold that shows a full message input bar (with attachment button and -/// placeholder) using [StreamChatMessageComposer] so we can inject a custom +/// placeholder) using [StreamChatMessageInput] so we can inject a custom /// [audioRecorderController] to control the recording state. /// -/// The outer [Material] + bottom padding mirrors what [StreamMessageInput] -/// wraps around [StreamChatMessageComposer] internally. +/// The outer [Material] + bottom padding mirrors what [StreamMessageComposer] +/// wraps around [StreamChatMessageInput] internally. Widget _buildVoiceRecordingComposerScaffold({ required MockClient client, required MockChannel channel, @@ -136,7 +136,7 @@ Widget _buildVoiceRecordingComposerScaffold({ ), child: Padding( padding: EdgeInsets.only(bottom: context.streamSpacing.md), - child: StreamChatMessageComposer( + child: StreamChatMessageInput( onSendPressed: () {}, onAttachmentButtonPressed: () {}, placeholder: 'Send a message', diff --git a/migrations/redesign/message_composer.md b/migrations/redesign/message_composer.md index d6c5d9cb3f..5cd336b0aa 100644 --- a/migrations/redesign/message_composer.md +++ b/migrations/redesign/message_composer.md @@ -7,8 +7,8 @@ This guide covers the migration for the message composer components in the Strea ## Table of Contents - [Overview](#overview) -- [StreamMessageInput](#streammessageinput) -- [StreamChatMessageComposer (new)](#streamchatmessagecomposer-new) +- [StreamMessageComposer](#streammessagecomposer) +- [StreamChatMessageInput (new)](#streamchatmessageinput-new) - [Message Input Placeholder API](#message-input-placeholder-api) - [Attachment Customization](#attachment-customization) - [Migration Checklist](#migration-checklist) @@ -21,16 +21,16 @@ There are two distinct composer components with different responsibilities: | Component | Responsibility | |-----------|---------------| -| `StreamMessageInput` | Full-featured widget: handles sending, editing, attachments, autocomplete, mentions, commands, OG previews, voice recording flow, etc. | -| `StreamChatMessageComposer` | UI-only component: renders the composer layout using design system primitives. No business logic. | +| `StreamMessageComposer` | Full-featured widget: handles sending, editing, attachments, autocomplete, mentions, commands, OG previews, voice recording flow, etc. | +| `StreamChatMessageInput` | UI-only component: renders the composer layout using design system primitives. No business logic. | -`StreamMessageInput` wraps `StreamChatMessageComposer` for its visual layer. If you are using `StreamMessageInput` today, it remains the right choice — it is not deprecated. `StreamChatMessageComposer` exists for cases where you want to build your own message-sending logic and use the new design system UI. +`StreamMessageComposer` wraps `StreamChatMessageInput` for its visual layer. If you are using `StreamMessageComposer` today, it remains the right choice — it is not deprecated. `StreamChatMessageInput` exists for cases where you want to build your own message-sending logic and use the new design system UI. --- -## StreamMessageInput +## StreamMessageComposer -`StreamMessageInput` handles all message composition logic. This section documents all breaking changes. +`StreamMessageComposer` handles all message composition logic. This section documents all breaking changes. ### Breaking Change: `hideSendAsDm` renamed to `canAlsoSendToChannelFromThread` (logic inverted) @@ -50,7 +50,7 @@ StreamMessageInput( **After:** ```dart -StreamMessageInput( +StreamMessageComposer( canAlsoSendToChannelFromThread: false, // hide the checkbox ) ``` @@ -70,25 +70,25 @@ StreamMessageInput( **After (with limit):** ```dart -StreamMessageInput( +StreamMessageComposer( attachmentLimit: 5, ) ``` **After (no limit — new default behaviour):** ```dart -StreamMessageInput( +StreamMessageComposer( // attachmentLimit not set — no limit applied ) ``` ### Removed parameters -Many parameters that existed in older versions of `StreamMessageInput` have been removed. The table below lists each removed parameter and the recommended migration path. +Many parameters that existed in older versions of `StreamMessageComposer` have been removed. The table below lists each removed parameter and the recommended migration path. #### Layout and visual parameters -These parameters have been removed. The composer layout is now fully owned by `StreamChatMessageComposer` and its sub-components, customizable via `StreamComponentFactory`. +These parameters have been removed. The composer layout is now fully owned by `StreamChatMessageInput` and its sub-components, customizable via `StreamComponentFactory`. | Removed parameter | Migration path | |-------------------|---------------| @@ -137,17 +137,17 @@ These parameters have been removed. Attachment rendering in the composer input h ### Attachment button visibility -Previously, the attachment button was always rendered (though inactive) when `disableAttachments: true` was set. The button is now fully hidden (removed from the layout) when no attachment callback is wired up. When you pass `disableAttachments: true` to `StreamMessageInput`, the attachment button no longer appears at all. +Previously, the attachment button was always rendered (though inactive) when `disableAttachments: true` was set. The button is now fully hidden (removed from the layout) when no attachment callback is wired up. When you pass `disableAttachments: true` to `StreamMessageComposer`, the attachment button no longer appears at all. -If you are using `StreamChatMessageComposer` directly, the button hides when `onAttachmentButtonPressed` is `null`. +If you are using `StreamChatMessageInput` directly, the button hides when `onAttachmentButtonPressed` is `null`. --- -## StreamChatMessageComposer (new) +## StreamChatMessageInput (new) -`StreamChatMessageComposer` is a pure UI component from the new design system. It renders the composer layout but contains no message-sending logic — your code is responsible for wiring up the controller and callbacks. +`StreamChatMessageInput` is a pure UI component from the new design system. It renders the composer layout but contains no message-sending logic — your code is responsible for wiring up the controller and callbacks. -Use this when you want the new design system visuals with custom business logic. If you want the full out-of-the-box experience (send, edit, attachments, mentions, commands, etc.), use `StreamMessageInput` instead. +Use this when you want the new design system visuals with custom business logic. If you want the full out-of-the-box experience (send, edit, attachments, mentions, commands, etc.), use `StreamMessageComposer` instead. ### Constructor Parameters @@ -159,7 +159,7 @@ Use this when you want the new design system visuals with custom business logic. | `isPickerOpen` | `bool` | `false` | Whether the inline attachment picker is currently open | | `focusNode` | `FocusNode?` | `null` | Focus node for the text field | | `currentUserId` | `String?` | `null` | Current user's ID | -| `placeholder` | `String?` | `null` | Placeholder text for the input field. `StreamChatMessageComposer` is a pure UI component — when wiring it up directly, compute this string yourself (use `MessageInputPlaceholder.resolve(controller)` from the [Message Input Placeholder API](#message-input-placeholder-api) if you want the built-in state machine), or pass `null` for no placeholder. `StreamMessageInput` resolves it for you reactively from its controller. | +| `placeholder` | `String?` | `null` | Placeholder text for the input field. `StreamChatMessageInput` is a pure UI component — when wiring it up directly, compute this string yourself (use `MessageInputPlaceholder.resolve(controller)` from the [Message Input Placeholder API](#message-input-placeholder-api) if you want the built-in state machine), or pass `null` for no placeholder. `StreamMessageComposer` resolves it for you reactively from its controller. | | `audioRecorderController` | `StreamAudioRecorderController?` | `null` | Enables the voice recording UI when provided | | `sendVoiceRecordingAutomatically` | `bool` | `false` | Sends the voice recording immediately on finish | | `feedback` | `AudioRecorderFeedback` | `const AudioRecorderFeedback()` | Haptic/audio feedback callbacks for the recording flow | @@ -202,11 +202,11 @@ StreamComponentFactory( ## Message Input Placeholder API -The input placeholder text (the dimmed text shown inside the input field when it is empty) is now driven by a sealed-class hierarchy that adapts to the current input state. The previous `HintType` enum and `HintGetter` typedef have been removed, and the customization hook on `StreamMessageInput` is now called `placeholderBuilder`. +The input placeholder text (the dimmed text shown inside the input field when it is empty) is now driven by a sealed-class hierarchy that adapts to the current input state. The previous `HintType` enum and `HintGetter` typedef have been removed, and the customization hook on `StreamMessageComposer` is now called `placeholderBuilder`. The new placeholder types live in `lib/src/message_input/message_input_placeholder.dart` and are re-exported from `package:stream_chat_flutter/stream_chat_flutter.dart`. -> **Layered model.** The placeholder *resolution* (state machine that turns controller state into a string) lives on `StreamMessageInput`, the higher-level full-featured widget. The lower-level `StreamChatMessageComposer` design-system component stays a pure UI primitive and accepts a plain `String placeholder` — see [StreamChatMessageComposer (new)](#streamchatmessagecomposer-new). If you build directly on `StreamChatMessageComposer`, call `MessageInputPlaceholder.resolve(controller)` and your own builder yourself, then pass the resulting string in. +> **Layered model.** The placeholder *resolution* (state machine that turns controller state into a string) lives on `StreamMessageComposer`, the higher-level full-featured widget. The lower-level `StreamChatMessageInput` design-system component stays a pure UI primitive and accepts a plain `String placeholder` — see [StreamChatMessageInput (new)](#streamchatmessageinput-new). If you build directly on `StreamChatMessageInput`, call `MessageInputPlaceholder.resolve(controller)` and your own builder yourself, then pass the resulting string in. ### What was removed @@ -216,8 +216,8 @@ The new placeholder types live in `lib/src/message_input/message_input_placehold | `typedef HintGetter = String? Function(BuildContext, HintType, Command?)` | `typedef MessageInputPlaceholderBuilder = String? Function(BuildContext, MessageInputPlaceholder)` | | `HintType resolveMessageInputHintType(controller)` | `MessageInputPlaceholder.resolve(controller)` factory | | `Command? resolveActiveMessageInputCommand(context, controller)` | Removed. Use `controller.message.command` (a `String?`) directly. The SDK no longer looks up the full `Command` object from the channel config when resolving the placeholder. | -| `String? defaultMessageInputHintGetter(...)` | Removed from the public API. The default behaviour is now baked into `StreamMessageInput.placeholderBuilder`'s default value. To customize, supply your own builder with an exhaustive `switch` over [`MessageInputPlaceholder`](#sealed-class-state-shape). | -| `StreamMessageInput.hintGetter` | `StreamMessageInput.placeholderBuilder` | +| `String? defaultMessageInputHintGetter(...)` | Removed from the public API. The default behaviour is now baked into `StreamMessageComposer.placeholderBuilder`'s default value. To customize, supply your own builder with an exhaustive `switch` over [`MessageInputPlaceholder`](#sealed-class-state-shape). | +| `StreamMessageInput.hintGetter` | `StreamMessageComposer.placeholderBuilder` | ### Behavior change: precedence @@ -265,7 +265,7 @@ Each case carries the contextual data relevant to that input state. Pattern-matc Example using the new fields (note that the sealed type forces an exhaustive switch — every case must be handled): ```dart -StreamMessageInput( +StreamMessageComposer( placeholderBuilder: (context, placeholder) { final translations = context.translations; return switch (placeholder) { @@ -310,7 +310,7 @@ StreamMessageInput( **After:** ```dart -StreamMessageInput( +StreamMessageComposer( placeholderBuilder: (context, placeholder) { return switch (placeholder) { SlowModePlaceholder() => 'Slow mode is on', @@ -327,7 +327,7 @@ StreamMessageInput( For backend-defined custom commands, pattern-match the relevant `CommandPlaceholder.command` values and use the SDK's localized labels for everything else: ```dart -StreamMessageInput( +StreamMessageComposer( placeholderBuilder: (context, placeholder) { final translations = context.translations; return switch (placeholder) { @@ -422,15 +422,17 @@ The following public widgets are provided as building blocks for custom attachme ## Migration Checklist -- [ ] Rename `hideSendAsDm` to `canAlsoSendToChannelFromThread` in all `StreamMessageInput` usages and invert the value +- [ ] Rename `StreamMessageInput` to `StreamMessageComposer` in all usages +- [ ] Rename `StreamChatMessageComposer` to `StreamChatMessageInput` in all usages +- [ ] Rename `hideSendAsDm` to `canAlsoSendToChannelFromThread` in all `StreamMessageComposer` usages and invert the value - [ ] Review usages of `attachmentLimit` — it is now `int?` and defaults to no limit; set an explicit value if you relied on the old default of `10` - [ ] Remove any usage of `maxHeight`, `maxLines`, `minLines`, `padding`, `textInputMargin`, `elevation`, `shadow`, `enableActionAnimation`, `contentInsertionConfiguration`, `sendButtonLocation` - [ ] Replace `actionsBuilder` / `actionsLocation` / button builder params (`attachmentButtonBuilder`, `commandButtonBuilder`, `sendButtonBuilder`, `idleSendIcon`, `activeSendIcon`, `showCommandsButton`) with sub-component overrides via `StreamComponentFactory` - [ ] Replace attachment list builder params (`attachmentListBuilder`, `fileAttachmentListBuilder`, `mediaAttachmentListBuilder`, `voiceRecordingAttachmentListBuilder`) with the `messageComposerAttachmentList` builder in `StreamComponentFactory` - [ ] Replace attachment item builder params (`fileAttachmentBuilder`, `mediaAttachmentBuilder`, `voiceRecordingAttachmentBuilder`) with the `messageComposerAttachment` builder in `StreamComponentFactory` - [ ] Replace `quotedMessageBuilder` / `quotedMessageAttachmentThumbnailBuilders` with `messageComposerInputHeader` or `messageComposerAttachment` overrides in `StreamComponentFactory` -- [ ] If adopting `StreamChatMessageComposer` directly, wire up your own send/attachment logic via `onSendPressed` and `onAttachmentButtonPressed` +- [ ] If adopting `StreamChatMessageInput` directly, wire up your own send/attachment logic via `onSendPressed` and `onAttachmentButtonPressed` - [ ] Move any composer UI customizations to `StreamComponentFactory` -- [ ] Rename `StreamMessageInput.hintGetter` to `placeholderBuilder` and rewrite the callback to switch over `MessageInputPlaceholder` cases (`SlowModePlaceholder`, `CommandPlaceholder`, `AttachmentsPlaceholder`, `WriteMessagePlaceholder`) instead of the removed `HintType` enum. If you build directly on `StreamChatMessageComposer`, compute the placeholder string yourself via `MessageInputPlaceholder.resolve(controller)` and pass it via the `placeholder: String` parameter. +- [ ] Rename `StreamMessageInput.hintGetter` to `placeholderBuilder` and rewrite the callback to switch over `MessageInputPlaceholder` cases (`SlowModePlaceholder`, `CommandPlaceholder`, `AttachmentsPlaceholder`, `WriteMessagePlaceholder`) instead of the removed `HintType` enum. If you build directly on `StreamChatMessageInput`, compute the placeholder string yourself via `MessageInputPlaceholder.resolve(controller)` and pass it via the `placeholder: String` parameter. - [ ] Review the new placeholder precedence (`slowMode > command > attachments > writeMessage`) and override `placeholderBuilder` if you need to preserve the old order - [ ] Add command-specific placeholders for any backend-defined commands you ship by pattern-matching on `CommandPlaceholder.command` in your `placeholderBuilder` diff --git a/migrations/v10-migration.md b/migrations/v10-migration.md index c9600a3b5c..ae7ca2f026 100644 --- a/migrations/v10-migration.md +++ b/migrations/v10-migration.md @@ -267,7 +267,7 @@ StreamSystemAttachmentPickerBottomSheet( **Before:** ```dart -StreamMessageInput( +StreamMessageComposer( customAttachmentPickerOptions: [ TabbedAttachmentPickerOption( key: 'custom-location', @@ -283,7 +283,7 @@ StreamMessageInput( **After:** ```dart -StreamMessageInput( +StreamMessageComposer( attachmentPickerOptionsBuilder: (context, defaultOptions) { // You can now modify, filter, reorder, or extend default options return [ @@ -303,7 +303,7 @@ StreamMessageInput( **Example: Filtering default options** ```dart -StreamMessageInput( +StreamMessageComposer( attachmentPickerOptionsBuilder: (context, defaultOptions) { // Remove poll option return defaultOptions.where((option) => option.key != 'poll').toList(); @@ -313,7 +313,7 @@ StreamMessageInput( **Example: Reordering options** ```dart -StreamMessageInput( +StreamMessageComposer( attachmentPickerOptionsBuilder: (context, defaultOptions) { // Reverse the order return defaultOptions.reversed.toList(); @@ -360,7 +360,7 @@ final result = await showStreamAttachmentPickerModalBottomSheet( **Before:** ```dart -StreamMessageInput( +StreamMessageComposer( onCustomAttachmentPickerResult: (result) { if (result is CustomAttachmentPickerResult) { final data = result.data; @@ -372,7 +372,7 @@ StreamMessageInput( **After:** ```dart -StreamMessageInput( +StreamMessageComposer( onAttachmentPickerResult: (result) { if (result is CustomAttachmentPickerResult) { final data = result.data; diff --git a/packages/stream_chat_flutter/README.md b/packages/stream_chat_flutter/README.md index 379a15eb9a..446b12d1f1 100644 --- a/packages/stream_chat_flutter/README.md +++ b/packages/stream_chat_flutter/README.md @@ -99,7 +99,7 @@ Every widget uses the `StreamChat` or `StreamChannel` widgets to manage the stat - [StreamChannelHeader](https://getstream.io/chat/docs/sdk/flutter/stream_chat_flutter/stream_channel_header/) - [StreamChannelListView](https://getstream.io/chat/docs/sdk/flutter/stream_chat_flutter/stream_channel_list_view/) -- [StreamMessageInput](https://getstream.io/chat/docs/sdk/flutter/stream_chat_flutter/stream_message_input/) +- [StreamMessageComposer](https://getstream.io/chat/docs/sdk/flutter/stream_chat_flutter/stream_message_composer/) - [StreamMessageListView](https://getstream.io/chat/docs/sdk/flutter/stream_chat_flutter/stream_message_list_view/) - [StreamMessageWidget](https://getstream.io/chat/docs/sdk/flutter/stream_chat_flutter/stream_message_widget/) - [StreamChatTheme](https://getstream.io/chat/docs/sdk/flutter/stream_chat_flutter/stream_chat_and_theming/) diff --git a/packages/stream_chat_flutter/example/lib/main.dart b/packages/stream_chat_flutter/example/lib/main.dart index a9719055b3..94a8635c1f 100644 --- a/packages/stream_chat_flutter/example/lib/main.dart +++ b/packages/stream_chat_flutter/example/lib/main.dart @@ -256,7 +256,7 @@ class _ChannelPageState extends State { swipeToReply: true, ), ), - StreamMessageInput( + StreamMessageComposer( enableVoiceRecording: true, onQuotedMessageCleared: messageInputController.clearQuotedMessage, focusNode: focusNode, @@ -303,7 +303,7 @@ class ThreadPage extends StatelessWidget { parentMessage: parent, ), ), - StreamMessageInput( + StreamMessageComposer( enableVoiceRecording: true, messageInputController: StreamMessageInputController( message: Message(parentId: parent.id), diff --git a/packages/stream_chat_flutter/example/lib/split_view.dart b/packages/stream_chat_flutter/example/lib/split_view.dart index a873f92265..2d460a94e9 100644 --- a/packages/stream_chat_flutter/example/lib/split_view.dart +++ b/packages/stream_chat_flutter/example/lib/split_view.dart @@ -137,7 +137,7 @@ class ChannelPage extends StatelessWidget { Expanded( child: StreamMessageListView(), ), - StreamMessageInput(), + StreamMessageComposer(), ], ), ), diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_1.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_1.dart index ac6908e3e8..b8ee350817 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_1.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_1.dart @@ -28,7 +28,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// /// - We create a single [ChannelPage] widget under [StreamChat] with three /// widgets: [StreamChannelHeader], [StreamMessageListView] -/// and [StreamMessageInput] +/// and [StreamMessageComposer] /// /// If you now run the simulator you will see a single channel UI. Future main() async { @@ -98,7 +98,7 @@ class ChannelPage extends StatelessWidget { Expanded( child: StreamMessageListView(), ), - StreamMessageInput(), + StreamMessageComposer(), ], ), ); diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart index 8516cb40aa..54c7beba10 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart @@ -133,7 +133,7 @@ class ChannelPage extends StatelessWidget { Expanded( child: StreamMessageListView(), ), - StreamMessageInput(), + StreamMessageComposer(), ], ), ); diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart index d6cbd0d3a2..202488b45c 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart @@ -174,7 +174,7 @@ class ChannelPage extends StatelessWidget { Expanded( child: StreamMessageListView(), ), - StreamMessageInput(), + StreamMessageComposer(), ], ), ); diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart index f271dae858..3201de70cd 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart @@ -116,7 +116,7 @@ class ChannelPage extends StatelessWidget { ), ), ), - const StreamMessageInput(), + const StreamMessageComposer(), ], ), ); @@ -144,7 +144,7 @@ class ThreadPage extends StatelessWidget { parentMessage: parent, ), ), - StreamMessageInput( + StreamMessageComposer( messageInputController: StreamMessageInputController( message: Message(parentId: parent!.id), ), diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart index d667748af8..e0a4e22b0c 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart @@ -120,7 +120,7 @@ class ChannelPage extends StatelessWidget { messageBuilder: _messageBuilder, ), ), - const StreamMessageInput(), + const StreamMessageComposer(), ], ), ); diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart index a9db625f1c..3606e0729a 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart @@ -146,7 +146,7 @@ class ChannelPage extends StatelessWidget { ), ), ), - const StreamMessageInput(), + const StreamMessageComposer(), ], ), ); @@ -174,7 +174,7 @@ class ThreadPage extends StatelessWidget { parentMessage: parent, ), ), - StreamMessageInput( + StreamMessageComposer( messageInputController: StreamMessageInputController( message: Message(parentId: parent!.id), ), diff --git a/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer.dart b/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer.dart index 08601961f1..c637420ea1 100644 --- a/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer.dart +++ b/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer.dart @@ -1,4 +1,4 @@ export 'message_composer_component_props.dart'; export 'message_composer_input_trailing.dart' show DefaultStreamMessageComposerInputTrailing; export 'message_composer_leading.dart' show DefaultStreamMessageComposerLeading; -export 'stream_chat_message_composer.dart'; +export 'stream_chat_message_input.dart'; diff --git a/packages/stream_chat_flutter/lib/src/components/message_composer/stream_chat_message_composer.dart b/packages/stream_chat_flutter/lib/src/components/message_composer/stream_chat_message_input.dart similarity index 94% rename from packages/stream_chat_flutter/lib/src/components/message_composer/stream_chat_message_composer.dart rename to packages/stream_chat_flutter/lib/src/components/message_composer/stream_chat_message_input.dart index d15950af9a..e1a3f02f8e 100644 --- a/packages/stream_chat_flutter/lib/src/components/message_composer/stream_chat_message_composer.dart +++ b/packages/stream_chat_flutter/lib/src/components/message_composer/stream_chat_message_input.dart @@ -13,8 +13,8 @@ import 'package:stream_core_flutter/stream_core_flutter.dart' as core; /// A widget that shows the message composer. /// Uses the factory to show custom components or the default implementation. -class StreamChatMessageComposer extends StatefulWidget { - /// Creates a new instance of [StreamChatMessageComposer]. +class StreamChatMessageInput extends StatefulWidget { + /// Creates a new instance of [StreamChatMessageInput]. /// [controller] is the controller for the message composer. /// [onSendPressed] is the callback for when the send button is pressed. /// [onMicrophonePressed] is the callback for when the microphone button is pressed. @@ -22,7 +22,7 @@ class StreamChatMessageComposer extends StatefulWidget { /// [focusNode] is the focus node for the message composer. /// [currentUserId] is the current user id. /// [placeholder] is the placeholder text of the message composer. - StreamChatMessageComposer({ + StreamChatMessageInput({ super.key, StreamMessageInputController? controller, required VoidCallback onSendPressed, @@ -70,10 +70,10 @@ class StreamChatMessageComposer extends StatefulWidget { final MessageComposerProps props; @override - State createState() => _StreamChatMessageComposerState(); + State createState() => _StreamChatMessageInputState(); } -class _StreamChatMessageComposerState extends State { +class _StreamChatMessageInputState extends State { late StreamMessageInputController _controller; @override @@ -83,7 +83,7 @@ class _StreamChatMessageComposerState extends State { } @override - void didUpdateWidget(StreamChatMessageComposer oldWidget) { + void didUpdateWidget(StreamChatMessageInput oldWidget) { super.didUpdateWidget(oldWidget); if (widget.controller != oldWidget.controller) { _disposeController(oldWidget); @@ -101,7 +101,7 @@ class _StreamChatMessageComposerState extends State { _controller = widget.controller ?? StreamMessageInputController(); } - void _disposeController(StreamChatMessageComposer widget) { + void _disposeController(StreamChatMessageInput widget) { if (widget.controller == null) { _controller.dispose(); } @@ -115,7 +115,7 @@ class _StreamChatMessageComposerState extends State { final audioRecorderController = widget.props.audioRecorderController; if (audioRecorderController == null) { - return DefaultStreamChatMessageComposer( + return DefaultStreamChatMessageInput( props: widget.props, inputController: _controller, ); @@ -170,7 +170,7 @@ class _StreamChatMessageComposerState extends State { ), visible: state is RecordStateRecording, portalFollower: SwipeToLockButton(isLocked: state is RecordStateRecordingLocked), - child: DefaultStreamChatMessageComposer( + child: DefaultStreamChatMessageInput( props: widget.props, inputController: _controller, audioRecorderState: state, @@ -228,10 +228,10 @@ class MessageComposerProps { /// The placeholder text of the message composer. /// /// May be `null` to render the input with no placeholder. The wrapping - /// [StreamMessageInput] resolves this string reactively from its + /// [StreamMessageComposer] resolves this string reactively from its /// [StreamMessageInputController] via [MessageInputPlaceholder.resolve] and - /// [StreamMessageInput.placeholderBuilder]; when using - /// [StreamChatMessageComposer] directly, supply the string yourself. + /// [StreamMessageComposer.placeholderBuilder]; when using + /// [StreamChatMessageInput] directly, supply the string yourself. final String? placeholder; /// The callback for when the send button is pressed. @@ -292,13 +292,13 @@ extension on StreamAudioRecorderController { /// Default implementation of the message composer. /// Shows the message composer with the default components. /// Does not include the audio recording flow in the body. -class DefaultStreamChatMessageComposer extends StatelessWidget { - /// Creates a new instance of [DefaultStreamChatMessageComposer]. +class DefaultStreamChatMessageInput extends StatelessWidget { + /// Creates a new instance of [DefaultStreamChatMessageInput]. /// [props] contains the properties for the message composer. /// [inputController] is the controller for the message input. /// [audioRecorderState] is the state of the audio recorder. /// [body] is the body of the message composer. - const DefaultStreamChatMessageComposer({ + const DefaultStreamChatMessageInput({ super.key, required this.props, required this.inputController, diff --git a/packages/stream_chat_flutter/lib/src/keyboard_shortcuts/keysets.dart b/packages/stream_chat_flutter/lib/src/keyboard_shortcuts/keysets.dart index 495be7d922..262ba493e2 100644 --- a/packages/stream_chat_flutter/lib/src/keyboard_shortcuts/keysets.dart +++ b/packages/stream_chat_flutter/lib/src/keyboard_shortcuts/keysets.dart @@ -3,7 +3,7 @@ import 'package:flutter/widgets.dart'; /// The "enter" keyset. /// -/// Use to quickly send a message in [StreamMessageInput]. +/// Use to quickly send a message in [StreamMessageComposer]. final enterKeySet = LogicalKeySet( LogicalKeyboardKey.enter, ); @@ -11,7 +11,7 @@ final enterKeySet = LogicalKeySet( /// The "escape" keyset. /// /// Use for: -/// * Removing a reply from [StreamMessageInput]. +/// * Removing a reply from [StreamMessageComposer]. /// * Closing [FullScreenMediaDesktop]. final escapeKeySet = LogicalKeySet( LogicalKeyboardKey.escape, diff --git a/packages/stream_chat_flutter/lib/src/localization/translations.dart b/packages/stream_chat_flutter/lib/src/localization/translations.dart index bde5e382f3..b189b619d5 100644 --- a/packages/stream_chat_flutter/lib/src/localization/translations.dart +++ b/packages/stream_chat_flutter/lib/src/localization/translations.dart @@ -100,7 +100,7 @@ abstract class Translations { String get reconnectingLabel; /// The label for also send - /// as direct message "checkbox"" in [StreamMessageInput] + /// as direct message "checkbox"" in [StreamMessageComposer] String get alsoSendAsDirectMessageLabel; /// The label for search Gif @@ -110,13 +110,13 @@ abstract class Translations { String get sendMessagePermissionError; /// The label for add a comment or send in case of - /// attachments inside [StreamMessageInput] + /// attachments inside [StreamMessageComposer] String get addACommentOrSendLabel; - /// The label for write a message in [StreamMessageInput] + /// The label for write a message in [StreamMessageComposer] String get writeAMessageLabel; - /// The label for slow mode enabled in [StreamMessageInput] + /// The label for slow mode enabled in [StreamMessageComposer] String get slowModeOnLabel; /// The placeholder shown in the composer when a user-target command (for @@ -126,15 +126,15 @@ abstract class Translations { /// should select or type a username. String get commandUsernameLabel; - /// The label for instant commands in [StreamMessageInput] + /// The label for instant commands in [StreamMessageComposer] String get instantCommandsLabel; /// The error shown in case the file is too large even after compression - /// while uploading via [StreamMessageInput] + /// while uploading via [StreamMessageComposer] String fileTooLargeAfterCompressionError(double limitInMB); /// The error shown in case the file is too large - /// while uploading via [StreamMessageInput] + /// while uploading via [StreamMessageComposer] String fileTooLargeError(double limitInMB); /// The error shown when the file being read has no bytes diff --git a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker_controller.dart b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker_controller.dart index 35c1449951..69f47d4b27 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker_controller.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker_controller.dart @@ -75,7 +75,7 @@ class StreamAttachmentPickerController extends ValueNotifier get customResults => _customResultController.stream; /// Emits a [CustomAttachmentPickerResult] to notify the parent widget - /// (e.g. [StreamMessageInput]) that a custom attachment has been picked. + /// (e.g. [StreamMessageComposer]) that a custom attachment has been picked. /// /// Use this from a [TabbedAttachmentPickerOption.optionViewBuilder] instead /// of calling `Navigator.pop` — the picker is an inline widget, not a modal diff --git a/packages/stream_chat_flutter/lib/src/message_input/enums.dart b/packages/stream_chat_flutter/lib/src/message_input/enums.dart index 5b34b7f132..7fcd315490 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/enums.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/enums.dart @@ -1,4 +1,4 @@ -/// Location for actions on the [StreamMessageInput]. +/// Location for actions on the [StreamMessageComposer]. enum ActionsLocation { /// Align to left left, diff --git a/packages/stream_chat_flutter/lib/src/message_input/message_input_placeholder.dart b/packages/stream_chat_flutter/lib/src/message_input/message_input_placeholder.dart index 33f1b0b4e4..0c7ab7b6c8 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/message_input_placeholder.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/message_input_placeholder.dart @@ -2,13 +2,13 @@ import 'package:flutter/widgets.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// Sealed hierarchy describing why a particular placeholder is shown in -/// [StreamMessageInput]. +/// [StreamMessageComposer]. /// /// The state is resolved once per rebuild from the current /// [StreamMessageInputController] using [MessageInputPlaceholder.resolve], /// then handed to a [MessageInputPlaceholderBuilder] to produce the actual /// placeholder string that gets passed down to the underlying -/// [StreamChatMessageComposer]. +/// [StreamChatMessageInput]. /// /// Each case carries the contextual data relevant to that state — for example /// [SlowModePlaceholder.cooldownTimeOut] for the remaining cooldown, or @@ -133,7 +133,7 @@ final class AttachmentsPlaceholder extends MessageInputPlaceholder { final List attachments; } -/// Returns the placeholder string shown inside [StreamMessageInput]'s text +/// Returns the placeholder string shown inside [StreamMessageComposer]'s text /// field. /// /// Receives the current [MessageInputPlaceholder] state and may return a diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart similarity index 98% rename from packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart rename to packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart index 2ee55e31f8..be277a6632 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart @@ -44,7 +44,7 @@ typedef OgPreviewFilter = bool Function(Uri matchedUri, String messageText); /// ), /// ), /// ), -/// const StreamMessageInput(), +/// const StreamMessageComposer(), /// ], /// ), /// ); @@ -56,9 +56,9 @@ typedef OgPreviewFilter = bool Function(Uri matchedUri, String messageText); /// /// The widget renders the ui based on the first ancestor of /// type [StreamChatTheme]. Modify it to change the widget appearance. -class StreamMessageInput extends StatefulWidget { - /// Instantiate a new MessageInput - const StreamMessageInput({ +class StreamMessageComposer extends StatefulWidget { + /// Instantiate a new MessageComposer + const StreamMessageComposer({ super.key, this.onMessageSent, this.preMessageSending, @@ -147,7 +147,7 @@ class StreamMessageInput extends StatefulWidget { /// /// To disable feedback: /// ```dart - /// StreamMessageInput( + /// StreamMessageComposer( /// voiceRecordingFeedback: const AudioRecorderFeedback.disabled(), /// ) /// ``` @@ -165,7 +165,7 @@ class StreamMessageInput extends StatefulWidget { /// } /// } /// - /// StreamMessageInput( + /// StreamMessageComposer( /// voiceRecordingFeedback: CustomFeedback(), /// ) /// ``` @@ -196,7 +196,7 @@ class StreamMessageInput extends StatefulWidget { /// Defaults to false. final bool mentionAllAppUsers; - /// Defines if the [StreamMessageInput] loses focuses after a message is sent. + /// Defines if the [StreamMessageComposer] loses focuses after a message is sent. /// The default behaviour keeps focus until a command is enabled. final bool? shouldKeepFocusAfterMessage; @@ -206,7 +206,7 @@ class StreamMessageInput extends StatefulWidget { /// Restoration ID to save and restore the state of the MessageInput. final String? restorationId; - /// Wrap [StreamMessageInput] with a [SafeArea widget] + /// Wrap [StreamMessageComposer] with a [SafeArea widget] final bool? enableSafeArea; /// Disable the mentions overlay by passing false @@ -348,12 +348,12 @@ class StreamMessageInput extends StatefulWidget { } @override - StreamMessageInputState createState() => StreamMessageInputState(); + StreamMessageComposerState createState() => StreamMessageComposerState(); } -/// State of [StreamMessageInput] -class StreamMessageInputState extends State - with RestorationMixin, SingleTickerProviderStateMixin { +/// State of [StreamMessageComposer] +class StreamMessageComposerState extends State + with RestorationMixin, SingleTickerProviderStateMixin { bool get _commandEnabled => _effectiveController.message.command != null; bool get _isPickerVisible => _pickerController != null; @@ -502,7 +502,7 @@ class StreamMessageInputState extends State } @override - void didUpdateWidget(covariant StreamMessageInput oldWidget) { + void didUpdateWidget(covariant StreamMessageComposer oldWidget) { super.didUpdateWidget(oldWidget); if (widget.messageInputController == null && oldWidget.messageInputController != null) { _createLocalController(oldWidget.messageInputController!.message); @@ -707,7 +707,7 @@ class StreamMessageInputState extends State child: Focus( skipTraversal: true, onKeyEvent: _handleKeyPressed, - child: StreamChatMessageComposer( + child: StreamChatMessageInput( controller: controller, currentUserId: currentUserId, onAttachmentButtonPressed: widget.disableAttachments ? null : _onAttachmentButtonPressed, diff --git a/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart index 0ebcfdca90..22bb5cd2f3 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart @@ -79,7 +79,7 @@ typedef StreamMessageWidgetBuilder = /// }, /// ), /// ), -/// StreamMessageInput(), +/// StreamMessageComposer(), /// ], /// ), /// ); diff --git a/packages/stream_chat_flutter/lib/src/utils/typedefs.dart b/packages/stream_chat_flutter/lib/src/utils/typedefs.dart index ac598088bd..aaae85eda6 100644 --- a/packages/stream_chat_flutter/lib/src/utils/typedefs.dart +++ b/packages/stream_chat_flutter/lib/src/utils/typedefs.dart @@ -101,7 +101,7 @@ typedef AttachmentActionsBuilder = ); /// {@template errorListener} -/// A callback that can be passed to [StreamMessageInput.onError]. +/// A callback that can be passed to [StreamMessageComposer.onError]. /// /// This callback should not throw. /// @@ -115,7 +115,7 @@ typedef ErrorListener = /// {@template attachmentLimitExceededListener} /// A callback that can be passed to -/// [StreamMessageInput.onAttachmentLimitExceed]. +/// [StreamMessageComposer.onAttachmentLimitExceed]. /// /// This callback should not throw. /// diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index 030c7ceb9a..edc141b2d4 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -160,7 +160,7 @@ export 'src/message_input/countdown_button.dart'; export 'src/message_input/enums.dart'; export 'src/message_input/message_input_placeholder.dart'; export 'src/message_input/stream_message_composer_attachment_list.dart'; -export 'src/message_input/stream_message_input.dart'; +export 'src/message_input/stream_message_composer.dart'; export 'src/message_input/stream_message_text_field.dart'; export 'src/message_list_view/message_details.dart'; export 'src/message_list_view/message_list_view.dart'; diff --git a/packages/stream_chat_flutter/test/src/message_input/message_input_test.dart b/packages/stream_chat_flutter/test/src/message_input/message_input_test.dart index 205ca60b76..c44104031a 100644 --- a/packages/stream_chat_flutter/test/src/message_input/message_input_test.dart +++ b/packages/stream_chat_flutter/test/src/message_input/message_input_test.dart @@ -25,7 +25,7 @@ void main() { (WidgetTester tester) async { await tester.pumpWidget( buildWidget( - const StreamMessageInput(), + const StreamMessageComposer(), ), ); @@ -99,7 +99,7 @@ void main() { child: StreamChannel( channel: channel, child: const Scaffold( - body: StreamMessageInput(), + body: StreamMessageComposer(), ), ), ), @@ -152,7 +152,7 @@ void main() { child: StreamChannel( channel: channel, child: const Scaffold( - bottomNavigationBar: StreamMessageInput(), + bottomNavigationBar: StreamMessageComposer(), ), ), ), @@ -194,7 +194,7 @@ void main() { child: StreamChannel( channel: channel, child: const Scaffold( - bottomNavigationBar: StreamMessageInput(), + bottomNavigationBar: StreamMessageComposer(), ), ), ), @@ -242,7 +242,7 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - bottomNavigationBar: StreamMessageInput( + bottomNavigationBar: StreamMessageComposer( messageInputController: messageInputController, onQuotedMessageCleared: () { onQuotedMessageClearedCalled = true; @@ -291,7 +291,7 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - bottomNavigationBar: StreamMessageInput( + bottomNavigationBar: StreamMessageComposer( messageInputController: messageInputController, onQuotedMessageCleared: () { onQuotedMessageClearedCalled = true; @@ -378,7 +378,7 @@ void main() { final messageInputController = StreamMessageInputController()..editMessage(existingMessage); addTearDown(messageInputController.dispose); - final key = GlobalKey(); + final key = GlobalKey(); await tester.pumpWidget( MaterialApp( @@ -387,7 +387,7 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - bottomNavigationBar: StreamMessageInput( + bottomNavigationBar: StreamMessageComposer( key: key, messageInputController: messageInputController, ), @@ -420,7 +420,7 @@ void main() { ); addTearDown(messageInputController.dispose); - final key = GlobalKey(); + final key = GlobalKey(); await tester.pumpWidget( MaterialApp( @@ -429,7 +429,7 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - bottomNavigationBar: StreamMessageInput( + bottomNavigationBar: StreamMessageComposer( key: key, messageInputController: messageInputController, ), @@ -497,7 +497,7 @@ void main() { child: StreamChannel( channel: channel, child: const Scaffold( - bottomNavigationBar: StreamMessageInput( + bottomNavigationBar: StreamMessageComposer( canAlsoSendToChannelFromThread: false, ), ), @@ -523,7 +523,7 @@ void main() { child: StreamChannel( channel: channel, child: const Scaffold( - bottomNavigationBar: StreamMessageInput(), + bottomNavigationBar: StreamMessageComposer(), ), ), ), @@ -552,7 +552,7 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - bottomNavigationBar: StreamMessageInput( + bottomNavigationBar: StreamMessageComposer( messageInputController: messageInputController, ), ), @@ -588,7 +588,7 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - bottomNavigationBar: StreamMessageInput( + bottomNavigationBar: StreamMessageComposer( messageInputController: messageInputController, ), ), @@ -677,7 +677,7 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - bottomNavigationBar: StreamMessageInput( + bottomNavigationBar: StreamMessageComposer( messageInputController: controller, onQuotedMessageCleared: () { onQuotedMessageClearedCalled = true; @@ -729,7 +729,7 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - bottomNavigationBar: StreamMessageInput( + bottomNavigationBar: StreamMessageComposer( messageInputController: controller, onQuotedMessageCleared: () { onQuotedMessageClearedCalled = true; @@ -779,7 +779,7 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - bottomNavigationBar: StreamMessageInput( + bottomNavigationBar: StreamMessageComposer( messageInputController: controller, ), ), @@ -831,7 +831,7 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - bottomNavigationBar: StreamMessageInput( + bottomNavigationBar: StreamMessageComposer( messageInputController: controller, ), ), @@ -877,7 +877,7 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - bottomNavigationBar: StreamMessageInput( + bottomNavigationBar: StreamMessageComposer( messageInputController: controller, ), ), @@ -925,7 +925,7 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - bottomNavigationBar: StreamMessageInput( + bottomNavigationBar: StreamMessageComposer( messageInputController: controller, ), ), @@ -969,7 +969,7 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - bottomNavigationBar: StreamMessageInput( + bottomNavigationBar: StreamMessageComposer( messageInputController: controller, ), ), @@ -1013,7 +1013,7 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - bottomNavigationBar: StreamMessageInput( + bottomNavigationBar: StreamMessageComposer( messageInputController: controller, ), ), @@ -1039,7 +1039,7 @@ void main() { }); } -MaterialApp buildWidget(StreamMessageInput input) { +MaterialApp buildWidget(StreamMessageComposer input) { final client = MockClient(); final clientState = MockClientState(); final channel = MockChannel(); diff --git a/packages/stream_chat_localizations/example/lib/add_new_lang.dart b/packages/stream_chat_localizations/example/lib/add_new_lang.dart index 19b2be6011..4ae4272d9d 100644 --- a/packages/stream_chat_localizations/example/lib/add_new_lang.dart +++ b/packages/stream_chat_localizations/example/lib/add_new_lang.dart @@ -947,7 +947,7 @@ class ChannelPage extends StatelessWidget { Expanded( child: StreamMessageListView(), ), - StreamMessageInput(), + StreamMessageComposer(), ], ), ); diff --git a/packages/stream_chat_localizations/example/lib/main.dart b/packages/stream_chat_localizations/example/lib/main.dart index 0d1b4fbd23..2fb3267438 100644 --- a/packages/stream_chat_localizations/example/lib/main.dart +++ b/packages/stream_chat_localizations/example/lib/main.dart @@ -113,7 +113,7 @@ class ChannelPage extends StatelessWidget { Expanded( child: StreamMessageListView(), ), - StreamMessageInput(), + StreamMessageComposer(), ], ), ); diff --git a/packages/stream_chat_localizations/example/lib/override_lang.dart b/packages/stream_chat_localizations/example/lib/override_lang.dart index 9cd76c9849..63f6427a10 100644 --- a/packages/stream_chat_localizations/example/lib/override_lang.dart +++ b/packages/stream_chat_localizations/example/lib/override_lang.dart @@ -138,7 +138,7 @@ class ChannelPage extends StatelessWidget { Expanded( child: StreamMessageListView(), ), - StreamMessageInput(), + StreamMessageComposer(), ], ), ); diff --git a/sample_app/lib/pages/channel_page.dart b/sample_app/lib/pages/channel_page.dart index adca6ef203..e69b203969 100644 --- a/sample_app/lib/pages/channel_page.dart +++ b/sample_app/lib/pages/channel_page.dart @@ -136,7 +136,7 @@ class _ChannelPageState extends State { final locationEnabled = appConfig.enableLocationSharing && config?.sharedLocations == true && channel.canShareLocation; - return StreamMessageInput( + return StreamMessageComposer( focusNode: _focusNode, messageInputController: _messageInputController, onQuotedMessageCleared: _messageInputController.clearQuotedMessage, diff --git a/sample_app/lib/pages/new_chat_screen.dart b/sample_app/lib/pages/new_chat_screen.dart index 3a23d869bf..8e5e5e7a0a 100644 --- a/sample_app/lib/pages/new_chat_screen.dart +++ b/sample_app/lib/pages/new_chat_screen.dart @@ -367,7 +367,7 @@ class _NewChatScreenState extends State { }, ), ), - StreamMessageInput( + StreamMessageComposer( focusNode: _messageInputFocusNode, preMessageSending: (message) async { await channel!.watch(); diff --git a/sample_app/lib/pages/thread_page.dart b/sample_app/lib/pages/thread_page.dart index 5fdf66a523..e65a76ae67 100644 --- a/sample_app/lib/pages/thread_page.dart +++ b/sample_app/lib/pages/thread_page.dart @@ -63,7 +63,7 @@ class _ThreadPageState extends State { ), ), if (widget.parent.type != 'deleted') - StreamMessageInput( + StreamMessageComposer( focusNode: _focusNode, messageInputController: _messageInputController, enableVoiceRecording: true, From edac372ae35d2c070203581c295559a3b484d0af Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Tue, 5 May 2026 13:35:19 +0200 Subject: [PATCH 02/13] Move factory for composer --- .../stream_message_composer_test.dart | 4 +- .../voice_recording/voice_recording_test.dart | 2 +- .../example/lib/split_view.dart | 6 +- .../example/lib/tutorial_part_1.dart | 6 +- .../example/lib/tutorial_part_2.dart | 6 +- .../example/lib/tutorial_part_3.dart | 6 +- .../example/lib/tutorial_part_4.dart | 2 +- .../example/lib/tutorial_part_5.dart | 2 +- .../example/lib/tutorial_part_6.dart | 2 +- .../stream_chat_message_input.dart | 266 +++++++++--------- .../stream_message_composer.dart | 236 +++++++++++----- .../src/message_input/message_input_test.dart | 16 +- .../example/lib/add_new_lang.dart | 6 +- .../example/lib/main.dart | 4 +- .../example/lib/override_lang.dart | 6 +- 15 files changed, 333 insertions(+), 237 deletions(-) diff --git a/docs/docs_screenshots/test/message_input/stream_message_composer_test.dart b/docs/docs_screenshots/test/message_input/stream_message_composer_test.dart index c74aa7f2c1..ddc3e68afa 100644 --- a/docs/docs_screenshots/test/message_input/stream_message_composer_test.dart +++ b/docs/docs_screenshots/test/message_input/stream_message_composer_test.dart @@ -28,7 +28,7 @@ Widget _buildMessageInputScaffold({ body: Column( children: [ Expanded(child: Container()), - messageInput ?? const StreamMessageComposer(), + messageInput ?? StreamMessageComposer(), ], ), ), @@ -181,7 +181,7 @@ void main() { body: Column( children: [ Expanded(child: Container()), - const StreamMessageComposer(), + StreamMessageComposer(), ], ), ), diff --git a/docs/docs_screenshots/test/voice_recording/voice_recording_test.dart b/docs/docs_screenshots/test/voice_recording/voice_recording_test.dart index ba927f3170..ba8dd7c96c 100644 --- a/docs/docs_screenshots/test/voice_recording/voice_recording_test.dart +++ b/docs/docs_screenshots/test/voice_recording/voice_recording_test.dart @@ -93,7 +93,7 @@ Widget _buildVoiceRecordingContextScaffold({ ], ), ), - const StreamMessageComposer(enableVoiceRecording: true), + StreamMessageComposer(enableVoiceRecording: true), ], ), ), diff --git a/packages/stream_chat_flutter/example/lib/split_view.dart b/packages/stream_chat_flutter/example/lib/split_view.dart index 2d460a94e9..5a46c2a7ea 100644 --- a/packages/stream_chat_flutter/example/lib/split_view.dart +++ b/packages/stream_chat_flutter/example/lib/split_view.dart @@ -128,13 +128,13 @@ class ChannelPage extends StatelessWidget { @override Widget build(BuildContext context) => Navigator( onGenerateRoute: (settings) => MaterialPageRoute( - builder: (context) => const Scaffold( - appBar: StreamChannelHeader( + builder: (context) => Scaffold( + appBar: const StreamChannelHeader( showBackButton: false, ), body: Column( children: [ - Expanded( + const Expanded( child: StreamMessageListView(), ), StreamMessageComposer(), diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_1.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_1.dart index b8ee350817..0e7a848d28 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_1.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_1.dart @@ -91,11 +91,11 @@ class ChannelPage extends StatelessWidget { @override // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { - return const Scaffold( - appBar: StreamChannelHeader(), + return Scaffold( + appBar: const StreamChannelHeader(), body: Column( children: [ - Expanded( + const Expanded( child: StreamMessageListView(), ), StreamMessageComposer(), diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart index 54c7beba10..6cd42d2d31 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart @@ -126,11 +126,11 @@ class ChannelPage extends StatelessWidget { @override Widget build(BuildContext context) { - return const Scaffold( - appBar: StreamChannelHeader(), + return Scaffold( + appBar: const StreamChannelHeader(), body: Column( children: [ - Expanded( + const Expanded( child: StreamMessageListView(), ), StreamMessageComposer(), diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart index 202488b45c..91c639b921 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart @@ -167,11 +167,11 @@ class ChannelPage extends StatelessWidget { @override Widget build(BuildContext context) { - return const Scaffold( - appBar: StreamChannelHeader(), + return Scaffold( + appBar: const StreamChannelHeader(), body: Column( children: [ - Expanded( + const Expanded( child: StreamMessageListView(), ), StreamMessageComposer(), diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart index 3201de70cd..3efac6d0a4 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart @@ -116,7 +116,7 @@ class ChannelPage extends StatelessWidget { ), ), ), - const StreamMessageComposer(), + StreamMessageComposer(), ], ), ); diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart index e0a4e22b0c..652128f1c6 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart @@ -120,7 +120,7 @@ class ChannelPage extends StatelessWidget { messageBuilder: _messageBuilder, ), ), - const StreamMessageComposer(), + StreamMessageComposer(), ], ), ); diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart index 3606e0729a..51bdcd5360 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart @@ -146,7 +146,7 @@ class ChannelPage extends StatelessWidget { ), ), ), - const StreamMessageComposer(), + StreamMessageComposer(), ], ), ); diff --git a/packages/stream_chat_flutter/lib/src/components/message_composer/stream_chat_message_input.dart b/packages/stream_chat_flutter/lib/src/components/message_composer/stream_chat_message_input.dart index e1a3f02f8e..05d942ff8d 100644 --- a/packages/stream_chat_flutter/lib/src/components/message_composer/stream_chat_message_input.dart +++ b/packages/stream_chat_flutter/lib/src/components/message_composer/stream_chat_message_input.dart @@ -13,7 +13,7 @@ import 'package:stream_core_flutter/stream_core_flutter.dart' as core; /// A widget that shows the message composer. /// Uses the factory to show custom components or the default implementation. -class StreamChatMessageInput extends StatefulWidget { +class StreamChatMessageInput extends StatelessWidget { /// Creates a new instance of [StreamChatMessageInput]. /// [controller] is the controller for the message composer. /// [onSendPressed] is the callback for when the send button is pressed. @@ -41,7 +41,7 @@ class StreamChatMessageInput extends StatefulWidget { TextCapitalization textCapitalization = TextCapitalization.sentences, bool autofocus = false, bool autocorrect = true, - }) : props = MessageComposerProps( + }) : props = StreamChatMessageInputProps( controller: controller, isFloating: false, message: null, @@ -67,125 +67,17 @@ class StreamChatMessageInput extends StatefulWidget { StreamMessageInputController? get controller => props.controller; /// The properties for the message composer. - final MessageComposerProps props; - - @override - State createState() => _StreamChatMessageInputState(); -} - -class _StreamChatMessageInputState extends State { - late StreamMessageInputController _controller; - - @override - void initState() { - super.initState(); - _initController(); - } - - @override - void didUpdateWidget(StreamChatMessageInput oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.controller != oldWidget.controller) { - _disposeController(oldWidget); - _initController(); - } - } - - @override - void dispose() { - _disposeController(widget); - super.dispose(); - } - - void _initController() { - _controller = widget.controller ?? StreamMessageInputController(); - } - - void _disposeController(StreamChatMessageInput widget) { - if (widget.controller == null) { - _controller.dispose(); - } - } + final StreamChatMessageInputProps props; @override Widget build(BuildContext context) { - if (context.chatComponentBuilder()?.call(context, widget.props) case final messageComposer?) { - return messageComposer; - } - - final audioRecorderController = widget.props.audioRecorderController; - if (audioRecorderController == null) { - return DefaultStreamChatMessageInput( - props: widget.props, - inputController: _controller, - ); - } - - return ValueListenableBuilder( - valueListenable: audioRecorderController, - builder: (context, state, _) { - final body = switch (state) { - RecordStateRecordingLocked() => MessageComposerRecordingLocked( - audioRecorderController: audioRecorderController, - feedback: widget.props.feedback, - messageInputController: _controller, - sendMessageCallback: widget.props.sendVoiceRecordingAutomatically ? widget.props.onSendPressed : null, - state: state, - ), - RecordStateStopped() => MessageComposerRecordingStopped( - audioRecorderController: audioRecorderController, - feedback: widget.props.feedback, - messageInputController: _controller, - sendMessageCallback: widget.props.sendVoiceRecordingAutomatically ? widget.props.onSendPressed : null, - recordingState: state, - ), - RecordStateRecording() => StreamMessageComposerRecordingOngoing( - audioRecorderController: audioRecorderController, - ), - _ => null, - }; - - final streamSpacing = context.streamSpacing; - final textDirection = Directionality.maybeOf(context); - - const targetAlignment = AlignmentDirectional.topEnd; - const followerAlignment = AlignmentDirectional.bottomEnd; - - final idleMessage = state is RecordStateIdle ? state.message : null; - final showIdleTooltip = idleMessage != null && idleMessage.isNotEmpty; - - return PortalTarget( - visible: showIdleTooltip, - anchor: Aligned( - target: Alignment.topCenter, - follower: Alignment.bottomCenter, - offset: Offset(0, -streamSpacing.md), - ), - portalFollower: showIdleTooltip ? HoldToRecordInfoTooltip(message: idleMessage) : const SizedBox.shrink(), - child: PortalTarget( - anchor: Aligned( - target: targetAlignment.resolve(textDirection), - follower: followerAlignment.resolve(textDirection), - offset: Offset(-streamSpacing.md, -streamSpacing.md).directional(textDirection), - ), - visible: state is RecordStateRecording, - portalFollower: SwipeToLockButton(isLocked: state is RecordStateRecordingLocked), - child: DefaultStreamChatMessageInput( - props: widget.props, - inputController: _controller, - audioRecorderState: state, - body: body, - ), - ), - ); - }, - ); + return DefaultStreamChatMessageInput(props: props); } } -/// Properties to build the main message composer component -class MessageComposerProps { - /// Creates a new instance of [MessageComposerProps]. +/// Properties for [StreamChatMessageInput] and [DefaultStreamChatMessageInput]. +class StreamChatMessageInputProps { + /// Creates a new instance of [StreamChatMessageInputProps]. /// [isFloating] is whether the message composer is floating. /// [message] is the message for the message composer. /// [placeholder] is the placeholder text of the message composer. @@ -194,7 +86,7 @@ class MessageComposerProps { /// [onAttachmentButtonPressed] is the callback for when the attachment button is pressed. /// [focusNode] is the focus node for the message composer. /// [currentUserId] is the current user id. - const MessageComposerProps({ + const StreamChatMessageInputProps({ this.controller, this.isFloating = false, this.message, @@ -290,39 +182,141 @@ extension on StreamAudioRecorderController { } /// Default implementation of the message composer. -/// Shows the message composer with the default components. -/// Does not include the audio recording flow in the body. -class DefaultStreamChatMessageInput extends StatelessWidget { +/// Manages the controller lifecycle and handles the audio recording state. +class DefaultStreamChatMessageInput extends StatefulWidget { /// Creates a new instance of [DefaultStreamChatMessageInput]. /// [props] contains the properties for the message composer. - /// [inputController] is the controller for the message input. - /// [audioRecorderState] is the state of the audio recorder. - /// [body] is the body of the message composer. - const DefaultStreamChatMessageInput({ - super.key, + const DefaultStreamChatMessageInput({super.key, required this.props}); + + /// The properties for the message composer. + final StreamChatMessageInputProps props; + + @override + State createState() => _DefaultStreamChatMessageInputState(); +} + +class _DefaultStreamChatMessageInputState extends State { + late StreamMessageInputController _controller; + + @override + void initState() { + super.initState(); + _initController(); + } + + @override + void didUpdateWidget(DefaultStreamChatMessageInput oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.props.controller != oldWidget.props.controller) { + _disposeController(oldWidget.props); + _initController(); + } + } + + @override + void dispose() { + _disposeController(widget.props); + super.dispose(); + } + + void _initController() { + _controller = widget.props.controller ?? StreamMessageInputController(); + } + + void _disposeController(StreamChatMessageInputProps props) { + if (props.controller == null) { + _controller.dispose(); + } + } + + @override + Widget build(BuildContext context) { + final audioRecorderController = widget.props.audioRecorderController; + if (audioRecorderController == null) { + return _StreamChatMessageInputContent( + props: widget.props, + inputController: _controller, + ); + } + + return ValueListenableBuilder( + valueListenable: audioRecorderController, + builder: (context, state, _) { + final body = switch (state) { + RecordStateRecordingLocked() => MessageComposerRecordingLocked( + audioRecorderController: audioRecorderController, + feedback: widget.props.feedback, + messageInputController: _controller, + sendMessageCallback: widget.props.sendVoiceRecordingAutomatically ? widget.props.onSendPressed : null, + state: state, + ), + RecordStateStopped() => MessageComposerRecordingStopped( + audioRecorderController: audioRecorderController, + feedback: widget.props.feedback, + messageInputController: _controller, + sendMessageCallback: widget.props.sendVoiceRecordingAutomatically ? widget.props.onSendPressed : null, + recordingState: state, + ), + RecordStateRecording() => StreamMessageComposerRecordingOngoing( + audioRecorderController: audioRecorderController, + ), + _ => null, + }; + + final streamSpacing = context.streamSpacing; + final textDirection = Directionality.maybeOf(context); + + const targetAlignment = AlignmentDirectional.topEnd; + const followerAlignment = AlignmentDirectional.bottomEnd; + + final idleMessage = state is RecordStateIdle ? state.message : null; + final showIdleTooltip = idleMessage != null && idleMessage.isNotEmpty; + + return PortalTarget( + visible: showIdleTooltip, + anchor: Aligned( + target: Alignment.topCenter, + follower: Alignment.bottomCenter, + offset: Offset(0, -streamSpacing.md), + ), + portalFollower: showIdleTooltip ? HoldToRecordInfoTooltip(message: idleMessage) : const SizedBox.shrink(), + child: PortalTarget( + anchor: Aligned( + target: targetAlignment.resolve(textDirection), + follower: followerAlignment.resolve(textDirection), + offset: Offset(-streamSpacing.md, -streamSpacing.md).directional(textDirection), + ), + visible: state is RecordStateRecording, + portalFollower: SwipeToLockButton(isLocked: state is RecordStateRecordingLocked), + child: _StreamChatMessageInputContent( + props: widget.props, + inputController: _controller, + audioRecorderState: state, + body: body, + ), + ), + ); + }, + ); + } +} + +// The actual UI content of the message composer. +// Does not include the audio recording flow in the body. +class _StreamChatMessageInputContent extends StatelessWidget { + const _StreamChatMessageInputContent({ required this.props, required this.inputController, this.audioRecorderState = const RecordStateIdle(), this.body, }); - /// The properties for the message composer. - final MessageComposerProps props; - - /// The controller for the message input. + final StreamChatMessageInputProps props; final StreamMessageInputController inputController; - - /// The state of the audio recorder. - /// Used for the microphone button state. final AudioRecorderState audioRecorderState; - - /// The body of the message composer. final Widget? body; - /// The threshold to lock the recording. static const double _lockRecordThreshold = 50; - - /// The threshold to cancel the recording. static const double _cancelRecordThreshold = 75; @override diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart index be277a6632..effa554ca9 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart @@ -56,10 +56,99 @@ typedef OgPreviewFilter = bool Function(Uri matchedUri, String messageText); /// /// The widget renders the ui based on the first ancestor of /// type [StreamChatTheme]. Modify it to change the widget appearance. -class StreamMessageComposer extends StatefulWidget { - /// Instantiate a new MessageComposer - const StreamMessageComposer({ +class StreamMessageComposer extends StatelessWidget { + /// Instantiate a new StreamMessageComposer + StreamMessageComposer({ super.key, + void Function(Message)? onMessageSent, + FutureOr Function(Message)? preMessageSending, + StreamMessageInputController? messageInputController, + FocusNode? focusNode, + bool disableAttachments = false, + int maxAttachmentSize = kDefaultMaxAttachmentSize, + bool canAlsoSendToChannelFromThread = true, + bool enableVoiceRecording = false, + bool sendVoiceRecordingAutomatically = false, + AudioRecorderFeedback voiceRecordingFeedback = const AudioRecorderFeedback(), + UserMentionTileBuilder? userMentionsTileBuilder, + ErrorListener? onError, + int? attachmentLimit, + List allowedAttachmentPickerTypes = AttachmentPickerType.values, + AttachmentLimitExceedListener? onAttachmentLimitExceed, + Iterable customAutocompleteTriggers = const [], + bool mentionAllAppUsers = false, + bool? shouldKeepFocusAfterMessage, + MessageValidator validator = MessageComposerProps._defaultValidator, + String? restorationId, + bool? enableSafeArea, + bool enableMentionsOverlay = true, + VoidCallback? onQuotedMessageCleared, + OgPreviewFilter ogPreviewFilter = MessageComposerProps._defaultOgPreviewFilter, + MessageInputPlaceholderBuilder placeholderBuilder = MessageComposerProps._defaultPlaceholderBuilder, + bool useSystemAttachmentPicker = false, + PollConfig? pollConfig, + AttachmentPickerOptionsBuilder? attachmentPickerOptionsBuilder, + OnAttachmentPickerResult? onAttachmentPickerResult, + KeyEventPredicate sendMessageKeyPredicate = MessageComposerProps._defaultSendMessageKeyPredicate, + KeyEventPredicate clearQuotedMessageKeyPredicate = MessageComposerProps._defaultClearQuotedMessageKeyPredicate, + TextInputAction? textInputAction, + TextInputType? keyboardType, + TextCapitalization textCapitalization = TextCapitalization.sentences, + bool autofocus = false, + bool autoCorrect = true, + }) : props = MessageComposerProps( + onMessageSent: onMessageSent, + preMessageSending: preMessageSending, + messageInputController: messageInputController, + focusNode: focusNode, + disableAttachments: disableAttachments, + maxAttachmentSize: maxAttachmentSize, + canAlsoSendToChannelFromThread: canAlsoSendToChannelFromThread, + enableVoiceRecording: enableVoiceRecording, + sendVoiceRecordingAutomatically: sendVoiceRecordingAutomatically, + voiceRecordingFeedback: voiceRecordingFeedback, + userMentionsTileBuilder: userMentionsTileBuilder, + onError: onError, + attachmentLimit: attachmentLimit, + allowedAttachmentPickerTypes: allowedAttachmentPickerTypes, + onAttachmentLimitExceed: onAttachmentLimitExceed, + customAutocompleteTriggers: customAutocompleteTriggers, + mentionAllAppUsers: mentionAllAppUsers, + shouldKeepFocusAfterMessage: shouldKeepFocusAfterMessage, + validator: validator, + restorationId: restorationId, + enableSafeArea: enableSafeArea, + enableMentionsOverlay: enableMentionsOverlay, + onQuotedMessageCleared: onQuotedMessageCleared, + ogPreviewFilter: ogPreviewFilter, + placeholderBuilder: placeholderBuilder, + useSystemAttachmentPicker: useSystemAttachmentPicker, + pollConfig: pollConfig, + attachmentPickerOptionsBuilder: attachmentPickerOptionsBuilder, + onAttachmentPickerResult: onAttachmentPickerResult, + sendMessageKeyPredicate: sendMessageKeyPredicate, + clearQuotedMessageKeyPredicate: clearQuotedMessageKeyPredicate, + textInputAction: textInputAction, + keyboardType: keyboardType, + textCapitalization: textCapitalization, + autofocus: autofocus, + autoCorrect: autoCorrect, + ); + + /// The properties for the message composer. + final MessageComposerProps props; + + @override + Widget build(BuildContext context) { + return context.chatComponentBuilder()?.call(context, props) ?? + DefaultStreamMessageComposer(props: props); + } +} + +/// Properties for [StreamMessageComposer] and [DefaultStreamMessageComposer]. +class MessageComposerProps { + /// Creates a new instance of [MessageComposerProps]. + const MessageComposerProps({ this.onMessageSent, this.preMessageSending, this.messageInputController, @@ -98,9 +187,6 @@ class StreamMessageComposer extends StatefulWidget { this.autoCorrect = true, }); - /// List of triggers for showing autocomplete. - final Iterable customAutocompleteTriggers; - /// Function called after sending the message. final void Function(Message)? onMessageSent; @@ -191,6 +277,9 @@ class StreamMessageComposer extends StatefulWidget { /// This will override the default error alert behaviour. final AttachmentLimitExceedListener? onAttachmentLimitExceed; + /// List of triggers for showing autocomplete. + final Iterable customAutocompleteTriggers; + /// When enabled mentions search users across the entire app. /// /// Defaults to false. @@ -242,7 +331,7 @@ class StreamMessageComposer extends StatefulWidget { /// ``` final MessageInputPlaceholderBuilder placeholderBuilder; - /// If True, allows you to use the system’s default media picker instead of + /// If True, allows you to use the system's default media picker instead of /// the custom media picker provided by the library. This can be beneficial /// for several reasons: /// @@ -346,14 +435,27 @@ class StreamMessageComposer extends StatefulWidget { CommandPlaceholder() || AttachmentsPlaceholder() || WriteMessagePlaceholder() => translations.writeAMessageLabel, }; } +} + +/// Default implementation of [StreamMessageComposer]. +/// +/// Contains the full stateful implementation. To provide a custom composer, +/// register a [StreamComponentBuilder] for [MessageComposerProps] via +/// [StreamComponentFactory] instead of subclassing this widget. +class DefaultStreamMessageComposer extends StatefulWidget { + /// Creates a new instance of [DefaultStreamMessageComposer]. + const DefaultStreamMessageComposer({super.key, required this.props}); + + /// The properties for the message composer. + final MessageComposerProps props; @override - StreamMessageComposerState createState() => StreamMessageComposerState(); + DefaultStreamMessageComposerState createState() => DefaultStreamMessageComposerState(); } -/// State of [StreamMessageComposer] -class StreamMessageComposerState extends State - with RestorationMixin, SingleTickerProviderStateMixin { +/// State of [DefaultStreamMessageComposer]. +class DefaultStreamMessageComposerState extends State + with RestorationMixin, SingleTickerProviderStateMixin { bool get _commandEnabled => _effectiveController.message.command != null; bool get _isPickerVisible => _pickerController != null; @@ -370,10 +472,10 @@ class StreamMessageComposerState extends State late final _audioRecorderController = StreamAudioRecorderController(); - FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode()); + FocusNode get _effectiveFocusNode => widget.props.focusNode ?? (_focusNode ??= FocusNode()); FocusNode? _focusNode; - StreamMessageInputController get _effectiveController => widget.messageInputController ?? _controller!.value; + StreamMessageInputController get _effectiveController => widget.props.messageInputController ?? _controller!.value; StreamRestorableMessageInputController? _controller; void _createLocalController([Message? message]) { @@ -411,7 +513,7 @@ class StreamMessageComposerState extends State parent: _pickerAnimationController, curve: Curves.easeInOut, ); - if (widget.messageInputController == null) { + if (widget.props.messageInputController == null) { _createLocalController(); } else { _initialiseEffectiveController(); @@ -469,7 +571,7 @@ class StreamMessageComposerState extends State if (deletedMessageId == null) return; if (_effectiveController.message.quotedMessageId == deletedMessageId) { - widget.onQuotedMessageCleared?.call(); + widget.props.onQuotedMessageCleared?.call(); } if (_isEditing && _effectiveController.message.id == deletedMessageId) { @@ -502,11 +604,11 @@ class StreamMessageComposerState extends State } @override - void didUpdateWidget(covariant StreamMessageComposer oldWidget) { + void didUpdateWidget(covariant DefaultStreamMessageComposer oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.messageInputController == null && oldWidget.messageInputController != null) { - _createLocalController(oldWidget.messageInputController!.message); - } else if (widget.messageInputController != null && oldWidget.messageInputController == null) { + if (widget.props.messageInputController == null && oldWidget.props.messageInputController != null) { + _createLocalController(oldWidget.props.messageInputController!.message); + } else if (widget.props.messageInputController != null && oldWidget.props.messageInputController == null) { unregisterFromRestoration(_controller!); _controller!.dispose(); _controller = null; @@ -514,9 +616,9 @@ class StreamMessageComposerState extends State } // Update _focusNode - if (widget.focusNode != oldWidget.focusNode) { - (oldWidget.focusNode ?? _focusNode)?.removeListener(_focusNodeListener); - (widget.focusNode ?? _focusNode)?.addListener(_focusNodeListener); + if (widget.props.focusNode != oldWidget.props.focusNode) { + (oldWidget.props.focusNode ?? _focusNode)?.removeListener(_focusNodeListener); + (widget.props.focusNode ?? _focusNode)?.addListener(_focusNodeListener); } } @@ -528,7 +630,7 @@ class StreamMessageComposerState extends State } @override - String? get restorationId => widget.restorationId; + String? get restorationId => widget.props.restorationId; void _focusNodeListener() { if (_effectiveFocusNode.hasFocus && _isPickerVisible) { @@ -537,15 +639,15 @@ class StreamMessageComposerState extends State } KeyEventResult _handleKeyPressed(FocusNode node, KeyEvent event) { - if (widget.sendMessageKeyPredicate(node, event)) { + if (widget.props.sendMessageKeyPredicate(node, event)) { sendMessage(); return KeyEventResult.handled; } - if (widget.clearQuotedMessageKeyPredicate(node, event)) { + if (widget.props.clearQuotedMessageKeyPredicate(node, event)) { final hasQuote = _effectiveController.message.quotedMessage != null; if (hasQuote && _effectiveController.text.isEmpty) { _effectiveController.clearQuotedMessage(); - widget.onQuotedMessageCleared?.call(); + widget.props.onQuotedMessageCleared?.call(); return KeyEventResult.handled; } return KeyEventResult.ignored; @@ -589,7 +691,7 @@ class StreamMessageComposerState extends State }; final spacing = context.streamSpacing; - final safeAreaEnabled = widget.enableSafeArea ?? true; + final safeAreaEnabled = widget.props.enableSafeArea ?? true; final viewPadding = MediaQuery.paddingOf(context); return Material( @@ -626,7 +728,7 @@ class StreamMessageComposerState extends State messageEditingController: _effectiveController, fieldViewBuilder: _buildMessageInput, autocompleteTriggers: [ - ...widget.customAutocompleteTriggers, + ...widget.props.customAutocompleteTriggers, StreamAutocompleteTrigger( trigger: _kCommandTrigger, triggerOnlyAtStart: true, @@ -648,7 +750,7 @@ class StreamMessageComposerState extends State ); }, ), - if (widget.enableMentionsOverlay) + if (widget.props.enableMentionsOverlay) StreamAutocompleteTrigger( trigger: _kMentionTrigger, optionsViewBuilder: @@ -661,8 +763,8 @@ class StreamMessageComposerState extends State return StreamMentionAutocompleteOptions( query: query, channel: StreamChannel.of(context).channel, - mentionAllAppUsers: widget.mentionAllAppUsers, - mentionsTileBuilder: widget.userMentionsTileBuilder, + mentionAllAppUsers: widget.props.mentionAllAppUsers, + mentionsTileBuilder: widget.props.userMentionsTileBuilder, onMentionUserTap: (user) { // adding the mentioned user to the controller. _effectiveController.addMentionedUser(user); @@ -710,24 +812,24 @@ class StreamMessageComposerState extends State child: StreamChatMessageInput( controller: controller, currentUserId: currentUserId, - onAttachmentButtonPressed: widget.disableAttachments ? null : _onAttachmentButtonPressed, + onAttachmentButtonPressed: widget.props.disableAttachments ? null : _onAttachmentButtonPressed, isPickerOpen: _isPickerVisible, placeholder: _buildPlaceholder(context), focusNode: focusNode, onSendPressed: sendMessage, canAlsoSendToChannel: _shouldShowSendToChannelCheckbox(), - audioRecorderController: widget.enableVoiceRecording ? _audioRecorderController : null, - sendVoiceRecordingAutomatically: widget.sendVoiceRecordingAutomatically, - feedback: widget.voiceRecordingFeedback, + audioRecorderController: widget.props.enableVoiceRecording ? _audioRecorderController : null, + sendVoiceRecordingAutomatically: widget.props.sendVoiceRecordingAutomatically, + feedback: widget.props.voiceRecordingFeedback, onQuotedMessageCleared: () { _effectiveController.clearQuotedMessage(); - widget.onQuotedMessageCleared?.call(); + widget.props.onQuotedMessageCleared?.call(); }, - textInputAction: widget.textInputAction, - keyboardType: widget.keyboardType, - textCapitalization: widget.textCapitalization, - autofocus: widget.autofocus, - autocorrect: widget.autoCorrect, + textInputAction: widget.props.textInputAction, + keyboardType: widget.props.keyboardType, + textCapitalization: widget.props.textCapitalization, + autofocus: widget.props.autofocus, + autocorrect: widget.props.autoCorrect, ), ), ), @@ -751,15 +853,15 @@ class StreamMessageComposerState extends State PlatformType.android || PlatformType.ios => false, _ => true, }; - final useSystemPicker = widget.useSystemAttachmentPicker || isWebOrDesktop; + final useSystemPicker = widget.props.useSystemAttachmentPicker || isWebOrDesktop; final child = useSystemPicker ? systemAttachmentPickerBuilder( context: context, controller: _pickerController!, allowedTypes: allowedTypes, - pollConfig: widget.pollConfig, - optionsBuilder: widget.attachmentPickerOptionsBuilder, + pollConfig: widget.props.pollConfig, + optionsBuilder: widget.props.attachmentPickerOptionsBuilder, onError: _onPickerError, onPollCreated: _onPollCreated, ) @@ -767,8 +869,8 @@ class StreamMessageComposerState extends State context: context, controller: _pickerController!, allowedTypes: allowedTypes, - pollConfig: widget.pollConfig, - optionsBuilder: widget.attachmentPickerOptionsBuilder, + pollConfig: widget.props.pollConfig, + optionsBuilder: widget.props.attachmentPickerOptionsBuilder, onError: _onPickerError, onPollCreated: _onPollCreated, onCommandSelected: _onCommandSelectedFromPicker, @@ -784,7 +886,7 @@ class StreamMessageComposerState extends State } bool _shouldShowSendToChannelCheckbox() { - if (!widget.canAlsoSendToChannelFromThread) return false; + if (!widget.props.canAlsoSendToChannelFromThread) return false; final insideThread = _effectiveController.message.parentId != null; return insideThread; @@ -812,7 +914,7 @@ class StreamMessageComposerState extends State // Returns the list of allowed attachment picker types based on the // current channel configuration and context. List _getAllowedAttachmentPickerTypes() { - final allowedTypes = widget.allowedAttachmentPickerTypes.where((type) { + final allowedTypes = widget.props.allowedAttachmentPickerTypes.where((type) { if (type != AttachmentPickerType.poll) return true; // We don't allow editing polls. @@ -841,8 +943,8 @@ class StreamMessageComposerState extends State _pickerController = StreamAttachmentPickerController( initialAttachments: _effectiveController.attachments, initialPoll: _effectiveController.poll, - maxAttachmentCount: widget.attachmentLimit, - maxAttachmentSize: widget.maxAttachmentSize, + maxAttachmentCount: widget.props.attachmentLimit, + maxAttachmentSize: widget.props.maxAttachmentSize, ); _startPickerSync(); @@ -883,7 +985,7 @@ class StreamMessageComposerState extends State } Future _onCustomResult(CustomAttachmentPickerResult result) async { - final handled = await widget.onAttachmentPickerResult?.call(result) ?? false; + final handled = await widget.props.onAttachmentPickerResult?.call(result) ?? false; if (handled && mounted) _hidePicker(); } @@ -924,7 +1026,7 @@ class StreamMessageComposerState extends State } void _onPickerError(AttachmentPickerError error) { - widget.onError?.call(error.error, error.stackTrace); + widget.props.onError?.call(error.error, error.stackTrace); } late final _onChangedThrottled = throttle( @@ -938,7 +1040,7 @@ class StreamMessageComposerState extends State if (value.isNotEmpty && channel.canUseTypingEvents) { channel.keyStroke(_effectiveController.message.parentId).onError( (error, stackTrace) { - widget.onError?.call(error!, stackTrace); + widget.props.onError?.call(error!, stackTrace); }, ); } @@ -962,7 +1064,7 @@ class StreamMessageComposerState extends State String? _buildPlaceholder(BuildContext context) { final state = MessageInputPlaceholder.resolve(_effectiveController); - return widget.placeholderBuilder.call(context, state); + return widget.props.placeholderBuilder.call(context, state); } String? _lastSearchedContainsUrlText; @@ -984,7 +1086,7 @@ class StreamMessageComposerState extends State final _parsedMatch = Uri.tryParse(it.group(0) ?? '')?.withScheme; if (_parsedMatch == null) return false; - return _parsedMatch.host.split('.').last.isValidTLD() && widget.ogPreviewFilter.call(_parsedMatch, value); + return _parsedMatch.host.split('.').last.isValidTLD() && widget.props.ogPreviewFilter.call(_parsedMatch, value); }).toList(); // Reset the og attachment if the text doesn't contain any url @@ -1013,7 +1115,7 @@ class StreamMessageComposerState extends State onError: (error, stackTrace) { // Reset the ogAttachment if there was an error _effectiveController.clearOGAttachment(); - widget.onError?.call(error, stackTrace); + widget.props.onError?.call(error, stackTrace); }, ); } @@ -1038,10 +1140,10 @@ class StreamMessageComposerState extends State /// Adds an attachment to the [messageInputController.attachments] map void _addAttachments(Iterable attachments) { - if (widget.attachmentLimit case final limit?) { + if (widget.props.attachmentLimit case final limit?) { final length = _effectiveController.attachments.length + attachments.length; if (length > limit) { - final onAttachmentLimitExceed = widget.onAttachmentLimitExceed; + final onAttachmentLimitExceed = widget.props.onAttachmentLimitExceed; if (onAttachmentLimitExceed != null) { return onAttachmentLimitExceed( limit, @@ -1061,7 +1163,7 @@ class StreamMessageComposerState extends State /// Sends the current message Future sendMessage() async { if (_effectiveController.isSlowModeActive) return; - if (!widget.validator(_effectiveController.message)) return; + if (!widget.props.validator(_effectiveController.message)) return; _hidePicker(); @@ -1090,10 +1192,10 @@ class StreamMessageComposerState extends State } _maybeDeleteDraftMessage(message, channel); - widget.onQuotedMessageCleared?.call(); + widget.props.onQuotedMessageCleared?.call(); _effectiveController.reset(); - if (widget.preMessageSending case final onPreMessageSending?) { + if (widget.props.preMessageSending case final onPreMessageSending?) { message = await onPreMessageSending.call(message); } @@ -1110,7 +1212,7 @@ class StreamMessageComposerState extends State await _sendOrUpdateMessage(message: message, channel: channel); if (mounted) { - if (widget.shouldKeepFocusAfterMessage ?? !_commandEnabled) { + if (widget.props.shouldKeepFocusAfterMessage ?? !_commandEnabled) { FocusScope.of(context).requestFocus(_effectiveFocusNode); } else { FocusScope.of(context).unfocus(); @@ -1134,10 +1236,10 @@ class StreamMessageComposerState extends State }; _effectiveController.startCooldown(channel.getRemainingCooldown()); - widget.onMessageSent?.call(resp.message); + widget.props.onMessageSent?.call(resp.message); } catch (e, stk) { - if (widget.onError != null) { - return widget.onError?.call(e, stk); + if (widget.props.onError != null) { + return widget.props.onError?.call(e, stk); } rethrow; @@ -1165,7 +1267,7 @@ class StreamMessageComposerState extends State if (channel == null) return; final message = _effectiveController.message; - final isMessageValid = widget.validator.call(message); + final isMessageValid = widget.props.validator.call(message); // If the message is valid, we need to create or update it as a draft // message for the channel or thread. @@ -1184,7 +1286,7 @@ class StreamMessageComposerState extends State final draftMessage = message.toDraftMessage(); // If the draft message is not valid, we don't need to update it. - final isDraftValid = widget.validator.call(draftMessage.toMessage()); + final isDraftValid = widget.props.validator.call(draftMessage.toMessage()); if (!isDraftValid) return; // If the draft message didn't change, we don't need to update it. diff --git a/packages/stream_chat_flutter/test/src/message_input/message_input_test.dart b/packages/stream_chat_flutter/test/src/message_input/message_input_test.dart index c44104031a..f7a81099fe 100644 --- a/packages/stream_chat_flutter/test/src/message_input/message_input_test.dart +++ b/packages/stream_chat_flutter/test/src/message_input/message_input_test.dart @@ -25,7 +25,7 @@ void main() { (WidgetTester tester) async { await tester.pumpWidget( buildWidget( - const StreamMessageComposer(), + StreamMessageComposer(), ), ); @@ -98,7 +98,7 @@ void main() { client: client, child: StreamChannel( channel: channel, - child: const Scaffold( + child: Scaffold( body: StreamMessageComposer(), ), ), @@ -151,7 +151,7 @@ void main() { client: client, child: StreamChannel( channel: channel, - child: const Scaffold( + child: Scaffold( bottomNavigationBar: StreamMessageComposer(), ), ), @@ -193,7 +193,7 @@ void main() { client: client, child: StreamChannel( channel: channel, - child: const Scaffold( + child: Scaffold( bottomNavigationBar: StreamMessageComposer(), ), ), @@ -378,7 +378,7 @@ void main() { final messageInputController = StreamMessageInputController()..editMessage(existingMessage); addTearDown(messageInputController.dispose); - final key = GlobalKey(); + final key = GlobalKey(); await tester.pumpWidget( MaterialApp( @@ -420,7 +420,7 @@ void main() { ); addTearDown(messageInputController.dispose); - final key = GlobalKey(); + final key = GlobalKey(); await tester.pumpWidget( MaterialApp( @@ -496,7 +496,7 @@ void main() { client: client, child: StreamChannel( channel: channel, - child: const Scaffold( + child: Scaffold( bottomNavigationBar: StreamMessageComposer( canAlsoSendToChannelFromThread: false, ), @@ -522,7 +522,7 @@ void main() { client: client, child: StreamChannel( channel: channel, - child: const Scaffold( + child: Scaffold( bottomNavigationBar: StreamMessageComposer(), ), ), diff --git a/packages/stream_chat_localizations/example/lib/add_new_lang.dart b/packages/stream_chat_localizations/example/lib/add_new_lang.dart index 4ae4272d9d..2fc328604a 100644 --- a/packages/stream_chat_localizations/example/lib/add_new_lang.dart +++ b/packages/stream_chat_localizations/example/lib/add_new_lang.dart @@ -940,11 +940,11 @@ class ChannelPage extends StatelessWidget { @override Widget build(BuildContext context) { - return const Scaffold( - appBar: StreamChannelHeader(), + return Scaffold( + appBar: const StreamChannelHeader(), body: Column( children: [ - Expanded( + const Expanded( child: StreamMessageListView(), ), StreamMessageComposer(), diff --git a/packages/stream_chat_localizations/example/lib/main.dart b/packages/stream_chat_localizations/example/lib/main.dart index 2fb3267438..94bc36eaa2 100644 --- a/packages/stream_chat_localizations/example/lib/main.dart +++ b/packages/stream_chat_localizations/example/lib/main.dart @@ -106,8 +106,8 @@ class ChannelPage extends StatelessWidget { @override Widget build(BuildContext context) { - return const Scaffold( - appBar: StreamChannelHeader(), + return Scaffold( + appBar: const StreamChannelHeader(), body: Column( children: [ Expanded( diff --git a/packages/stream_chat_localizations/example/lib/override_lang.dart b/packages/stream_chat_localizations/example/lib/override_lang.dart index 63f6427a10..453966a361 100644 --- a/packages/stream_chat_localizations/example/lib/override_lang.dart +++ b/packages/stream_chat_localizations/example/lib/override_lang.dart @@ -131,11 +131,11 @@ class ChannelPage extends StatelessWidget { @override Widget build(BuildContext context) { - return const Scaffold( - appBar: StreamChannelHeader(), + return Scaffold( + appBar: const StreamChannelHeader(), body: Column( children: [ - Expanded( + const Expanded( child: StreamMessageListView(), ), StreamMessageComposer(), From 3d3f69fc4a58d6cb5a5b847a5edc71434ab6aa5f Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Tue, 5 May 2026 13:37:25 +0200 Subject: [PATCH 03/13] fix picker sync --- .../src/message_input/stream_message_composer.dart | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart index effa554ca9..2142160ffd 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart @@ -1013,13 +1013,22 @@ class DefaultStreamMessageComposerState extends State a.id).toSet(); final removedIds = pickerIds.difference(messageIds); - if (removedIds.isEmpty) return; + final addedIds = messageIds.difference(pickerIds); + + if (removedIds.isEmpty && addedIds.isEmpty) return; + + final addedAttachments = addedIds + .map((id) => pickerController.value.attachments.firstWhere((a) => a.id == id)) + .toList(); _isSyncingControllers = true; try { for (final id in removedIds) { pickerController.removeAttachmentById(id); } + for (final attachment in addedAttachments) { + pickerController.addAttachment(attachment); + } } finally { _isSyncingControllers = false; } From e733955dd053931c40087670971624d3d7080218 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Tue, 5 May 2026 14:20:30 +0200 Subject: [PATCH 04/13] Add separate composer input for factory --- .../message_composer/message_composer.dart | 1 + .../message_composer_component_props.dart | 89 +++++- .../message_composer_input.dart | 102 +++++++ .../stream_chat_message_input.dart | 280 +++++------------- 4 files changed, 249 insertions(+), 223 deletions(-) create mode 100644 packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_input.dart diff --git a/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer.dart b/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer.dart index c637420ea1..3bc3a26eda 100644 --- a/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer.dart +++ b/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer.dart @@ -1,4 +1,5 @@ export 'message_composer_component_props.dart'; +export 'message_composer_input.dart' show DefaultStreamMessageComposerInput; export 'message_composer_input_trailing.dart' show DefaultStreamMessageComposerInputTrailing; export 'message_composer_leading.dart' show DefaultStreamMessageComposerLeading; export 'stream_chat_message_input.dart'; diff --git a/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_component_props.dart b/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_component_props.dart index 4df376246f..71d9b4536f 100644 --- a/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_component_props.dart +++ b/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_component_props.dart @@ -154,24 +154,87 @@ class MessageComposerInputProps extends MessageComposerComponentProps { required super.currentUserId, required super.audioRecorderState, required super.onQuotedMessageCleared, + this.placeholder, + this.textInputAction, + this.keyboardType, + this.textCapitalization = TextCapitalization.sentences, + this.autofocus = false, + this.autocorrect = true, + this.canAlsoSendToChannel = false, + this.audioRecorderController, + this.feedback = const AudioRecorderFeedback(), + this.sendVoiceRecordingAutomatically = false, }) : super(); - /// Creates a new instance of [MessageComposerInputProps] from a [MessageComposerComponentProps]. - factory MessageComposerInputProps.from(MessageComposerComponentProps props) { + /// Creates a new instance of [MessageComposerInputProps] from a + /// [MessageComposerComponentProps] and named input-level configuration values. + factory MessageComposerInputProps.from( + MessageComposerComponentProps componentProps, { + String? placeholder, + TextInputAction? textInputAction, + TextInputType? keyboardType, + TextCapitalization textCapitalization = TextCapitalization.sentences, + bool autofocus = false, + bool autocorrect = true, + bool canAlsoSendToChannel = false, + StreamAudioRecorderController? audioRecorderController, + AudioRecorderFeedback feedback = const AudioRecorderFeedback(), + bool sendVoiceRecordingAutomatically = false, + }) { return MessageComposerInputProps._( - controller: props.controller, - isFloating: props.isFloating, - message: props.message, - onSendPressed: props.onSendPressed, - voiceRecordingCallback: props.voiceRecordingCallback, - onAttachmentButtonPressed: props.onAttachmentButtonPressed, - isPickerOpen: props.isPickerOpen, - focusNode: props.focusNode, - currentUserId: props.currentUserId, - audioRecorderState: props.audioRecorderState, - onQuotedMessageCleared: props.onQuotedMessageCleared, + controller: componentProps.controller, + isFloating: componentProps.isFloating, + message: componentProps.message, + onSendPressed: componentProps.onSendPressed, + voiceRecordingCallback: componentProps.voiceRecordingCallback, + onAttachmentButtonPressed: componentProps.onAttachmentButtonPressed, + isPickerOpen: componentProps.isPickerOpen, + focusNode: componentProps.focusNode, + currentUserId: componentProps.currentUserId, + audioRecorderState: componentProps.audioRecorderState, + onQuotedMessageCleared: componentProps.onQuotedMessageCleared, + placeholder: placeholder, + textInputAction: textInputAction, + keyboardType: keyboardType, + textCapitalization: textCapitalization, + autofocus: autofocus, + autocorrect: autocorrect, + canAlsoSendToChannel: canAlsoSendToChannel, + audioRecorderController: audioRecorderController, + feedback: feedback, + sendVoiceRecordingAutomatically: sendVoiceRecordingAutomatically, ); } + + /// The placeholder text shown inside the input field when it is empty. + final String? placeholder; + + /// 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; + + /// Whether to show the "also send to channel" checkbox. + final bool canAlsoSendToChannel; + + /// The audio recorder controller. + final StreamAudioRecorderController? audioRecorderController; + + /// The feedback handler for voice recording interactions. + final AudioRecorderFeedback feedback; + + /// Whether to send the voice recording automatically when recording stops. + final bool sendVoiceRecordingAutomatically; } /// Properties for building the input leading component of the message composer. diff --git a/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_input.dart b/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_input.dart new file mode 100644 index 0000000000..e2327680e5 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_input.dart @@ -0,0 +1,102 @@ +import 'package:flutter/widgets.dart'; +import 'package:stream_chat_flutter/src/components/message_composer/message_composer_recording_locked.dart'; +import 'package:stream_chat_flutter/src/components/message_composer/message_composer_recording_ongoing.dart'; +import 'package:stream_chat_flutter/src/message_input/dm_checkbox_list_tile.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart' as core; + +/// A widget that shows the input body of the message composer. +/// Uses the factory to show custom components or the default implementation. +/// By default shows the text field and an optional "also send to channel" +/// checkbox, or the appropriate audio recording UI when recording is active. +class StreamMessageComposerInput extends StatelessWidget { + /// Creates a new instance of [StreamMessageComposerInput]. + /// [props] contains the full input properties including text field + /// configuration and audio recording settings. + const StreamMessageComposerInput({super.key, required this.props}); + + /// The properties for the message composer input. + final MessageComposerInputProps props; + + @override + Widget build(BuildContext context) { + return context.chatComponentBuilder()?.call(context, props) ?? + DefaultStreamMessageComposerInput(props: props); + } +} + +/// Default implementation of the input body of the message composer. +/// +/// Shows the appropriate audio recording UI when a recording state is active, +/// or the text input field (and an optional "also send to channel" checkbox) +/// when idle. +class DefaultStreamMessageComposerInput extends StatelessWidget { + /// Creates a new instance of [DefaultStreamMessageComposerInput]. + const DefaultStreamMessageComposerInput({super.key, required this.props}); + + /// The properties for the message composer input. + final MessageComposerInputProps props; + + @override + Widget build(BuildContext context) { + final recorder = props.audioRecorderController; + if (recorder != null) { + final sendMessageCallback = props.sendVoiceRecordingAutomatically ? props.onSendPressed : null; + final audioState = props.audioRecorderState; + + final recordingBody = switch (audioState) { + RecordStateRecordingLocked() => MessageComposerRecordingLocked( + audioRecorderController: recorder, + feedback: props.feedback, + messageInputController: props.controller, + sendMessageCallback: sendMessageCallback, + state: audioState, + ), + RecordStateStopped() => MessageComposerRecordingStopped( + audioRecorderController: recorder, + feedback: props.feedback, + messageInputController: props.controller, + sendMessageCallback: sendMessageCallback, + recordingState: audioState, + ), + RecordStateRecording() => StreamMessageComposerRecordingOngoing( + audioRecorderController: recorder, + ), + _ => null, + }; + + if (recordingBody != null) return recordingBody; + } + + final controller = props.controller; + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + core.StreamMessageComposerInputField( + controller: controller.textFieldController, + placeholder: props.placeholder, + focusNode: props.focusNode, + command: controller.message.command?.toUpperCase(), + onDismissCommand: controller.clearCommand, + textInputAction: props.textInputAction, + keyboardType: props.keyboardType, + textCapitalization: props.textCapitalization, + autofocus: props.autofocus, + autocorrect: props.autocorrect, + ), + if (props.canAlsoSendToChannel) + DmCheckboxListTile( + value: controller.showInChannel, + // height of list tile is 34px, height of checkbox is 16px, so we need to subtract 8px to make the spacing correct. + contentPadding: EdgeInsets.only( + right: context.streamSpacing.md, + left: context.streamSpacing.md, + bottom: context.streamSpacing.md - 8, + ), + onChanged: (value) => controller.showInChannel = value, + ), + ], + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/components/message_composer/stream_chat_message_input.dart b/packages/stream_chat_flutter/lib/src/components/message_composer/stream_chat_message_input.dart index 05d942ff8d..27463a1464 100644 --- a/packages/stream_chat_flutter/lib/src/components/message_composer/stream_chat_message_input.dart +++ b/packages/stream_chat_flutter/lib/src/components/message_composer/stream_chat_message_input.dart @@ -1,101 +1,33 @@ import 'package:flutter/material.dart'; import 'package:flutter_portal/flutter_portal.dart'; +import 'package:stream_chat_flutter/src/components/message_composer/message_composer_input.dart'; import 'package:stream_chat_flutter/src/components/message_composer/message_composer_input_header.dart'; import 'package:stream_chat_flutter/src/components/message_composer/message_composer_input_leading.dart'; import 'package:stream_chat_flutter/src/components/message_composer/message_composer_input_trailing.dart'; import 'package:stream_chat_flutter/src/components/message_composer/message_composer_leading.dart'; -import 'package:stream_chat_flutter/src/components/message_composer/message_composer_recording_locked.dart'; -import 'package:stream_chat_flutter/src/components/message_composer/message_composer_recording_ongoing.dart'; import 'package:stream_chat_flutter/src/components/message_composer/message_composer_trailing.dart'; -import 'package:stream_chat_flutter/src/message_input/dm_checkbox_list_tile.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_core_flutter/stream_core_flutter.dart' as core; /// A widget that shows the message composer. /// Uses the factory to show custom components or the default implementation. -class StreamChatMessageInput extends StatelessWidget { +class StreamChatMessageInput extends StatefulWidget { /// Creates a new instance of [StreamChatMessageInput]. /// [controller] is the controller for the message composer. /// [onSendPressed] is the callback for when the send button is pressed. - /// [onMicrophonePressed] is the callback for when the microphone button is pressed. /// [onAttachmentButtonPressed] is the callback for when the attachment button is pressed. /// [focusNode] is the focus node for the message composer. /// [currentUserId] is the current user id. /// [placeholder] is the placeholder text of the message composer. - StreamChatMessageInput({ + const StreamChatMessageInput({ super.key, - StreamMessageInputController? controller, - required VoidCallback onSendPressed, - VoidCallback? onAttachmentButtonPressed, - bool isPickerOpen = false, - FocusNode? focusNode, - String? currentUserId, - String? placeholder, - StreamAudioRecorderController? audioRecorderController, - bool sendVoiceRecordingAutomatically = false, - AudioRecorderFeedback feedback = const AudioRecorderFeedback(), - bool canAlsoSendToChannel = false, - VoidCallback? onQuotedMessageCleared, - TextInputAction? textInputAction, - TextInputType? keyboardType, - TextCapitalization textCapitalization = TextCapitalization.sentences, - bool autofocus = false, - bool autocorrect = true, - }) : props = StreamChatMessageInputProps( - controller: controller, - isFloating: false, - message: null, - onSendPressed: onSendPressed, - onAttachmentButtonPressed: onAttachmentButtonPressed, - isPickerOpen: isPickerOpen, - focusNode: focusNode, - currentUserId: currentUserId, - placeholder: placeholder, - audioRecorderController: audioRecorderController, - sendVoiceRecordingAutomatically: sendVoiceRecordingAutomatically, - feedback: feedback, - canAlsoSendToChannel: canAlsoSendToChannel, - onQuotedMessageCleared: onQuotedMessageCleared, - textInputAction: textInputAction, - keyboardType: keyboardType, - textCapitalization: textCapitalization, - autofocus: autofocus, - autocorrect: autocorrect, - ); - - /// The controller for the message composer. - StreamMessageInputController? get controller => props.controller; - - /// The properties for the message composer. - final StreamChatMessageInputProps props; - - @override - Widget build(BuildContext context) { - return DefaultStreamChatMessageInput(props: props); - } -} - -/// Properties for [StreamChatMessageInput] and [DefaultStreamChatMessageInput]. -class StreamChatMessageInputProps { - /// Creates a new instance of [StreamChatMessageInputProps]. - /// [isFloating] is whether the message composer is floating. - /// [message] is the message for the message composer. - /// [placeholder] is the placeholder text of the message composer. - /// [onSendPressed] is the callback for when the send button is pressed. - /// [onMicrophonePressed] is the callback for when the microphone button is pressed. - /// [onAttachmentButtonPressed] is the callback for when the attachment button is pressed. - /// [focusNode] is the focus node for the message composer. - /// [currentUserId] is the current user id. - const StreamChatMessageInputProps({ this.controller, - this.isFloating = false, - this.message, - this.placeholder, required this.onSendPressed, this.onAttachmentButtonPressed, this.isPickerOpen = false, this.focusNode, this.currentUserId, + this.placeholder, this.audioRecorderController, this.sendVoiceRecordingAutomatically = false, this.feedback = const AudioRecorderFeedback(), @@ -111,21 +43,6 @@ class StreamChatMessageInputProps { /// The controller for the message composer. final StreamMessageInputController? controller; - /// Whether the message composer is floating. - final bool isFloating; - - /// The message for the message composer. - final Message? message; - - /// The placeholder text of the message composer. - /// - /// May be `null` to render the input with no placeholder. The wrapping - /// [StreamMessageComposer] resolves this string reactively from its - /// [StreamMessageInputController] via [MessageInputPlaceholder.resolve] and - /// [StreamMessageComposer.placeholderBuilder]; when using - /// [StreamChatMessageInput] directly, supply the string yourself. - final String? placeholder; - /// The callback for when the send button is pressed. final VoidCallback onSendPressed; @@ -141,19 +58,25 @@ class StreamChatMessageInputProps { /// The current user id. final String? currentUserId; + /// The placeholder text of the message composer. + /// + /// May be `null` to render the input with no placeholder. The wrapping + /// [StreamMessageComposer] resolves this string reactively from its + /// [StreamMessageInputController] via [MessageInputPlaceholder.resolve] and + /// [StreamMessageComposer.placeholderBuilder]; when using + /// [StreamChatMessageInput] directly, supply the string yourself. + final String? placeholder; + /// The audio recorder controller. final StreamAudioRecorderController? audioRecorderController; - /// Whether the voice recording should be sent automatically. - /// If enabled, the voice recording will be sent automatically when the recording is finished. - /// If disabled, the voice recording will be added as an attachment to the message - /// and the user will need to send the message manually. + /// Whether the voice recording should be sent automatically when recording stops. final bool sendVoiceRecordingAutomatically; - /// The feedback for the audio recorder. + /// The feedback handler for voice recording interactions. final AudioRecorderFeedback feedback; - /// Whether the user can also send the message as a direct message. + /// Whether to show the "also send to channel" checkbox. /// Usually used in threads. final bool canAlsoSendToChannel; @@ -174,28 +97,12 @@ class StreamChatMessageInputProps { /// Whether to enable autocorrect. final bool autocorrect; -} - -extension on StreamAudioRecorderController { - bool get isRecording => value is RecordStateRecording; - bool get isLocked => isRecording && value is! RecordStateRecordingHold; -} - -/// Default implementation of the message composer. -/// Manages the controller lifecycle and handles the audio recording state. -class DefaultStreamChatMessageInput extends StatefulWidget { - /// Creates a new instance of [DefaultStreamChatMessageInput]. - /// [props] contains the properties for the message composer. - const DefaultStreamChatMessageInput({super.key, required this.props}); - - /// The properties for the message composer. - final StreamChatMessageInputProps props; @override - State createState() => _DefaultStreamChatMessageInputState(); + State createState() => _StreamChatMessageInputState(); } -class _DefaultStreamChatMessageInputState extends State { +class _StreamChatMessageInputState extends State { late StreamMessageInputController _controller; @override @@ -205,36 +112,30 @@ class _DefaultStreamChatMessageInputState extends State MessageComposerRecordingLocked( - audioRecorderController: audioRecorderController, - feedback: widget.props.feedback, - messageInputController: _controller, - sendMessageCallback: widget.props.sendVoiceRecordingAutomatically ? widget.props.onSendPressed : null, - state: state, - ), - RecordStateStopped() => MessageComposerRecordingStopped( - audioRecorderController: audioRecorderController, - feedback: widget.props.feedback, - messageInputController: _controller, - sendMessageCallback: widget.props.sendVoiceRecordingAutomatically ? widget.props.onSendPressed : null, - recordingState: state, - ), - RecordStateRecording() => StreamMessageComposerRecordingOngoing( - audioRecorderController: audioRecorderController, - ), - _ => null, - }; - final streamSpacing = context.streamSpacing; final textDirection = Directionality.maybeOf(context); @@ -289,10 +169,9 @@ class _DefaultStreamChatMessageInputState extends State value is RecordStateRecording; + bool get isLocked => isRecording && value is! RecordStateRecordingHold; +} + // The actual UI content of the message composer. -// Does not include the audio recording flow in the body. class _StreamChatMessageInputContent extends StatelessWidget { const _StreamChatMessageInputContent({ - required this.props, + required this.widget, required this.inputController, this.audioRecorderState = const RecordStateIdle(), - this.body, }); - final StreamChatMessageInputProps props; + final StreamChatMessageInput widget; final StreamMessageInputController inputController; final AudioRecorderState audioRecorderState; - final Widget? body; static const double _lockRecordThreshold = 50; static const double _cancelRecordThreshold = 75; @@ -323,82 +204,61 @@ class _StreamChatMessageInputContent extends StatelessWidget { Widget build(BuildContext context) { final componentProps = MessageComposerComponentProps( controller: inputController, - isFloating: props.isFloating, - message: props.message, - currentUserId: props.currentUserId, - onSendPressed: props.onSendPressed, + isFloating: false, + message: null, + currentUserId: widget.currentUserId, + onSendPressed: widget.onSendPressed, voiceRecordingCallback: _createVoiceRecordingCallback(context), - onAttachmentButtonPressed: props.onAttachmentButtonPressed, - isPickerOpen: props.isPickerOpen, + onAttachmentButtonPressed: widget.onAttachmentButtonPressed, + isPickerOpen: widget.isPickerOpen, audioRecorderState: audioRecorderState, - focusNode: props.focusNode, - onQuotedMessageCleared: props.onQuotedMessageCleared, + focusNode: widget.focusNode, + onQuotedMessageCleared: widget.onQuotedMessageCleared, + ); + + final inputProps = MessageComposerInputProps.from( + componentProps, + placeholder: widget.placeholder, + textInputAction: widget.textInputAction, + keyboardType: widget.keyboardType, + textCapitalization: widget.textCapitalization, + autofocus: widget.autofocus, + autocorrect: widget.autocorrect, + canAlsoSendToChannel: widget.canAlsoSendToChannel, + audioRecorderController: widget.audioRecorderController, + feedback: widget.feedback, + sendVoiceRecordingAutomatically: widget.sendVoiceRecordingAutomatically, ); return core.StreamCoreMessageComposer( - placeholder: props.placeholder, + placeholder: widget.placeholder, controller: inputController.textFieldController, - isFloating: props.isFloating, - focusNode: props.focusNode, + isFloating: false, + focusNode: widget.focusNode, composerLeading: StreamMessageComposerLeading(props: componentProps), - composerTrailing: StreamMessageComposerTrailing( - props: componentProps, - ), + composerTrailing: StreamMessageComposerTrailing(props: componentProps), inputHeader: StreamMessageComposerInputHeader(props: componentProps), - inputTrailing: StreamMessageComposerInputTrailing( - props: componentProps, - ), - inputLeading: StreamMessageComposerInputLeading( - props: componentProps, - ), - inputBody: - body ?? - Column( - mainAxisSize: MainAxisSize.min, - children: [ - core.StreamMessageComposerInputField( - controller: inputController.textFieldController, - placeholder: props.placeholder, - focusNode: props.focusNode, - command: inputController.message.command?.toUpperCase(), - onDismissCommand: inputController.clearCommand, - textInputAction: props.textInputAction, - keyboardType: props.keyboardType, - textCapitalization: props.textCapitalization, - autofocus: props.autofocus, - autocorrect: props.autocorrect, - ), - if (props.canAlsoSendToChannel) - DmCheckboxListTile( - value: props.controller?.showInChannel ?? false, - // height of list tile is 34px, height of checkbox is 16px, so we need to subtract 8px to make the spacing correct. - contentPadding: EdgeInsets.only( - right: context.streamSpacing.md, - left: context.streamSpacing.md, - bottom: context.streamSpacing.md - 8, - ), - onChanged: (value) => props.controller?.showInChannel = value, - ), - ], - ), + inputTrailing: StreamMessageComposerInputTrailing(props: componentProps), + inputLeading: StreamMessageComposerInputLeading(props: componentProps), + inputBody: StreamMessageComposerInput(props: inputProps), ); } core.VoiceRecordingCallback? _createVoiceRecordingCallback(BuildContext context) { - if (props.audioRecorderController case final audioRecorderController?) { + if (widget.audioRecorderController case final audioRecorderController?) { return core.VoiceRecordingCallback( onLongPressStart: () async { // Return if the recording is already started. if (audioRecorderController.isRecording) return; - await props.feedback.onRecordStart(context); + await widget.feedback.onRecordStart(context); return audioRecorderController.startRecord(); }, onLongPressEnd: (_) async { // Return if the recording not yet started or already locked. if (!audioRecorderController.isRecording || audioRecorderController.isLocked) return; - await props.feedback.onRecordFinish(context); + await widget.feedback.onRecordFinish(context); final audio = await audioRecorderController.finishRecord(); if (audio != null) { inputController.addAttachment(audio); @@ -409,8 +269,8 @@ class _StreamChatMessageInputContent extends StatelessWidget { // Send the message if the user has enabled the option to // send the voice recording automatically. - if (props.sendVoiceRecordingAutomatically) { - return props.onSendPressed.call(); + if (widget.sendVoiceRecordingAutomatically) { + return widget.onSendPressed.call(); } }, onLongPressCancel: () async { @@ -418,7 +278,7 @@ class _StreamChatMessageInputContent extends StatelessWidget { if (audioRecorderController.isRecording) return; // Notify the parent that the recorder is canceled before it starts. - await props.feedback.onRecordStartCancel(context); + await widget.feedback.onRecordStartCancel(context); // Show a message to the user to hold to record. audioRecorderController.showInfo( context.translations.holdToRecordLabel, @@ -431,12 +291,12 @@ class _StreamChatMessageInputContent extends StatelessWidget { // Lock recording if the drag offset is greater than the threshold. if (dragOffset.dy <= -_lockRecordThreshold) { - await props.feedback.onRecordLock(context); + await widget.feedback.onRecordLock(context); return audioRecorderController.lockRecord(); } // Cancel recording if the drag offset is greater than the threshold. if (dragOffset.dx <= -_cancelRecordThreshold) { - await props.feedback.onRecordCancel(context); + await widget.feedback.onRecordCancel(context); return audioRecorderController.cancelRecord(); } From 826f2524805da76880e973d25e497826af0f0fac Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Tue, 5 May 2026 14:55:31 +0200 Subject: [PATCH 05/13] formatting --- packages/stream_chat_flutter/example/lib/tutorial_part_1.dart | 2 +- packages/stream_chat_flutter/example/lib/tutorial_part_2.dart | 2 +- packages/stream_chat_flutter/example/lib/tutorial_part_4.dart | 2 +- packages/stream_chat_flutter/example/lib/tutorial_part_5.dart | 2 +- packages/stream_chat_flutter/example/lib/tutorial_part_6.dart | 2 +- .../stream_chat_localizations/example/lib/add_new_lang.dart | 2 +- packages/stream_chat_localizations/example/lib/main.dart | 4 ++-- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_1.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_1.dart index 0e7a848d28..f316e5249c 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_1.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_1.dart @@ -91,7 +91,7 @@ class ChannelPage extends StatelessWidget { @override // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { - return Scaffold( + return Scaffold( appBar: const StreamChannelHeader(), body: Column( children: [ diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart index 6cd42d2d31..78b7dec215 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart @@ -126,7 +126,7 @@ class ChannelPage extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( + return Scaffold( appBar: const StreamChannelHeader(), body: Column( children: [ diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart index 3efac6d0a4..1afeaf6da8 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart @@ -116,7 +116,7 @@ class ChannelPage extends StatelessWidget { ), ), ), - StreamMessageComposer(), + StreamMessageComposer(), ], ), ); diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart index 652128f1c6..dae82f5ab3 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart @@ -120,7 +120,7 @@ class ChannelPage extends StatelessWidget { messageBuilder: _messageBuilder, ), ), - StreamMessageComposer(), + StreamMessageComposer(), ], ), ); diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart index 51bdcd5360..d574652349 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart @@ -146,7 +146,7 @@ class ChannelPage extends StatelessWidget { ), ), ), - StreamMessageComposer(), + StreamMessageComposer(), ], ), ); diff --git a/packages/stream_chat_localizations/example/lib/add_new_lang.dart b/packages/stream_chat_localizations/example/lib/add_new_lang.dart index 2fc328604a..d700dd1c1f 100644 --- a/packages/stream_chat_localizations/example/lib/add_new_lang.dart +++ b/packages/stream_chat_localizations/example/lib/add_new_lang.dart @@ -940,7 +940,7 @@ class ChannelPage extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( + return Scaffold( appBar: const StreamChannelHeader(), body: Column( children: [ diff --git a/packages/stream_chat_localizations/example/lib/main.dart b/packages/stream_chat_localizations/example/lib/main.dart index 94bc36eaa2..d10937249e 100644 --- a/packages/stream_chat_localizations/example/lib/main.dart +++ b/packages/stream_chat_localizations/example/lib/main.dart @@ -106,11 +106,11 @@ class ChannelPage extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( + return Scaffold( appBar: const StreamChannelHeader(), body: Column( children: [ - Expanded( + const Expanded( child: StreamMessageListView(), ), StreamMessageComposer(), From 4a97a7b421b722b7fd84c464093116e0f723d4ed Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Tue, 5 May 2026 14:59:39 +0200 Subject: [PATCH 06/13] remove unused prop --- .../message_composer_component_props.dart | 16 ---------------- .../stream_chat_message_input.dart | 7 +++++-- 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_component_props.dart b/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_component_props.dart index 71d9b4536f..9ae66f8d03 100644 --- a/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_component_props.dart +++ b/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_component_props.dart @@ -18,7 +18,6 @@ class MessageComposerComponentProps { const MessageComposerComponentProps({ required this.controller, this.isFloating = false, - this.message, required this.onSendPressed, this.voiceRecordingCallback, this.onAttachmentButtonPressed, @@ -35,9 +34,6 @@ class MessageComposerComponentProps { /// Whether the message composer is floating. final bool isFloating; - /// The message for the message composer component. - final Message? message; - /// The callback for when the send button is pressed. final VoidCallback onSendPressed; @@ -77,7 +73,6 @@ class MessageComposerLeadingProps extends MessageComposerComponentProps { const MessageComposerLeadingProps._({ required super.controller, required super.isFloating, - required super.message, required super.onSendPressed, required super.voiceRecordingCallback, required super.onAttachmentButtonPressed, @@ -93,7 +88,6 @@ class MessageComposerLeadingProps extends MessageComposerComponentProps { return MessageComposerLeadingProps._( controller: props.controller, isFloating: props.isFloating, - message: props.message, onSendPressed: props.onSendPressed, voiceRecordingCallback: props.voiceRecordingCallback, onAttachmentButtonPressed: props.onAttachmentButtonPressed, @@ -111,7 +105,6 @@ class MessageComposerTrailingProps extends MessageComposerComponentProps { const MessageComposerTrailingProps._({ required super.controller, required super.isFloating, - required super.message, required super.onSendPressed, required super.voiceRecordingCallback, required super.onAttachmentButtonPressed, @@ -127,7 +120,6 @@ class MessageComposerTrailingProps extends MessageComposerComponentProps { return MessageComposerTrailingProps._( controller: props.controller, isFloating: props.isFloating, - message: props.message, onSendPressed: props.onSendPressed, voiceRecordingCallback: props.voiceRecordingCallback, onAttachmentButtonPressed: props.onAttachmentButtonPressed, @@ -145,7 +137,6 @@ class MessageComposerInputProps extends MessageComposerComponentProps { const MessageComposerInputProps._({ required super.controller, required super.isFloating, - required super.message, required super.onSendPressed, required super.voiceRecordingCallback, required super.onAttachmentButtonPressed, @@ -184,7 +175,6 @@ class MessageComposerInputProps extends MessageComposerComponentProps { return MessageComposerInputProps._( controller: componentProps.controller, isFloating: componentProps.isFloating, - message: componentProps.message, onSendPressed: componentProps.onSendPressed, voiceRecordingCallback: componentProps.voiceRecordingCallback, onAttachmentButtonPressed: componentProps.onAttachmentButtonPressed, @@ -242,7 +232,6 @@ class MessageComposerInputLeadingProps extends MessageComposerComponentProps { const MessageComposerInputLeadingProps._({ required super.controller, required super.isFloating, - required super.message, required super.onSendPressed, required super.voiceRecordingCallback, required super.onAttachmentButtonPressed, @@ -258,7 +247,6 @@ class MessageComposerInputLeadingProps extends MessageComposerComponentProps { return MessageComposerInputLeadingProps._( controller: props.controller, isFloating: props.isFloating, - message: props.message, onSendPressed: props.onSendPressed, voiceRecordingCallback: props.voiceRecordingCallback, onAttachmentButtonPressed: props.onAttachmentButtonPressed, @@ -276,7 +264,6 @@ class MessageComposerInputHeaderProps extends MessageComposerComponentProps { const MessageComposerInputHeaderProps._({ required super.controller, required super.isFloating, - required super.message, required super.onSendPressed, required super.voiceRecordingCallback, required super.onAttachmentButtonPressed, @@ -292,7 +279,6 @@ class MessageComposerInputHeaderProps extends MessageComposerComponentProps { return MessageComposerInputHeaderProps._( controller: props.controller, isFloating: props.isFloating, - message: props.message, onSendPressed: props.onSendPressed, voiceRecordingCallback: props.voiceRecordingCallback, onAttachmentButtonPressed: props.onAttachmentButtonPressed, @@ -310,7 +296,6 @@ class MessageComposerInputTrailingProps extends MessageComposerComponentProps { const MessageComposerInputTrailingProps._({ required super.controller, required super.isFloating, - required super.message, required super.onSendPressed, required super.voiceRecordingCallback, required super.onAttachmentButtonPressed, @@ -326,7 +311,6 @@ class MessageComposerInputTrailingProps extends MessageComposerComponentProps { return MessageComposerInputTrailingProps._( controller: props.controller, isFloating: props.isFloating, - message: props.message, onSendPressed: props.onSendPressed, voiceRecordingCallback: props.voiceRecordingCallback, onAttachmentButtonPressed: props.onAttachmentButtonPressed, diff --git a/packages/stream_chat_flutter/lib/src/components/message_composer/stream_chat_message_input.dart b/packages/stream_chat_flutter/lib/src/components/message_composer/stream_chat_message_input.dart index 27463a1464..b19315b996 100644 --- a/packages/stream_chat_flutter/lib/src/components/message_composer/stream_chat_message_input.dart +++ b/packages/stream_chat_flutter/lib/src/components/message_composer/stream_chat_message_input.dart @@ -38,6 +38,7 @@ class StreamChatMessageInput extends StatefulWidget { this.textCapitalization = TextCapitalization.sentences, this.autofocus = false, this.autocorrect = true, + this.isFloating = false, }); /// The controller for the message composer. @@ -98,6 +99,9 @@ class StreamChatMessageInput extends StatefulWidget { /// Whether to enable autocorrect. final bool autocorrect; + /// Whether the message composer is floating. + final bool isFloating; + @override State createState() => _StreamChatMessageInputState(); } @@ -204,8 +208,7 @@ class _StreamChatMessageInputContent extends StatelessWidget { Widget build(BuildContext context) { final componentProps = MessageComposerComponentProps( controller: inputController, - isFloating: false, - message: null, + isFloating: widget.isFloating, currentUserId: widget.currentUserId, onSendPressed: widget.onSendPressed, voiceRecordingCallback: _createVoiceRecordingCallback(context), From c1e640ceceab037ef58dde695dbf8f64f8559321 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Tue, 5 May 2026 15:17:05 +0200 Subject: [PATCH 07/13] Fix isFloating bool --- .../components/message_composer/stream_chat_message_input.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/components/message_composer/stream_chat_message_input.dart b/packages/stream_chat_flutter/lib/src/components/message_composer/stream_chat_message_input.dart index b19315b996..47bc45aabf 100644 --- a/packages/stream_chat_flutter/lib/src/components/message_composer/stream_chat_message_input.dart +++ b/packages/stream_chat_flutter/lib/src/components/message_composer/stream_chat_message_input.dart @@ -236,7 +236,7 @@ class _StreamChatMessageInputContent extends StatelessWidget { return core.StreamCoreMessageComposer( placeholder: widget.placeholder, controller: inputController.textFieldController, - isFloating: false, + isFloating: widget.isFloating, focusNode: widget.focusNode, composerLeading: StreamMessageComposerLeading(props: componentProps), composerTrailing: StreamMessageComposerTrailing(props: componentProps), From 616714af7113fb6aab380e4a58f83063fa62c435 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Tue, 5 May 2026 15:21:56 +0200 Subject: [PATCH 08/13] format exports --- packages/stream_chat_flutter/lib/stream_chat_flutter.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index edc141b2d4..45552ff270 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -159,8 +159,8 @@ export 'src/message_input/audio_recorder/stream_audio_recorder.dart'; export 'src/message_input/countdown_button.dart'; export 'src/message_input/enums.dart'; export 'src/message_input/message_input_placeholder.dart'; -export 'src/message_input/stream_message_composer_attachment_list.dart'; export 'src/message_input/stream_message_composer.dart'; +export 'src/message_input/stream_message_composer_attachment_list.dart'; export 'src/message_input/stream_message_text_field.dart'; export 'src/message_list_view/message_details.dart'; export 'src/message_list_view/message_list_view.dart'; @@ -181,7 +181,6 @@ export 'src/misc/info_tile.dart'; export 'src/misc/markdown_message.dart'; export 'src/misc/option_list_tile.dart'; export 'src/misc/reaction_icon_resolver.dart'; - export 'src/misc/stream_modal.dart'; export 'src/misc/stream_neumorphic_button.dart'; export 'src/misc/swipeable.dart'; From d271b71941e1350049db5c4c40ccaac98f8183a8 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Tue, 5 May 2026 15:33:25 +0200 Subject: [PATCH 09/13] Fix review issues --- migrations/redesign/message_composer.md | 2 +- .../message_composer_component_props.dart | 1 - .../lib/src/localization/translations.dart | 2 +- .../src/message_input/stream_message_composer.dart | 11 ++++++++++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/migrations/redesign/message_composer.md b/migrations/redesign/message_composer.md index 5cd336b0aa..5c67dd3c5c 100644 --- a/migrations/redesign/message_composer.md +++ b/migrations/redesign/message_composer.md @@ -84,7 +84,7 @@ StreamMessageComposer( ### Removed parameters -Many parameters that existed in older versions of `StreamMessageComposer` have been removed. The table below lists each removed parameter and the recommended migration path. +Many parameters that existed in `StreamMessageInput` have been removed from `StreamMessageComposer`. The table below lists each removed parameter and the recommended migration path. #### Layout and visual parameters diff --git a/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_component_props.dart b/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_component_props.dart index 9ae66f8d03..deb84b5ef8 100644 --- a/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_component_props.dart +++ b/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_component_props.dart @@ -9,7 +9,6 @@ class MessageComposerComponentProps { /// Creates a new instance of [MessageComposerComponentProps]. /// [controller] is the controller for the message composer component. /// [isFloating] is whether the message composer is floating. - /// [message] is the message for the message composer component. /// [onSendPressed] is the callback for when the send button is pressed. /// [onMicrophonePressed] is the callback for when the microphone button is pressed. /// [onAttachmentButtonPressed] is the callback for when the attachment button is pressed. diff --git a/packages/stream_chat_flutter/lib/src/localization/translations.dart b/packages/stream_chat_flutter/lib/src/localization/translations.dart index b189b619d5..ba1a340bad 100644 --- a/packages/stream_chat_flutter/lib/src/localization/translations.dart +++ b/packages/stream_chat_flutter/lib/src/localization/translations.dart @@ -100,7 +100,7 @@ abstract class Translations { String get reconnectingLabel; /// The label for also send - /// as direct message "checkbox"" in [StreamMessageComposer] + /// as direct message "checkbox" in [StreamMessageComposer] String get alsoSendAsDirectMessageLabel; /// The label for search Gif diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart index 2142160ffd..5ef40fe568 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart @@ -135,6 +135,15 @@ class StreamMessageComposer extends StatelessWidget { autoCorrect: autoCorrect, ); + /// Creates a [StreamMessageComposer] from a pre-built [MessageComposerProps]. + /// + /// Use this constructor when you have already assembled a [MessageComposerProps] + /// instance and want to avoid re-specifying every field individually. + const StreamMessageComposer.fromProps({ + super.key, + required this.props, + }); + /// The properties for the message composer. final MessageComposerProps props; @@ -1018,7 +1027,7 @@ class DefaultStreamMessageComposerState extends State pickerController.value.attachments.firstWhere((a) => a.id == id)) + .map((id) => _effectiveController.value.attachments.firstWhere((a) => a.id == id)) .toList(); _isSyncingControllers = true; From 37556a9b68f2c281317f0a1ea9b611acfee5537b Mon Sep 17 00:00:00 2001 From: renefloor <15101411+renefloor@users.noreply.github.com> Date: Tue, 5 May 2026 13:37:59 +0000 Subject: [PATCH 10/13] chore: Update Goldens --- .../ci/stream_message_composer_default.png | Bin 0 -> 3483 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/docs_screenshots/test/message_input/goldens/ci/stream_message_composer_default.png diff --git a/docs/docs_screenshots/test/message_input/goldens/ci/stream_message_composer_default.png b/docs/docs_screenshots/test/message_input/goldens/ci/stream_message_composer_default.png new file mode 100644 index 0000000000000000000000000000000000000000..ea33d6843ffa4372eb55fe5de966da1e55f9a9f1 GIT binary patch literal 3483 zcmb`KXEYpo)4+EhofR#Jh_(nzv`ds|!K#Ux5YcL6{inGSc6s2LJ$!`g%|^000<5 zKD*FTlV#+oFPywl259O(r6n&ItxFR54h%5U)dVzty|YU;($n);4ja{C%~Fh#(+A;Q`uy}XKjjk&cE{elD{(-w|(v#$yc&1Kx=7K zy7M5biqfnFmTA13(`zx`HZ}2p+L@=&$Elv%gq>;_b|WhXMeDCrKpA~f24_9fV0Nn7 zoiya8GS|>BHxa9Q*By^g!Hk$pmveIjPV2}Q#}hUDP$55O^;pTBjtm^#vsj zl9@GhPe5e4RhUXtntj&&zFBY{6&?I4=}NEX>~L)RrcibfQ1JKlAd$j7AuQ84*9q8e z;J3@wG~>(XL0wo3Joe;B_0;}Rcc1oOay?|7c%AkXCpD3c?toJP&^JQ*Bu|Q|3A?vX zPKH6lL_H@8B9cgWEh2kp>0svC2xFAY}lL59&6Z-%JrEjHa za#d=`8jAOZk=1C`6Zfa%WckPonKWDKs_Y&_<7>kdp^Vg%a>Q|lp=dOcNR>~#=fp?1 zb3~Ed(zAwg%8^Tpi#krr=Os#9HK~=Y@*SSJ?L`+N6{j+H@QV1qn|o|^C6S1>20b*^ zPgOW9+<9a_Mj{FJ`VwoJAh*#iet}b;im?*Z%BEk68Dj=6U*Jv?2bpd}S1wzpANT;& zBN#%jRdKRXK~MB**fB-VjNIMg-N@+9m%^`_3GCRi6QaU3h+G z)wsR>;Tyz9-qU1m!;uQida&{<=KVF>p~9;##D05{5-vAChLb3%{!I39kz53-wI@CY zcX~L4CY{yC`J37%7$3w&xmR3At)F6R@YM1wA@h&$`o>*`LM?glWy%P z79qaX#NScT%MK|G;o6}CN$#gnfz+@=@rMCI`-L>NI4ygsU)IwYuNm)n7Sh z*~rJ)$S3>QFNq{K3HbzHgkJrcD8LN(orIYhQUG}Qu0v~1 zUMOyRZHquQZn`H>v53!b-UykHE*JZ}bo%2NBvX&S)}@Slwt71)h|NHrQQt{5xB_*) zWN1tBCFIp<*s$%Ttd1I0@vd5AcQME@ZeELRX%)?*tOwZ7nUFv11wFz4LFW)=oy5ih6qt2@Y}5MrzBOU$ z=w}PMPhqfiys&3*SE4?HiKQVu-=1=Y-5Xy;y2 z>4unVJTPq^f;7w>lq6yHOE)0Uw($9zhkr5M8^B_E<<8%b`DJ6QH^wILNMKu{yvOq- zM>J1}&QmhEx$O2hv`sB)JwJbC>i|a1TeFX*`5&6{=PVrig?^Dkk@ZN?trt?JHcta? z;gg?v2v(dP?@QnU22j1fqVR%)sgOl!uLQ)lFv?{yC4YGR$(fp{-d;hnczCoDTE>>dLGr*Lz{RwM_%xdQD}E&RdTSt`i(32wt3y>P5cI8C;WE7 zpgnB+^0Y~HF=GGazXq-xajgT=ixg(vj;mGQ(O9g+YV|OgUDBW3k_Ny28nREZawUaZyk@+_99gH6{x-xpt5d-Cv~h6< zg`~oc^|f#7cYRje`Lp|bZ+`9t(}e(hZJ9(Gczv)x{>{?Rr6D6{Exp|K7mS}CEMNtG z1^>*vbW9TM_Z}H0qYe3;-hfKx5#Ner%ftgS2$wRF8#Mr@d_g7dPF5h1(0Z{vfX`Iy zr>U%DNX{KAP=l?Eov|M)M+|4)ZH~tBn5He2(xaI#5^LRO#58{je{rhfawjH`c_ra3 z=EnLDix+g-ZJ-TcA1-oBYFw+X`AYiJvJubxm$l&!H3*fZ((t7{Fi6YWVmx!=iE?A2 zMMyLh9K)UJDfF_JZSKS`bShRzxXFAan)Qe;HK~zv+TV9}R{#{#NBI4in@YmdV>L}g z@TWxxz?=<&f`>-IS)!6yzMbMwyoJST zpmCi(GtKH8PP3cd9FG`@wZ8=!8@>X<%r#cWyq39d3^D)HR3axqY^k2-2F4|?w`hP5 zNG!k6GRI0(=S$}#gxwYtiXS6Vqy$kEI%-TCdgn0InZM4UWORGcp5+VoRGgKGVx3eH zjwAoo`dR!U*%_M~!}6ge2j~-6Xn#6Ct;|f|o0`<#w`FfI4K7ttT$mH`%T59nwk^}J zwrs0KuhwO>B_*c_YLFMNmZ<|BXQKq#VBp8cxNcr@eG&e*DecUGya{QU@`dO5SV~K9 z=dJe(e>rmDo33>%HLNXnnU;Oo)@;-!Lfl62s$dfF7l$fL8ciJ67YKKwtel4ic~Zxa x;;eEP+0pm<@(qUM=^<0Gi1zM(cf7b_3Ora}^tn&aAWv5SeQhIXqozab{{Yzvv+Do= literal 0 HcmV?d00001 From b911efc84a3e54428bd3af8de1553b712fd584fb Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Tue, 5 May 2026 15:52:10 +0200 Subject: [PATCH 11/13] fix unit tests --- .../test/src/message_input/message_input_test.dart | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/stream_chat_flutter/test/src/message_input/message_input_test.dart b/packages/stream_chat_flutter/test/src/message_input/message_input_test.dart index f7a81099fe..e9d8dccf70 100644 --- a/packages/stream_chat_flutter/test/src/message_input/message_input_test.dart +++ b/packages/stream_chat_flutter/test/src/message_input/message_input_test.dart @@ -387,9 +387,11 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - bottomNavigationBar: StreamMessageComposer( + bottomNavigationBar: DefaultStreamMessageComposer( key: key, - messageInputController: messageInputController, + props: MessageComposerProps( + messageInputController: messageInputController, + ), ), ), ), @@ -429,9 +431,11 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - bottomNavigationBar: StreamMessageComposer( + bottomNavigationBar: DefaultStreamMessageComposer( key: key, - messageInputController: messageInputController, + props: MessageComposerProps( + messageInputController: messageInputController, + ), ), ), ), From 976871d7599d7f929cc4fbe6bd33ab6d8cb741bd Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Tue, 5 May 2026 16:18:49 +0200 Subject: [PATCH 12/13] re-enable skipped tests --- .../src/message_input/message_input_test.dart | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/packages/stream_chat_flutter/test/src/message_input/message_input_test.dart b/packages/stream_chat_flutter/test/src/message_input/message_input_test.dart index e9d8dccf70..9dee71047a 100644 --- a/packages/stream_chat_flutter/test/src/message_input/message_input_test.dart +++ b/packages/stream_chat_flutter/test/src/message_input/message_input_test.dart @@ -21,7 +21,6 @@ void main() { testWidgets( 'checks message input features', - skip: true, (WidgetTester tester) async { await tester.pumpWidget( buildWidget( @@ -33,13 +32,11 @@ void main() { await tester.pumpAndSettle(); expect(find.byType(TextField), findsOneWidget); - expect(find.byKey(const Key('messageInputText')), findsOneWidget); }, ); testWidgets( 'checks message input slow mode', - skip: true, (WidgetTester tester) async { final client = MockClient(); final clientState = MockClientState(); @@ -136,7 +133,6 @@ void main() { testWidgets( 'should send message when Enter key is pressed on desktop', - skip: true, (tester) async { when(() => channel.sendMessage(any())).thenAnswer( (i) async => SendMessageResponse() @@ -162,7 +158,7 @@ void main() { await tester.pumpAndSettle(); // Add some text to the input field - final textField = find.byType(StreamMessageTextField); + final textField = find.byType(TextField); await tester.enterText(textField, 'Hello world'); await tester.pump(); @@ -178,7 +174,6 @@ void main() { testWidgets( 'should not send message when Shift+Enter key is pressed on desktop', - skip: true, (tester) async { when(() => channel.sendMessage(any())).thenAnswer( (_) async => SendMessageResponse() @@ -204,7 +199,7 @@ void main() { await tester.pumpAndSettle(); // Add some text to the input field - final textField = find.byType(StreamMessageTextField); + final textField = find.byType(TextField); await tester.enterText(textField, 'Hello world'); await tester.pump(); @@ -224,7 +219,6 @@ void main() { testWidgets( 'should clear quoted message when Esc key is pressed on desktop', - skip: true, (tester) async { final quotedMessage = Message(text: 'I am a quoted message'); final initialMessage = Message(quotedMessage: quotedMessage); @@ -257,7 +251,7 @@ void main() { await tester.pumpAndSettle(); // Tap the message input to focus it - final textField = find.byType(StreamMessageTextField); + final textField = find.byType(TextField); await tester.tap(textField); await tester.pump(); @@ -273,7 +267,6 @@ void main() { testWidgets( 'should not clear quoted message contains text and Esc key is pressed on desktop', - skip: true, (tester) async { final quotedMessage = Message(text: 'I am a quoted message'); final initialMessage = Message(quotedMessage: quotedMessage); @@ -306,7 +299,7 @@ void main() { await tester.pumpAndSettle(); // Add some text to the input field - final textField = find.byType(StreamMessageTextField); + final textField = find.byType(TextField); await tester.enterText(textField, 'Hello world'); await tester.pump(); @@ -492,7 +485,6 @@ void main() { testWidgets( 'should not show DmCheckboxListTile when hideSendAsDm is true', - skip: true, (tester) async { await tester.pumpWidget( MaterialApp( @@ -518,7 +510,6 @@ void main() { testWidgets( 'should not show DmCheckboxListTile when not in a thread', - skip: true, (tester) async { await tester.pumpWidget( MaterialApp( @@ -542,7 +533,6 @@ void main() { testWidgets( 'should show DmCheckboxListTile when in a thread and hideSendAsDm is false', - skip: true, (tester) async { // Set up a message controller with a parent message ID (thread) final messageInputController = StreamMessageInputController( @@ -573,7 +563,6 @@ void main() { testWidgets( 'should toggle showInChannel value when DmCheckboxListTile is tapped', - skip: true, (tester) async { // Set up a message controller with a parent message ID (thread) final messageInputController = StreamMessageInputController( From 13e1d0c23ee615a0ef181b76611e38b42c1609f5 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Tue, 5 May 2026 16:40:58 +0200 Subject: [PATCH 13/13] don't run pub get parallel --- melos.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/melos.yaml b/melos.yaml index 94eed485db..2fce2ed44d 100644 --- a/melos.yaml +++ b/melos.yaml @@ -19,6 +19,7 @@ categories: command: bootstrap: + runPubGetInParallel: false # Dart and Flutter environment used in the project. environment: sdk: ^3.10.0