1206 lines
38 KiB
Dart
1206 lines
38 KiB
Dart
import 'dart:io';
|
||
import 'dart:math';
|
||
|
||
import 'package:cached_network_image/cached_network_image.dart';
|
||
import 'package:chewie/chewie.dart';
|
||
import 'package:flutter/foundation.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter/widgets.dart';
|
||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||
import 'package:jiffy/jiffy.dart';
|
||
import 'package:stream_chat/stream_chat.dart';
|
||
import 'package:stream_chat_flutter/src/full_screen_image.dart';
|
||
import 'package:stream_chat_flutter/src/message_input.dart';
|
||
import 'package:stream_chat_flutter/src/message_list_view.dart';
|
||
import 'package:stream_chat_flutter/src/reaction_picker.dart';
|
||
import 'package:stream_chat_flutter/src/stream_channel.dart';
|
||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||
import 'package:stream_chat_flutter/src/user_avatar.dart';
|
||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||
import 'package:url_launcher/url_launcher.dart';
|
||
import 'package:video_player/video_player.dart';
|
||
|
||
import 'stream_chat.dart';
|
||
|
||
/// 
|
||
/// 
|
||
///
|
||
/// It shows a message with reactions, replies and user avatar.
|
||
///
|
||
/// Usually you don't use this widget as it's the default message widget used by [MessageListView].
|
||
///
|
||
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme].
|
||
/// Modify it to change the widget appearance.
|
||
class MessageWidget extends StatefulWidget {
|
||
/// Instantiate a new MessageWidget
|
||
const MessageWidget({
|
||
Key key,
|
||
@required this.previousMessage,
|
||
@required this.message,
|
||
@required this.nextMessage,
|
||
this.onThreadTap,
|
||
this.onMessageActions,
|
||
this.isParent = false,
|
||
this.onMentionTap,
|
||
this.showOtherMessageUsername = false,
|
||
this.showVideoFullScreen = true,
|
||
}) : super(key: key);
|
||
|
||
/// Function called on mention tap
|
||
final void Function(User) onMentionTap;
|
||
|
||
/// Function called on long press
|
||
final Function(BuildContext, Message) onMessageActions;
|
||
|
||
/// If true show the other users username next to the timestamp of the message
|
||
final bool showOtherMessageUsername;
|
||
|
||
/// This message
|
||
final Message message;
|
||
|
||
/// The previous message
|
||
final Message previousMessage;
|
||
|
||
/// The next message
|
||
final Message nextMessage;
|
||
|
||
/// The function called when tapping on replies
|
||
final void Function(Message) onThreadTap;
|
||
|
||
/// True if this is the parent of the thread being showed
|
||
final bool isParent;
|
||
|
||
/// True if the video player will allow fullscreen mode
|
||
final bool showVideoFullScreen;
|
||
|
||
@override
|
||
_MessageWidgetState createState() => _MessageWidgetState();
|
||
}
|
||
|
||
class _MessageWidgetState extends State<MessageWidget>
|
||
with AutomaticKeepAliveClientMixin, TickerProviderStateMixin {
|
||
final Map<String, ChangeNotifier> _videoControllers = {};
|
||
final Map<String, ChangeNotifier> _chuwieControllers = {};
|
||
|
||
MessageTheme _messageTheme;
|
||
StreamChatState _streamChat;
|
||
StreamChannelState _streamChannel;
|
||
bool _isMyMessage;
|
||
|
||
String _currentUserId;
|
||
String _messageUserId;
|
||
String _previousUserId;
|
||
String _nextUserId;
|
||
bool _isLastUser;
|
||
bool _isNextUser;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
super.build(context);
|
||
|
||
_messageTheme = _isMyMessage
|
||
? StreamChatTheme.of(context).ownMessageTheme
|
||
: StreamChatTheme.of(context).otherMessageTheme;
|
||
|
||
final alignment =
|
||
_isMyMessage ? Alignment.centerRight : Alignment.centerLeft;
|
||
|
||
var row = List<Widget>.from([
|
||
Column(
|
||
crossAxisAlignment:
|
||
_isMyMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start,
|
||
children: <Widget>[
|
||
(widget.message.isDeleted &&
|
||
widget.message.status != MessageSendingStatus.FAILED_DELETE)
|
||
? _buildDeletedMessage(alignment)
|
||
: _buildBubble(context),
|
||
if (_streamChannel.channel.config?.replies == true)
|
||
_buildThreadIndicator(context),
|
||
if (!_isNextUser) _buildTimestamp(alignment),
|
||
],
|
||
),
|
||
_isNextUser
|
||
? Container(
|
||
width: 40,
|
||
)
|
||
: _buildUserAvatar(),
|
||
]);
|
||
|
||
if (!_isMyMessage) {
|
||
row = row.reversed.toList();
|
||
}
|
||
|
||
return Container(
|
||
padding: EdgeInsets.symmetric(
|
||
horizontal: (_isMyMessage && widget.nextMessage == null) ? 0.0 : 10,
|
||
),
|
||
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,
|
||
),
|
||
);
|
||
}
|
||
|
||
Padding _buildUserAvatar() {
|
||
return Padding(
|
||
padding: EdgeInsets.only(
|
||
left: _isMyMessage ? 8.0 : 0,
|
||
right: _isMyMessage ? 0 : 8.0,
|
||
),
|
||
child: Row(
|
||
children: <Widget>[
|
||
UserAvatar(user: widget.message.user),
|
||
if (_isMyMessage &&
|
||
widget.nextMessage == null &&
|
||
(widget.message.status == MessageSendingStatus.SENT ||
|
||
widget.message.status == null))
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 1.0,
|
||
),
|
||
child: CircleAvatar(
|
||
radius: 4,
|
||
backgroundColor: StreamChatTheme.of(context).accentColor,
|
||
child: Icon(
|
||
Icons.done,
|
||
color: Colors.white,
|
||
size: 4,
|
||
),
|
||
),
|
||
),
|
||
if (_isMyMessage &&
|
||
(widget.message.status == MessageSendingStatus.SENDING ||
|
||
widget.message.status == MessageSendingStatus.UPDATING))
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 1.0,
|
||
),
|
||
child: CircleAvatar(
|
||
radius: 4,
|
||
backgroundColor: Colors.grey,
|
||
child: Icon(
|
||
Icons.access_time,
|
||
size: 4,
|
||
color: Colors.white,
|
||
),
|
||
),
|
||
),
|
||
if (_isMyMessage &&
|
||
(widget.message.status == MessageSendingStatus.FAILED ||
|
||
widget.message.status == MessageSendingStatus.FAILED_UPDATE ||
|
||
widget.message.status == MessageSendingStatus.FAILED_DELETE))
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 1.0,
|
||
),
|
||
child: CircleAvatar(
|
||
radius: 4,
|
||
backgroundColor: Color(0xffd0021B).withOpacity(.1),
|
||
child: Icon(
|
||
Icons.error_outline,
|
||
size: 4,
|
||
color: Colors.white,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
|
||
_streamChat = StreamChat.of(context);
|
||
_streamChannel = StreamChannel.of(context);
|
||
|
||
_currentUserId = _streamChat.client.state.user.id;
|
||
_messageUserId = widget.message.user.id;
|
||
_previousUserId = widget.previousMessage?.user?.id;
|
||
_nextUserId = widget.nextMessage?.user?.id;
|
||
_isLastUser = _previousUserId == _messageUserId;
|
||
_isNextUser = _nextUserId == _messageUserId;
|
||
|
||
_isMyMessage = _messageUserId == _currentUserId;
|
||
}
|
||
|
||
Align _buildDeletedMessage(Alignment alignment) {
|
||
return Align(
|
||
alignment: alignment,
|
||
child: Padding(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 14,
|
||
vertical: 14,
|
||
),
|
||
child: Text(
|
||
'This message was deleted...',
|
||
style: _messageTheme.messageText.copyWith(
|
||
fontStyle: FontStyle.italic,
|
||
color: Theme.of(context).brightness == Brightness.dark
|
||
? Colors.white
|
||
: Colors.black,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildThreadIndicator(BuildContext context) {
|
||
var row = [
|
||
Text(
|
||
'Replies: ${widget.message.replyCount}',
|
||
style: _messageTheme.replies,
|
||
),
|
||
Transform(
|
||
transform: Matrix4.rotationY(_isMyMessage ? 0 : pi),
|
||
alignment: Alignment.center,
|
||
child: Icon(
|
||
Icons.subdirectory_arrow_left,
|
||
color: Theme.of(context).brightness == Brightness.dark
|
||
? Colors.white12
|
||
: Colors.black12,
|
||
),
|
||
),
|
||
];
|
||
|
||
if (!_isMyMessage) {
|
||
row = row.reversed.toList();
|
||
}
|
||
|
||
return (widget.message.replyCount ?? 0) > 0
|
||
? GestureDetector(
|
||
onTap: () {
|
||
if (widget.isParent) {
|
||
return;
|
||
}
|
||
if (widget.onThreadTap != null) {
|
||
widget.onThreadTap(widget.message);
|
||
}
|
||
},
|
||
child: Padding(
|
||
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
||
child: Row(
|
||
children: row,
|
||
),
|
||
),
|
||
)
|
||
: SizedBox();
|
||
}
|
||
|
||
Widget _buildBubble(
|
||
BuildContext context,
|
||
) {
|
||
var nOfAttachmentWidgets = 0;
|
||
|
||
final column =
|
||
List<Widget>.from(widget.message.attachments?.map((attachment) {
|
||
nOfAttachmentWidgets++;
|
||
|
||
Widget attachmentWidget;
|
||
if (attachment.type == 'video') {
|
||
attachmentWidget = _buildVideo(attachment);
|
||
} else if (attachment.type == 'image' ||
|
||
attachment.type == 'giphy') {
|
||
attachmentWidget = _buildImage(attachment);
|
||
}
|
||
|
||
if (attachmentWidget != null) {
|
||
return _buildAttachment(
|
||
attachmentWidget,
|
||
attachment,
|
||
nOfAttachmentWidgets,
|
||
context,
|
||
);
|
||
}
|
||
|
||
nOfAttachmentWidgets--;
|
||
return SizedBox();
|
||
}) ??
|
||
[]);
|
||
|
||
if (widget.message.text.trim().isNotEmpty) {
|
||
var text = widget.message.text;
|
||
text = _replaceMentions(text);
|
||
|
||
column.addAll(
|
||
<Widget>[
|
||
Column(
|
||
crossAxisAlignment: _isMyMessage
|
||
? CrossAxisAlignment.end
|
||
: CrossAxisAlignment.start,
|
||
children: <Widget>[
|
||
if (_streamChannel.channel.config?.reactions == true &&
|
||
nOfAttachmentWidgets == 0)
|
||
Align(
|
||
child: _buildReactions(),
|
||
alignment: _isMyMessage
|
||
? Alignment.centerLeft
|
||
: Alignment.centerRight,
|
||
),
|
||
Stack(
|
||
overflow: Overflow.visible,
|
||
children: <Widget>[
|
||
if (nOfAttachmentWidgets == 0 &&
|
||
_streamChannel.channel.config?.reactions == true)
|
||
_buildReactionPaint(),
|
||
_buildMessageText(nOfAttachmentWidgets, text, context),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
if (_streamChannel.channel.config?.reactions == true &&
|
||
nOfAttachmentWidgets > 0) {
|
||
column.insert(
|
||
0,
|
||
Align(
|
||
child: _buildReactions(),
|
||
alignment:
|
||
_isMyMessage ? Alignment.centerLeft : Alignment.centerRight,
|
||
),
|
||
);
|
||
column[1] = Stack(
|
||
overflow: Overflow.visible,
|
||
children: <Widget>[
|
||
Padding(
|
||
padding: EdgeInsets.only(
|
||
right: _isMyMessage ? 0.0 : 8.0,
|
||
left: _isMyMessage ? 8.0 : 0.0,
|
||
),
|
||
child: column[1],
|
||
),
|
||
_buildReactionPaint(),
|
||
],
|
||
);
|
||
}
|
||
|
||
return GestureDetector(
|
||
child: IntrinsicWidth(
|
||
child: Column(
|
||
children: column,
|
||
crossAxisAlignment:
|
||
_isMyMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start,
|
||
),
|
||
),
|
||
onTap: () {
|
||
final channel = StreamChannel.of(context).channel;
|
||
if (widget.message.status == MessageSendingStatus.FAILED) {
|
||
channel.sendMessage(widget.message);
|
||
return;
|
||
}
|
||
if (widget.message.status == MessageSendingStatus.FAILED_UPDATE) {
|
||
StreamChat.of(context).client.updateMessage(
|
||
widget.message,
|
||
channel.cid,
|
||
);
|
||
return;
|
||
}
|
||
|
||
if (widget.message.status == MessageSendingStatus.FAILED_DELETE) {
|
||
StreamChat.of(context).client.deleteMessage(
|
||
widget.message,
|
||
channel.cid,
|
||
);
|
||
return;
|
||
}
|
||
},
|
||
onLongPress: () {
|
||
if (widget.message.isEphemeral ||
|
||
widget.message.status == MessageSendingStatus.SENDING) {
|
||
return;
|
||
}
|
||
|
||
if (widget.onMessageActions != null) {
|
||
widget.onMessageActions(context, widget.message);
|
||
} else {
|
||
_showMessageBottomSheet(context);
|
||
}
|
||
},
|
||
);
|
||
}
|
||
|
||
Padding _buildAttachment(
|
||
Widget attachmentWidget,
|
||
Attachment attachment,
|
||
int nOfAttachmentWidgets,
|
||
BuildContext context,
|
||
) {
|
||
final boxDecoration = _buildBoxDecoration(_isLastUser);
|
||
return Padding(
|
||
padding: const EdgeInsets.only(bottom: 2.0),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: <Widget>[
|
||
ClipRRect(
|
||
borderRadius: boxDecoration.borderRadius,
|
||
child: Container(
|
||
decoration: boxDecoration,
|
||
constraints: BoxConstraints.loose(
|
||
Size.fromWidth(MediaQuery.of(context).size.width * 0.7),
|
||
),
|
||
child: Stack(
|
||
children: <Widget>[
|
||
Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: <Widget>[
|
||
attachmentWidget,
|
||
if (attachment.title != null)
|
||
_buildAttachmentTitle(attachment),
|
||
],
|
||
),
|
||
if (attachment.type == 'image' &&
|
||
attachment.titleLink != null)
|
||
_buildPreviewInkwell(attachment),
|
||
],
|
||
),
|
||
margin: EdgeInsets.only(
|
||
top: nOfAttachmentWidgets > 1 ? 5 : 0,
|
||
),
|
||
),
|
||
),
|
||
if (attachment.actions != null)
|
||
_buildAttachmentActions(attachment, context),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildSendingError(Widget child) {
|
||
return Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: <Widget>[
|
||
if (widget.message.status == MessageSendingStatus.FAILED)
|
||
Text(
|
||
'MESSAGE FAILED · CLICK TO TRY AGAIN',
|
||
style: _messageTheme.messageText.copyWith(
|
||
color: Theme.of(context).brightness == Brightness.dark
|
||
? Colors.white.withOpacity(.5)
|
||
: Colors.black.withOpacity(.5),
|
||
fontSize: 11,
|
||
),
|
||
),
|
||
if (widget.message.status == MessageSendingStatus.FAILED_UPDATE)
|
||
Text(
|
||
'MESSAGE UPDATE FAILED · CLICK TO TRY AGAIN',
|
||
style: _messageTheme.messageText.copyWith(
|
||
color: Theme.of(context).brightness == Brightness.dark
|
||
? Colors.white.withOpacity(.5)
|
||
: Colors.black.withOpacity(.5),
|
||
fontSize: 11,
|
||
),
|
||
),
|
||
if (widget.message.status == MessageSendingStatus.FAILED_DELETE)
|
||
Text(
|
||
'MESSAGE DELETE FAILED · CLICK TO TRY AGAIN',
|
||
style: _messageTheme.messageText.copyWith(
|
||
color: Theme.of(context).brightness == Brightness.dark
|
||
? Colors.white.withOpacity(.5)
|
||
: Colors.black.withOpacity(.5),
|
||
fontSize: 11,
|
||
),
|
||
),
|
||
child,
|
||
],
|
||
);
|
||
}
|
||
|
||
Padding _buildMessageText(
|
||
int nOfAttachmentWidgets,
|
||
String text,
|
||
BuildContext context,
|
||
) {
|
||
return Padding(
|
||
padding: EdgeInsets.only(
|
||
right: _isMyMessage ? 0.0 : 8.0,
|
||
left: _isMyMessage ? 8.0 : 0.0,
|
||
),
|
||
child: Container(
|
||
decoration:
|
||
_buildBoxDecoration(_isLastUser || nOfAttachmentWidgets > 0),
|
||
padding: EdgeInsets.all(10),
|
||
constraints: BoxConstraints.loose(
|
||
Size.fromWidth(MediaQuery.of(context).size.width * 0.7),
|
||
),
|
||
child: _buildSendingError(
|
||
MarkdownBody(
|
||
data: text,
|
||
onTapLink: (link) {
|
||
if (link.startsWith('@')) {
|
||
final mentionedUser = widget.message.mentionedUsers.firstWhere(
|
||
(u) => '@${u.name.replaceAll(' ', '')}' == link,
|
||
orElse: () => null,
|
||
);
|
||
|
||
if (widget.onMentionTap != null) {
|
||
widget.onMentionTap(mentionedUser);
|
||
} else {
|
||
print('tap on ${mentionedUser.name}');
|
||
}
|
||
} else {
|
||
_launchURL(link);
|
||
}
|
||
},
|
||
styleSheet: MarkdownStyleSheet.fromTheme(
|
||
Theme.of(context).copyWith(
|
||
textTheme: Theme.of(context).textTheme.apply(
|
||
bodyColor: _messageTheme.messageText.color,
|
||
decoration: _messageTheme.messageText.decoration,
|
||
decorationColor:
|
||
_messageTheme.messageText.decorationColor,
|
||
decorationStyle:
|
||
_messageTheme.messageText.decorationStyle,
|
||
fontFamily: _messageTheme.messageText.fontFamily,
|
||
),
|
||
),
|
||
).copyWith(
|
||
p: _messageTheme.messageText,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
String _replaceMentions(String text) {
|
||
widget.message.mentionedUsers?.forEach((u) {
|
||
text = text.replaceAll(
|
||
'@${u.name}', '[@${u.name}](@${u.name.replaceAll(' ', '')})');
|
||
});
|
||
return text;
|
||
}
|
||
|
||
Row _buildAttachmentActions(Attachment attachment, BuildContext context) {
|
||
return Row(
|
||
mainAxisAlignment: MainAxisAlignment.end,
|
||
children: attachment.actions?.map((action) {
|
||
if (action.style == 'primary') {
|
||
return Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||
child: FlatButton(
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(16),
|
||
),
|
||
child: Text('${action.text}'),
|
||
color: action.style == 'primary'
|
||
? StreamChatTheme.of(context).accentColor
|
||
: null,
|
||
textColor: Colors.white,
|
||
onPressed: () {
|
||
_streamChannel.channel.sendAction(widget.message.id, {
|
||
action.name: action.value,
|
||
});
|
||
},
|
||
),
|
||
);
|
||
}
|
||
return Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||
child: OutlineButton(
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(16),
|
||
),
|
||
child: Text('${action.text}'),
|
||
color: StreamChatTheme.of(context).accentColor,
|
||
onPressed: () {
|
||
_streamChannel.channel.sendAction(widget.message.id, {
|
||
action.name: action.value,
|
||
});
|
||
},
|
||
),
|
||
);
|
||
})?.toList(),
|
||
);
|
||
}
|
||
|
||
Positioned _buildPreviewInkwell(Attachment attachment) {
|
||
return Positioned.fill(
|
||
child: Material(
|
||
color: Colors.transparent,
|
||
child: InkWell(
|
||
onTap: () => _launchURL(attachment.titleLink),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
GestureDetector _buildAttachmentTitle(Attachment attachment) {
|
||
return GestureDetector(
|
||
onTap: () {
|
||
if (attachment.titleLink != null) {
|
||
_launchURL(attachment.titleLink);
|
||
}
|
||
},
|
||
child: Container(
|
||
constraints: BoxConstraints.loose(
|
||
Size(
|
||
MediaQuery.of(context).size.width * 0.7,
|
||
500,
|
||
),
|
||
),
|
||
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: _messageTheme.messageText.copyWith(
|
||
color: StreamChatTheme.of(context).accentColor,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
if (attachment.titleLink != null ||
|
||
attachment.ogScrapeUrl != null)
|
||
Text(
|
||
Uri.parse(attachment.titleLink ?? attachment.ogScrapeUrl)
|
||
.authority
|
||
.split('.')
|
||
.reversed
|
||
.take(2)
|
||
.toList()
|
||
.reversed
|
||
.join('.'),
|
||
overflow: TextOverflow.ellipsis,
|
||
style: _messageTheme.createdAt,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildReactionPaint() {
|
||
return widget.message.reactionCounts?.isNotEmpty == true
|
||
? Positioned(
|
||
left: _isMyMessage ? 8 : null,
|
||
right: !_isMyMessage ? 8 : null,
|
||
top: -6,
|
||
child: CustomPaint(
|
||
painter: _ReactionBubblePainter(
|
||
Theme.of(context).brightness == Brightness.dark
|
||
? Colors.white
|
||
: Colors.black,
|
||
),
|
||
),
|
||
)
|
||
: SizedBox();
|
||
}
|
||
|
||
void _showMessageBottomSheet(BuildContext context) {
|
||
if (!_streamChannel.channel.config.reactions &&
|
||
!_streamChannel.channel.config.replies) {
|
||
return;
|
||
}
|
||
|
||
final theme = Theme.of(context);
|
||
|
||
showModalBottomSheet(
|
||
clipBehavior: Clip.hardEdge,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.only(
|
||
topLeft: Radius.circular(32),
|
||
topRight: Radius.circular(32),
|
||
),
|
||
),
|
||
context: context,
|
||
builder: (_) {
|
||
return SafeArea(
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: <Widget>[
|
||
Container(
|
||
color: Colors.black87,
|
||
child: (_streamChannel.channel.config.reactions &&
|
||
widget.message.status != MessageSendingStatus.FAILED)
|
||
? ReactionPicker(
|
||
channel: StreamChannel.of(context).channel,
|
||
reactionToEmoji: reactionToEmoji,
|
||
message: widget.message,
|
||
)
|
||
: SizedBox(),
|
||
),
|
||
_isMyMessage
|
||
? FlatButton(
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(16.0),
|
||
child: Text(
|
||
'Delete message',
|
||
style: theme.textTheme.headline
|
||
.copyWith(color: Colors.red),
|
||
),
|
||
),
|
||
onPressed: () {
|
||
Navigator.pop(context);
|
||
StreamChat.of(context).client.deleteMessage(
|
||
widget.message,
|
||
_streamChannel.channel.cid,
|
||
);
|
||
},
|
||
)
|
||
: SizedBox(),
|
||
_isMyMessage
|
||
? FlatButton(
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(16.0),
|
||
child: Text(
|
||
'Edit message',
|
||
style: theme.textTheme.headline,
|
||
),
|
||
),
|
||
onPressed: () async {
|
||
Navigator.pop(context);
|
||
|
||
_showEditBottomSheet(context);
|
||
},
|
||
)
|
||
: SizedBox(),
|
||
(_streamChannel.channel.config.replies &&
|
||
widget.message.status != MessageSendingStatus.FAILED &&
|
||
widget.message.parentId == null &&
|
||
!widget.isParent)
|
||
? FlatButton(
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(16.0),
|
||
child: Text(
|
||
'Start a thread',
|
||
style: theme.textTheme.headline,
|
||
),
|
||
),
|
||
onPressed: () {
|
||
Navigator.pop(context);
|
||
widget.onThreadTap(widget.message);
|
||
},
|
||
)
|
||
: SizedBox(),
|
||
],
|
||
),
|
||
);
|
||
});
|
||
}
|
||
|
||
void _showEditBottomSheet(BuildContext context) {
|
||
showModalBottomSheet(
|
||
context: context,
|
||
elevation: 2,
|
||
clipBehavior: Clip.hardEdge,
|
||
isScrollControlled: true,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.only(
|
||
topLeft: Radius.circular(32),
|
||
topRight: Radius.circular(32),
|
||
),
|
||
),
|
||
builder: (context) {
|
||
return StreamChannel(
|
||
channel: _streamChannel.channel,
|
||
child: Flex(
|
||
direction: Axis.vertical,
|
||
mainAxisAlignment: MainAxisAlignment.end,
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: <Widget>[
|
||
Padding(
|
||
padding: const EdgeInsets.only(
|
||
top: 16.0,
|
||
left: 16.0,
|
||
right: 16.0,
|
||
),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: <Widget>[
|
||
Text(
|
||
'Edit message',
|
||
style: Theme.of(context).textTheme.title,
|
||
),
|
||
Container(
|
||
height: 30,
|
||
padding: const EdgeInsets.all(2.0),
|
||
child: AspectRatio(
|
||
aspectRatio: 1,
|
||
child: RawMaterialButton(
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(4),
|
||
),
|
||
elevation: 0,
|
||
highlightElevation: 0,
|
||
focusElevation: 0,
|
||
disabledElevation: 0,
|
||
hoverElevation: 0,
|
||
onPressed: () {
|
||
Navigator.of(context).pop();
|
||
},
|
||
fillColor:
|
||
Theme.of(context).brightness == Brightness.dark
|
||
? Colors.white.withOpacity(.1)
|
||
: Colors.black.withOpacity(.1),
|
||
padding: EdgeInsets.all(4),
|
||
child: Icon(
|
||
Icons.close,
|
||
size: 15,
|
||
color:
|
||
Theme.of(context).brightness == Brightness.dark
|
||
? Colors.white
|
||
: Colors.black,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
Padding(
|
||
padding: EdgeInsets.only(
|
||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||
),
|
||
child: MessageInput(
|
||
editMessage: widget.message,
|
||
parentMessage: widget.isParent
|
||
? StreamChannel.of(context)
|
||
.channel
|
||
.state
|
||
.messages
|
||
.firstWhere((message) =>
|
||
message.id == widget.message.parentId)
|
||
: null,
|
||
onMessageSent: (_) {
|
||
FocusScope.of(context).unfocus();
|
||
Navigator.pop(context);
|
||
},
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
Widget _buildReactions() {
|
||
return GestureDetector(
|
||
onTap: () {
|
||
if (widget.onMessageActions != null) {
|
||
widget.onMessageActions(context, widget.message);
|
||
} else {
|
||
_showMessageBottomSheet(context);
|
||
}
|
||
},
|
||
child: Padding(
|
||
padding: EdgeInsets.symmetric(
|
||
vertical: widget.message.reactionCounts?.isNotEmpty == true ? 4.0 : 0,
|
||
),
|
||
child: Container(
|
||
padding: widget.message.reactionCounts?.isNotEmpty == true
|
||
? const EdgeInsets.all(8)
|
||
: EdgeInsets.zero,
|
||
decoration: BoxDecoration(
|
||
color: Theme.of(context).brightness == Brightness.dark
|
||
? Colors.white
|
||
: Colors.black,
|
||
borderRadius: BorderRadius.all(Radius.circular(14))),
|
||
child: AnimatedSwitcher(
|
||
duration: Duration(milliseconds: 300),
|
||
reverseDuration: Duration(milliseconds: 0),
|
||
child: (widget.message.reactionCounts != null &&
|
||
widget.message.reactionCounts.isNotEmpty)
|
||
? _buildReactionRow()
|
||
: SizedBox(),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Row _buildReactionRow() {
|
||
return Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
mainAxisAlignment: MainAxisAlignment.start,
|
||
children: [
|
||
...widget.message.reactionCounts.keys.map((reactionType) {
|
||
return Text(
|
||
reactionToEmoji[reactionType] ?? '?',
|
||
);
|
||
}),
|
||
Padding(
|
||
padding: const EdgeInsets.only(left: 4.0),
|
||
child: Text(
|
||
widget.message.reactionCounts.values
|
||
.fold(0, (t, v) => v + t)
|
||
.toString(),
|
||
style: TextStyle(
|
||
color: Theme.of(context).brightness == Brightness.dark
|
||
? Colors.black
|
||
: Colors.white,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
final Map<String, String> reactionToEmoji = {
|
||
'love': '❤️️',
|
||
'haha': '😂',
|
||
'like': '👍',
|
||
'sad': '😕',
|
||
'angry': '😡',
|
||
'wow': '😲',
|
||
};
|
||
|
||
Widget _buildImage(
|
||
Attachment attachment,
|
||
) {
|
||
if (attachment.thumbUrl == null &&
|
||
attachment.imageUrl == null &&
|
||
attachment.assetUrl == null) {
|
||
return _buildErrorImage(attachment);
|
||
}
|
||
|
||
return Hero(
|
||
tag: attachment.imageUrl ?? attachment.assetUrl ?? attachment.thumbUrl,
|
||
child: CachedNetworkImage(
|
||
imageBuilder: (context, provider) {
|
||
return GestureDetector(
|
||
child: Image(image: provider),
|
||
onTap: () {
|
||
Navigator.push(context, MaterialPageRoute(builder: (_) {
|
||
return FullScreenImage(
|
||
url: attachment.imageUrl ??
|
||
attachment.assetUrl ??
|
||
attachment.thumbUrl,
|
||
);
|
||
}));
|
||
},
|
||
);
|
||
},
|
||
placeholder: (_, __) {
|
||
return Container(
|
||
width: 200,
|
||
height: 140,
|
||
);
|
||
},
|
||
imageUrl:
|
||
attachment.thumbUrl ?? attachment.imageUrl ?? attachment.assetUrl,
|
||
errorWidget: (context, url, error) => _buildErrorImage(attachment),
|
||
fit: BoxFit.cover,
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildErrorImage(Attachment attachment) {
|
||
if (attachment.localUri != null) {
|
||
return Image.file(
|
||
File(attachment.localUri.path),
|
||
);
|
||
}
|
||
return Center(
|
||
child: Container(
|
||
width: 200,
|
||
height: 140,
|
||
color: Color(0xffd0021B).withOpacity(.1),
|
||
child: Center(
|
||
child: Icon(
|
||
Icons.error_outline,
|
||
color: Colors.white,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildErrorImage(Attachment attachment) {
|
||
return Center(
|
||
child: Container(
|
||
width: 200,
|
||
height: 140,
|
||
color: Color(0xffd0021B).withOpacity(.1),
|
||
child: Center(
|
||
child: Icon(
|
||
Icons.error_outline,
|
||
color: Colors.white,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildVideo(
|
||
Attachment attachment,
|
||
) {
|
||
VideoPlayerController videoController;
|
||
if (_videoControllers.containsKey(attachment.assetUrl)) {
|
||
videoController = _videoControllers[attachment.assetUrl];
|
||
} else {
|
||
videoController = VideoPlayerController.network(attachment.assetUrl);
|
||
_videoControllers[attachment.assetUrl] = videoController;
|
||
}
|
||
|
||
return FutureBuilder<void>(
|
||
future: videoController.value.initialized
|
||
? Future.value(true)
|
||
: videoController.initialize(),
|
||
builder: (_, snapshot) {
|
||
if (snapshot.connectionState != ConnectionState.done) {
|
||
return Container(
|
||
height: 100,
|
||
width: 100,
|
||
child: Center(
|
||
child: CircularProgressIndicator(),
|
||
),
|
||
);
|
||
}
|
||
ChewieController chewieController;
|
||
if (_chuwieControllers.containsKey(attachment.assetUrl)) {
|
||
chewieController = _chuwieControllers[attachment.assetUrl];
|
||
} else {
|
||
chewieController = ChewieController(
|
||
allowFullScreen: widget.showVideoFullScreen,
|
||
videoPlayerController: videoController,
|
||
autoInitialize: false,
|
||
aspectRatio: videoController.value.aspectRatio,
|
||
errorBuilder: (_, e) {
|
||
if (attachment.thumbUrl != null) {
|
||
return Stack(
|
||
children: <Widget>[
|
||
Container(
|
||
decoration: BoxDecoration(
|
||
image: DecorationImage(
|
||
fit: BoxFit.cover,
|
||
image: CachedNetworkImageProvider(
|
||
attachment.thumbUrl,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
if (attachment.titleLink != null)
|
||
Material(
|
||
color: Colors.transparent,
|
||
child: InkWell(
|
||
onTap: () => _launchURL(attachment.titleLink),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
return _buildErrorImage(attachment);
|
||
});
|
||
_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(Alignment alignment) {
|
||
return Padding(
|
||
padding: const EdgeInsets.only(top: 5.0),
|
||
child: RichText(
|
||
text: TextSpan(
|
||
style: _messageTheme.createdAt,
|
||
children: <TextSpan>[
|
||
if (!_isMyMessage && widget.showOtherMessageUsername)
|
||
TextSpan(
|
||
text: widget.message.user.name,
|
||
style: TextStyle(fontWeight: FontWeight.bold),
|
||
),
|
||
if (widget.message.createdAt != null)
|
||
TextSpan(
|
||
text:
|
||
Jiffy(widget.message.createdAt.toLocal()).format(' HH:mm'),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
BoxDecoration _buildBoxDecoration(bool rectBorders) {
|
||
return BoxDecoration(
|
||
border: _isMyMessage
|
||
? null
|
||
: Border.all(
|
||
color: Theme.of(context).brightness == Brightness.dark
|
||
? Colors.white.withAlpha(24)
|
||
: Colors.black.withAlpha(24),
|
||
),
|
||
borderRadius: BorderRadius.only(
|
||
topLeft: Radius.circular((_isMyMessage || !rectBorders) ? 16 : 2),
|
||
bottomLeft: Radius.circular(_isMyMessage ? 16 : 2),
|
||
topRight: Radius.circular((_isMyMessage && rectBorders) ? 2 : 16),
|
||
bottomRight: Radius.circular(_isMyMessage ? 2 : 16),
|
||
),
|
||
color: (widget.message.status == MessageSendingStatus.FAILED ||
|
||
widget.message.status == MessageSendingStatus.FAILED_UPDATE ||
|
||
widget.message.status == MessageSendingStatus.FAILED_DELETE)
|
||
? Color(0xffd0021B).withOpacity(.1)
|
||
: _messageTheme.messageBackgroundColor,
|
||
);
|
||
}
|
||
|
||
@override
|
||
bool get wantKeepAlive {
|
||
return widget.message.attachments?.isNotEmpty == true;
|
||
}
|
||
}
|
||
|
||
class _ReactionBubblePainter extends CustomPainter {
|
||
final Color color;
|
||
|
||
_ReactionBubblePainter(this.color);
|
||
|
||
@override
|
||
void paint(Canvas canvas, Size size) {
|
||
final paint = Paint()..color = color;
|
||
final path = Path();
|
||
path.arcToPoint(Offset(-6, -6));
|
||
path.arcToPoint(Offset(0, 10));
|
||
path.arcToPoint(Offset(6, -6));
|
||
canvas.drawPath(path, paint);
|
||
}
|
||
|
||
@override
|
||
bool shouldRepaint(CustomPainter oldDelegate) {
|
||
return true;
|
||
}
|
||
}
|