Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
- `StreamMessageInput` (legacy) / `StreamMessageComposer` (new design system) — 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
- `StreamMessageComposer` — 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.

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import '../src/mocks.dart';
Widget _buildMessageInputScaffold({
required MockClient client,
required MockChannel channel,
StreamMessageInput? messageInput,
Widget? messageComposer,
}) {
return MaterialApp(
theme: docsScreenshotsTheme(),
Expand All @@ -28,7 +28,7 @@ Widget _buildMessageInputScaffold({
body: Column(
children: [
Expanded(child: Container()),
messageInput ?? const StreamMessageInput(),
messageComposer ?? const StreamMessageComposer(),
],
),
),
Expand Down Expand Up @@ -66,8 +66,8 @@ void main() {
);

goldenTest(
'message input default',
fileName: 'message_input',
'message input with text',
fileName: 'message_input_with_text',
constraints: const BoxConstraints.tightFor(width: 375, height: 100),
builder: () {
final client = MockClient();
Expand All @@ -82,28 +82,8 @@ void main() {
channelState: channelState,
);

return _buildMessageInputScaffold(client: client, channel: channel);
},
);

goldenTest(
'message input actions on right',
fileName: 'message_input_change_position',
constraints: const BoxConstraints.tightFor(width: 375, height: 100),
builder: () {
final client = MockClient();
final clientState = MockClientState();
final channel = MockChannel();
final channelState = MockChannelState();

setupMockChannel(
client: client,
clientState: clientState,
channel: channel,
channelState: channelState,
);

final controller = StreamMessageInputController();
final controller = StreamMessageComposerController();
controller.textFieldController.text = 'Hello world!';

return MaterialApp(
theme: docsScreenshotsTheme(),
Expand All @@ -125,7 +105,7 @@ void main() {
body: Column(
children: [
const Expanded(child: SizedBox()),
StreamMessageInput(messageInputController: controller),
StreamMessageComposer(controller: controller),
],
),
),
Expand Down Expand Up @@ -181,7 +161,7 @@ void main() {
body: Column(
children: [
Expanded(child: Container()),
const StreamMessageInput(),
const StreamMessageComposer(),
],
),
),
Expand All @@ -208,17 +188,20 @@ void main() {
channelState: channelState,
);

final controller = StreamMessageInputController()
..quotedMessage = Message(
id: 'quoted-msg',
text: 'This is the original message',
user: User(id: 'other-user', name: 'Alice'),
);
final controller = StreamMessageComposerController(
message: Message(
quotedMessage: Message(
id: 'quoted-msg',
text: 'This is the original message',
user: User(id: 'other-user', name: 'Alice'),
),
),
);

return _buildMessageInputScaffold(
client: client,
channel: channel,
messageInput: StreamMessageInput(messageInputController: controller),
messageComposer: StreamMessageComposer(controller: controller),
);
},
);
Expand Down
Binary file modified docs/docs_screenshots/test/polls/goldens/macos/poll_creator.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/docs_screenshots/test/polls/goldens/macos/polls_composer.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ StreamAudioRecorderController _makeRecorderController(AudioRecorderState initial
Widget _buildVoiceRecordingMessageInputScaffold({
required MockClient client,
required MockChannel channel,
StreamMessageInputController? messageInputController,
StreamMessageComposerController? controller,
}) {
return MaterialApp(
theme: docsScreenshotsTheme(),
Expand All @@ -41,9 +41,9 @@ Widget _buildVoiceRecordingMessageInputScaffold({
body: Column(
children: [
Expanded(child: Container()),
StreamMessageInput(
StreamMessageComposer(
enableVoiceRecording: true,
messageInputController: messageInputController,
controller: controller,
),
],
),
Expand Down Expand Up @@ -93,7 +93,7 @@ Widget _buildVoiceRecordingContextScaffold({
],
),
),
const StreamMessageInput(enableVoiceRecording: true),
const StreamMessageComposer(enableVoiceRecording: true),
],
),
),
Expand Down Expand Up @@ -134,14 +134,10 @@ Widget _buildVoiceRecordingComposerScaffold({
decoration: BoxDecoration(
color: context.streamColorScheme.backgroundElevation1,
),
child: Padding(
padding: EdgeInsets.only(bottom: context.streamSpacing.md),
child: StreamChatMessageComposer(
onSendPressed: () {},
onAttachmentButtonPressed: () {},
placeholder: 'Send a message',
audioRecorderController: audioRecorderController,
),
child: StreamMessageComposer(
enableVoiceRecording: true,
controller: StreamMessageComposerController(),
audioRecorderController: audioRecorderController,
),
),
);
Expand Down Expand Up @@ -301,7 +297,7 @@ void main() {
final channelState = MockChannelState();
_setupChannel(client, clientState, channel, channelState);

final messageInputController = StreamMessageInputController()
final controller = StreamMessageComposerController()
..addAttachment(
Attachment(
type: 'voiceRecording',
Expand All @@ -317,7 +313,7 @@ void main() {
return _buildVoiceRecordingMessageInputScaffold(
client: client,
channel: channel,
messageInputController: messageInputController,
controller: controller,
);
},
);
Expand Down
36 changes: 17 additions & 19 deletions migrations/redesign/message_composer.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ This guide covers the migration for the message composer components in the Strea

- [Overview](#overview)
- [StreamMessageInput](#streammessageinput)
- [StreamChatMessageComposer (new)](#streamchatmessagecomposer-new)
- [StreamMessageComposer (new)](#streammessagecomposer-new)
- [Message Input Placeholder API](#message-input-placeholder-api)
- [Attachment Customization](#attachment-customization)
- [Migration Checklist](#migration-checklist)
Expand All @@ -22,9 +22,9 @@ 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` | 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.
`StreamMessageInput` wraps `StreamMessageComposer` for its visual layer. If you are using `StreamMessageInput` today, it remains the right choice — it is not deprecated. `StreamMessageComposer` exists for cases where you want to build your own message-sending logic and use the new design system UI.

---

Expand Down Expand Up @@ -88,7 +88,7 @@ Many parameters that existed in older versions of `StreamMessageInput` have been

#### 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 `StreamMessageComposer` and its sub-components, customizable via `StreamComponentFactory`.

| Removed parameter | Migration path |
|-------------------|---------------|
Expand Down Expand Up @@ -139,13 +139,13 @@ These parameters have been removed. Attachment rendering in the composer input h

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.

If you are using `StreamChatMessageComposer` directly, the button hides when `onAttachmentButtonPressed` is `null`.
If you are using `StreamMessageComposer` directly, the button hides when `onAttachmentButtonPressed` is `null`.

---

## StreamChatMessageComposer (new)
## StreamMessageComposer (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.
`StreamMessageComposer` 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.

Comment thread
renefloor marked this conversation as resolved.
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.

Expand All @@ -154,7 +154,7 @@ Use this when you want the new design system visuals with custom business logic.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `onSendPressed` | `VoidCallback` | **required** | Called when the send button is pressed |
| `controller` | `StreamMessageInputController?` | `null` | Controller for the input; created internally if not provided |
| `controller` | `StreamMessageComposerController?` | `null` | Controller for the input; created internally if not provided |
| `onAttachmentButtonPressed` | `VoidCallback?` | `null` | Called when the attachment button is pressed. When `null`, the attachment button is hidden. |
| `isPickerOpen` | `bool` | `false` | Whether the inline attachment picker is currently open |
| `focusNode` | `FocusNode?` | `null` | Focus node for the text field |
Expand Down Expand Up @@ -202,12 +202,10 @@ 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.

### What was removed

| Removed | Replacement |
Expand All @@ -216,8 +214,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

Expand Down Expand Up @@ -257,15 +255,15 @@ Each case carries the contextual data relevant to that input state. Pattern-matc
| Case | Field | Type | Description |
|------|-------|------|-------------|
| `WriteMessagePlaceholder` | `isEditing` | `bool` | `true` when the input is editing an existing message instead of composing a new one. Useful for swapping the placeholder while editing. |
| `SlowModePlaceholder` | `cooldownTimeOut` | `int` | Remaining slow-mode cooldown in seconds. Mirrors `StreamMessageInputController.cooldownTimeOut`. |
| `SlowModePlaceholder` | `cooldownTimeOut` | `int` | Remaining slow-mode cooldown in seconds. Mirrors `StreamMessageComposerController.cooldownTimeOut`. |
| `SlowModePlaceholder` | `cooldown` | `Duration` | Convenience getter wrapping `cooldownTimeOut` for formatting timer strings. |
| `CommandPlaceholder` | `command` | `String` | Active command name (e.g. `'giphy'`, `'mute'`, `'ban'`, or any backend-defined command). |
| `AttachmentsPlaceholder` | `attachments` | `List<Attachment>` | Pending attachments held by the input. OG link previews are still included — filter via `Attachment.ogScrapeUrl` if you only want user-added ones. |

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) {
Expand Down Expand Up @@ -310,7 +308,7 @@ StreamMessageInput(
**After:**

```dart
StreamMessageInput(
StreamMessageComposer(
placeholderBuilder: (context, placeholder) {
return switch (placeholder) {
SlowModePlaceholder() => 'Slow mode is on',
Expand All @@ -327,7 +325,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) {
Expand Down Expand Up @@ -429,8 +427,8 @@ The following public widgets are provided as building blocks for custom attachme
- [ ] 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 `StreamMessageComposer` 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 `StreamMessageComposer.placeholderBuilder` and rewrite the callback to switch over `MessageInputPlaceholder` cases (`SlowModePlaceholder`, `CommandPlaceholder`, `AttachmentsPlaceholder`, `WriteMessagePlaceholder`) instead of the removed `HintType` enum.
- [ ] 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`
5 changes: 5 additions & 0 deletions packages/stream_chat_flutter/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

- Replaced `StreamMessageInput.hintGetter` with `placeholderBuilder` over a sealed `MessageInputPlaceholder`.
See [`migrations/redesign/message_composer.md`](../../migrations/redesign/message_composer.md).
- Removed `StreamMessageInput` and `StreamMessageTextField`; migrate to `StreamMessageComposer`.
- Removed `KeyEventPredicate` from `src/utils/typedefs.dart`; it is now exported from `stream_message_composer.dart` directly.
- `MessageComposerComponentProps` now carries additional text-input props (`canAlsoSendToChannel`, `textInputAction`, `keyboardType`, `textCapitalization`, `autofocus`, `autocorrect`).
- Renamed `StreamMessageComposer.autoCorrect` → `autocorrect` and `MessageComposerProps.autoCorrect` → `autocorrect` to align with Flutter's `TextField.autocorrect` convention.

- Removed `StreamMessageListView.unreadIndicatorBuilder`; use `StreamComponentFactory.jumpToUnreadButton`.
- Renamed `UnreadIndicatorButton.onTap` → `onJumpTap`.
- Renamed stream icons to remove the size suffix from the icon names.
Expand Down
Loading