Files
stream-chat-flutter/lib/src/message_widget.dart
T
Salvatore Giordano ca4bef0ad0 add reactions
2020-02-25 18:10:58 +01:00

469 lines
15 KiB
Dart

import 'package:cached_network_image/cached_network_image.dart';
import 'package:chewie/chewie.dart';
import 'package:date_format/date_format.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/stream_channel.dart';
import 'package:stream_chat_flutter/src/user_avatar.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:video_player/video_player.dart';
import 'stream_chat.dart';
class MessageWidget extends StatefulWidget {
const MessageWidget({
Key key,
@required this.previousMessage,
@required this.message,
@required this.nextMessage,
this.onThreadTap,
}) : super(key: key);
final Message previousMessage;
final Message message;
final Message nextMessage;
final void Function(Message) onThreadTap;
@override
_MessageWidgetState createState() => _MessageWidgetState();
}
class _MessageWidgetState extends State<MessageWidget>
with AutomaticKeepAliveClientMixin {
final Map<String, ChangeNotifier> _videoControllers = {};
final Map<String, ChangeNotifier> _chuwieControllers = {};
@override
Widget build(BuildContext context) {
super.build(context);
final streamChat = StreamChat.of(context);
final currentUserId = streamChat.user.id;
final messageUserId = widget.message.user.id;
final previousUserId = widget.previousMessage?.user?.id;
final nextUserId = widget.nextMessage?.user?.id;
final isMyMessage = messageUserId == currentUserId;
final isLastUser = previousUserId == messageUserId;
final isNextUser = nextUserId == messageUserId;
final alignment =
isMyMessage ? Alignment.centerRight : Alignment.centerLeft;
var row = <Widget>[
Column(
crossAxisAlignment:
isMyMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: <Widget>[
_buildBubble(context, isMyMessage, isLastUser),
_buildThreadIndicator(context),
isNextUser ? SizedBox() : _buildTimestamp(isMyMessage, alignment),
],
),
isNextUser
? Container(
width: 40,
)
: Padding(
padding: EdgeInsets.only(
left: isMyMessage ? 8.0 : 0,
right: isMyMessage ? 0 : 8.0,
),
child: UserAvatar(user: widget.message.user),
),
];
if (!isMyMessage) {
row = row.reversed.toList();
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10.0),
margin: EdgeInsets.only(
top: isLastUser ? 5 : 24,
bottom: widget.nextMessage == null ? 30 : 0,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment:
isMyMessage ? MainAxisAlignment.end : MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: row,
),
);
}
Widget _buildThreadIndicator(BuildContext context) {
return widget.message.replyCount > 0
? GestureDetector(
onTap: () {
if (widget.onThreadTap != null) {
widget.onThreadTap(widget.message);
}
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0),
child: Row(
children: <Widget>[
Text(
'Replies: ${widget.message.replyCount}',
style: Theme.of(context)
.textTheme
.subtitle
.copyWith(color: Colors.blue),
),
Icon(
Icons.subdirectory_arrow_left,
color: Colors.black12,
),
],
),
),
)
: SizedBox();
}
Widget _buildBubble(
BuildContext context,
bool isMyMessage,
bool isLastUser,
) {
var nOfAttachmentWidgets = 0;
final column = Column(
crossAxisAlignment:
isMyMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: List<Widget>.from(widget.message.attachments.map((attachment) {
nOfAttachmentWidgets++;
Widget attachmentWidget;
if (attachment.type == 'video') {
attachmentWidget = _buildVideo(attachment, isMyMessage, isLastUser);
} else if (attachment.type == 'image' || attachment.type == 'giphy') {
attachmentWidget = _buildImage(isMyMessage, isLastUser, attachment);
}
if (attachmentWidget != null) {
final boxDecoration = _buildBoxDecoration(isMyMessage, isLastUser)
.copyWith(color: Color(0xffebebeb));
return ClipRRect(
borderRadius: boxDecoration.borderRadius,
child: Container(
decoration: boxDecoration,
constraints: BoxConstraints.loose(Size.fromWidth(300)),
child: Stack(
children: <Widget>[
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
attachmentWidget,
attachment.title != null
? Container(
constraints:
BoxConstraints.loose(Size.fromHeight(70)),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
attachment.title,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.subtitle
.copyWith(color: Colors.blue),
),
Text(
Uri.parse(attachment.thumbUrl)
.authority
.split('.')
.reversed
.take(2)
.toList()
.reversed
.join('.'),
overflow: TextOverflow.ellipsis,
style:
Theme.of(context).textTheme.caption,
),
],
),
),
color: Color(0xffebebeb),
)
: Container(),
],
),
attachment.type == 'image' && attachment.titleLink != null
? Positioned.fill(
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => _launchURL(attachment.titleLink),
),
),
)
: SizedBox(),
],
),
margin: EdgeInsets.only(
top: nOfAttachmentWidgets > 1 ? 5 : 0,
),
),
);
}
nOfAttachmentWidgets--;
return SizedBox();
})),
);
if (widget.message.text.trim().isNotEmpty) {
final topPadding = widget.message.latestReactions.isNotEmpty ? 36.0 : 0.0;
column.children.add(Stack(
overflow: Overflow.visible,
children: <Widget>[
widget.message.latestReactions.isNotEmpty
? Positioned(
left: isMyMessage ? 0 : null,
right: !isMyMessage ? 0 : null,
top: 0,
child: _buildReactions(isMyMessage))
: SizedBox(),
Container(
margin: EdgeInsets.only(
top: nOfAttachmentWidgets > 0 ? (topPadding) : (topPadding),
),
decoration: _buildBoxDecoration(
isMyMessage, isLastUser || nOfAttachmentWidgets > 0),
padding: EdgeInsets.all(10),
constraints: BoxConstraints.loose(Size.fromWidth(300)),
child: MarkdownBody(
data: '${widget.message.text}',
onTapLink: (link) {
_launchURL(link);
},
styleSheet: MarkdownStyleSheet.fromTheme(Theme.of(context)),
),
),
],
));
}
return GestureDetector(
child: column,
onLongPress: () {
showModalBottomSheet(
context: context,
builder: (context) {
final fontSize = 40.0;
final textStyle = TextStyle(
fontSize: fontSize,
);
return Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: reactionToEmoji.keys.map((reactionType) {
return IconButton(
iconSize: fontSize + 10,
icon: Text(
reactionToEmoji[reactionType],
style: textStyle,
),
onPressed: () {
sendReaction(reactionType);
},
);
}).toList(),
);
});
});
}
Widget _buildReactions(bool isMyMessage) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0),
child: Stack(
overflow: Overflow.visible,
children: <Widget>[
widget.message.latestReactions.isNotEmpty
? Positioned(
left: isMyMessage ? 0 : null,
right: !isMyMessage ? 0 : null,
bottom: 1,
child: CustomPaint(
painter: ReactionBubblePainter(),
),
)
: SizedBox(),
AnimatedContainer(
transform: Matrix4.translationValues(
-16 * (isMyMessage ? 1.0 : -1.0), 0, 0),
padding: widget.message.latestReactions.isNotEmpty
? const EdgeInsets.all(8)
: EdgeInsets.zero,
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.all(Radius.circular(14))),
duration: Duration(milliseconds: 300),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[]
..addAll(widget.message.latestReactions.map((reaction) {
return Text(reactionToEmoji[reaction.type] ?? '?');
})),
),
),
],
),
);
}
final Map<String, String> reactionToEmoji = {
'love': '♥️',
'haha': '😂',
'like': '👍',
'sad': '😕',
'angry': '😡',
'wow': '😲',
};
void sendReaction(String reactionType) {
StreamChannel.of(context)
.channelClient
.sendReaction(widget.message.id, reactionType);
Navigator.of(context).pop();
}
Widget _buildImage(
bool isMyMessage,
bool isLastUser,
Attachment attachment,
) {
return CachedNetworkImage(
imageUrl: attachment.thumbUrl ?? attachment.imageUrl,
fit: BoxFit.cover,
);
}
Widget _buildVideo(
Attachment attachment,
bool isMyMessage,
bool isLastUser,
) {
VideoPlayerController videoController;
if (_videoControllers.containsKey(attachment.assetUrl)) {
videoController = _videoControllers[attachment.assetUrl];
} else {
videoController = VideoPlayerController.network(attachment.assetUrl);
_videoControllers[attachment.assetUrl] = videoController;
}
ChewieController chewieController;
if (_chuwieControllers.containsKey(attachment.assetUrl)) {
chewieController = _chuwieControllers[attachment.assetUrl];
} else {
chewieController = ChewieController(
videoPlayerController: videoController,
autoInitialize: true,
errorBuilder: (_, e) {
return Stack(
children: <Widget>[
Container(
decoration: BoxDecoration(
image: DecorationImage(
fit: BoxFit.cover,
image: CachedNetworkImageProvider(
attachment.thumbUrl,
),
),
),
),
Material(
color: Colors.transparent,
child: InkWell(
onTap: () => _launchURL(attachment.titleLink),
),
),
],
);
});
_chuwieControllers[attachment.assetUrl] = chewieController;
}
return Chewie(
key: ValueKey<String>(
'ATTACHMENT-${attachment.title}-${widget.message.id}'),
controller: chewieController,
);
}
Future<void> _launchURL(String url) async {
if (await canLaunch(url)) {
await launch(url);
} else {
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text('Cannot launch the url'),
),
);
}
}
@override
void dispose() {
_videoControllers.values.forEach((element) {
element.dispose();
});
super.dispose();
}
Widget _buildTimestamp(bool isMyMessage, Alignment alignment) {
return Padding(
padding: const EdgeInsets.only(top: 5.0),
child: Text(
formatDate(widget.message.createdAt.toLocal(), [HH, ':', nn]),
),
);
}
BoxDecoration _buildBoxDecoration(bool isMyMessage, bool isLastUser) {
return BoxDecoration(
border: isMyMessage ? null : Border.all(color: Colors.black.withAlpha(8)),
borderRadius: BorderRadius.only(
topLeft: Radius.circular((isMyMessage || !isLastUser) ? 16 : 2),
bottomLeft: Radius.circular(isMyMessage ? 16 : 2),
topRight: Radius.circular((isMyMessage && isLastUser) ? 2 : 16),
bottomRight: Radius.circular(isMyMessage ? 2 : 16),
),
color: isMyMessage ? Color(0xffebebeb) : Colors.white,
);
}
@override
bool get wantKeepAlive {
return widget.message.attachments.isNotEmpty;
}
}
class ReactionBubblePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()..color = Colors.black;
final path = Path();
path.arcToPoint(Offset(-5, 0));
path.arcToPoint(Offset(0, 10), radius: Radius.circular(16));
path.arcToPoint(Offset(5, 0), radius: Radius.circular(16));
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}