Skip to content

Commit eb9d91c

Browse files
authored
refactor(ui)!: improve reaction bubble implementation (#2277)
Co-authored-by: xsahil03x <25670178+xsahil03x@users.noreply.github.com>
1 parent 396fcca commit eb9d91c

36 files changed

+1227
-555
lines changed

packages/stream_chat/lib/src/core/models/reaction_group.dart

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,26 @@ class ReactionGroup extends Equatable {
5757
lastReactionAt,
5858
];
5959
}
60+
61+
/// A group of comparators for sorting [ReactionGroup]s.
62+
final class ReactionSorting {
63+
/// Sorts [ReactionGroup]s by the sum of their scores.
64+
static int byScore(ReactionGroup a, ReactionGroup b) {
65+
return a.sumScores.compareTo(b.sumScores);
66+
}
67+
68+
/// Sorts [ReactionGroup]s by the count of reactions.
69+
static int byCount(ReactionGroup a, ReactionGroup b) {
70+
return a.count.compareTo(b.count);
71+
}
72+
73+
/// Sorts [ReactionGroup]s by the date of their first reaction.
74+
static int byFirstReactionAt(ReactionGroup a, ReactionGroup b) {
75+
return a.firstReactionAt.compareTo(b.firstReactionAt);
76+
}
77+
78+
/// Sorts [ReactionGroup]s by the date of their last reaction.
79+
static int byLastReactionAt(ReactionGroup a, ReactionGroup b) {
80+
return a.lastReactionAt.compareTo(b.lastReactionAt);
81+
}
82+
}

packages/stream_chat_flutter/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- `StreamReactionPicker` now requires reactions to be explicitly handled via `onReactionPicked`. *(Automatic handling is no longer supported.)*
66
- `StreamMessageAction` is now generic `(StreamMessageAction<T>)`, enhancing type safety. Individual onTap callbacks have been removed; actions are now handled centrally by widgets like `StreamMessageWidget.onCustomActionTap` or modals using action types.
77
- `StreamMessageReactionsModal` no longer requires the `messageTheme` parameter. The theme now automatically derives from the `reverse` property.
8+
- `StreamMessageWidget` no longer requires the `showReactionTail` parameter. The reaction picker tail is now always shown when the reaction picker is visible.
89

910
For more details, please refer to the [migration guide](Unpublished).
1011

packages/stream_chat_flutter/lib/src/message_modal/message_actions_modal.dart

Lines changed: 18 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import 'package:flutter/material.dart';
2-
import 'package:stream_chat_flutter/src/reactions/reactions_align.dart';
2+
import 'package:stream_chat_flutter/src/reactions/picker/reaction_picker_bubble_overlay.dart';
33

44
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
55

