Merge pull request #1638 from GetStream/fix/remove-emoji-web

This commit is contained in:
Sahil Kumar
2023-06-27 18:33:10 +05:30
committed by GitHub
11 changed files with 365 additions and 133 deletions
+10
View File
@@ -9,6 +9,8 @@
- [[#1069]](https://github.com/GetStream/stream-chat-flutter/issues/1069) Fixed message swipe to reply using same
direction for both current user and other users. It now uses `SwipeDirection.startToEnd` for current user
and `SwipeDirection.endToStart` for other users.
- [[#1590]](https://github.com/GetStream/stream-chat-flutter/issues/1590)
Fixed `StreamMessageWidget.showReactionPickerIndicator` not toggling the reaction picker indicator visibility.
✅ Added
@@ -77,6 +79,14 @@
},
)
```
- Deprecated `StreamMessageWidget.showReactionPickerIndicator` in favor of `StreamMessageWidget.showReactionPicker`.
```diff
StreamMessageWidget(
- showReactionPickerIndicator: true/false,
+ showReactionPicker: true/false,
)
```
## 6.4.0
@@ -204,6 +204,7 @@
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
@@ -356,7 +356,11 @@ class _ChannelPageState extends State<ChannelPage> {
),
);
},
child: defaultWidget.copyWith(onReplyTap: reply),
child: defaultWidget.copyWith(
onReplyTap: reply,
// showReactionPicker: true,
// showReactionPickerIndicator: false,
),
);
},
),
@@ -29,14 +29,16 @@ class StreamAttachmentHandlerDesktop extends StreamAttachmentHandler {
options: options,
);
// Open the native file browser so the user can select the download path.
final path = await getSavePath(suggestedName: data.fileName);
if (path == null) {
// Open the native file browser so the user can select the save location.
final saveLocation = await getSaveLocation(suggestedName: data.fileName);
if (saveLocation == null) {
// Operation was canceled by the user.
return null;
}
// Get the path to the user's selected location.
final path = saveLocation.path;
// Create an XFile for proper file saving.
final file = data.toXFile(path: path);
@@ -15,7 +15,7 @@ class MessageActionsModal extends StatefulWidget {
required this.message,
required this.messageWidget,
required this.messageTheme,
this.showReactions = true,
this.showReactionPicker = true,
this.showDeleteMessage = true,
this.showEditMessage = true,
this.onReplyTap,
@@ -54,8 +54,8 @@ class MessageActionsModal extends StatefulWidget {
/// [StreamMessageThemeData] for message
final StreamMessageThemeData messageTheme;
/// Flag for showing reactions
final bool showReactions;
/// Flag for showing reaction picker.
final bool showReactionPicker;
/// Callback when copy is tapped
final OnMessageTap? onCopyTap;
@@ -105,6 +105,10 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
final user = StreamChat.of(context).currentUser;
final orientation = mediaQueryData.orientation;
final _userPermissions = StreamChannel.of(context).channel.ownCapabilities;
final hasReactionPermission =
_userPermissions.contains(PermissionType.sendReaction);
final fontSize = widget.messageTheme.messageTextStyle?.fontSize;
final streamChatThemeData = StreamChatTheme.of(context);
@@ -115,12 +119,10 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
child: Padding(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: widget.reverse
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
if (widget.showReactions &&
(widget.message.status == MessageSendingStatus.sent))
if (widget.showReactionPicker && hasReactionPermission)
LayoutBuilder(
builder: (context, constraints) {
return Align(
@@ -2,6 +2,7 @@ import 'package:contextmenu/contextmenu.dart';
import 'package:flutter/material.dart' hide ButtonStyle;
import 'package:flutter/services.dart';
import 'package:flutter_portal/flutter_portal.dart';
import 'package:meta/meta.dart';
import 'package:stream_chat_flutter/conditional_parent_builder/conditional_parent_builder.dart';
import 'package:stream_chat_flutter/platform_widget_builder/platform_widget_builder.dart';
import 'package:stream_chat_flutter/src/context_menu_items/context_menu_reaction_picker.dart';
@@ -54,7 +55,10 @@ class StreamMessageWidget extends StatefulWidget {
this.attachmentBorderRadiusGeometry,
this.onMentionTap,
this.onMessageTap,
this.showReactionPickerIndicator = false,
bool? showReactionPicker,
@Deprecated('Use `showReactionPicker` instead')
bool showReactionPickerIndicator = true,
@internal this.showReactionPickerTail = false,
this.showUserAvatar = DisplayWidget.show,
this.showSendingIndicator = true,
this.showThreadReplyIndicator = false,
@@ -114,6 +118,7 @@ class StreamMessageWidget extends StatefulWidget {
bottomRowBuilder == null || bottomRowBuilderWithDefaultWidget == null,
'You can only use one of the two bottom row builders',
),
showReactionPicker = showReactionPicker ?? showReactionPickerIndicator,
attachmentBuilders = {
'image': (context, message, attachments) {
final border = RoundedRectangleBorder(
@@ -460,10 +465,22 @@ class StreamMessageWidget extends StatefulWidget {
/// {@endtemplate}
final void Function(String)? onLinkTap;
/// {@template showReactionPicker}
/// Whether or not to show the reaction picker.
/// Used in [StreamMessageReactionsModal] and [MessageActionsModal].
/// {@endtemplate}
final bool showReactionPicker;
/// {@template showReactionPickerIndicator}
/// Used in [StreamMessageReactionsModal] and [MessageActionsModal]
/// {@endtemplate} @Deprecated('Use `showReactionPicker` instead')
bool get showReactionPickerIndicator => showReactionPicker;
/// {@template showReactionPickerTail}
/// Whether or not to show the reaction picker tail
/// {@endtemplate}
final bool showReactionPickerIndicator;
@internal
final bool showReactionPickerTail;
/// {@template onShowMessage}
/// Callback when show message is tapped
@@ -619,7 +636,10 @@ class StreamMessageWidget extends StatefulWidget {
bool? showInChannelIndicator,
void Function(User)? onUserAvatarTap,
void Function(String)? onLinkTap,
bool? showReactionPicker,
@Deprecated('Use `showReactionPicker` instead')
bool? showReactionPickerIndicator,
@internal bool? showReactionPickerTail,
List<Read>? readList,
ShowMessageCallback? onShowMessage,
bool? showUsername,
@@ -703,8 +723,11 @@ class StreamMessageWidget extends StatefulWidget {
showInChannelIndicator ?? this.showInChannelIndicator,
onUserAvatarTap: onUserAvatarTap ?? this.onUserAvatarTap,
onLinkTap: onLinkTap ?? this.onLinkTap,
showReactionPickerIndicator:
showReactionPickerIndicator ?? this.showReactionPickerIndicator,
showReactionPicker: showReactionPicker ??
showReactionPickerIndicator ??
this.showReactionPicker,
showReactionPickerTail:
showReactionPickerTail ?? this.showReactionPickerTail,
onShowMessage: onShowMessage ?? this.onShowMessage,
showUsername: showUsername ?? this.showUsername,
showTimestamp: showTimestamp ?? this.showTimestamp,
@@ -885,12 +908,12 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
if (!widget.message.isDeleted) {
return ContextMenuArea(
verticalPadding: 0,
builder: (context) => _buildContextMenu(),
builder: (_) => _buildContextMenu(),
child: child,
);
} else {
return child;
}
return child;
},
child: Material(
type: MaterialType.transparency,
@@ -963,9 +986,9 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
messageWidget: widget,
showBottomRow: showBottomRow,
showPinHighlight: widget.showPinHighlight,
showReactionPickerIndicator:
widget.showReactionPickerIndicator,
showReactionPickerTail: widget.showReactionPickerTail,
showReactions: showReactions,
onReactionsTap: () => _showMessageReactionsModal(context),
showUserAvatar: widget.showUserAvatar,
streamChat: _streamChat,
translateUserAvatar: widget.translateUserAvatar,
@@ -996,14 +1019,15 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
final channel = StreamChannel.of(context).channel;
return [
StreamChatContextMenuItem(
child: StreamChannel(
channel: channel,
child: ContextMenuReactionPicker(
message: widget.message,
if (widget.showReactionPicker)
StreamChatContextMenuItem(
child: StreamChannel(
channel: channel,
child: ContextMenuReactionPicker(
message: widget.message,
),
),
),
),
if (shouldShowReplyAction) ...[
StreamChatContextMenuItem(
leading: StreamSvgIcon.reply(),
@@ -1149,21 +1173,7 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
];
}
void onLongPress(BuildContext context) {
if (widget.message.isEphemeral ||
widget.message.status == MessageSendingStatus.sending) {
return;
}
if (widget.onMessageActions != null) {
widget.onMessageActions!(context, widget.message);
} else {
_showMessageActionModalBottomSheet(context);
}
return;
}
void _showMessageActionModalBottomSheet(BuildContext context) {
void _showMessageReactionsModal(BuildContext context) {
final channel = StreamChannel.of(context).channel;
showDialog(
@@ -1173,7 +1183,8 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
barrierColor: _streamChatTheme.colorTheme.overlay,
builder: (context) => StreamChannel(
channel: channel,
child: MessageActionsModal(
child: StreamMessageReactionsModal(
showReactionPicker: widget.showReactionPicker,
messageWidget: widget.copyWith(
key: const Key('MessageWidget'),
message: widget.message.copyWith(
@@ -1187,37 +1198,93 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
translateUserAvatar: false,
showSendingIndicator: false,
padding: EdgeInsets.zero,
showReactionPickerIndicator: widget.showReactions &&
(widget.message.status == MessageSendingStatus.sent),
// Show the tail if the reaction picker is visible.
showReactionPickerTail: widget.showReactionPicker,
showPinHighlight: false,
showUserAvatar:
widget.message.user!.id == channel.client.state.currentUser!.id
? DisplayWidget.gone
: DisplayWidget.show,
),
onCopyTap: (message) {
final text = message.text;
if (text != null) Clipboard.setData(ClipboardData(text: text));
},
onUserAvatarTap: widget.onUserAvatarTap,
messageTheme: widget.messageTheme,
reverse: widget.reverse,
showDeleteMessage: shouldShowDeleteAction,
onConfirmDeleteTap: widget.onConfirmDeleteTap,
message: widget.message,
editMessageInputBuilder: widget.editMessageInputBuilder,
onReplyTap: widget.onReplyTap,
onThreadReplyTap: widget.onThreadTap,
showResendMessage: shouldShowResendAction,
showCopyMessage: shouldShowCopyAction,
showEditMessage: shouldShowEditAction,
showReactions: widget.showReactions,
showReplyMessage: shouldShowReplyAction,
showThreadReplyMessage: shouldShowThreadReplyAction,
showFlagButton: widget.showFlagButton,
showPinButton: widget.showPinButton,
customActions: widget.customActions,
),
),
);
}
void onLongPress(BuildContext context) {
if (widget.message.isEphemeral ||
widget.message.status == MessageSendingStatus.sending) {
return;
}
if (widget.onMessageActions != null) {
return widget.onMessageActions!(context, widget.message);
}
return _showMessageActionModalBottomSheet(context);
}
void _showMessageActionModalBottomSheet(BuildContext context) {
final channel = StreamChannel.of(context).channel;
showDialog(
useRootNavigator: false,
context: context,
useSafeArea: false,
barrierColor: _streamChatTheme.colorTheme.overlay,
builder: (context) {
return StreamChannel(
channel: channel,
child: MessageActionsModal(
messageWidget: widget.copyWith(
key: const Key('MessageWidget'),
message: widget.message.copyWith(
text: (widget.message.text?.length ?? 0) > 200
? '${widget.message.text!.substring(0, 200)}...'
: widget.message.text,
),
showReactions: false,
showUsername: false,
showTimestamp: false,
translateUserAvatar: false,
showSendingIndicator: false,
padding: EdgeInsets.zero,
// Show both the tail and indicator if the indicator is shown.
showReactionPickerTail: widget.showReactionPickerIndicator,
showPinHighlight: false,
showUserAvatar: widget.message.user!.id ==
channel.client.state.currentUser!.id
? DisplayWidget.gone
: DisplayWidget.show,
),
onCopyTap: (message) {
final text = message.text;
if (text != null) Clipboard.setData(ClipboardData(text: text));
},
messageTheme: widget.messageTheme,
reverse: widget.reverse,
showDeleteMessage: shouldShowDeleteAction,
onConfirmDeleteTap: widget.onConfirmDeleteTap,
message: widget.message,
editMessageInputBuilder: widget.editMessageInputBuilder,
onReplyTap: widget.onReplyTap,
onThreadReplyTap: widget.onThreadTap,
showResendMessage: shouldShowResendAction,
showCopyMessage: shouldShowCopyAction,
showEditMessage: shouldShowEditAction,
showReactionPicker: widget.showReactionPickerIndicator,
showReplyMessage: shouldShowReplyAction,
showThreadReplyMessage: shouldShowThreadReplyAction,
showFlagButton: widget.showFlagButton,
showPinButton: widget.showPinButton,
customActions: widget.customActions,
),
);
},
);
}
}
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_portal/flutter_portal.dart';
import 'package:meta/meta.dart';
import 'package:stream_chat_flutter/src/message_widget/message_widget_content_components.dart';
import 'package:stream_chat_flutter/src/message_widget/reactions/desktop_reactions_builder.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
@@ -21,6 +22,7 @@ typedef BottomRowBuilderWithDefaultWidget = Widget Function(
///
/// Should not be used outside of [MessageWidget.
/// {@endtemplate}
@internal
class MessageWidgetContent extends StatelessWidget {
/// {@macro messageWidgetContent}
const MessageWidgetContent({
@@ -33,6 +35,7 @@ class MessageWidgetContent extends StatelessWidget {
required this.showUserAvatar,
required this.avatarWidth,
required this.showReactions,
required this.onReactionsTap,
required this.messageTheme,
required this.shouldShowReactions,
required this.streamChatTheme,
@@ -45,7 +48,7 @@ class MessageWidgetContent extends StatelessWidget {
required this.attachmentBuilders,
required this.attachmentPadding,
required this.textPadding,
required this.showReactionPickerIndicator,
required this.showReactionPickerTail,
required this.translateUserAvatar,
required this.bottomRowPadding,
required this.showInChannel,
@@ -111,6 +114,11 @@ class MessageWidgetContent extends StatelessWidget {
/// {@macro showReactions}
final bool showReactions;
/// Callback called when the reactions icon is tapped.
///
/// Do not confuse this with the tap action on the reactions picker.
final VoidCallback onReactionsTap;
/// {@macro messageTheme}
final StreamMessageThemeData messageTheme;
@@ -174,8 +182,8 @@ class MessageWidgetContent extends StatelessWidget {
/// {@macro quotedMessageBuilder}
final Widget Function(BuildContext, Message)? quotedMessageBuilder;
/// {@macro showReactionPickerIndicator}
final bool showReactionPickerIndicator;
/// {@macro showReactionPickerTail}
final bool showReactionPickerTail;
/// {@macro translateUserAvatar}
final bool translateUserAvatar;
@@ -288,9 +296,7 @@ class MessageWidgetContent extends StatelessWidget {
ownId: streamChat.currentUser!.id,
reverse: reverse,
shouldShowReactions: shouldShowReactions,
onTap: () => _showMessageReactionsModal(
context,
),
onTap: onReactionsTap,
)
: null,
anchor: Aligned(
@@ -363,7 +369,8 @@ class MessageWidgetContent extends StatelessWidget {
shape: shape,
),
),
if (showReactionPickerIndicator)
// TODO: Make tail part of the Reaction Picker.
if (showReactionPickerTail)
Positioned(
right: reverse ? null : 4,
left: reverse ? 4 : null,
@@ -374,6 +381,7 @@ class MessageWidgetContent extends StatelessWidget {
Colors.transparent,
Colors.transparent,
tailCirclesSpace: 1,
flipTail: !reverse,
),
),
),
@@ -434,47 +442,6 @@ class MessageWidgetContent extends StatelessWidget {
);
}
void _showMessageReactionsModal(BuildContext context) {
final channel = StreamChannel.of(context).channel;
showDialog(
useRootNavigator: false,
context: context,
useSafeArea: false,
barrierColor: streamChatTheme.colorTheme.overlay,
builder: (context) => StreamChannel(
channel: channel,
child: StreamMessageReactionsModal(
messageWidget: messageWidget.copyWith(
key: const Key('MessageWidget'),
message: message.copyWith(
text: (message.text?.length ?? 0) > 200
? '${message.text!.substring(0, 200)}...'
: message.text,
),
showReactions: false,
showUsername: false,
showTimestamp: false,
translateUserAvatar: false,
showSendingIndicator: false,
padding: EdgeInsets.zero,
showReactionPickerIndicator:
showReactions && (message.status == MessageSendingStatus.sent),
showPinHighlight: false,
showUserAvatar:
message.user!.id == channel.client.state.currentUser!.id
? DisplayWidget.gone
: DisplayWidget.show,
),
onUserAvatarTap: onUserAvatarTap,
messageTheme: messageTheme,
reverse: reverse,
message: message,
showReactions: showReactions,
),
),
);
}
Widget _buildBottomRow(BuildContext context) {
final defaultWidget = BottomRow(
message: message,
@@ -15,7 +15,7 @@ class StreamMessageReactionsModal extends StatelessWidget {
required this.message,
required this.messageWidget,
required this.messageTheme,
this.showReactions,
this.showReactionPicker = true,
this.reverse = false,
this.onUserAvatarTap,
});
@@ -32,8 +32,8 @@ class StreamMessageReactionsModal extends StatelessWidget {
/// {@macro reverse}
final bool reverse;
/// {@macro showReactions}
final bool? showReactions;
/// Flag for showing reaction picker.
final bool showReactionPicker;
/// {@macro onUserAvatarTap}
final void Function(User)? onUserAvatarTap;
@@ -56,8 +56,7 @@ class StreamMessageReactionsModal extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
if ((showReactions ?? hasReactionPermission) &&
(message.status == MessageSendingStatus.sent))
if (showReactionPicker && hasReactionPermission)
LayoutBuilder(
builder: (context, constraints) {
return Align(
@@ -43,19 +43,9 @@ double calculateReactionsHorizontalAlignment(
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;
}
// 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.
return result.clamp(-1, 1);
}
@@ -0,0 +1,190 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import '../mocks.dart';
Widget wrapWithStreamChat(
Widget child, {
StreamChatClient? client,
}) {
return MaterialApp(
home: StreamChat(
client: client ?? MockClient(),
child: child,
),
);
}
void main() {
group('StreamMessageInputAttachmentList tests', () {
testWidgets(
'StreamMessageInputAttachmentList should render attachments',
(WidgetTester tester) async {
final attachments = [
Attachment(type: 'file', id: 'file1'),
Attachment(type: 'file', id: 'file2'),
Attachment(type: 'media', id: 'media1'),
];
await tester.pumpWidget(
wrapWithStreamChat(
StreamMessageInputAttachmentList(
attachments: attachments,
),
),
);
// Expect 2 file attachments and 1 media attachment
expect(find.byType(MessageInputFileAttachments), findsOneWidget);
expect(find.byType(MessageInputMediaAttachments), findsOneWidget);
},
);
testWidgets(
'StreamMessageInputAttachmentList should call onRemovePressed callback',
(WidgetTester tester) async {
Attachment? removedAttachment;
final attachments = [
Attachment(type: 'file', id: 'file1'),
Attachment(type: 'file', id: 'file2'),
];
await tester.pumpWidget(
wrapWithStreamChat(
StreamMessageInputAttachmentList(
attachments: attachments,
onRemovePressed: (attachment) {
removedAttachment = attachment;
},
),
),
);
final removeButtons = find.byType(RemoveAttachmentButton);
// Tap the first remove button
await tester.tap(removeButtons.first);
await tester.pump();
// Expect the onRemovePressed callback to be called with the second
// attachment as they are reversed in the UI.
expect(removedAttachment, attachments[1]);
},
);
testWidgets(
'''StreamMessageInputAttachmentList should display empty box if no attachments''',
(WidgetTester tester) async {
final attachments = <Attachment>[];
await tester.pumpWidget(
wrapWithStreamChat(
StreamMessageInputAttachmentList(
attachments: attachments,
),
),
);
// Expect an empty box
expect(find.byType(SizedBox), findsOneWidget);
},
);
});
group('MessageInputFileAttachments tests', () {
testWidgets(
'MessageInputFileAttachments should render file attachments',
(WidgetTester tester) async {
final attachments = [
Attachment(type: 'file', id: 'file1'),
Attachment(type: 'file', id: 'file2'),
];
await tester.pumpWidget(
wrapWithStreamChat(
MessageInputFileAttachments(
attachments: attachments,
),
),
);
// Expect 2 file attachments
expect(find.byType(ClipRRect), findsNWidgets(2));
},
);
testWidgets(
'MessageInputFileAttachments should call onRemovePressed callback',
(WidgetTester tester) async {
Attachment? removedAttachment;
final attachments = [
Attachment(type: 'file', id: 'file1'),
];
await tester.pumpWidget(
wrapWithStreamChat(
MessageInputFileAttachments(
attachments: attachments,
onRemovePressed: (attachment) {
removedAttachment = attachment;
},
),
),
);
final removeButton = find.byType(RemoveAttachmentButton);
// Tap the remove button
await tester.tap(removeButton);
await tester.pump();
// Expect the onRemovePressed callback to be called with the attachment
expect(removedAttachment, attachments.first);
},
);
});
group('MessageInputMediaAttachments tests', () {
testWidgets(
'MessageInputMediaAttachments should render media attachments',
(WidgetTester tester) async {
final attachments = [
Attachment(type: 'media', id: 'media1'),
Attachment(type: 'media', id: 'media2'),
];
await tester.pumpWidget(
wrapWithStreamChat(
MessageInputMediaAttachments(
attachments: attachments,
),
),
);
// Expect 2 media attachments
expect(find.byType(Stack), findsNWidgets(2));
},
);
testWidgets(
'MessageInputMediaAttachments should display empty box if no attachments',
(WidgetTester tester) async {
final attachments = <Attachment>[];
await tester.pumpWidget(
wrapWithStreamChat(
MessageInputMediaAttachments(
attachments: attachments,
),
),
);
// Expect an empty box
expect(find.byType(SizedBox), findsOneWidget);
},
);
});
}
@@ -104,7 +104,7 @@ void main() {
message: message,
messageTheme: streamTheme.ownMessageTheme,
reverse: true,
showReactions: false,
showReactionPicker: false,
onUserAvatarTap: onUserAvatarTap,
),
),