diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index bf30eec8..931a42fd 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -14,6 +14,10 @@ - Updated dependencies to resolvable versions. +🚀 Improved +- +- Improved draw of reaction options. [#1455](https://github.com/GetStream/stream-chat-flutter/pull/1455) + ## 5.3.0 🔄 Changed diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal/message_actions_modal.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal/message_actions_modal.dart index 8a5d4740..2ccb0bc2 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal/message_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal/message_actions_modal.dart @@ -2,6 +2,7 @@ import 'dart:ui'; import 'package:flutter/material.dart' hide ButtonStyle; import 'package:stream_chat_flutter/src/message_actions_modal/mam_widgets.dart'; +import 'package:stream_chat_flutter/src/message_widget/reactions/reactions_align.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// {@template messageActionsModal} @@ -97,34 +98,12 @@ class _MessageActionsModalState extends State { Widget _showMessageOptionsModal() { final mediaQueryData = MediaQuery.of(context); - final size = mediaQueryData.size; final user = StreamChat.of(context).currentUser; + final orientation = mediaQueryData.orientation; - final roughMaxSize = size.width * 2 / 3; - var messageTextLength = widget.message.text!.length; - if (widget.message.quotedMessage != null) { - var quotedMessageLength = - (widget.message.quotedMessage!.text?.length ?? 0) + 40; - if (widget.message.quotedMessage!.attachments.isNotEmpty) { - quotedMessageLength += 40; - } - if (quotedMessageLength > messageTextLength) { - messageTextLength = quotedMessageLength; - } - } - final roughSentenceSize = messageTextLength * - (widget.messageTheme.messageTextStyle?.fontSize ?? 1) * - 1.2; - final divFactor = widget.message.attachments.isNotEmpty - ? 1 - : (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize)); - + final fontSize = widget.messageTheme.messageTextStyle?.fontSize; final streamChatThemeData = StreamChatTheme.of(context); - final numberOfReactions = - StreamChatConfiguration.of(context).reactionIcons.length; - final shiftFactor = - numberOfReactions < 5 ? (5 - numberOfReactions) * 0.1 : 0.0; final channel = StreamChannel.of(context).channel; final child = Center( @@ -142,11 +121,12 @@ class _MessageActionsModalState extends State { builder: (context, constraints) { return Align( alignment: Alignment( - _calculateReactionsHorizontalAlignmentValue( + calculateReactionsHorizontalAlignment( user, - divFactor, - shiftFactor, + widget.message, constraints, + fontSize, + orientation, ), 0, ), @@ -156,7 +136,7 @@ class _MessageActionsModalState extends State { ); }, ), - const SizedBox(height: 8), + const SizedBox(height: 10), IgnorePointer( child: widget.messageWidget, ), @@ -281,93 +261,6 @@ class _MessageActionsModalState extends State { ); } - double _calculateReactionsHorizontalAlignmentValue( - User? user, - num divFactor, - double shiftFactor, - BoxConstraints constraints, - ) { - var result = 0.0; - var cont = true; - if (user?.id == widget.message.user?.id) { - if (divFactor >= 1.0) { - // This calculation is hacky and does not cover all bases!!! - // A better option is needed! - - // Landscape calculations - if (constraints.maxWidth == 1350) { - // 12.7 iPad Pro - result = shiftFactor + 0.5; - cont = false; - } else if (constraints.maxWidth == 1178) { - // 11 inch iPad Pro - result = shiftFactor + 0.42; - cont = false; - } else if (constraints.maxWidth == 1164) { - // iPad Air 4 - result = shiftFactor + 0.4; - cont = false; - } else if (constraints.maxWidth == 1117) { - // iPad Mini 6 - result = shiftFactor + 0.37; - cont = false; - } else if (constraints.maxWidth == 1064) { - // iPad 9th gen - result = shiftFactor + 0.33; - cont = false; - } else if (constraints.maxWidth == 1008) { - // 9.7 inch iPad Pro - result = shiftFactor + 0.3; - cont = false; - } else if (constraints.maxWidth >= 200 && constraints.maxWidth <= 400) { - // Phone (?) - result = shiftFactor - 0.2; - cont = false; - } - - if (cont) { - // Portrait calculations - if (constraints.maxWidth == 1008) { - // 12.7 iPad Pro - result = shiftFactor + 0.3; - } else if (constraints.maxWidth == 818) { - // 11 inch iPad Pro - result = shiftFactor + 0.07; - } else if (constraints.maxWidth == 804) { - // iPad Air 4 - result = shiftFactor + 0.04; - } else if (constraints.maxWidth == 794) { - // iPad 9th gen - result = shiftFactor + 0.02; - } else if (constraints.maxWidth >= 752) { - // 9.7 inch iPad Pro - result = shiftFactor - 0.05; - } else if (constraints.maxWidth == 728) { - // iPad Mini 6 - result = shiftFactor - 0.1; - } - } - } else { - result = 1.2 - divFactor; - } - } else { - if (divFactor >= 1.0) { - result = shiftFactor + 0.2; - } else { - result = -(1.2 - divFactor); - } - } - - // Ensure reactions don't get pushed past the edge of the screen. - // - // Hacky!!! Needs improvement!!! - if (result > 1) { - return 1; - } else { - return result; - } - } - InkWell _buildCustomAction( BuildContext context, StreamMessageAction messageAction, diff --git a/packages/stream_chat_flutter/lib/src/message_widget/reactions/message_reactions_modal.dart b/packages/stream_chat_flutter/lib/src/message_widget/reactions/message_reactions_modal.dart index 281a37cc..0703ce19 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/reactions/message_reactions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/reactions/message_reactions_modal.dart @@ -2,6 +2,7 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/message_widget/reactions/reaction_bubble.dart'; +import 'package:stream_chat_flutter/src/message_widget/reactions/reactions_align.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// {@template streamMessageReactionsModal} @@ -39,35 +40,13 @@ class StreamMessageReactionsModal extends StatelessWidget { @override Widget build(BuildContext context) { - final size = MediaQuery.of(context).size; final user = StreamChat.of(context).currentUser; final _userPermissions = StreamChannel.of(context).channel.ownCapabilities; + final orientation = MediaQuery.of(context).orientation; final hasReactionPermission = _userPermissions.contains(PermissionType.sendReaction); - - final roughMaxSize = size.width * 2 / 3; - var messageTextLength = message.text!.length; - if (message.quotedMessage != null) { - var quotedMessageLength = message.quotedMessage!.text!.length + 40; - if (message.quotedMessage!.attachments.isNotEmpty) { - quotedMessageLength += 40; - } - if (quotedMessageLength > messageTextLength) { - messageTextLength = quotedMessageLength; - } - } - final roughSentenceSize = messageTextLength * - (messageTheme.messageTextStyle?.fontSize ?? 1) * - 1.2; - final divFactor = message.attachments.isNotEmpty - ? 1 - : (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize)); - - final numberOfReactions = - StreamChatConfiguration.of(context).reactionIcons.length; - final shiftFactor = - numberOfReactions < 5 ? (5 - numberOfReactions) * 0.1 : 0.0; + final fontSize = messageTheme.messageTextStyle?.fontSize; final child = Center( child: SingleChildScrollView( @@ -79,22 +58,26 @@ class StreamMessageReactionsModal extends StatelessWidget { children: [ if ((showReactions ?? hasReactionPermission) && (message.status == MessageSendingStatus.sent)) - Align( - alignment: Alignment( - user!.id == message.user!.id - ? (divFactor >= 1.0 - ? -0.2 - shiftFactor - : (1.2 - divFactor)) - : (divFactor >= 1.0 - ? shiftFactor + 0.2 - : -(1.2 - divFactor)), - 0, - ), - child: StreamReactionPicker( - message: message, - ), + LayoutBuilder( + builder: (context, constraints) { + return Align( + alignment: Alignment( + calculateReactionsHorizontalAlignment( + user, + message, + constraints, + fontSize, + orientation, + ), + 0, + ), + child: StreamReactionPicker( + message: message, + ), + ); + }, ), - const SizedBox(height: 8), + const SizedBox(height: 10), IgnorePointer( child: messageWidget, ), diff --git a/packages/stream_chat_flutter/lib/src/message_widget/reactions/reactions_align.dart b/packages/stream_chat_flutter/lib/src/message_widget/reactions/reactions_align.dart new file mode 100644 index 00000000..e4e1ad48 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_widget/reactions/reactions_align.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// This method calculates the align that the modal of reactions should have. +/// This is an approximation based on the size of the message and the +/// available space in the screen. +double calculateReactionsHorizontalAlignment( + User? user, + Message message, + BoxConstraints constraints, + double? fontSize, + Orientation orientation, +) { + final maxWidth = constraints.maxWidth; + + final roughSentenceSize = message.roughMessageSize(fontSize); + final hasAttachments = message.attachments.isNotEmpty; + final isReply = message.quotedMessageId != null; + final isAttachment = hasAttachments && !isReply; + + // divFactor is the percentage of the available space that the message takes. + // When the divFactor is bigger than 0.5 that means that the messages is + // bigger than 50% of the available space and the modal should have an offset + // in the direction that the message grows. When the divFactor is smaller + // than 0.5 then the offset should be to he side opposite of the message + // growth. + // In resume, when divFactor > 0.5 then result > 0, when divFactor < 0.5 + // then result < 0. + var divFactor = 0.5; + + // When in portrait, attachments normally take 75% of the screen, when in + // landscape, attachments normally take 50% of the screen. + if (isAttachment) { + if (orientation == Orientation.portrait) { + divFactor = 0.75; + } else { + divFactor = 0.5; + } + } else { + divFactor = roughSentenceSize == 0 ? 0.5 : (roughSentenceSize / maxWidth); + } + + final signal = user?.id == message.user?.id ? 1 : -1; + final result = signal * (1 - divFactor * 2.0); + + return _capResult(result); +} + +// Ensure reactions don't get pushed past the edge of the screen. +// +// This happens if divFactor is really big. When this happens, we can simply +// move the model all the way to the end of screen. +double _capResult(double result) { + if (result > 1.0) { + return 1; + } else if (result < -1.0) { + return -1; + } else { + return result; + } +} diff --git a/packages/stream_chat_flutter/lib/src/utils/extensions.dart b/packages/stream_chat_flutter/lib/src/utils/extensions.dart index 448674dd..67b622db 100644 --- a/packages/stream_chat_flutter/lib/src/utils/extensions.dart +++ b/packages/stream_chat_flutter/lib/src/utils/extensions.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:diacritic/diacritic.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/foundation.dart'; @@ -12,6 +14,16 @@ extension StringExtension on String { String capitalize() => isNotEmpty ? '${this[0].toUpperCase()}${substring(1).toLowerCase()}' : ''; + /// Returns the biggest line of a text. + String biggestLine() { + if (contains('\n')) { + return split('\n') + .reduce((curr, next) => curr.length > next.length ? curr : next); + } else { + return this; + } + } + /// Returns whether the string contains only emoji's or not. /// /// Emojis guidelines @@ -353,6 +365,33 @@ extension MessageX on Message { return copyWith(text: messageTextToRender); } + /// Returns an approximation of message size + double roughMessageSize(double? fontSize) { + var messageTextLength = min(text!.biggestLine().length, 65); + + if (quotedMessage != null) { + var quotedMessageLength = + (min(quotedMessage!.text?.biggestLine().length ?? 0, 65)) + 8; + + if (quotedMessage!.attachments.isNotEmpty) { + quotedMessageLength += 8; + } + + if (quotedMessageLength > messageTextLength * 1.2) { + messageTextLength = quotedMessageLength; + } + } + + // Quoted message have a smaller font, so it is necessary to reduce the + // size of the multiplier to count for the smaller font. + var multiplier = 0.55; + if (quotedMessage != null) { + multiplier = 0.45; + } + + return messageTextLength * (fontSize ?? 1) * multiplier; + } + /// It returns the message with the translated text if available locally Message translate(String language) => copyWith(text: i18n?['${language}_text'] ?? text);