469 lines
15 KiB
Dart
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;
|
|
}
|
|
}
|