Skip to content

feat(ui): factories for composer attachments#2566

Merged
renefloor merged 8 commits intofeat/design-refreshfrom
feature/factories-for-composer-attachments
Mar 26, 2026
Merged

feat(ui): factories for composer attachments#2566
renefloor merged 8 commits intofeat/design-refreshfrom
feature/factories-for-composer-attachments

Conversation

@renefloor
Copy link
Contributor

@renefloor renefloor commented Mar 24, 2026

Submit a pull request

CLA

  • I have signed the Stream CLA (required).
  • The code changes follow best practices
  • Code changes are tested (add some information if not applicable)

Description of the pull request

Main change in this PR is that it removes all the builder methods and only creates a factories for the attachment list or individual attachments.

It also re-adds some missing features and removes unused properties.

needs: GetStream/stream-core-flutter#83

Summary by CodeRabbit

Release Notes

  • New Features

    • Added customizable text input options including keyboard type, capitalization, autofocus, and autocorrect settings.
    • Enhanced quoted message handling with improved lifecycle callbacks.
  • Improvements

    • Refined attachment rendering and lifecycle management in the message composer.
    • Improved attachment button visibility to prevent non-functional controls from displaying.
  • Refactor

    • Modernized message input component architecture for improved maintainability.
    • Updated core dependencies for enhanced stability.

@renefloor renefloor changed the title Feature/factories for composer attachments feat(ui): factories for composer attachments Mar 24, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 24, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d107b498-e5d9-496b-a639-396329b23f62

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/factories-for-composer-attachments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link

codecov bot commented Mar 24, 2026

Codecov Report

❌ Patch coverage is 46.78112% with 124 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.12%. Comparing base (d01f500) to head (9dfcd06).
⚠️ Report is 4 commits behind head on feat/design-refresh.

Files with missing lines Patch % Lines
...sage_input/stream_message_composer_attachment.dart 42.39% 53 Missing ⚠️
...er/lib/src/message_input/stream_message_input.dart 28.81% 42 Missing ⚠️
...input/stream_message_composer_attachment_list.dart 69.84% 19 Missing ⚠️
...age_composer/message_composer_component_props.dart 0.00% 6 Missing ⚠️
...essage_composer/message_composer_input_header.dart 0.00% 4 Missing ⚠️
Additional details and impacted files
@@                   Coverage Diff                   @@
##           feat/design-refresh    #2566      +/-   ##
=======================================================
+ Coverage                64.23%   65.12%   +0.88%     
=======================================================
  Files                      433      432       -1     
  Lines                    26396    26056     -340     
=======================================================
+ Hits                     16956    16968      +12     
+ Misses                    9440     9088     -352     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_input_header.dart (1)

208-216: ⚠️ Potential issue | 🟠 Major

Bug: Container result is discarded — trailing is never assigned.

The Container widget is created but not assigned to trailing. The image case will always result in trailing being null.

