Skip to content
Merged
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
2 changes: 1 addition & 1 deletion melos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ command:
stream_core_flutter:
git:
url: https://github.com/GetStream/stream-core-flutter.git
ref: 57785868e62299901361affbddd06f71253e872f
ref: c7a31449e8632ea43f8c769be95a30ef9393a792
path: packages/stream_core_flutter
synchronized: ^3.1.0+1
thumblr: ^0.0.4
Expand Down
1 change: 1 addition & 0 deletions migrations/redesign/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ class MyCustomButton extends StatelessWidget {
| Message Actions | [message_actions.md](message_actions.md) |
| Reaction Picker / Reactions | [reaction_picker.md](reaction_picker.md) |
| Image CDN & Thumbnails | [image_cdn.md](image_cdn.md) |
| Message Widget & Message List | [message_widget.md](message_widget.md) |

## Need Help?

Expand Down
499 changes: 499 additions & 0 deletions migrations/redesign/message_widget.md

Large diffs are not rendered by default.

48 changes: 33 additions & 15 deletions migrations/redesign/reaction_picker.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ This guide covers the migration for the redesigned reaction picker and reaction
- [StreamChatConfigurationData](#streamchatconfigurationdata)
- [Removed Icon-List APIs](#removed-icon-list-apis)
- [ReactionIconResolver and DefaultReactionIconResolver](#reactioniconresolver-and-defaultreactioniconresolver)
- [StreamReactionPicker](#streamreactionpicker)
- [StreamMessageReactionPicker](#streammessagereactionpicker-formerly-streamreactionpicker)
- [StreamReactionIndicator](#streamreactionindicator)
- [New Components](#new-components)
- [Migration Checklist](#migration-checklist)
Expand All @@ -27,7 +27,8 @@ This guide covers the migration for the redesigned reaction picker and reaction
| `DefaultReactionIconResolver` | **New** — ready-to-use default; extend to customize `defaultReactions`, `emojiCode`, or rendering hooks |
| `ReactionPickerIconList` / `ReactionIndicatorIconList` | **Removed** — list rendering now lives inside picker/indicator widgets |
| `ReactionPickerIcon` / `ReactionIndicatorIcon` | **Removed** — use resolver-based reaction mapping instead |
| `StreamReactionPicker` | **Changed** — reaction set from `config.reactionIconResolver.defaultReactions` only |
| `StreamReactionPicker` | **Renamed** to `StreamMessageReactionPicker` — reaction set from `config.reactionIconResolver.defaultReactions` only |
| `StreamReactionPickerTheme` / `StreamReactionPickerThemeData` | **New** (from `stream_core_flutter`) — theme-based visual customisation for the picker |
| `StreamReactionIndicator` | **Changed** — uses `config.reactionIconResolver.resolve(context, type)` only |
| `ReactionDetailSheet` | **New** — `ReactionDetailSheet.show()` for reaction details bottom sheet |

Expand Down Expand Up @@ -204,27 +205,36 @@ class MyReactionIconResolver extends DefaultReactionIconResolver {

---

## StreamReactionPicker
## StreamMessageReactionPicker (formerly StreamReactionPicker)

### Breaking Changes:

- **Renamed** from `StreamReactionPicker` to `StreamMessageReactionPicker`
- `StreamReactionPicker` now refers to the domain-agnostic core component from `stream_core_flutter`
- Picker icons are no longer configured with per-widget icon models
- Quick-pick entries now come from `config.reactionIconResolver.defaultReactions`
- Visual properties (`backgroundColor`, `padding`, `shape`) removed from the widget — use `StreamReactionPickerTheme` instead
- The core picker now uses a `StreamComponentFactory` pattern with `StreamReactionPickerProps` for full customization

### Migration

**Before:**
```dart
StreamChat(
client: client,
streamChatConfigData: StreamChatConfigurationData(
reactionIcons: [ /* old icon list */ ],
),
child: MyApp(),
StreamReactionPicker(
message: message,
)
```

**After:**
```dart
StreamMessageReactionPicker(
message: message,
onReactionPicked: onReactionPicked,
)
```

Configure reactions globally via `reactionIconResolver`:

```dart
StreamChat(
client: client,
Expand All @@ -235,17 +245,23 @@ StreamChat(
)
```

Then keep picker usage unchanged:
Customize visual appearance via theme:

```dart
StreamReactionPicker(
message: message,
onReactionPicked: onReactionPicked,
StreamReactionPickerTheme(
data: StreamReactionPickerThemeData(
backgroundColor: Colors.white,
elevation: 4,
spacing: 2,
shape: RoundedSuperellipseBorder(
borderRadius: BorderRadius.all(Radius.circular(24)),
),
side: BorderSide(color: Colors.grey),
),
child: // ...
)
```

Set `reactionIconResolver` on `StreamChatConfigurationData` to customize.

---

## StreamReactionIndicator
Expand Down Expand Up @@ -315,7 +331,9 @@ Exported for `StreamChatConfigurationData`. See [ReactionIconResolver and Defaul

## Migration Checklist

- [ ] Rename `StreamReactionPicker` → `StreamMessageReactionPicker` in your code
- [ ] Remove `reactionIcons` from `StreamChatConfigurationData`
- [ ] Remove `backgroundColor`, `padding`, `shape` props from picker usage — use `StreamReactionPickerTheme` instead
- [ ] Custom quick-pick: extend `DefaultReactionIconResolver`, override `defaultReactions` with types from `streamSupportedEmojis` (so `emojiCode` returns emoji); set `reactionIconResolver`
- [ ] Custom types not in `streamSupportedEmojis`: also override `emojiCode` to return Unicode emoji for each; optionally `supportedReactions`
- [ ] Custom rendering (e.g. Twemoji): extend `DefaultReactionIconResolver`, override `resolve(context, type)` and branch by type, set `reactionIconResolver`
Expand Down
133 changes: 62 additions & 71 deletions packages/stream_chat_flutter/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -254,83 +254,74 @@ class _ChannelPageState extends State<ChannelPage> {
Expanded(
child: StreamMessageListView(
threadBuilder: (_, parent) => ThreadPage(parent: parent!),
messageBuilder:
(
context,
messageDetails,
messages,
defaultWidget,
) {
// The threshold after which the message is considered
// swiped.
const threshold = 0.2;

final isMyMessage = messageDetails.isMyMessage;

// The direction in which the message can be swiped.
final swipeDirection = isMyMessage
? SwipeDirection
.endToStart //
: SwipeDirection.startToEnd;

return Swipeable(
key: ValueKey(messageDetails.message.id),
direction: swipeDirection,
swipeThreshold: threshold,
onSwiped: (details) => reply(messageDetails.message),
backgroundBuilder: (context, details) {
// The alignment of the swipe action.
final alignment = isMyMessage
? Alignment
.centerRight //
: Alignment.centerLeft;

// The progress of the swipe action.
final progress = math.min(details.progress, threshold) / threshold;

// The offset for the reply icon.
var offset = Offset.lerp(
const Offset(-24, 0),
const Offset(12, 0),
progress,
)!;

// If the message is mine, we need to flip the offset.
if (isMyMessage) {
offset = Offset(-offset.dx, -offset.dy);
}

final _streamTheme = StreamChatTheme.of(context);

return Align(
alignment: alignment,
child: Transform.translate(
offset: offset,
child: Opacity(
opacity: progress,
child: SizedBox.square(
dimension: 30,
child: CustomPaint(
painter: AnimatedCircleBorderPainter(
progress: progress,
color: _streamTheme.colorTheme.borders,
),
child: Center(
child: Icon(
context.streamIcons.arrowShareLeft,
size: lerpDouble(0, 18, progress),
color: _streamTheme.colorTheme.accentPrimary,
),
),
messageBuilder: (context, message, defaultProps) {
// The threshold after which the message is considered
// swiped.
const threshold = 0.2;

final currentUser = StreamChat.of(context).currentUser;
final isMyMessage = message.user?.id == currentUser?.id;

// The direction in which the message can be swiped.
final swipeDirection = isMyMessage ? SwipeDirection.endToStart : SwipeDirection.startToEnd;

return Swipeable(
key: ValueKey(message.id),
direction: swipeDirection,
swipeThreshold: threshold,
onSwiped: (details) => reply(message),
backgroundBuilder: (context, details) {
// The alignment of the swipe action.
final alignment = isMyMessage ? Alignment.centerRight : Alignment.centerLeft;

// The progress of the swipe action.
final progress = math.min(details.progress, threshold) / threshold;

// The offset for the reply icon.
var offset = Offset.lerp(
const Offset(-24, 0),
const Offset(12, 0),
progress,
)!;

// If the message is mine, we need to flip the offset.
if (isMyMessage) {
offset = Offset(-offset.dx, -offset.dy);
}

final _streamTheme = StreamChatTheme.of(context);

return Align(
alignment: alignment,
child: Transform.translate(
offset: offset,
child: Opacity(
opacity: progress,
child: SizedBox.square(
dimension: 30,
child: CustomPaint(
painter: AnimatedCircleBorderPainter(
progress: progress,
color: _streamTheme.colorTheme.borders,
),
child: Center(
child: Icon(
context.streamIcons.arrowShareLeft,
size: lerpDouble(0, 18, progress),
color: _streamTheme.colorTheme.accentPrimary,
),
),
),
),
);
},
child: defaultWidget.copyWith(onReplyTap: reply),
),
),
);
},
child: DefaultStreamMessage(
props: defaultProps.copyWith(onReplyTap: reply),
),
);
},
),
),
StreamMessageInput(
Expand Down
6 changes: 2 additions & 4 deletions packages/stream_chat_flutter/example/lib/tutorial_part_5.dart
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,9 @@ class ChannelPage extends StatelessWidget {

Widget _messageBuilder(
BuildContext context,
MessageDetails details,
List<Message> messages,
StreamMessageWidget _,
Message message,
StreamMessageWidgetProps defaultProps,
) {
final message = details.message;
final isCurrentUser = StreamChat.of(context).currentUser!.id == message.user!.id;
final textAlign = isCurrentUser ? TextAlign.right : TextAlign.left;
final color = isCurrentUser ? Colors.blueGrey : Colors.blue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,75 +68,77 @@ abstract class StreamAttachmentWidgetBuilder {
static List<StreamAttachmentWidgetBuilder> defaultBuilders({
required Message message,
ShapeBorder? shape,
EdgeInsetsGeometry padding = const EdgeInsets.all(4),
EdgeInsetsGeometry? padding,
StreamAttachmentWidgetTapCallback? onAttachmentTap,
List<StreamAttachmentWidgetBuilder>? customAttachmentBuilders,
}) {
final effectivePadding = padding ?? const EdgeInsets.symmetric(horizontal: 8);

return [
...?customAttachmentBuilders,

// Handles poll attachments.
PollAttachmentBuilder(
shape: shape,
padding: padding,
padding: effectivePadding,
),

// Handles a mix of image, gif, video, url, file and voice recording
// attachments.
MixedAttachmentBuilder(
padding: padding,
padding: effectivePadding,
onAttachmentTap: onAttachmentTap,
),

// Handles a mix of image, gif, and video attachments.
GalleryAttachmentBuilder(
shape: shape,
padding: padding,
runSpacing: padding.vertical / 2,
spacing: padding.horizontal / 2,
padding: effectivePadding,
runSpacing: effectivePadding.vertical / 2,
spacing: effectivePadding.horizontal / 2,
onAttachmentTap: onAttachmentTap,
),

// Handles file attachments.
FileAttachmentBuilder(
shape: shape,
padding: padding,
padding: effectivePadding,
onAttachmentTap: onAttachmentTap,
),

// Handles giphy attachments.
GiphyAttachmentBuilder(
shape: shape,
padding: padding,
padding: effectivePadding,
onAttachmentTap: onAttachmentTap,
),

// Handles image attachments.
ImageAttachmentBuilder(
shape: shape,
padding: padding,
padding: effectivePadding,
onAttachmentTap: onAttachmentTap,
),

// Handles video attachments.
VideoAttachmentBuilder(
shape: shape,
padding: padding,
padding: effectivePadding,
onAttachmentTap: onAttachmentTap,
),

// Handles voice recording attachments.
VoiceRecordingAttachmentPlaylistBuilder(
shape: shape,
padding: padding,
padding: effectivePadding,
onAttachmentTap: onAttachmentTap,
),

// We don't handle URL attachments if the message is a reply.
if (message.quotedMessage == null)
UrlAttachmentBuilder(
shape: shape,
padding: padding,
padding: effectivePadding,
onAttachmentTap: onAttachmentTap,
),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,12 @@ class MixedAttachmentBuilder extends StreamAttachmentWidgetBuilder {

final shouldBuildGallery = [...?images, ...?videos, ...?giphys].length > 1;

final spacing = context.streamSpacing;

return Padding(
padding: padding,
child: Column(
spacing: padding.vertical / 2,
spacing: spacing.xs,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
if (urls != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Iterable<StreamComponentBuilderExtension<Object>> streamChatComponentBuilders({
StreamComponentBuilder<MessageComposerInputLeadingProps>? messageComposerInputLeading,
StreamComponentBuilder<MessageComposerInputHeaderProps>? messageComposerInputHeader,
StreamComponentBuilder<MessageComposerInputTrailingProps>? messageComposerInputTrailing,
StreamComponentBuilder<StreamMessageWidgetProps>? messageWidget,
}) {
final builders = [
if (channelListItem != null) StreamComponentBuilderExtension(builder: channelListItem),
Expand All @@ -21,6 +22,7 @@ Iterable<StreamComponentBuilderExtension<Object>> streamChatComponentBuilders({
if (messageComposerInputLeading != null) StreamComponentBuilderExtension(builder: messageComposerInputLeading),
if (messageComposerInputHeader != null) StreamComponentBuilderExtension(builder: messageComposerInputHeader),
if (messageComposerInputTrailing != null) StreamComponentBuilderExtension(builder: messageComposerInputTrailing),
if (messageWidget != null) StreamComponentBuilderExtension(builder: messageWidget),
];

return builders;
Expand Down
Loading
Loading