@@ -71,56 +71,30 @@ class StreamMessageActionsModal extends StatelessWidget {
7171
Widget build(BuildContext context) {
7272
final theme = StreamChatTheme.of(context);
7373

74-
final Widget? reactionPicker = switch (showReactionPicker) {
75-
false => null,
76-
true => LayoutBuilder(
77-
builder: (context, constraints) {
78-
final orientation = MediaQuery.of(context).orientation;
79-
final messageTheme = theme.getMessageTheme(reverse: reverse);
80-
final messageFontSize = messageTheme.messageTextStyle?.fontSize;
81-
82-
final alignment = message.calculateReactionPickerAlignment(
83-
constraints: constraints,
84-
fontSize: messageFontSize,
85-
orientation: orientation,
86-
reverse: reverse,
87-
);
88-
89-
final onReactionPicked = switch (onActionTap) {
90-
null => null,
91-
final onActionTap => (reaction) {
92-
return onActionTap.call(
93-
SelectReaction(message: message, reaction: reaction),
94-
);
95-
},
96-
};
97-
98-
return Align(
99-
alignment: alignment,
100-
child: reactionPickerBuilder(context, message, onReactionPicked),
101-
);
102-
},
103-
),
104-
};
105-
10674
final alignment = switch (reverse) {
10775
true => AlignmentDirectional.centerEnd,
10876
false => AlignmentDirectional.centerStart,
10977
};
11078

79+
final onReactionPicked = switch (onActionTap) {
80+
null => null,
81+
final onActionTap => (reaction) => onActionTap(
82+
SelectReaction(message: message, reaction: reaction),
83+
),
84+
};
85+
11186
return StreamMessageModal(
87+
spacing: 4,
11288
alignment: alignment,
113-
headerBuilder: (context) {
114-
return Column(
115-
spacing: 10,
116-
mainAxisSize: MainAxisSize.min,
117-
crossAxisAlignment: alignment.toColumnCrossAxisAlignment(),
118-
children: <Widget?>[
119-
reactionPicker,
120-
IgnorePointer(child: messageWidget),
121-
].nonNulls.toList(growable: false),
122-
);
123-
},
89+
headerBuilder: (context) => ReactionPickerBubbleOverlay(
90+
message: message,
91+
reverse: reverse,
92+
visible: showReactionPicker,
93+
anchorOffset: const Offset(0, -8),
94+
onReactionPicked: onReactionPicked,
95+
reactionPickerBuilder: reactionPickerBuilder,
96+
child: IgnorePointer(child: messageWidget),
97+
),
12498
contentBuilder: (context) {
12599
final actions = Column(
126100
mainAxisSize: MainAxisSize.min,

packages/stream_chat_flutter/lib/src/message_modal/message_reactions_modal.dart

Lines changed: 20 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import 'package:flutter/material.dart';
22
import 'package:stream_chat_flutter/src/misc/empty_widget.dart';
3-
import 'package:stream_chat_flutter/src/reactions/reactions_align.dart';
3+
import 'package:stream_chat_flutter/src/reactions/picker/reaction_picker_bubble_overlay.dart';
44
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
55

66
/// {@template streamMessageReactionsModal}
@@ -64,58 +64,32 @@ class StreamMessageReactionsModal extends StatelessWidget {
6464

6565
@override
6666
Widget build(BuildContext context) {
67-
final theme = StreamChatTheme.of(context);
68-
final messageTheme = theme.getMessageTheme(reverse: reverse);
69-
70-
final Widget? reactionPicker = switch (showReactionPicker) {
71-
false => null,
72-
true => LayoutBuilder(
73-
builder: (context, constraints) {
74-
final orientation = MediaQuery.of(context).orientation;
75-
final messageFontSize = messageTheme.messageTextStyle?.fontSize;
76-
77-
final alignment = message.calculateReactionPickerAlignment(
78-
constraints: constraints,
79-
fontSize: messageFontSize,
80-
orientation: orientation,
81-
reverse: reverse,
82-
);
83-
84-
final onReactionPicked = switch (this.onReactionPicked) {
85-
null => null,
86-
final onPicked => (reaction) {
87-
return onPicked.call(
88-
SelectReaction(message: message, reaction: reaction),
89-
);
90-
},
91-
};
92-
93-
return Align(
94-
alignment: alignment,
95-
child: reactionPickerBuilder(context, message, onReactionPicked),
96-
);
97-
},
98-
),
99-
};
100-
10167
final alignment = switch (reverse) {
10268
true => AlignmentDirectional.centerEnd,
10369
false => AlignmentDirectional.centerStart,
10470
};
10571

72+
final onReactionPicked = switch (this.onReactionPicked) {
73+
null => null,
74+
final onPicked => (reaction) {
75+
return onPicked.call(
76+
SelectReaction(message: message, reaction: reaction),
77+
);
78+
},
79+
};
80+
10681
return StreamMessageModal(
82+
spacing: 4,
10783
alignment: alignment,
108-
headerBuilder: (context) {
109-
return Column(
110-
spacing: 10,
111-
mainAxisSize: MainAxisSize.min,
112-
crossAxisAlignment: alignment.toColumnCrossAxisAlignment(),
113-
children: <Widget?>[
114-
reactionPicker,
115-
IgnorePointer(child: messageWidget),
116-
].nonNulls.toList(growable: false),
117-
);
118-
},
84+
headerBuilder: (context) => ReactionPickerBubbleOverlay(
85+
message: message,
86+
reverse: reverse,
87+
visible: showReactionPicker,
88+
anchorOffset: const Offset(0, -8),
89+
onReactionPicked: onReactionPicked,
90+
reactionPickerBuilder: reactionPickerBuilder,
91+
child: IgnorePointer(child: messageWidget),
92+
),
11993
contentBuilder: (context) {
12094
final reactions = message.latestReactions;
12195
final hasReactions = reactions != null && reactions.isNotEmpty;

packages/stream_chat_flutter/lib/src/message_widget/message_card.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,9 @@ class _MessageCardState extends State<MessageCard> {
164164

165165
return Container(
166166
constraints: const BoxConstraints().copyWith(maxWidth: widthLimit),
167-
margin: EdgeInsets.symmetric(
168-
horizontal: (widget.isFailedState ? 12.0 : 0.0) +
169-
(widget.showUserAvatar == DisplayWidget.gone ? 0 : 4.0),
167+
margin: EdgeInsetsDirectional.only(
168+
end: widget.reverse && widget.isFailedState ? 12.0 : 0.0,
169+
start: !widget.reverse && widget.isFailedState ? 12.0 : 0.0,
170170
),
171171
clipBehavior: Clip.hardEdge,
172172
decoration: ShapeDecoration(

0 commit comments

Comments
 (0)