feat(ui): added support for MessageWidget.onReactionsHover.

Signed-off-by: Sahil Kumar <[email protected]>
This commit is contained in:
Sahil Kumar
2023-09-06 17:57:15 +05:30
parent f3d4fbb275
commit e00664f5a8
8 changed files with 93 additions and 101 deletions
+10 -1
View File
@@ -1,3 +1,11 @@
## Upcoming
✅ Added
- Added support for overriding the `MessageWidget.onReactionsHover` callback.
> **Note**
> Used only in desktop devices (web and desktop).
## 6.9.0 ## 6.9.0
🐞 Fixed 🐞 Fixed
@@ -11,7 +19,8 @@
- Added support for listening error events in AttachmentPickerBottomSheet. - Added support for listening error events in AttachmentPickerBottomSheet.
- Added support for overriding the `MessageWidget.onReactionTap` callback. - Added support for overriding the `MessageWidget.onReactionTap` callback.
- Added support for `StreamMessageInput.contentInsertionConfiguration` to specify the content insertion configuration. - Added support for `StreamMessageInput.contentInsertionConfiguration` to specify the content
insertion configuration.
[#1613](https://github.com/GetStream/stream-chat-flutter/issues/1613) [#1613](https://github.com/GetStream/stream-chat-flutter/issues/1613)
```dart ```dart
@@ -159,7 +159,6 @@
C4CD72858CD59598795BB48E /* Pods-Runner.release.xcconfig */, C4CD72858CD59598795BB48E /* Pods-Runner.release.xcconfig */,
2BCA7399119839DE435DACD6 /* Pods-Runner.profile.xcconfig */, 2BCA7399119839DE435DACD6 /* Pods-Runner.profile.xcconfig */,
); );
name = Pods;
path = Pods; path = Pods;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@@ -203,7 +202,7 @@
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
LastSwiftUpdateCheck = 0920; LastSwiftUpdateCheck = 0920;
LastUpgradeCheck = 1300; LastUpgradeCheck = 1430;
ORGANIZATIONNAME = ""; ORGANIZATIONNAME = "";
TargetAttributes = { TargetAttributes = {
33CC10EC2044A3C60003C045 = { 33CC10EC2044A3C60003C045 = {
@@ -427,7 +426,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 10.15; MACOSX_DEPLOYMENT_TARGET = 11.0;
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
}; };
@@ -554,7 +553,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 10.15; MACOSX_DEPLOYMENT_TARGET = 11.0;
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@@ -575,7 +574,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 10.15; MACOSX_DEPLOYMENT_TARGET = 11.0;
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
}; };
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1300" LastUpgradeVersion = "1430"
version = "1.3"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
@@ -56,6 +56,7 @@ class StreamMessageWidget extends StatefulWidget {
this.onMentionTap, this.onMentionTap,
this.onMessageTap, this.onMessageTap,
this.onReactionsTap, this.onReactionsTap,
this.onReactionsHover,
bool? showReactionPicker, bool? showReactionPicker,
@Deprecated('Use `showReactionPicker` instead') @Deprecated('Use `showReactionPicker` instead')
bool showReactionPickerIndicator = true, bool showReactionPickerIndicator = true,
@@ -474,7 +475,8 @@ class StreamMessageWidget extends StatefulWidget {
/// {@template showReactionPickerIndicator} /// {@template showReactionPickerIndicator}
/// Used in [StreamMessageReactionsModal] and [MessageActionsModal] /// Used in [StreamMessageReactionsModal] and [MessageActionsModal]
/// {@endtemplate} @Deprecated('Use `showReactionPicker` instead') /// {@endtemplate}
@Deprecated('Use `showReactionPicker` instead')
bool get showReactionPickerIndicator => showReactionPicker; bool get showReactionPickerIndicator => showReactionPicker;
/// {@template showReactionPickerTail} /// {@template showReactionPickerTail}
@@ -565,8 +567,16 @@ class StreamMessageWidget extends StatefulWidget {
final void Function(Message)? onMessageTap; final void Function(Message)? onMessageTap;
/// {@macro onReactionsTap} /// {@macro onReactionsTap}
///
/// Note: Only used in mobile devices (iOS and Android). Do not confuse this
/// with the tap action on the reactions picker.
final OnReactionsTap? onReactionsTap; final OnReactionsTap? onReactionsTap;
/// {@template onReactionsHover}
///
/// Note: Only used in desktop devices (web and desktop).
final OnReactionsHover? onReactionsHover;
/// {@template customActions} /// {@template customActions}
/// List of custom actions shown on message long tap /// List of custom actions shown on message long tap
/// {@endtemplate} /// {@endtemplate}
@@ -640,6 +650,7 @@ class StreamMessageWidget extends StatefulWidget {
bool? showInChannelIndicator, bool? showInChannelIndicator,
void Function(User)? onUserAvatarTap, void Function(User)? onUserAvatarTap,
void Function(String)? onLinkTap, void Function(String)? onLinkTap,
bool? showReactionBrowser,
bool? showReactionPicker, bool? showReactionPicker,
@Deprecated('Use `showReactionPicker` instead') @Deprecated('Use `showReactionPicker` instead')
bool? showReactionPickerIndicator, bool? showReactionPickerIndicator,
@@ -662,6 +673,7 @@ class StreamMessageWidget extends StatefulWidget {
OnQuotedMessageTap? onQuotedMessageTap, OnQuotedMessageTap? onQuotedMessageTap,
void Function(Message)? onMessageTap, void Function(Message)? onMessageTap,
OnReactionsTap? onReactionsTap, OnReactionsTap? onReactionsTap,
OnReactionsHover? onReactionsHover,
List<StreamMessageAction>? customActions, List<StreamMessageAction>? customActions,
void Function(Message message, Attachment attachment)? onAttachmentTap, void Function(Message message, Attachment attachment)? onAttachmentTap,
Widget Function(BuildContext, User)? userAvatarBuilder, Widget Function(BuildContext, User)? userAvatarBuilder,
@@ -752,6 +764,7 @@ class StreamMessageWidget extends StatefulWidget {
onQuotedMessageTap: onQuotedMessageTap ?? this.onQuotedMessageTap, onQuotedMessageTap: onQuotedMessageTap ?? this.onQuotedMessageTap,
onMessageTap: onMessageTap ?? this.onMessageTap, onMessageTap: onMessageTap ?? this.onMessageTap,
onReactionsTap: onReactionsTap ?? this.onReactionsTap, onReactionsTap: onReactionsTap ?? this.onReactionsTap,
onReactionsHover: onReactionsHover ?? this.onReactionsHover,
customActions: customActions ?? this.customActions, customActions: customActions ?? this.customActions,
onAttachmentTap: onAttachmentTap ?? this.onAttachmentTap, onAttachmentTap: onAttachmentTap ?? this.onAttachmentTap,
userAvatarBuilder: userAvatarBuilder ?? this.userAvatarBuilder, userAvatarBuilder: userAvatarBuilder ?? this.userAvatarBuilder,
@@ -978,7 +991,6 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
reverse: widget.reverse, reverse: widget.reverse,
message: widget.message, message: widget.message,
hasNonUrlAttachments: hasNonUrlAttachments, hasNonUrlAttachments: hasNonUrlAttachments,
shouldShowReactions: shouldShowReactions,
hasQuotedMessage: hasQuotedMessage, hasQuotedMessage: hasQuotedMessage,
textPadding: widget.textPadding, textPadding: widget.textPadding,
attachmentBuilders: widget.attachmentBuilders, attachmentBuilders: widget.attachmentBuilders,
@@ -997,6 +1009,7 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
? widget.onReactionsTap!(widget.message) ? widget.onReactionsTap!(widget.message)
: _showMessageReactionsModal(context); : _showMessageReactionsModal(context);
}, },
onReactionsHover: widget.onReactionsHover,
showUserAvatar: widget.showUserAvatar, showUserAvatar: widget.showUserAvatar,
streamChat: _streamChat, streamChat: _streamChat,
translateUserAvatar: widget.translateUserAvatar, translateUserAvatar: widget.translateUserAvatar,
@@ -1258,8 +1271,9 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
translateUserAvatar: false, translateUserAvatar: false,
showSendingIndicator: false, showSendingIndicator: false,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
// Show both the tail and indicator if the indicator is shown. // Show both the tail if the picker is shown.
showReactionPickerTail: widget.showReactionPickerIndicator, showReactionPicker: widget.showReactionPicker,
showReactionPickerTail: widget.showReactionPicker,
showPinHighlight: false, showPinHighlight: false,
showUserAvatar: widget.message.user!.id == showUserAvatar: widget.message.user!.id ==
channel.client.state.currentUser!.id channel.client.state.currentUser!.id
@@ -1281,7 +1295,6 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
showResendMessage: shouldShowResendAction, showResendMessage: shouldShowResendAction,
showCopyMessage: shouldShowCopyAction, showCopyMessage: shouldShowCopyAction,
showEditMessage: shouldShowEditAction, showEditMessage: shouldShowEditAction,
showReactionPicker: widget.showReactionPickerIndicator,
showReplyMessage: shouldShowReplyAction, showReplyMessage: shouldShowReplyAction,
showThreadReplyMessage: shouldShowThreadReplyAction, showThreadReplyMessage: shouldShowThreadReplyAction,
showFlagButton: widget.showFlagButton, showFlagButton: widget.showFlagButton,
@@ -36,8 +36,8 @@ class MessageWidgetContent extends StatelessWidget {
required this.avatarWidth, required this.avatarWidth,
required this.showReactions, required this.showReactions,
required this.onReactionsTap, required this.onReactionsTap,
required this.onReactionsHover,
required this.messageTheme, required this.messageTheme,
required this.shouldShowReactions,
required this.streamChatTheme, required this.streamChatTheme,
required this.isFailedState, required this.isFailedState,
required this.hasQuotedMessage, required this.hasQuotedMessage,
@@ -114,17 +114,15 @@ class MessageWidgetContent extends StatelessWidget {
/// {@macro showReactions} /// {@macro showReactions}
final bool showReactions; final bool showReactions;
/// Callback called when the reactions icon is tapped. /// {@macro onReactionsTap}
///
/// Do not confuse this with the tap action on the reactions picker.
final VoidCallback onReactionsTap; final VoidCallback onReactionsTap;
/// {@macro onReactionsHover}
final OnReactionsHover? onReactionsHover;
/// {@macro messageTheme} /// {@macro messageTheme}
final StreamMessageThemeData messageTheme; final StreamMessageThemeData messageTheme;
/// {@macro shouldShowReactions}
final bool shouldShowReactions;
/// {@macro onUserAvatarTap} /// {@macro onUserAvatarTap}
final void Function(User)? onUserAvatarTap; final void Function(User)? onUserAvatarTap;
@@ -295,7 +293,6 @@ class MessageWidgetContent extends StatelessWidget {
messageTheme: messageTheme, messageTheme: messageTheme,
ownId: streamChat.currentUser!.id, ownId: streamChat.currentUser!.id,
reverse: reverse, reverse: reverse,
shouldShowReactions: shouldShowReactions,
onTap: onReactionsTap, onTap: onReactionsTap,
) )
: null, : null,
@@ -314,25 +311,15 @@ class MessageWidgetContent extends StatelessWidget {
children: [ children: [
Padding( Padding(
padding: showReactions padding: showReactions
? EdgeInsets.only( ? const EdgeInsets.only(top: 18)
top: message.reactionCounts
?.isNotEmpty ==
true
? 18
: 0,
)
: EdgeInsets.zero, : EdgeInsets.zero,
child: (message.isDeleted && !isFailedState) child: (message.isDeleted && !isFailedState)
? Container( ? Container(
// ignore: lines_longer_than_80_chars
margin: EdgeInsets.symmetric( margin: EdgeInsets.symmetric(
horizontal: horizontal: showUserAvatar ==
// ignore: lines_longer_than_80_chars DisplayWidget.gone
showUserAvatar == ? 0
// ignore: lines_longer_than_80_chars : 4.0,
DisplayWidget.gone
? 0
: 4.0,
), ),
child: StreamDeletedMessage( child: StreamDeletedMessage(
borderRadiusGeometry: borderRadiusGeometry:
@@ -405,7 +392,7 @@ class MessageWidgetContent extends StatelessWidget {
SizedBox(width: avatarWidth + 4), SizedBox(width: avatarWidth + 4),
], ],
), ),
if (isDesktopDeviceOrWeb && shouldShowReactions) ...[ if (isDesktopDeviceOrWeb && showReactions) ...[
Padding( Padding(
padding: showUserAvatar != DisplayWidget.gone padding: showUserAvatar != DisplayWidget.gone
? EdgeInsets.only( ? EdgeInsets.only(
@@ -416,7 +403,7 @@ class MessageWidgetContent extends StatelessWidget {
child: DesktopReactionsBuilder( child: DesktopReactionsBuilder(
message: message, message: message,
messageTheme: messageTheme, messageTheme: messageTheme,
shouldShowReactions: shouldShowReactions, onHover: onReactionsHover,
borderSide: borderSide, borderSide: borderSide,
reverse: reverse, reverse: reverse,
), ),
@@ -1,3 +1,5 @@
// ignore_for_file: cascade_invocations
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -5,8 +7,6 @@ import 'package:flutter_portal/flutter_portal.dart';
import 'package:stream_chat_flutter/src/message_widget/reactions/reactions_card.dart'; import 'package:stream_chat_flutter/src/message_widget/reactions/reactions_card.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
// ignore_for_file: cascade_invocations
/// {@template desktopReactionsBuilder} /// {@template desktopReactionsBuilder}
/// Builds a list of reactions to a message on desktop & web. /// Builds a list of reactions to a message on desktop & web.
/// ///
@@ -16,16 +16,13 @@ class DesktopReactionsBuilder extends StatefulWidget {
/// {@macro desktopReactionsBuilder} /// {@macro desktopReactionsBuilder}
const DesktopReactionsBuilder({ const DesktopReactionsBuilder({
super.key, super.key,
required this.shouldShowReactions,
required this.message, required this.message,
required this.messageTheme, required this.messageTheme,
this.onHover,
this.borderSide, this.borderSide,
required this.reverse, required this.reverse,
}); });
/// Whether reactions should be shown.
final bool shouldShowReactions;
/// The message to show reactions for. /// The message to show reactions for.
final Message message; final Message message;
@@ -35,6 +32,9 @@ class DesktopReactionsBuilder extends StatefulWidget {
/// reactions matches the design spec for messages. /// reactions matches the design spec for messages.
final StreamMessageThemeData messageTheme; final StreamMessageThemeData messageTheme;
/// Callback to run when the mouse enters or exits the reactions.
final OnReactionsHover? onHover;
/// {@macro borderSide} /// {@macro borderSide}
final BorderSide? borderSide; final BorderSide? borderSide;
@@ -48,12 +48,6 @@ class DesktopReactionsBuilder extends StatefulWidget {
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties); super.debugFillProperties(properties);
properties.add(
DiagnosticsProperty<bool>(
'shouldShowReactions',
shouldShowReactions,
),
);
properties.add( properties.add(
DiagnosticsProperty<Message>('message', message), DiagnosticsProperty<Message>('message', message),
); );
@@ -81,18 +75,15 @@ class _DesktopReactionsBuilderState extends State<DesktopReactionsBuilder> {
final streamChatTheme = StreamChatTheme.of(context); final streamChatTheme = StreamChatTheme.of(context);
final reactionsMap = <String, Reaction>{}; final reactionsMap = <String, Reaction>{};
var reactionsList = <Reaction>[]; widget.message.latestReactions?.forEach((element) {
if (widget.shouldShowReactions) { if (!reactionsMap.containsKey(element.type) ||
widget.message.latestReactions?.forEach((element) { element.user!.id == currentUser.id) {
if (!reactionsMap.containsKey(element.type) || reactionsMap[element.type] = element;
element.user!.id == currentUser.id) { }
reactionsMap[element.type] = element; });
}
});
reactionsList = reactionsMap.values.toList() final reactionsList = reactionsMap.values.toList()
..sort((a, b) => a.user!.id == currentUser.id ? 1 : -1); ..sort((a, b) => a.user!.id == currentUser.id ? 1 : -1);
}
return PortalTarget( return PortalTarget(
visible: _showReactionsPopup, visible: _showReactionsPopup,
@@ -100,17 +91,11 @@ class _DesktopReactionsBuilderState extends State<DesktopReactionsBuilder> {
anchor: Aligned( anchor: Aligned(
target: widget.reverse ? Alignment.topRight : Alignment.topLeft, target: widget.reverse ? Alignment.topRight : Alignment.topLeft,
follower: widget.reverse ? Alignment.bottomRight : Alignment.bottomLeft, follower: widget.reverse ? Alignment.bottomRight : Alignment.bottomLeft,
shiftToWithinBound: const AxisFlag( shiftToWithinBound: const AxisFlag(y: true),
y: true,
),
), ),
portalFollower: MouseRegion( portalFollower: MouseRegion(
onEnter: (event) async { onEnter: (_) => _onReactionsHover(true),
setState(() => _showReactionsPopup = !_showReactionsPopup); onExit: (_) => _onReactionsHover(false),
},
onExit: (event) {
setState(() => _showReactionsPopup = !_showReactionsPopup);
},
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints( constraints: const BoxConstraints(
maxWidth: 336, maxWidth: 336,
@@ -125,12 +110,8 @@ class _DesktopReactionsBuilderState extends State<DesktopReactionsBuilder> {
), ),
child: MouseRegion( child: MouseRegion(
cursor: SystemMouseCursors.click, cursor: SystemMouseCursors.click,
onEnter: (event) async { onEnter: (_) => _onReactionsHover(true),
setState(() => _showReactionsPopup = !_showReactionsPopup); onExit: (_) => _onReactionsHover(false),
},
onExit: (event) {
setState(() => _showReactionsPopup = !_showReactionsPopup);
},
child: Padding( child: Padding(
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
vertical: 2, vertical: 2,
@@ -161,6 +142,14 @@ class _DesktopReactionsBuilderState extends State<DesktopReactionsBuilder> {
), ),
); );
} }
void _onReactionsHover(bool isHovering) {
if (widget.onHover != null) {
return widget.onHover!(isHovering);
}
setState(() => _showReactionsPopup = isHovering);
}
} }
class _BottomReaction extends StatelessWidget { class _BottomReaction extends StatelessWidget {
@@ -13,7 +13,6 @@ class ReactionIndicator extends StatelessWidget {
super.key, super.key,
required this.ownId, required this.ownId,
required this.message, required this.message,
required this.shouldShowReactions,
required this.onTap, required this.onTap,
required this.reverse, required this.reverse,
required this.messageTheme, required this.messageTheme,
@@ -25,9 +24,6 @@ class ReactionIndicator extends StatelessWidget {
/// {@macro message} /// {@macro message}
final Message message; final Message message;
/// {@macro shouldShowReactions}
final bool shouldShowReactions;
/// The callback to perform when the widget is tapped or clicked. /// The callback to perform when the widget is tapped or clicked.
final VoidCallback onTap; final VoidCallback onTap;
@@ -50,34 +46,27 @@ class ReactionIndicator extends StatelessWidget {
..sort((a, b) => a.user!.id == ownId ? 1 : -1); ..sort((a, b) => a.user!.id == ownId ? 1 : -1);
return Transform( return Transform(
transform: Matrix4.translationValues( transform: Matrix4.translationValues(reverse ? 12 : -12, 0, 0),
reverse ? 12 : -12,
0,
0,
),
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints( constraints: const BoxConstraints(
maxWidth: 22 * 6.0, maxWidth: 22 * 6.0,
), ),
child: AnimatedSwitcher( child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
child: shouldShowReactions child: GestureDetector(
? GestureDetector( onTap: onTap,
onTap: onTap, child: StreamReactionBubble(
child: StreamReactionBubble( key: ValueKey('${message.id}.reactions'),
key: ValueKey('${message.id}.reactions'), reverse: reverse,
reverse: reverse, flipTail: reverse,
flipTail: reverse, backgroundColor:
backgroundColor: messageTheme.reactionsBackgroundColor ?? messageTheme.reactionsBackgroundColor ?? Colors.transparent,
Colors.transparent, borderColor:
borderColor: messageTheme.reactionsBorderColor ?? Colors.transparent,
messageTheme.reactionsBorderColor ?? Colors.transparent, maskColor: messageTheme.reactionsMaskColor ?? Colors.transparent,
maskColor: reactions: reactionsList,
messageTheme.reactionsMaskColor ?? Colors.transparent, ),
reactions: reactionsList, ),
),
)
: const SizedBox(),
), ),
), ),
); );
@@ -235,6 +235,12 @@ typedef OnMessageTap = void Function(Message);
/// {@endtemplate} /// {@endtemplate}
typedef OnReactionsTap = void Function(Message); typedef OnReactionsTap = void Function(Message);
/// {@template onReactionsHover}
/// The action to perform when a message's reactions are hovered.
/// {@endtemplate}
// ignore: avoid_positional_boolean_parameters
typedef OnReactionsHover = void Function(bool isHovering);
/// {@template messageSearchItemTapCallback} /// {@template messageSearchItemTapCallback}
/// The action to perform when tapping or clicking on a user in a /// The action to perform when tapping or clicking on a user in a
// ignore: deprecated_member_use_from_same_package // ignore: deprecated_member_use_from_same_package