🐛 Proposed fix
     if (image != null) {
-      Container(
+      trailing = Container(
         width: 40,
         height: 40,
         decoration: BoxDecoration(
           borderRadius: BorderRadius.all(context.streamRadius.md),
           image: DecorationImage(image: image, fit: BoxFit.cover),
         ),
       );
     } else if (mimeType != null) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_input_header.dart`
around lines 208 - 216, The Container built for the image is created but never
assigned to the variable trailing, so trailing remains null; update the image
branch in the component (where the Container with width:40, height:40,
BoxDecoration and DecorationImage is constructed) to assign that Container to
trailing (e.g., trailing = Container(...)) so the image is actually used,
preserving existing properties like borderRadius:
BorderRadius.all(context.streamRadius.md) and fit: BoxFit.cover.
packages/stream_chat_flutter/lib/src/components/stream_chat_component_builders.dart (1)

16-32: ⚠️ Potential issue | 🔴 Critical

Bug: New builder parameters are declared but never added to the builders list.

The messageComposerAttachmentList and messageComposerAttachment parameters are declared (lines 16-17) but not included in the builders list. This means consumers cannot customize these components via streamChatComponentBuilders().

🐛 Proposed fix to include the new builders
     if (messageWidget != null) StreamComponentBuilderExtension(builder: messageWidget),
     if (unreadIndicator != null) StreamComponentBuilderExtension(builder: unreadIndicator),
+    if (messageComposerAttachmentList != null) StreamComponentBuilderExtension(builder: messageComposerAttachmentList),
+    if (messageComposerAttachment != null) StreamComponentBuilderExtension(builder: messageComposerAttachment),
   ];

   return builders;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/stream_chat_flutter/lib/src/components/stream_chat_component_builders.dart`
around lines 16 - 32, The new parameters messageComposerAttachmentList and
messageComposerAttachment are declared but never appended to the builders list;
update the builders list in the streamChatComponentBuilders function to include
conditionally-wrapped StreamComponentBuilderExtension entries for
messageComposerAttachmentList and messageComposerAttachment (e.g., if
(messageComposerAttachmentList != null) StreamComponentBuilderExtension(builder:
messageComposerAttachmentList), and similarly for messageComposerAttachment) so
consumers can override those components.
🧹 Nitpick comments (4)
packages/stream_chat_flutter/test/src/message_input/message_input_attachment_list_test.dart (1)

21-23: Consider updating test group and description names to match the widget under test.

The test group is named 'StreamMessageInputAttachmentList tests' but the widget being tested is now StreamMessageComposerAttachmentList. The same applies to test descriptions at lines 23, 47, and 80.

♻️ Proposed fix for consistency
-  group('StreamMessageInputAttachmentList tests', () {
+  group('StreamMessageComposerAttachmentList tests', () {
     testWidgets(
-      'StreamMessageInputAttachmentList should render attachments',
+      'StreamMessageComposerAttachmentList should render attachments',
       (WidgetTester tester) async {

Similar updates needed for:

  • Line 47: 'StreamMessageInputAttachmentList should call onRemovePressed callback'
  • Line 80: 'StreamMessageInputAttachmentList should display empty box if no attachments'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/stream_chat_flutter/test/src/message_input/message_input_attachment_list_test.dart`
around lines 21 - 23, Update the test group and its test descriptions to
reference the current widget name StreamMessageComposerAttachmentList instead of
StreamMessageInputAttachmentList: change the group label string (currently
'StreamMessageInputAttachmentList tests') and the test titles
('StreamMessageInputAttachmentList should render attachments',
'StreamMessageInputAttachmentList should call onRemovePressed callback',
'StreamMessageInputAttachmentList should display empty box if no attachments')
to use 'StreamMessageComposerAttachmentList' so the group and all test
descriptions match the widget under test.
packages/stream_chat_flutter/lib/src/message_input/stream_message_composer_attachment.dart (1)

85-107: Minor optimization: consolidate track lookups.

The code performs two passes over the tracks list: .any() followed by .indexWhere(). These can be combined into a single lookup.

♻️ Suggested refactor
     if (attachment.type == AttachmentType.audio || attachment.type == AttachmentType.voiceRecording) {
       if (audioPlaylistController == null) {
         return const SizedBox.shrink();
       }
 
-      final hasTrack = audioPlaylistController!.value.tracks.any((it) => it.key == attachment);
-
-      if (!hasTrack) {
-        return const SizedBox.shrink();
-      }
-
       final trackIndex = audioPlaylistController!.value.tracks.indexWhere((it) => it.key == attachment);
+      if (trackIndex == -1) {
+        return const SizedBox.shrink();
+      }
 
       return SizedBox(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/stream_chat_flutter/lib/src/message_input/stream_message_composer_attachment.dart`
around lines 85 - 107, The code currently checks
audioPlaylistController!.value.tracks twice using .any(...) then
.indexWhere(...); replace this with a single lookup by using indexWhere once
(e.g., compute trackIndex =
audioPlaylistController!.value.tracks.indexWhere((it) => it.key == attachment))
and then check if trackIndex is -1 to decide to return SizedBox.shrink() or
proceed to build MessageInputVoiceRecordingAttachment with controller:
audioPlaylistController!, index: trackIndex, and onRemovePressed; keep the
existing null check for audioPlaylistController before the lookup.
packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart (1)

761-773: Consider simplifying conditional controller instantiation.

The two branches duplicate most parameters. You could use a single constructor call with a spread or named parameter approach if the API supports it, or extract common params.

♻️ Suggested refactor
     setState(() {
       final attachmentLimit = widget.attachmentLimit;
-      _pickerController = attachmentLimit != null
-          ? StreamAttachmentPickerController(
-              initialAttachments: _effectiveController.attachments,
-              initialPoll: _effectiveController.poll,
-              maxAttachmentCount: attachmentLimit,
-              maxAttachmentSize: widget.maxAttachmentSize,
-            )
-          : StreamAttachmentPickerController(
-              initialAttachments: _effectiveController.attachments,
-              initialPoll: _effectiveController.poll,
-              maxAttachmentSize: widget.maxAttachmentSize,
-            );
+      _pickerController = StreamAttachmentPickerController(
+        initialAttachments: _effectiveController.attachments,
+        initialPoll: _effectiveController.poll,
+        maxAttachmentSize: widget.maxAttachmentSize,
+        // Only pass maxAttachmentCount when limit is set
+        ...(attachmentLimit != null ? { maxAttachmentCount: attachmentLimit } : {}),
+      );

Note: This depends on whether StreamAttachmentPickerController accepts null for maxAttachmentCount. If not, the current approach is fine.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart`
around lines 761 - 773, The controller instantiation for _pickerController
duplicates parameters across the conditional; refactor by building a single
StreamAttachmentPickerController call using common arguments
(initialAttachments: _effectiveController.attachments, initialPoll:
_effectiveController.poll, maxAttachmentSize: widget.maxAttachmentSize) and
supply maxAttachmentCount only when widget.attachmentLimit is non-null (e.g.,
pass widget.attachmentLimit directly if the constructor accepts null, or
construct the args map/variables and include maxAttachmentCount conditionally
before calling StreamAttachmentPickerController) so you eliminate the duplicated
branches while keeping the same behavior.
packages/stream_chat_flutter/lib/src/message_input/stream_message_composer_attachment_list.dart (1)

80-81: Consider moving computed getter to State class.

The _audioAttachments getter performs filtering computation on the widget's data. While functional, placing computed logic directly on a StatefulWidget is unconventional—widgets are typically just data holders. Consider moving this to the State class or computing inline where needed for clarity.

♻️ Suggested refactor
 class DefaultMessageComposerAttachmentList extends StatefulWidget {
   // ...
   
   /// Callback called when the remove button is pressed.
   ValueSetter<Attachment>? get onRemovePressed => props.onRemovePressed;
 
-  List<Attachment> get _audioAttachments =>
-      attachments.where((it) => it.type == AttachmentType.audio || it.type == AttachmentType.voiceRecording).toList();
-
   `@override`
   State<DefaultMessageComposerAttachmentList> createState() => _DefaultMessageComposerAttachmentListState();
 }
 
 class _DefaultMessageComposerAttachmentListState extends State<DefaultMessageComposerAttachmentList> {
-  late List<Attachment> _audioAttachments = widget._audioAttachments;
+  late List<Attachment> _audioAttachments = _computeAudioAttachments();
+
+  List<Attachment> _computeAudioAttachments() =>
+      widget.attachments.where((it) => it.type == AttachmentType.audio || it.type == AttachmentType.voiceRecording).toList();
 
   // In didUpdateWidget:
-    final newAudioAttachments = widget._audioAttachments;
+    final newAudioAttachments = _computeAudioAttachments();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/stream_chat_flutter/lib/src/message_input/stream_message_composer_attachment_list.dart`
around lines 80 - 81, The _audioAttachments getter on the
StreamMessageComposerAttachmentList widget performs filtering work on the widget
class; move this computed logic into the corresponding State class (e.g., the
State for StreamMessageComposerAttachmentList) or compute the filtered list
inline where it's used (e.g., inside build or event handlers) so the widget
remains a pure data holder; relocate the implementation of _audioAttachments
(the where(... type == AttachmentType.audio || type ==
AttachmentType.voiceRecording) logic) into State and update all references to
use the State-level member or local variable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@packages/stream_chat_flutter/lib/src/message_input/stream_message_composer_attachment_list.dart`:
- Around line 111-122: The post-frame callback that calls
_scrollController.animateTo can throw if the controller is not attached; wrap
the animateTo call in a safety guard (e.g., check _scrollController.hasClients
and mounted) before accessing _scrollController.position.maxScrollExtent or
calling animateTo in the callback for StreamMessageComposerAttachmentList; if
the guard fails, skip the animation. Ensure you keep the existing
addPostFrameCallback and only perform the animateTo when the controller is
valid.

In
`@packages/stream_chat_flutter/lib/src/message_input/stream_message_composer_attachment.dart`:
- Around line 174-178: The current builder in ValueListenableBuilder uses
state.tracks.where((it) => it.key == attachment).first which can throw
StateError if the track was removed between updates; update the lookup in the
builder (ValueListenableBuilder, controller, state, tracks, attachment, track)
to use a safe lookup such as state.tracks.firstWhereOrNull((it) => it.key ==
attachment) (collection package is available) or otherwise check for empty
result and handle the null/missing case before accessing track to avoid runtime
exceptions during rebuilds.

In
`@packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart`:
- Around line 493-500: The handler currently returns KeyEventResult.handled
whenever clearQuotedMessageKeyPredicate(node, event) is true even if no quote
was cleared; update the logic in the block inside stream_message_input.dart so
that after calling clearQuotedMessageKeyPredicate(node, event) you only return
KeyEventResult.handled when an actual action occurs (i.e., when
_effectiveController.message.quotedMessage != null &&
_effectiveController.text.isEmpty, you call
_effectiveController.clearQuotedMessage() and
widget.onQuotedMessageCleared?.call() and return handled); otherwise return
KeyEventResult.ignored so other widgets can process the key.

In `@packages/stream_chat_flutter/lib/stream_chat_flutter.dart`:
- Line 105: Update the test group and test descriptions to reference the new
class name StreamMessageComposerAttachmentList instead of the old
StreamMessageInputAttachmentList; locate the test file(s) that still mention
StreamMessageInputAttachmentList in group/test descriptions and replace those
strings (e.g., test group titles and test case descriptions) so they match the
actual widget under test (StreamMessageComposerAttachmentList) and avoid
breaking public API naming consistency.

---

Outside diff comments:
In
`@packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_input_header.dart`:
- Around line 208-216: The Container built for the image is created but never
assigned to the variable trailing, so trailing remains null; update the image
branch in the component (where the Container with width:40, height:40,
BoxDecoration and DecorationImage is constructed) to assign that Container to
trailing (e.g., trailing = Container(...)) so the image is actually used,
preserving existing properties like borderRadius:
BorderRadius.all(context.streamRadius.md) and fit: BoxFit.cover.

In
`@packages/stream_chat_flutter/lib/src/components/stream_chat_component_builders.dart`:
- Around line 16-32: The new parameters messageComposerAttachmentList and
messageComposerAttachment are declared but never appended to the builders list;
update the builders list in the streamChatComponentBuilders function to include
conditionally-wrapped StreamComponentBuilderExtension entries for
messageComposerAttachmentList and messageComposerAttachment (e.g., if
(messageComposerAttachmentList != null) StreamComponentBuilderExtension(builder:
messageComposerAttachmentList), and similarly for messageComposerAttachment) so
consumers can override those components.

---

Nitpick comments:
In
`@packages/stream_chat_flutter/lib/src/message_input/stream_message_composer_attachment_list.dart`:
- Around line 80-81: The _audioAttachments getter on the
StreamMessageComposerAttachmentList widget performs filtering work on the widget
class; move this computed logic into the corresponding State class (e.g., the
State for StreamMessageComposerAttachmentList) or compute the filtered list
inline where it's used (e.g., inside build or event handlers) so the widget
remains a pure data holder; relocate the implementation of _audioAttachments
(the where(... type == AttachmentType.audio || type ==
AttachmentType.voiceRecording) logic) into State and update all references to
use the State-level member or local variable.

In
`@packages/stream_chat_flutter/lib/src/message_input/stream_message_composer_attachment.dart`:
- Around line 85-107: The code currently checks
audioPlaylistController!.value.tracks twice using .any(...) then
.indexWhere(...); replace this with a single lookup by using indexWhere once
(e.g., compute trackIndex =
audioPlaylistController!.value.tracks.indexWhere((it) => it.key == attachment))
and then check if trackIndex is -1 to decide to return SizedBox.shrink() or
proceed to build MessageInputVoiceRecordingAttachment with controller:
audioPlaylistController!, index: trackIndex, and onRemovePressed; keep the
existing null check for audioPlaylistController before the lookup.

In
`@packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart`:
- Around line 761-773: The controller instantiation for _pickerController
duplicates parameters across the conditional; refactor by building a single
StreamAttachmentPickerController call using common arguments
(initialAttachments: _effectiveController.attachments, initialPoll:
_effectiveController.poll, maxAttachmentSize: widget.maxAttachmentSize) and
supply maxAttachmentCount only when widget.attachmentLimit is non-null (e.g.,
pass widget.attachmentLimit directly if the constructor accepts null, or
construct the args map/variables and include maxAttachmentCount conditionally
before calling StreamAttachmentPickerController) so you eliminate the duplicated
branches while keeping the same behavior.

In
`@packages/stream_chat_flutter/test/src/message_input/message_input_attachment_list_test.dart`:
- Around line 21-23: Update the test group and its test descriptions to
reference the current widget name StreamMessageComposerAttachmentList instead of
StreamMessageInputAttachmentList: change the group label string (currently
'StreamMessageInputAttachmentList tests') and the test titles
('StreamMessageInputAttachmentList should render attachments',
'StreamMessageInputAttachmentList should call onRemovePressed callback',
'StreamMessageInputAttachmentList should display empty box if no attachments')
to use 'StreamMessageComposerAttachmentList' so the group and all test
descriptions match the widget under test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: da79dced-58ea-40b9-ab1a-bb13e2eeb3f1

📥 Commits

Reviewing files that changed from the base of the PR and between c86af06 and f3aa694.

📒 Files selected for processing (15)
  • melos.yaml
  • packages/stream_chat_flutter/lib/src/bottom_sheets/edit_message_sheet.dart
  • packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_component_props.dart
  • packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_input_header.dart
  • packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_leading.dart
  • packages/stream_chat_flutter/lib/src/components/message_composer/stream_chat_message_composer.dart
  • packages/stream_chat_flutter/lib/src/components/stream_chat_component_builders.dart
  • packages/stream_chat_flutter/lib/src/message_input/stream_message_composer_attachment.dart
  • packages/stream_chat_flutter/lib/src/message_input/stream_message_composer_attachment_list.dart
  • packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart
  • packages/stream_chat_flutter/lib/src/message_input/stream_message_input_attachment_list.dart
  • packages/stream_chat_flutter/lib/stream_chat_flutter.dart
  • packages/stream_chat_flutter/pubspec.yaml
  • packages/stream_chat_flutter/test/src/message_input/message_input_attachment_list_test.dart
  • packages/stream_chat_flutter/test/src/message_input/message_input_test.dart
💤 Files with no reviewable changes (3)
  • packages/stream_chat_flutter/lib/src/bottom_sheets/edit_message_sheet.dart
  • packages/stream_chat_flutter/test/src/message_input/message_input_test.dart
  • packages/stream_chat_flutter/lib/src/message_input/stream_message_input_attachment_list.dart

…r-attachments

# Conflicts:
#	melos.yaml
#	packages/stream_chat_flutter/pubspec.yaml
@renefloor renefloor merged commit 9c3737e into feat/design-refresh Mar 26, 2026
12 of 13 checks passed
@renefloor renefloor deleted the feature/factories-for-composer-attachments branch March 26, 2026 16:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants