Merge pull request #209 from GetStream/qa4-fixes

QA4-Fixes
This commit is contained in:
Salvatore Giordano
2021-01-13 17:00:21 +01:00
committed by GitHub
13 changed files with 600 additions and 494 deletions
+3 -1
View File
@@ -74,7 +74,7 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
), ),
borderRadius: BorderRadius.circular(32.0), borderRadius: BorderRadius.circular(32.0),
onlineIndicatorConstraints: onlineIndicatorConstraints:
BoxConstraints.tight(Size(16.0, 16.0)), BoxConstraints.tight(Size(12.0, 12.0)),
), ),
SizedBox( SizedBox(
height: 6.0, height: 6.0,
@@ -110,6 +110,8 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
maxWidth: 64.0, maxWidth: 64.0,
), ),
borderRadius: BorderRadius.circular(32.0), borderRadius: BorderRadius.circular(32.0),
onlineIndicatorConstraints:
BoxConstraints.tight(Size(12.0, 12.0)),
), ),
SizedBox( SizedBox(
height: 6.0, height: 6.0,
+26 -31
View File
@@ -62,16 +62,9 @@ class _FileAttachmentState extends State<FileAttachment> {
child: Container( child: Container(
width: widget.size?.width ?? 100, width: widget.size?.width ?? 100,
height: 56.0, height: 56.0,
decoration: BoxDecoration( color: StreamChatTheme.of(context).colorTheme.white,
color: StreamChatTheme.of(context).colorTheme.white,
borderRadius:
widget.trailing != null ? BorderRadius.circular(16.0) : null,
border: widget.trailing != null
? Border.fromBorderSide(BorderSide(
color: StreamChatTheme.of(context).colorTheme.greyWhisper))
: null,
),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Container( Container(
child: _getFileTypeImage(), child: _getFileTypeImage(),
@@ -79,9 +72,7 @@ class _FileAttachmentState extends State<FileAttachment> {
width: 33.33, width: 33.33,
margin: EdgeInsets.all(8.0), margin: EdgeInsets.all(8.0),
), ),
SizedBox( SizedBox(width: 8.0),
width: 6.0,
),
Expanded( Expanded(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@@ -93,32 +84,36 @@ class _FileAttachmentState extends State<FileAttachment> {
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
SizedBox( SizedBox(height: 3.0),
height: 3.0,
),
Text( Text(
'${getSizeText(widget.attachment.extraData['file_size'])}', '${getSizeText(widget.attachment.extraData['file_size'])}',
style: StreamChatTheme.of(context).textTheme.body.copyWith( style: StreamChatTheme.of(context)
color: StreamChatTheme.of(context) .textTheme
.colorTheme .footnote
.black .copyWith(
.withOpacity(0.5)), color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.5)),
), ),
], ],
), ),
), ),
Column( SizedBox(width: 8.0),
children: [ Material(
widget.trailing ?? type: MaterialType.transparency,
IconButton( child: widget.trailing ??
icon: StreamSvgIcon.cloud_download( IconButton(
color: StreamChatTheme.of(context).colorTheme.black, icon: StreamSvgIcon.cloud_download(
), color: StreamChatTheme.of(context).colorTheme.black,
onPressed: () {
launchURL(context, widget.attachment.assetUrl);
},
), ),
], padding: const EdgeInsets.all(8),
visualDensity: VisualDensity.compact,
splashRadius: 16,
onPressed: () {
launchURL(context, widget.attachment.assetUrl);
},
),
), ),
], ],
), ),
+102 -104
View File
@@ -8,6 +8,7 @@ import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import '../stream_chat_flutter.dart'; import '../stream_chat_flutter.dart';
import 'extension.dart';
class ImageActionsModal extends StatelessWidget { class ImageActionsModal extends StatelessWidget {
final Message message; final Message message;
@@ -35,127 +36,124 @@ class ImageActionsModal extends StatelessWidget {
} }
Widget _buildPage(context) { Widget _buildPage(context) {
return Material( return Column(
color: Colors.transparent, crossAxisAlignment: CrossAxisAlignment.end,
child: Column( children: [
crossAxisAlignment: CrossAxisAlignment.end, SizedBox(height: kToolbarHeight),
children: [ Padding(
SizedBox( padding: const EdgeInsets.only(right: 8.0),
height: kToolbarHeight, child: Container(
child: IconButton( width: MediaQuery.of(context).size.width * 0.5,
icon: StreamSvgIcon.close(), clipBehavior: Clip.hardEdge,
onPressed: () => Navigator.maybePop(context), decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16.0),
), ),
),
Align(
alignment: Alignment.centerRight,
child: Container( child: Container(
width: MediaQuery.of(context).size.width / 1.8, child: Column(
child: Padding( crossAxisAlignment: CrossAxisAlignment.end,
padding: const EdgeInsets.all(8.0), mainAxisSize: MainAxisSize.min,
child: Material( children: [
clipBehavior: Clip.hardEdge, _buildButton(
shape: RoundedRectangleBorder( context,
borderRadius: BorderRadius.circular(16), 'Reply',
StreamSvgIcon.Icon_curve_line_left_up(
size: 24.0,
color: StreamChatTheme.of(context).colorTheme.grey,
),
() {},
), ),
child: Column( _buildButton(
crossAxisAlignment: CrossAxisAlignment.stretch, context,
children: ListTile.divideTiles( 'Show in Chat',
color: StreamChatTheme.of(context).colorTheme.greyWhisper, StreamSvgIcon.eye(
context: context, size: 24.0,
tiles: [ color: StreamChatTheme.of(context).colorTheme.black,
_buildButton( ),
context, onShowMessage,
'Reply', ),
StreamSvgIcon.Icon_curve_line_left_up( _buildButton(
size: 24.0, context,
color: 'Save ${urls[currentIndex].type == 'video' ? 'Video' : 'Image'}',
StreamChatTheme.of(context).colorTheme.grey, StreamSvgIcon.Icon_save(
), size: 24.0,
() {}), color: StreamChatTheme.of(context).colorTheme.grey,
_buildButton( ),
context, () async {
'Show in Chat', var url = urls[currentIndex].imageUrl ??
StreamSvgIcon.eye( urls[currentIndex].assetUrl ??
size: 24.0, urls[currentIndex].thumbUrl;
color:
StreamChatTheme.of(context).colorTheme.black,
),
onShowMessage),
_buildButton(
context,
'Save ${urls[currentIndex].type == 'video' ? 'Video' : 'Image'}',
StreamSvgIcon.Icon_save(
size: 24.0,
color:
StreamChatTheme.of(context).colorTheme.grey,
), () async {
var url = urls[currentIndex].imageUrl ??
urls[currentIndex].assetUrl ??
urls[currentIndex].thumbUrl;
if (urls[currentIndex].type == 'video') { if (urls[currentIndex].type == 'video') {
await _saveVideo(url); await _saveVideo(url);
Navigator.pop(context); Navigator.pop(context);
} else { } else {
await _saveImage(url); await _saveImage(url);
Navigator.pop(context); Navigator.pop(context);
} }
}), },
if (StreamChat.of(context).user.id == message.user.id)
_buildButton(
context,
'Delete',
StreamSvgIcon.delete(
size: 24.0,
color: StreamChatTheme.of(context)
.colorTheme
.accentRed,
),
() {
Navigator.pop(context);
Navigator.pop(context);
StreamChat.of(context).client.deleteMessage(
message,
StreamChannel.of(context).channel.cid,
);
},
color: StreamChatTheme.of(context)
.colorTheme
.accentRed,
),
],
).toList(),
), ),
), if (StreamChat.of(context).user.id == message.user.id)
_buildButton(
context,
'Delete',
StreamSvgIcon.delete(
size: 24.0,
color: StreamChatTheme.of(context).colorTheme.accentRed,
),
() {
Navigator.pop(context);
Navigator.pop(context);
StreamChat.of(context).client.deleteMessage(
message,
StreamChannel.of(context).channel.cid,
);
},
color: StreamChatTheme.of(context).colorTheme.accentRed,
),
]
.map<Widget>((e) =>
Align(alignment: Alignment.centerRight, child: e))
.insertBetween(
Container(
height: 1,
color:
StreamChatTheme.of(context).colorTheme.greyWhisper,
),
),
), ),
), ),
), ),
], )
), ],
); );
} }
Widget _buildButton( Widget _buildButton(
context, String title, StreamSvgIcon icon, VoidCallback onTap, context,
{Color color}) { String title,
var titleStyle = TextStyle( StreamSvgIcon icon,
fontSize: 14.5, VoidCallback onTap, {
color: StreamChatTheme.of(context).colorTheme.black, Color color,
); }) {
return Material( return Material(
color: StreamChatTheme.of(context).colorTheme.white, color: StreamChatTheme.of(context).colorTheme.white,
child: InkWell( child: InkWell(
onTap: onTap, onTap: onTap,
child: ListTile( child: Padding(
dense: true, padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
title: Text( child: Row(
title, children: [
style: icon,
color == null ? titleStyle : titleStyle.copyWith(color: color), SizedBox(width: 16),
Text(
title,
style: StreamChatTheme.of(context)
.textTheme
.body
.copyWith(color: color),
),
],
), ),
leading: icon,
), ),
), ),
); );
+163 -137
View File
@@ -14,6 +14,7 @@ import 'message_input.dart';
import 'message_widget.dart'; import 'message_widget.dart';
import 'stream_chat.dart'; import 'stream_chat.dart';
import 'stream_chat_theme.dart'; import 'stream_chat_theme.dart';
import 'extension.dart';
class MessageActionsModal extends StatefulWidget { class MessageActionsModal extends StatefulWidget {
final Widget Function(BuildContext, Message) editMessageInputBuilder; final Widget Function(BuildContext, Message) editMessageInputBuilder;
@@ -89,6 +90,8 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
duration: Duration(milliseconds: 300), duration: Duration(milliseconds: 300),
curve: Curves.easeInOutBack, curve: Curves.easeInOutBack,
builder: (context, val, snapshot) { builder: (context, val, snapshot) {
final hasFileAttachment =
widget.message.attachments?.any((it) => it.type == 'file') == true;
return GestureDetector( return GestureDetector(
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
onTap: () => Navigator.maybePop(context), onTap: () => Navigator.maybePop(context),
@@ -110,7 +113,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
child: Center( child: Center(
child: SingleChildScrollView( child: SingleChildScrollView(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0), padding: const EdgeInsets.all(8.0),
child: Column( child: Column(
crossAxisAlignment: widget.reverse crossAxisAlignment: widget.reverse
? CrossAxisAlignment.end ? CrossAxisAlignment.end
@@ -135,6 +138,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
messageTheme: widget.messageTheme, messageTheme: widget.messageTheme,
), ),
), ),
SizedBox(height: 8),
IgnorePointer( IgnorePointer(
child: MessageWidget( child: MessageWidget(
key: Key('MessageWidget'), key: Key('MessageWidget'),
@@ -150,8 +154,17 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
showThreadReplyIndicator: false, showThreadReplyIndicator: false,
showReplyMessage: false, showReplyMessage: false,
showUserAvatar: widget.showUserAvatar, showUserAvatar: widget.showUserAvatar,
attachmentPadding: EdgeInsets.all(
hasFileAttachment ? 4 : 2,
),
showTimestamp: false, showTimestamp: false,
translateUserAvatar: false, translateUserAvatar: false,
padding: const EdgeInsets.all(0),
textPadding: EdgeInsets.symmetric(
vertical: 8.0,
horizontal:
widget.message.text.isOnlyEmoji ? 0 : 16.0,
),
showReactionPickerIndicator: showReactionPickerIndicator:
widget.showReactions && widget.showReactions &&
(widget.message.status == (widget.message.status ==
@@ -162,10 +175,10 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
shape: widget.messageShape, shape: widget.messageShape,
), ),
), ),
SizedBox(height: 8),
Padding( Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
right: widget.reverse ? 8 : 0, left: widget.reverse ? 0 : 40,
left: widget.reverse ? 0 : 48,
), ),
child: SizedBox( child: SizedBox(
width: MediaQuery.of(context).size.width * 0.75, width: MediaQuery.of(context).size.width * 0.75,
@@ -180,36 +193,37 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
child: Column( child: Column(
crossAxisAlignment: crossAxisAlignment:
CrossAxisAlignment.stretch, CrossAxisAlignment.stretch,
children: ListTile.divideTiles( children: [
color: StreamChatTheme.of(context) if (widget.showReplyMessage &&
.colorTheme (widget.message.status ==
.greyWhisper, MessageSendingStatus.SENT ||
context: context, widget.message.status == null) &&
tiles: [ widget.message.parentId == null)
if (widget.showReplyMessage && _buildReplyButton(context),
(widget.message.status == if (widget.showThreadReplyMessage &&
MessageSendingStatus.SENT || (widget.message.status ==
widget.message.status == null) && MessageSendingStatus.SENT ||
widget.message.parentId == null) widget.message.status == null) &&
_buildReplyButton(context), widget.message.parentId == null)
if (widget.showThreadReplyMessage && _buildThreadReplyButton(context),
(widget.message.status == if (widget.showResendMessage)
MessageSendingStatus.SENT || _buildResendMessage(context),
widget.message.status == null) && if (widget.showEditMessage)
widget.message.parentId == null) _buildEditMessage(context),
_buildThreadReplyButton(context), if (widget.showCopyMessage)
if (widget.showResendMessage) _buildCopyButton(context),
_buildResendMessage(context), if (widget.showFlagButton)
if (widget.showEditMessage) _buildFlagButton(context),
_buildEditMessage(context), if (widget.showDeleteMessage)
if (widget.showCopyMessage) _buildDeleteButton(context),
_buildCopyButton(context), ].insertBetween(
if (widget.showFlagButton) Container(
_buildFlagButton(context), height: 1,
if (widget.showDeleteMessage) color: StreamChatTheme.of(context)
_buildDeleteButton(context), .colorTheme
], .greyWhisper,
).toList(), ),
),
), ),
), ),
), ),
@@ -410,136 +424,131 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
} }
Widget _buildReplyButton(BuildContext context) { Widget _buildReplyButton(BuildContext context) {
return ListTile( return InkWell(
dense: true,
title: Row(
children: [
StreamSvgIcon.reply(
color: StreamChatTheme.of(context).primaryIconTheme.color,
),
const SizedBox(width: 16),
Text(
'Reply',
style: StreamChatTheme.of(context).textTheme.headline,
),
],
),
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
if (widget.onReplyTap != null) { if (widget.onReplyTap != null) {
widget.onReplyTap(widget.message); widget.onReplyTap(widget.message);
} }
}, },
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
child: Row(
children: [
StreamSvgIcon.reply(
color: StreamChatTheme.of(context).primaryIconTheme.color,
),
const SizedBox(width: 16),
Text(
'Reply',
style: StreamChatTheme.of(context).textTheme.body,
),
],
),
),
); );
} }
Widget _buildFlagButton(BuildContext context) { Widget _buildFlagButton(BuildContext context) {
return ListTile( return InkWell(
dense: true,
title: Row(
children: [
StreamSvgIcon.icon_flag(
color: StreamChatTheme.of(context).primaryIconTheme.color,
),
const SizedBox(width: 16),
Text(
'Flag',
style: StreamChatTheme.of(context).textTheme.headline,
),
],
),
onTap: () => _showFlagDialog(), onTap: () => _showFlagDialog(),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
child: Row(
children: [
StreamSvgIcon.icon_flag(
color: StreamChatTheme.of(context).primaryIconTheme.color,
),
const SizedBox(width: 16),
Text(
'Flag',
style: StreamChatTheme.of(context).textTheme.body,
),
],
),
),
); );
} }
Widget _buildDeleteButton(BuildContext context) { Widget _buildDeleteButton(BuildContext context) {
final isDeleteFailed = final isDeleteFailed =
widget.message.status == MessageSendingStatus.FAILED_DELETE; widget.message.status == MessageSendingStatus.FAILED_DELETE;
return ListTile( return InkWell(
dense: true, onTap: () => _showDeleteDialog(),
title: Row( child: Padding(
children: [ padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
StreamSvgIcon.delete( child: Row(
color: Colors.red, children: [
), StreamSvgIcon.delete(
const SizedBox(width: 16), color: Colors.red,
Text( ),
isDeleteFailed ? 'Retry Deleting Message' : 'Delete Message', const SizedBox(width: 16),
style: StreamChatTheme.of(context) Text(
.textTheme isDeleteFailed ? 'Retry Deleting Message' : 'Delete Message',
.headline style: StreamChatTheme.of(context)
.copyWith(color: Colors.red), .textTheme
), .body
], .copyWith(color: Colors.red),
),
],
),
), ),
onTap: () {
_showDeleteDialog();
},
); );
} }
Widget _buildCopyButton(BuildContext context) { Widget _buildCopyButton(BuildContext context) {
return ListTile( return InkWell(
dense: true,
title: Row(
children: [
StreamSvgIcon.copy(
size: 24,
color: StreamChatTheme.of(context).primaryIconTheme.color,
),
const SizedBox(width: 16),
Text(
'Copy Message',
style: StreamChatTheme.of(context).textTheme.headline,
),
],
),
onTap: () async { onTap: () async {
await Clipboard.setData(ClipboardData(text: widget.message.text)); await Clipboard.setData(ClipboardData(text: widget.message.text));
Navigator.pop(context); Navigator.pop(context);
}, },
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
child: Row(
children: [
StreamSvgIcon.copy(
size: 24,
color: StreamChatTheme.of(context).primaryIconTheme.color,
),
const SizedBox(width: 16),
Text(
'Copy Message',
style: StreamChatTheme.of(context).textTheme.body,
),
],
),
),
); );
} }
Widget _buildEditMessage(BuildContext context) { Widget _buildEditMessage(BuildContext context) {
return ListTile( return InkWell(
dense: true,
title: Row(
children: [
StreamSvgIcon.edit(
color: StreamChatTheme.of(context).primaryIconTheme.color,
),
const SizedBox(width: 16),
Text(
'Edit Message',
style: StreamChatTheme.of(context).textTheme.headline,
),
],
),
onTap: () async { onTap: () async {
Navigator.pop(context); Navigator.pop(context);
_showEditBottomSheet(context); _showEditBottomSheet(context);
}, },
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
child: Row(
children: [
StreamSvgIcon.edit(
color: StreamChatTheme.of(context).primaryIconTheme.color,
),
const SizedBox(width: 16),
Text(
'Edit Message',
style: StreamChatTheme.of(context).textTheme.body,
),
],
),
),
); );
} }
Widget _buildResendMessage(BuildContext context) { Widget _buildResendMessage(BuildContext context) {
final isUpdateFailed = final isUpdateFailed =
widget.message.status == MessageSendingStatus.FAILED_UPDATE; widget.message.status == MessageSendingStatus.FAILED_UPDATE;
return ListTile( return InkWell(
dense: true,
title: Row(
children: [
StreamSvgIcon.circle_up(
color: StreamChatTheme.of(context).colorTheme.accentBlue,
),
const SizedBox(width: 16),
Text(
isUpdateFailed ? 'Resend Edited Message' : 'Resend',
style: StreamChatTheme.of(context).textTheme.headline,
),
],
),
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
final client = StreamChat.of(context).client; final client = StreamChat.of(context).client;
@@ -550,6 +559,21 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
channel.sendMessage(widget.message); channel.sendMessage(widget.message);
} }
}, },
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
child: Row(
children: [
StreamSvgIcon.circle_up(
color: StreamChatTheme.of(context).colorTheme.accentBlue,
),
const SizedBox(width: 16),
Text(
isUpdateFailed ? 'Resend Edited Message' : 'Resend',
style: StreamChatTheme.of(context).textTheme.body,
),
],
),
),
); );
} }
@@ -623,26 +647,28 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
} }
Widget _buildThreadReplyButton(BuildContext context) { Widget _buildThreadReplyButton(BuildContext context) {
return ListTile( return InkWell(
dense: true,
title: Row(
children: [
StreamSvgIcon.thread(
color: StreamChatTheme.of(context).primaryIconTheme.color,
),
const SizedBox(width: 16),
Text(
'Thread Reply',
style: StreamChatTheme.of(context).textTheme.headline,
),
],
),
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
if (widget.onThreadReplyTap != null) { if (widget.onThreadReplyTap != null) {
widget.onThreadReplyTap(widget.message); widget.onThreadReplyTap(widget.message);
} }
}, },
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
child: Row(
children: [
StreamSvgIcon.thread(
color: StreamChatTheme.of(context).primaryIconTheme.color,
),
const SizedBox(width: 16),
Text(
'Thread Reply',
style: StreamChatTheme.of(context).textTheme.body,
),
],
),
),
); );
} }
} }
+34 -39
View File
@@ -1527,14 +1527,12 @@ class MessageInputState extends State<MessageInput> {
return Transform( return Transform(
transform: Matrix4.rotationY(pi), transform: Matrix4.rotationY(pi),
alignment: Alignment.center, alignment: Alignment.center,
child: Padding( child: QuotedMessageWidget(
reverse: true,
showBorder: !containsUrl,
message: widget.quotedMessage,
messageTheme: StreamChatTheme.of(context).otherMessageTheme,
padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), padding: const EdgeInsets.fromLTRB(8, 8, 8, 0),
child: QuotedMessageWidget(
reverse: true,
showBorder: !containsUrl,
message: widget.quotedMessage,
messageTheme: StreamChatTheme.of(context).otherMessageTheme,
),
), ),
); );
} }
@@ -1544,17 +1542,17 @@ class MessageInputState extends State<MessageInput> {
return Column( return Column(
children: [ children: [
if (_attachments.any((e) => e.attachment?.type == 'file')) if (_attachments.any((e) => e.attachment?.type == 'file'))
LimitedBox( Padding(
maxHeight: 136.0, padding: const EdgeInsets.fromLTRB(8, 8, 8, 0),
child: ListView( child: LimitedBox(
reverse: true, maxHeight: 136.0,
shrinkWrap: true, child: ListView(
children: _attachments.reversed reverse: true,
.where((e) => e.attachment?.type == 'file') shrinkWrap: true,
.map( children: _attachments.reversed
(e) => Padding( .where((e) => e.attachment?.type == 'file')
padding: const EdgeInsets.symmetric(horizontal: 8.0), .map<Widget>(
child: ClipRRect( (e) => ClipRRect(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: FileAttachment( child: FileAttachment(
@@ -1580,31 +1578,28 @@ class MessageInputState extends State<MessageInput> {
.white, .white,
), ),
), ),
onTap: () { onTap: () =>
setState(() { setState(() => _attachments.remove(e)),
_attachments.remove(e);
});
},
), ),
), ),
), ),
), ),
), )
) .insertBetween(const SizedBox(width: 8)),
.toList(), ),
), ),
), ),
if (_attachments.any((e) => e.attachment?.type != 'file')) if (_attachments.any((e) => e.attachment?.type != 'file'))
LimitedBox( Padding(
maxHeight: 104.0, padding: const EdgeInsets.fromLTRB(8, 8, 8, 0),
child: ListView( child: LimitedBox(
scrollDirection: Axis.horizontal, maxHeight: 104.0,
children: _attachments child: ListView(
.where((e) => e.attachment?.type != 'file') scrollDirection: Axis.horizontal,
.map( children: _attachments
(attachment) => Padding( .where((e) => e.attachment?.type != 'file')
padding: const EdgeInsets.all(8.0), .map<Widget>(
child: ClipRRect( (attachment) => ClipRRect(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: Stack( child: Stack(
@@ -1631,9 +1626,9 @@ class MessageInputState extends State<MessageInput> {
], ],
), ),
), ),
), )
) .insertBetween(const SizedBox(width: 8)),
.toList(), ),
), ),
), ),
], ],
+17 -10
View File
@@ -770,6 +770,10 @@ class _MessageListViewState extends State<MessageListView> {
topRight: Radius.circular(16), topRight: Radius.circular(16),
bottomRight: Radius.circular(16), bottomRight: Radius.circular(16),
), ),
textPadding: EdgeInsets.symmetric(
vertical: 8.0,
horizontal: isOnlyEmoji ? 0 : 16.0,
),
borderSide: isMyMessage || isOnlyEmoji ? BorderSide.none : null, borderSide: isMyMessage || isOnlyEmoji ? BorderSide.none : null,
showUserAvatar: isMyMessage ? DisplayWidget.gone : DisplayWidget.show, showUserAvatar: isMyMessage ? DisplayWidget.gone : DisplayWidget.show,
messageTheme: isMyMessage messageTheme: isMyMessage
@@ -817,7 +821,7 @@ class _MessageListViewState extends State<MessageListView> {
final allRead = readList.length >= (channel.memberCount ?? 0) - 1; final allRead = readList.length >= (channel.memberCount ?? 0) - 1;
final hasFileAttachment = final hasFileAttachment =
message.attachments.any((it) => it.type == 'file'); message.attachments?.any((it) => it.type == 'file') == true;
final isThreadMessage = final isThreadMessage =
message?.parentId != null && message?.showInChannel == true; message?.parentId != null && message?.showInChannel == true;
@@ -849,13 +853,11 @@ class _MessageListViewState extends State<MessageListView> {
final showThreadReplyIndicator = !_isThreadConversation && hasReplies; final showThreadReplyIndicator = !_isThreadConversation && hasReplies;
final isOnlyEmoji = message.text.isOnlyEmoji; final isOnlyEmoji = message.text.isOnlyEmoji;
final showMessageBorder = final hasUrlAttachment =
showThreadReplyIndicator || showInChannelIndicator; message.attachments?.any((it) => it.ogScrapeUrl != null) == true;
final borderSide = isMyMessage
? !showMessageBorder final borderSide =
? BorderSide.none isOnlyEmoji || hasUrlAttachment || (isMyMessage && !hasFileAttachment)
: null
: isOnlyEmoji && !showMessageBorder
? BorderSide.none ? BorderSide.none
: null; : null;
@@ -901,14 +903,15 @@ class _MessageListViewState extends State<MessageListView> {
attachmentBorderRadiusGeometry: BorderRadius.only( attachmentBorderRadiusGeometry: BorderRadius.only(
topLeft: Radius.circular(attachmentBorderRadius), topLeft: Radius.circular(attachmentBorderRadius),
bottomLeft: Radius.circular( bottomLeft: Radius.circular(
(timeDiff >= 1 || !isNextUserSame) && !(hasReplies || isThreadMessage) (timeDiff >= 1 || !isNextUserSame) &&
!(hasReplies || isThreadMessage || hasFileAttachment)
? 0 ? 0
: attachmentBorderRadius, : attachmentBorderRadius,
), ),
topRight: Radius.circular(attachmentBorderRadius), topRight: Radius.circular(attachmentBorderRadius),
bottomRight: Radius.circular(attachmentBorderRadius), bottomRight: Radius.circular(attachmentBorderRadius),
), ),
attachmentPadding: const EdgeInsets.all(2), attachmentPadding: EdgeInsets.all(hasFileAttachment ? 4 : 2),
borderRadiusGeometry: BorderRadius.only( borderRadiusGeometry: BorderRadius.only(
topLeft: Radius.circular(16), topLeft: Radius.circular(16),
bottomLeft: Radius.circular( bottomLeft: Radius.circular(
@@ -919,6 +922,10 @@ class _MessageListViewState extends State<MessageListView> {
topRight: Radius.circular(16), topRight: Radius.circular(16),
bottomRight: Radius.circular(16), bottomRight: Radius.circular(16),
), ),
textPadding: EdgeInsets.symmetric(
vertical: 8.0,
horizontal: isOnlyEmoji ? 0 : 16.0,
),
messageTheme: isMyMessage messageTheme: isMyMessage
? StreamChatTheme.of(context).ownMessageTheme ? StreamChatTheme.of(context).ownMessageTheme
: StreamChatTheme.of(context).otherMessageTheme, : StreamChatTheme.of(context).otherMessageTheme,
+57 -41
View File
@@ -9,6 +9,7 @@ import 'package:stream_chat_flutter/src/user_avatar.dart';
import 'message_widget.dart'; import 'message_widget.dart';
import 'stream_chat_theme.dart'; import 'stream_chat_theme.dart';
import 'extension.dart';
class MessageReactionsModal extends StatelessWidget { class MessageReactionsModal extends StatelessWidget {
final Widget Function(BuildContext, Message) editMessageInputBuilder; final Widget Function(BuildContext, Message) editMessageInputBuilder;
@@ -61,6 +62,8 @@ class MessageReactionsModal extends StatelessWidget {
duration: Duration(milliseconds: 300), duration: Duration(milliseconds: 300),
curve: Curves.easeInOutBack, curve: Curves.easeInOutBack,
builder: (context, val, snapshot) { builder: (context, val, snapshot) {
final hasFileAttachment =
message.attachments?.any((it) => it.type == 'file') == true;
return GestureDetector( return GestureDetector(
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
onTap: () => Navigator.maybePop(context), onTap: () => Navigator.maybePop(context),
@@ -82,7 +85,7 @@ class MessageReactionsModal extends StatelessWidget {
child: Center( child: Center(
child: SingleChildScrollView( child: SingleChildScrollView(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0), padding: const EdgeInsets.all(8.0),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
@@ -105,6 +108,7 @@ class MessageReactionsModal extends StatelessWidget {
messageTheme: messageTheme, messageTheme: messageTheme,
), ),
), ),
const SizedBox(height: 8),
IgnorePointer( IgnorePointer(
child: MessageWidget( child: MessageWidget(
key: Key('MessageWidget'), key: Key('MessageWidget'),
@@ -123,15 +127,25 @@ class MessageReactionsModal extends StatelessWidget {
translateUserAvatar: false, translateUserAvatar: false,
showSendingIndicator: false, showSendingIndicator: false,
shape: messageShape, shape: messageShape,
padding: const EdgeInsets.all(0),
attachmentPadding: EdgeInsets.all(
hasFileAttachment ? 4 : 2,
),
showInChannelIndicator: false, showInChannelIndicator: false,
textPadding: EdgeInsets.symmetric(
vertical: 8.0,
horizontal: message.text.isOnlyEmoji ? 0 : 16.0,
),
showReactionPickerIndicator: showReactions && showReactionPickerIndicator: showReactions &&
(message.status == (message.status ==
MessageSendingStatus.SENT || MessageSendingStatus.SENT ||
message.status == null), message.status == null),
), ),
), ),
if (message.latestReactions?.isNotEmpty == true) if (message.latestReactions?.isNotEmpty == true) ...[
const SizedBox(height: 8),
_buildReactionCard(context), _buildReactionCard(context),
]
], ],
), ),
), ),
@@ -145,47 +159,42 @@ class MessageReactionsModal extends StatelessWidget {
); );
} }
Padding _buildReactionCard(BuildContext context) { Widget _buildReactionCard(BuildContext context) {
final currentUser = StreamChat.of(context).user; final currentUser = StreamChat.of(context).user;
return Padding( return Card(
padding: const EdgeInsets.symmetric( color: StreamChatTheme.of(context).colorTheme.white,
horizontal: 8.0, clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
), ),
child: Card( margin: EdgeInsets.zero,
color: StreamChatTheme.of(context).colorTheme.white, child: Padding(
clipBehavior: Clip.hardEdge, padding: const EdgeInsets.all(16),
shape: RoundedRectangleBorder( child: Column(
borderRadius: BorderRadius.circular(16), mainAxisSize: MainAxisSize.min,
), children: [
margin: EdgeInsets.zero, Text(
child: Padding( 'Message Reactions',
padding: const EdgeInsets.all(16), style: StreamChatTheme.of(context).textTheme.headlineBold,
child: Column( ),
mainAxisSize: MainAxisSize.min, const SizedBox(height: 16),
children: [ Flexible(
Text( child: SingleChildScrollView(
'Message Reactions', child: Wrap(
style: StreamChatTheme.of(context).textTheme.headlineBold, spacing: 16,
), runSpacing: 16,
const SizedBox(height: 16), alignment: WrapAlignment.start,
Flexible( children: message.latestReactions
child: SingleChildScrollView( .map((e) => _buildReaction(
child: Wrap( e,
spacing: 16, currentUser,
runSpacing: 16, context,
alignment: WrapAlignment.start, ))
children: message.latestReactions .toList(),
.map((e) => _buildReaction(
e,
currentUser,
context,
))
.toList(),
),
), ),
), ),
], ),
), ],
), ),
), ),
); );
@@ -208,6 +217,7 @@ class MessageReactionsModal extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Stack( Stack(
overflow: Overflow.visible,
children: [ children: [
UserAvatar( UserAvatar(
onTap: onUserAvatarTap, onTap: onUserAvatarTap,
@@ -216,6 +226,10 @@ class MessageReactionsModal extends StatelessWidget {
height: 64, height: 64,
width: 64, width: 64,
), ),
onlineIndicatorConstraints: BoxConstraints.tightFor(
height: 12,
width: 12,
),
borderRadius: BorderRadius.circular(32), borderRadius: BorderRadius.circular(32),
), ),
Positioned( Positioned(
@@ -227,12 +241,14 @@ class MessageReactionsModal extends StatelessWidget {
flipTail: !reverse, flipTail: !reverse,
borderColor: messageTheme.reactionsBorderColor, borderColor: messageTheme.reactionsBorderColor,
backgroundColor: messageTheme.reactionsBackgroundColor, backgroundColor: messageTheme.reactionsBackgroundColor,
maskColor: StreamChatTheme.of(context).colorTheme.white,
tailCirclesSpacing: 1,
highlightOwnReactions: false, highlightOwnReactions: false,
), ),
), ),
bottom: 6, bottom: 6,
left: isCurrentUser ? 0 : null, left: isCurrentUser ? -3 : null,
right: isCurrentUser ? 0 : null, right: isCurrentUser ? -3 : null,
), ),
], ],
), ),
+69 -75
View File
@@ -278,6 +278,9 @@ class _MessageWidgetState extends State<MessageWidget> {
?.isNotEmpty == ?.isNotEmpty ==
true; true;
bool get hasUrlAttachments =>
widget.message.attachments?.any((it) => it.ogScrapeUrl != null) == true;
bool get showBottomRow => bool get showBottomRow =>
showThreadReplyIndicator || showThreadReplyIndicator ||
showUsername || showUsername ||
@@ -416,7 +419,7 @@ class _MessageWidgetState extends State<MessageWidget> {
if (hasQuotedMessage) if (hasQuotedMessage)
_buildQuotedMessage(), _buildQuotedMessage(),
if (hasNonUrlAttachments) if (hasNonUrlAttachments)
..._parseAttachments( _parseAttachments(
context), context),
if (widget.message.text if (widget.message.text
.trim() .trim()
@@ -441,9 +444,9 @@ class _MessageWidgetState extends State<MessageWidget> {
StreamChatTheme.of(context) StreamChatTheme.of(context)
.colorTheme .colorTheme
.white, .white,
StreamChatTheme.of(context) Colors.transparent,
.colorTheme Colors.transparent,
.white, tailCirclesSpace: 1,
), ),
), ),
), ),
@@ -494,21 +497,15 @@ class _MessageWidgetState extends State<MessageWidget> {
widget.onQuotedMessageTap != null widget.onQuotedMessageTap != null
? () => widget.onQuotedMessageTap(widget.message.quotedMessageId) ? () => widget.onQuotedMessageTap(widget.message.quotedMessageId)
: null; : null;
return Padding( return QuotedMessageWidget(
onTap: onTap,
message: widget.message.quotedMessage,
messageTheme: isMyMessage
? StreamChatTheme.of(context).otherMessageTheme
: StreamChatTheme.of(context).ownMessageTheme,
reverse: widget.reverse,
padding: EdgeInsets.only( padding: EdgeInsets.only(
right: 8, right: 8, left: 8, top: 8, bottom: hasNonUrlAttachments ? 8 : 0),
left: 8,
top: 8,
bottom: hasNonUrlAttachments ? 8 : 0,
),
child: QuotedMessageWidget(
onTap: onTap,
message: widget.message.quotedMessage,
messageTheme: isMyMessage
? StreamChatTheme.of(context).otherMessageTheme
: StreamChatTheme.of(context).ownMessageTheme,
reverse: widget.reverse,
),
); );
} }
@@ -564,11 +561,10 @@ class _MessageWidgetState extends State<MessageWidget> {
}; };
children.addAll([ children.addAll([
if (showSendingIndicator) _buildSendingIndicator(),
if (showInChannel || showThreadReplyIndicator) ...[ if (showInChannel || showThreadReplyIndicator) ...[
if (showThreadParticipants) if (showThreadParticipants)
SizedBox.fromSize( SizedBox.fromSize(
size: Size((threadParticipants.length * 8.0) + 10, 16), size: Size((threadParticipants.length * 8.0) + 8, 16),
child: _buildThreadParticipantsIndicator(), child: _buildThreadParticipantsIndicator(),
), ),
InkWell( InkWell(
@@ -588,16 +584,18 @@ class _MessageWidgetState extends State<MessageWidget> {
Jiffy(widget.message.createdAt.toLocal()).jm, Jiffy(widget.message.createdAt.toLocal()).jm,
style: widget.messageTheme.createdAt, style: widget.messageTheme.createdAt,
), ),
if (showSendingIndicator) _buildSendingIndicator(),
]); ]);
if (widget.reverse) children = children.reversed.toList(); final showThreadTail = !(hasUrlAttachments || isGiphy || isOnlyEmoji) &&
(showThreadReplyIndicator || showInChannel);
return Flex( return Flex(
direction: Axis.horizontal, direction: Axis.horizontal,
clipBehavior: Clip.none, clipBehavior: Clip.none,
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
if (showThreadReplyIndicator || showInChannel) if (showThreadTail)
Container( Container(
margin: EdgeInsets.only( margin: EdgeInsets.only(
bottom: widget.messageTheme.replies.fontSize / 2, bottom: widget.messageTheme.replies.fontSize / 2,
@@ -648,18 +646,20 @@ class _MessageWidgetState extends State<MessageWidget> {
var padding = 0.0; var padding = 0.0;
return Stack( return Stack(
children: widget.message.threadParticipants.map((user) { children: widget.message.threadParticipants.map((user) {
padding += 10.0; padding += 8.0;
return Positioned( return Positioned(
left: padding - 10, right: padding - 8,
bottom: 0, bottom: 0,
top: 0, top: 0,
child: Material( child: Container(
color: Colors.white, decoration: BoxDecoration(
clipBehavior: Clip.antiAlias, shape: BoxShape.circle,
shape: CircleBorder(), color: StreamChatTheme.of(context).colorTheme.white,
),
padding: const EdgeInsets.all(1),
child: UserAvatar( child: UserAvatar(
user: user, user: user,
constraints: BoxConstraints.loose(Size.fromRadius(8)), constraints: BoxConstraints.loose(Size.fromRadius(7)),
showOnlineStatus: false, showOnlineStatus: false,
), ),
), ),
@@ -694,6 +694,7 @@ class _MessageWidgetState extends State<MessageWidget> {
flipTail: widget.reverse, flipTail: widget.reverse,
backgroundColor: widget.messageTheme.reactionsBackgroundColor, backgroundColor: widget.messageTheme.reactionsBackgroundColor,
borderColor: widget.messageTheme.reactionsBorderColor, borderColor: widget.messageTheme.reactionsBorderColor,
maskColor: widget.messageTheme.reactionsMaskColor,
reactions: reactionsList, reactions: reactionsList,
), ),
) )
@@ -777,7 +778,7 @@ class _MessageWidgetState extends State<MessageWidget> {
side: widget.attachmentBorderSide ?? side: widget.attachmentBorderSide ??
widget.borderSide ?? widget.borderSide ??
BorderSide( BorderSide(
color: StreamChatTheme.of(context).colorTheme.greyGainsboro, color: StreamChatTheme.of(context).colorTheme.greyWhisper,
), ),
borderRadius: widget.attachmentBorderRadiusGeometry ?? borderRadius: widget.attachmentBorderRadiusGeometry ??
widget.borderRadiusGeometry ?? widget.borderRadiusGeometry ??
@@ -785,7 +786,7 @@ class _MessageWidgetState extends State<MessageWidget> {
); );
} }
List<Widget> _parseAttachments(BuildContext context) { Widget _parseAttachments(BuildContext context) {
final images = widget.message.attachments final images = widget.message.attachments
?.where((element) => ?.where((element) =>
element.type == 'image' && element.ogScrapeUrl == null) element.type == 'image' && element.ogScrapeUrl == null)
@@ -793,8 +794,9 @@ class _MessageWidgetState extends State<MessageWidget> {
[]; [];
if (images.length > 1) { if (images.length > 1) {
return [ return Padding(
wrapAttachmentWidget( padding: widget.attachmentPadding,
child: wrapAttachmentWidget(
context, context,
Material( Material(
color: widget.messageTheme.messageBackgroundColor, color: widget.messageTheme.messageBackgroundColor,
@@ -809,30 +811,36 @@ class _MessageWidgetState extends State<MessageWidget> {
), ),
), ),
), ),
]; );
} }
return widget.message.attachments return Padding(
?.where((element) => element.ogScrapeUrl == null) padding: widget.attachmentPadding,
?.map((attachment) { child: Column(
final attachmentBuilder = widget.attachmentBuilders[attachment.type]; mainAxisSize: MainAxisSize.min,
children: widget.message.attachments
?.where((element) => element.ogScrapeUrl == null)
?.map((attachment) {
final attachmentBuilder =
widget.attachmentBuilders[attachment.type];
if (attachmentBuilder == null) { if (attachmentBuilder == null) return SizedBox();
return SizedBox(); final attachmentWidget = attachmentBuilder(
} context,
widget.message,
final attachmentWidget = attachmentBuilder( attachment,
context, );
widget.message, return wrapAttachmentWidget(
attachment, context,
); attachmentWidget,
return wrapAttachmentWidget( attachment: attachment,
context, );
attachmentWidget, })?.insertBetween(SizedBox(
attachment: attachment, height: widget.attachmentPadding.vertical / 2,
); )) ??
})?.toList() ?? [],
[]; ),
);
} }
Widget wrapAttachmentWidget( Widget wrapAttachmentWidget(
@@ -843,21 +851,13 @@ class _MessageWidgetState extends State<MessageWidget> {
final attachmentShape = final attachmentShape =
widget.attachmentShape ?? widget.shape ?? _getDefaultShape(context); widget.attachmentShape ?? widget.shape ?? _getDefaultShape(context);
return Material( return Material(
color: _getBackgroundColor(),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
shape: attachmentShape, shape: attachmentShape,
child: Padding( type: MaterialType.transparency,
padding: widget.attachmentPadding, child: Transform(
child: Material( transform: Matrix4.rotationY(widget.reverse ? pi : 0),
clipBehavior: Clip.hardEdge, alignment: Alignment.center,
shape: attachmentShape, child: attachmentWidget,
type: MaterialType.transparency,
child: Transform(
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
alignment: Alignment.center,
child: attachmentWidget,
),
),
), ),
); );
} }
@@ -944,11 +944,7 @@ class _MessageWidgetState extends State<MessageWidget> {
: widget.messageTheme, : widget.messageTheme,
), ),
), ),
if (widget.message.attachments if (hasUrlAttachments && !hasQuotedMessage) _buildUrlAttachment(),
?.any((element) => element.ogScrapeUrl != null) ==
true &&
!hasQuotedMessage)
_buildUrlAttachment(),
], ],
), ),
); );
@@ -961,9 +957,7 @@ class _MessageWidgetState extends State<MessageWidget> {
return widget.messageTheme.messageBackgroundColor; return widget.messageTheme.messageBackgroundColor;
} }
if (widget.message.attachments if (hasUrlAttachments) {
?.any((element) => element.ogScrapeUrl != null) ==
true) {
return StreamChatTheme.of(context).colorTheme.blueAlice; return StreamChatTheme.of(context).colorTheme.blueAlice;
} }
+16 -10
View File
@@ -83,6 +83,8 @@ class QuotedMessageWidget extends StatelessWidget {
final Map<String, QuotedMessageAttachmentThumbnailBuilder> final Map<String, QuotedMessageAttachmentThumbnailBuilder>
attachmentThumbnailBuilders; attachmentThumbnailBuilders;
final EdgeInsetsGeometry padding;
final GestureTapCallback onTap; final GestureTapCallback onTap;
/// ///
@@ -94,6 +96,7 @@ class QuotedMessageWidget extends StatelessWidget {
this.showBorder = false, this.showBorder = false,
this.textLimit = 170, this.textLimit = 170,
this.attachmentThumbnailBuilders, this.attachmentThumbnailBuilders,
this.padding = const EdgeInsets.all(8),
this.onTap, this.onTap,
}) : super(key: key); }) : super(key: key);
@@ -107,16 +110,19 @@ class QuotedMessageWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return InkWell( return Padding(
onTap: onTap, padding: padding,
child: Row( child: InkWell(
crossAxisAlignment: CrossAxisAlignment.end, onTap: onTap,
mainAxisSize: MainAxisSize.min, child: Row(
children: [ crossAxisAlignment: CrossAxisAlignment.end,
Flexible(child: _buildMessage(context)), mainAxisSize: MainAxisSize.min,
SizedBox(width: 8), children: [
_buildUserAvatar(), Flexible(child: _buildMessage(context)),
], SizedBox(width: 8),
_buildUserAvatar(),
],
),
), ),
); );
} }
+99 -42
View File
@@ -12,17 +12,21 @@ class ReactionBubble extends StatelessWidget {
@required this.reactions, @required this.reactions,
@required this.borderColor, @required this.borderColor,
@required this.backgroundColor, @required this.backgroundColor,
@required this.maskColor,
this.reverse = false, this.reverse = false,
this.flipTail = false, this.flipTail = false,
this.highlightOwnReactions = true, this.highlightOwnReactions = true,
this.tailCirclesSpacing = 0,
}) : super(key: key); }) : super(key: key);
final List<Reaction> reactions; final List<Reaction> reactions;
final Color borderColor; final Color borderColor;
final Color backgroundColor; final Color backgroundColor;
final Color maskColor;
final bool reverse; final bool reverse;
final bool flipTail; final bool flipTail;
final bool highlightOwnReactions; final bool highlightOwnReactions;
final double tailCirclesSpacing;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -38,51 +42,58 @@ class ReactionBubble extends StatelessWidget {
Transform.translate( Transform.translate(
offset: Offset(reverse ? offset : -offset, 0), offset: Offset(reverse ? offset : -offset, 0),
child: Container( child: Container(
padding: EdgeInsets.symmetric( padding: const EdgeInsets.all(2),
vertical: 4,
horizontal: totalReactions > 1 ? 4 : 0,
),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all( color: maskColor,
color: borderColor, borderRadius: BorderRadius.all(Radius.circular(16)),
),
color: backgroundColor,
borderRadius: BorderRadius.all(Radius.circular(14)),
), ),
child: LayoutBuilder( child: Container(
builder: (context, constraints) { padding: EdgeInsets.symmetric(
return Flex( vertical: 4,
direction: Axis.horizontal, horizontal: totalReactions > 1 ? 4 : 0,
mainAxisSize: MainAxisSize.min, ),
children: [ decoration: BoxDecoration(
if (constraints.maxWidth < double.infinity) border: Border.all(
...reactions color: borderColor,
.take((constraints.maxWidth) ~/ 22) ),
.map((reaction) { color: backgroundColor,
return _buildReaction( borderRadius: BorderRadius.all(Radius.circular(14)),
reactionIcons, ),
reaction, child: LayoutBuilder(
context, builder: (context, constraints) {
); return Flex(
}).toList(), direction: Axis.horizontal,
if (constraints.maxWidth == double.infinity) mainAxisSize: MainAxisSize.min,
...reactions.map((reaction) { children: [
return _buildReaction( if (constraints.maxWidth < double.infinity)
reactionIcons, ...reactions
reaction, .take((constraints.maxWidth) ~/ 22)
context, .map((reaction) {
); return _buildReaction(
}).toList(), reactionIcons,
], reaction,
); context,
}, );
}).toList(),
if (constraints.maxWidth == double.infinity)
...reactions.map((reaction) {
return _buildReaction(
reactionIcons,
reaction,
context,
);
}).toList(),
],
);
},
),
), ),
), ),
), ),
Positioned( Positioned(
bottom: 0, bottom: 2,
left: reverse ? null : 11, left: reverse ? null : 13,
right: !reverse ? null : 11, right: !reverse ? null : 13,
child: _buildReactionsTail(context), child: _buildReactionsTail(context),
), ),
], ],
@@ -136,6 +147,8 @@ class ReactionBubble extends StatelessWidget {
painter: ReactionBubblePainter( painter: ReactionBubblePainter(
backgroundColor, backgroundColor,
borderColor, borderColor,
maskColor,
tailCirclesSpace: tailCirclesSpacing,
), ),
); );
return Transform( return Transform(
@@ -149,14 +162,22 @@ class ReactionBubble extends StatelessWidget {
class ReactionBubblePainter extends CustomPainter { class ReactionBubblePainter extends CustomPainter {
final Color color; final Color color;
final Color borderColor; final Color borderColor;
final Color maskColor;
final double tailCirclesSpace;
ReactionBubblePainter( ReactionBubblePainter(
this.color, this.color,
this.borderColor, this.borderColor,
); this.maskColor, {
this.tailCirclesSpace = 0,
});
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
_drawOvalMask(size, canvas);
_drawMask(size, canvas);
_drawOval(size, canvas); _drawOval(size, canvas);
_drawOvalBorder(size, canvas); _drawOvalBorder(size, canvas);
@@ -166,6 +187,21 @@ class ReactionBubblePainter extends CustomPainter {
_drawBorder(size, canvas); _drawBorder(size, canvas);
} }
void _drawOvalMask(Size size, Canvas canvas) {
final paint = Paint()
..color = maskColor
..style = PaintingStyle.fill;
final path = Path();
path.addOval(
Rect.fromCircle(
center: Offset(4, 3) + Offset(tailCirclesSpace, tailCirclesSpace),
radius: 4,
),
);
canvas.drawPath(path, paint);
}
void _drawOvalBorder(Size size, Canvas canvas) { void _drawOvalBorder(Size size, Canvas canvas) {
final paint = Paint() final paint = Paint()
..color = borderColor ..color = borderColor
@@ -175,7 +211,7 @@ class ReactionBubblePainter extends CustomPainter {
final path = Path(); final path = Path();
path.addOval( path.addOval(
Rect.fromCircle( Rect.fromCircle(
center: Offset(4, 3), center: Offset(4, 3) + Offset(tailCirclesSpace, tailCirclesSpace),
radius: 2, radius: 2,
), ),
); );
@@ -189,7 +225,7 @@ class ReactionBubblePainter extends CustomPainter {
final path = Path(); final path = Path();
path.addOval(Rect.fromCircle( path.addOval(Rect.fromCircle(
center: Offset(4, 3), center: Offset(4, 3) + Offset(tailCirclesSpace, tailCirclesSpace),
radius: 2, radius: 2,
)); ));
canvas.drawPath(path, paint); canvas.drawPath(path, paint);
@@ -236,6 +272,27 @@ class ReactionBubblePainter extends CustomPainter {
canvas.drawPath(path, paint); canvas.drawPath(path, paint);
} }
void _drawMask(Size size, Canvas canvas) {
final paint = Paint()
..color = maskColor
..strokeWidth = 1
..style = PaintingStyle.fill;
final dy = -2.2;
final startAngle = 1.1;
final sweepAngle = 1.2;
final path = Path();
path.addArc(
Rect.fromCircle(
center: Offset(1, dy),
radius: 6,
),
-pi * startAngle,
-pi / sweepAngle,
);
canvas.drawPath(path, paint);
}
@override @override
bool shouldRepaint(CustomPainter oldDelegate) { bool shouldRepaint(CustomPainter oldDelegate) {
return true; return true;
+7
View File
@@ -234,6 +234,7 @@ class StreamChatThemeData {
messageBackgroundColor: colorTheme.greyGainsboro, messageBackgroundColor: colorTheme.greyGainsboro,
reactionsBackgroundColor: colorTheme.white, reactionsBackgroundColor: colorTheme.white,
reactionsBorderColor: colorTheme.greyWhisper, reactionsBorderColor: colorTheme.greyWhisper,
reactionsMaskColor: colorTheme.whiteSnow,
messageBorderColor: colorTheme.greyGainsboro, messageBorderColor: colorTheme.greyGainsboro,
avatarTheme: AvatarTheme( avatarTheme: AvatarTheme(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
@@ -249,6 +250,7 @@ class StreamChatThemeData {
otherMessageTheme: MessageTheme( otherMessageTheme: MessageTheme(
reactionsBackgroundColor: colorTheme.greyGainsboro, reactionsBackgroundColor: colorTheme.greyGainsboro,
reactionsBorderColor: colorTheme.white, reactionsBorderColor: colorTheme.white,
reactionsMaskColor: colorTheme.whiteSnow,
messageText: textTheme.body, messageText: textTheme.body,
createdAt: textTheme.footnote.copyWith(color: colorTheme.grey), createdAt: textTheme.footnote.copyWith(color: colorTheme.grey),
replies: textTheme.footnoteBold.copyWith(color: accentColor), replies: textTheme.footnoteBold.copyWith(color: accentColor),
@@ -710,6 +712,7 @@ class MessageTheme {
final Color messageBorderColor; final Color messageBorderColor;
final Color reactionsBackgroundColor; final Color reactionsBackgroundColor;
final Color reactionsBorderColor; final Color reactionsBorderColor;
final Color reactionsMaskColor;
final AvatarTheme avatarTheme; final AvatarTheme avatarTheme;
const MessageTheme({ const MessageTheme({
@@ -721,6 +724,7 @@ class MessageTheme {
this.messageBorderColor, this.messageBorderColor,
this.reactionsBackgroundColor, this.reactionsBackgroundColor,
this.reactionsBorderColor, this.reactionsBorderColor,
this.reactionsMaskColor,
this.avatarTheme, this.avatarTheme,
this.createdAt, this.createdAt,
}); });
@@ -736,6 +740,7 @@ class MessageTheme {
AvatarTheme avatarTheme, AvatarTheme avatarTheme,
Color reactionsBackgroundColor, Color reactionsBackgroundColor,
Color reactionsBorderColor, Color reactionsBorderColor,
Color reactionsMaskColor,
}) => }) =>
MessageTheme( MessageTheme(
messageText: messageText ?? this.messageText, messageText: messageText ?? this.messageText,
@@ -750,6 +755,7 @@ class MessageTheme {
reactionsBackgroundColor: reactionsBackgroundColor:
reactionsBackgroundColor ?? this.reactionsBackgroundColor, reactionsBackgroundColor ?? this.reactionsBackgroundColor,
reactionsBorderColor: reactionsBorderColor ?? this.reactionsBorderColor, reactionsBorderColor: reactionsBorderColor ?? this.reactionsBorderColor,
reactionsMaskColor: reactionsMaskColor ?? this.reactionsMaskColor,
); );
MessageTheme merge(MessageTheme other) { MessageTheme merge(MessageTheme other) {
@@ -767,6 +773,7 @@ class MessageTheme {
avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme, avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme,
reactionsBackgroundColor: other.reactionsBackgroundColor, reactionsBackgroundColor: other.reactionsBackgroundColor,
reactionsBorderColor: other.reactionsBorderColor, reactionsBorderColor: other.reactionsBorderColor,
reactionsMaskColor: other.reactionsMaskColor,
); );
} }
} }
+4 -4
View File
@@ -92,18 +92,18 @@ class UserAvatar extends StatelessWidget {
child: Material( child: Material(
type: MaterialType.circle, type: MaterialType.circle,
child: Container( child: Container(
padding: const EdgeInsets.all(2.0), margin: const EdgeInsets.all(2.0),
constraints: onlineIndicatorConstraints ?? constraints: onlineIndicatorConstraints ??
BoxConstraints.tightFor( BoxConstraints.tightFor(
width: 12, width: 8,
height: 12, height: 8,
), ),
child: Material( child: Material(
shape: CircleBorder(), shape: CircleBorder(),
color: streamChatTheme.colorTheme.accentGreen, color: streamChatTheme.colorTheme.accentGreen,
), ),
), ),
color: StreamChatTheme.of(context).colorTheme.white, color: streamChatTheme.colorTheme.white,
), ),
), ),
), ),
+3
View File
@@ -19,6 +19,7 @@ void main() {
reactions: [], reactions: [],
borderColor: Colors.black, borderColor: Colors.black,
backgroundColor: Colors.white, backgroundColor: Colors.white,
maskColor: Colors.white,
), ),
), ),
), ),
@@ -56,6 +57,7 @@ void main() {
], ],
borderColor: Colors.black, borderColor: Colors.black,
backgroundColor: Colors.white, backgroundColor: Colors.white,
maskColor: Colors.white,
), ),
), ),
), ),
@@ -96,6 +98,7 @@ void main() {
], ],
borderColor: Colors.black, borderColor: Colors.black,
backgroundColor: Colors.white, backgroundColor: Colors.white,
maskColor: Colors.white,
), ),
), ),
), ),