Merge branch 'feature/new-ui' into feature/mono-repo
This commit is contained in:
@@ -9,11 +9,15 @@ class StreamBackButton extends StatelessWidget {
|
||||
Key key,
|
||||
this.onPressed,
|
||||
this.showUnreads = false,
|
||||
this.cid,
|
||||
}) : super(key: key);
|
||||
|
||||
final VoidCallback onPressed;
|
||||
final bool showUnreads;
|
||||
|
||||
/// Channel cid used to retrieve unread count
|
||||
final String cid;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
@@ -45,7 +49,9 @@ class StreamBackButton extends StatelessWidget {
|
||||
Positioned(
|
||||
top: 7,
|
||||
right: 7,
|
||||
child: UnreadIndicator(),
|
||||
child: UnreadIndicator(
|
||||
cid: cid,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -142,7 +142,9 @@ class _ChannelMediaDisplayScreenState extends State<ChannelMediaDisplayScreen> {
|
||||
|
||||
for (var item in snapshot.data) {
|
||||
item.message.attachments
|
||||
.where((e) => e.type == 'image' || e.type == 'video')
|
||||
.where((e) =>
|
||||
(e.type == 'image' || e.type == 'video') &&
|
||||
e.ogScrapeUrl == null)
|
||||
.forEach((e) {
|
||||
VideoPlayerController controller;
|
||||
if (e.type == 'video') {
|
||||
|
||||
@@ -5,10 +5,12 @@ import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
/// It shows a date divider depending on the date difference
|
||||
class DateDivider extends StatelessWidget {
|
||||
final DateTime dateTime;
|
||||
final bool uppercase;
|
||||
|
||||
const DateDivider({
|
||||
Key key,
|
||||
@required this.dateTime,
|
||||
this.uppercase = false,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -18,38 +20,37 @@ class DateDivider extends StatelessWidget {
|
||||
|
||||
String dayInfo;
|
||||
if (Jiffy(createdAt).isSame(now, Units.DAY)) {
|
||||
dayInfo = 'TODAY';
|
||||
dayInfo = 'Today';
|
||||
} else if (Jiffy(createdAt)
|
||||
.isSame(now.subtract(Duration(days: 1)), Units.DAY)) {
|
||||
dayInfo = 'YESTERDAY';
|
||||
dayInfo = 'Yesterday';
|
||||
} else if (Jiffy(createdAt).isAfter(
|
||||
now.subtract(Duration(days: 7)),
|
||||
Units.DAY,
|
||||
)) {
|
||||
dayInfo = createdAt.format('EEEE').toUpperCase();
|
||||
dayInfo = createdAt.format('EEEE');
|
||||
} else if (Jiffy(createdAt).isAfter(
|
||||
Jiffy(now).subtract(years: 1),
|
||||
Units.DAY,
|
||||
)) {
|
||||
dayInfo = createdAt.format('MMMM d').toUpperCase();
|
||||
dayInfo = createdAt.format('MMMM d');
|
||||
} else {
|
||||
dayInfo = createdAt.format('MMMM d').toUpperCase();
|
||||
dayInfo = createdAt.format('MMMM d');
|
||||
}
|
||||
|
||||
if (uppercase) dayInfo = dayInfo.toUpperCase();
|
||||
|
||||
return Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(
|
||||
8,
|
||||
),
|
||||
color: StreamChatTheme.of(context).colorTheme.overlayDark,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
dayInfo,
|
||||
style: StreamChatTheme.of(context).textTheme.footnoteBold.copyWith(
|
||||
style: StreamChatTheme.of(context).textTheme.footnote.copyWith(
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -63,10 +63,7 @@ class DeletedMessage extends StatelessWidget {
|
||||
'Message deleted',
|
||||
style: messageTheme.messageText.copyWith(
|
||||
fontStyle: FontStyle.italic,
|
||||
color: (Theme.of(context).brightness == Brightness.dark
|
||||
? StreamChatTheme.of(context).colorTheme.white
|
||||
: StreamChatTheme.of(context).colorTheme.black)
|
||||
.withOpacity(.5),
|
||||
color: messageTheme.createdAt.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -116,7 +116,7 @@ class _ImageFooterState extends State<ImageFooter> {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: StreamSvgIcon.icon_SHARE(
|
||||
icon: StreamSvgIcon.Icon_SHARE(
|
||||
size: 24.0,
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
|
||||
@@ -24,8 +24,8 @@ class MessageActionsModal extends StatelessWidget {
|
||||
final bool showCopyMessage;
|
||||
final bool showEditMessage;
|
||||
final bool showResendMessage;
|
||||
final bool showReply;
|
||||
final bool showThreadReply;
|
||||
final bool showReplyMessage;
|
||||
final bool showThreadReplyMessage;
|
||||
final bool reverse;
|
||||
final ShapeBorder messageShape;
|
||||
final DisplayWidget showUserAvatar;
|
||||
@@ -40,9 +40,9 @@ class MessageActionsModal extends StatelessWidget {
|
||||
this.onReplyTap,
|
||||
this.onThreadReplyTap,
|
||||
this.showCopyMessage = true,
|
||||
this.showReply = true,
|
||||
this.showReplyMessage = true,
|
||||
this.showResendMessage = true,
|
||||
this.showThreadReply = true,
|
||||
this.showThreadReplyMessage = true,
|
||||
this.showUserAvatar = DisplayWidget.show,
|
||||
this.editMessageInputBuilder,
|
||||
this.messageShape,
|
||||
@@ -92,8 +92,9 @@ class MessageActionsModal extends StatelessWidget {
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
crossAxisAlignment: reverse
|
||||
? CrossAxisAlignment.end
|
||||
: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
if (showReactions &&
|
||||
(message.status == MessageSendingStatus.SENT ||
|
||||
@@ -128,7 +129,7 @@ class MessageActionsModal extends StatelessWidget {
|
||||
showReactions: false,
|
||||
showUsername: false,
|
||||
showThreadReplyIndicator: false,
|
||||
showReplyIndicator: false,
|
||||
showReplyMessage: false,
|
||||
showUserAvatar: showUserAvatar,
|
||||
showTimestamp: false,
|
||||
translateUserAvatar: false,
|
||||
@@ -137,13 +138,12 @@ class MessageActionsModal extends StatelessWidget {
|
||||
MessageSendingStatus.SENT ||
|
||||
message.status == null),
|
||||
showInChannelIndicator: false,
|
||||
showSendingIndicator: DisplayWidget.gone,
|
||||
showSendingIndicator: false,
|
||||
shape: messageShape,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
SizedBox(height: 8),
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0.0, end: 1.0),
|
||||
duration: Duration(milliseconds: 300),
|
||||
@@ -153,47 +153,56 @@ class MessageActionsModal extends StatelessWidget {
|
||||
transform: Matrix4.identity()
|
||||
..scale(val)
|
||||
..rotateZ(-1.0 + val),
|
||||
alignment: Alignment.topRight,
|
||||
alignment: reverse
|
||||
? Alignment.topRight
|
||||
: Alignment.topLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 48.0,
|
||||
padding: EdgeInsets.only(
|
||||
right: reverse ? 16 : 0,
|
||||
left: reverse ? 0 : 48,
|
||||
),
|
||||
child: Material(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.whiteSnow,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.stretch,
|
||||
children: ListTile.divideTiles(
|
||||
context: context,
|
||||
tiles: [
|
||||
if (showReply &&
|
||||
(message.status ==
|
||||
MessageSendingStatus.SENT ||
|
||||
message.status == null) &&
|
||||
message.parentId == null)
|
||||
_buildReplyButton(context),
|
||||
if (showThreadReply &&
|
||||
(message.status ==
|
||||
MessageSendingStatus.SENT ||
|
||||
message.status == null) &&
|
||||
message.parentId == null)
|
||||
_buildThreadReplyButton(context),
|
||||
if (showResendMessage)
|
||||
_buildResendMessage(context),
|
||||
if (showEditMessage)
|
||||
_buildEditMessage(context),
|
||||
if (showDeleteMessage)
|
||||
_buildDeleteButton(context),
|
||||
if (showCopyMessage)
|
||||
_buildCopyButton(context),
|
||||
],
|
||||
).toList(),
|
||||
child: SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.75,
|
||||
child: Material(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.whiteSnow,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.stretch,
|
||||
children: ListTile.divideTiles(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.greyWhisper,
|
||||
context: context,
|
||||
tiles: [
|
||||
if (showReplyMessage &&
|
||||
(message.status ==
|
||||
MessageSendingStatus.SENT ||
|
||||
message.status == null) &&
|
||||
message.parentId == null)
|
||||
_buildReplyButton(context),
|
||||
if (showThreadReplyMessage &&
|
||||
(message.status ==
|
||||
MessageSendingStatus.SENT ||
|
||||
message.status == null) &&
|
||||
message.parentId == null)
|
||||
_buildThreadReplyButton(context),
|
||||
if (showResendMessage)
|
||||
_buildResendMessage(context),
|
||||
if (showEditMessage)
|
||||
_buildEditMessage(context),
|
||||
if (showCopyMessage)
|
||||
_buildCopyButton(context),
|
||||
if (showDeleteMessage)
|
||||
_buildDeleteButton(context),
|
||||
],
|
||||
).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -211,12 +220,18 @@ class MessageActionsModal extends StatelessWidget {
|
||||
|
||||
Widget _buildReplyButton(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Text(
|
||||
'Reply',
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
),
|
||||
leading: StreamSvgIcon.reply(
|
||||
color: StreamChatTheme.of(context).primaryIconTheme.color,
|
||||
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: () {
|
||||
Navigator.pop(context);
|
||||
@@ -230,13 +245,21 @@ class MessageActionsModal extends StatelessWidget {
|
||||
Widget _buildDeleteButton(BuildContext context) {
|
||||
final isDeleteFailed = message.status == MessageSendingStatus.FAILED_DELETE;
|
||||
return ListTile(
|
||||
title: Text(
|
||||
isDeleteFailed ? 'Retry deleting message' : 'Delete message',
|
||||
style:
|
||||
Theme.of(context).textTheme.headline6.copyWith(color: Colors.red),
|
||||
),
|
||||
leading: StreamSvgIcon.delete(
|
||||
color: Colors.red,
|
||||
dense: true,
|
||||
title: Row(
|
||||
children: [
|
||||
StreamSvgIcon.delete(
|
||||
color: Colors.red,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Text(
|
||||
isDeleteFailed ? 'Retry Deleting Message' : 'Delete Message',
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.headline
|
||||
.copyWith(color: Colors.red),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
@@ -250,12 +273,19 @@ class MessageActionsModal extends StatelessWidget {
|
||||
|
||||
Widget _buildCopyButton(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Text(
|
||||
'Copy message',
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
),
|
||||
leading: StreamSvgIcon.copy(
|
||||
color: StreamChatTheme.of(context).primaryIconTheme.color,
|
||||
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 {
|
||||
await Clipboard.setData(ClipboardData(text: message.text));
|
||||
@@ -266,12 +296,18 @@ class MessageActionsModal extends StatelessWidget {
|
||||
|
||||
Widget _buildEditMessage(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Text(
|
||||
'Edit message',
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
),
|
||||
leading: StreamSvgIcon.edit(
|
||||
color: StreamChatTheme.of(context).primaryIconTheme.color,
|
||||
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 {
|
||||
Navigator.pop(context);
|
||||
@@ -283,12 +319,18 @@ class MessageActionsModal extends StatelessWidget {
|
||||
Widget _buildResendMessage(BuildContext context) {
|
||||
final isUpdateFailed = message.status == MessageSendingStatus.FAILED_UPDATE;
|
||||
return ListTile(
|
||||
title: Text(
|
||||
isUpdateFailed ? 'Resend edited message' : 'Resend',
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
),
|
||||
leading: StreamSvgIcon.circle_up(
|
||||
color: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
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: () {
|
||||
Navigator.pop(context);
|
||||
@@ -374,12 +416,18 @@ class MessageActionsModal extends StatelessWidget {
|
||||
|
||||
Widget _buildThreadReplyButton(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Text(
|
||||
'Thread reply',
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
),
|
||||
leading: StreamSvgIcon.thread(
|
||||
color: StreamChatTheme.of(context).primaryIconTheme.color,
|
||||
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: () {
|
||||
Navigator.pop(context);
|
||||
|
||||
@@ -187,7 +187,6 @@ class MessageListView extends StatefulWidget {
|
||||
|
||||
class _MessageListViewState extends State<MessageListView> {
|
||||
ItemScrollController _scrollController;
|
||||
bool _bottomWasVisible = false;
|
||||
Function _onThreadTap;
|
||||
bool _showScrollToBottom = false;
|
||||
ItemPositionsListener _itemPositionListener;
|
||||
@@ -220,6 +219,8 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
|
||||
bool get _upToDate => streamChannel.channel.state.isUpToDate;
|
||||
|
||||
bool get _isThreadConversation => widget.parentMessage != null;
|
||||
|
||||
bool _topPaginationActive = false;
|
||||
bool _bottomPaginationActive = false;
|
||||
|
||||
@@ -234,7 +235,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final messagesStream = widget.parentMessage != null
|
||||
final messagesStream = _isThreadConversation
|
||||
? streamChannel.channel.state.threadsStream
|
||||
.where((threads) => threads.containsKey(widget.parentMessage.id))
|
||||
.map((threads) => threads[widget.parentMessage.id])
|
||||
@@ -324,7 +325,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
onInBetweenOfPage: () {
|
||||
_inBetweenList = true;
|
||||
},
|
||||
child: ScrollablePositionedList.builder(
|
||||
child: ScrollablePositionedList.separated(
|
||||
key: ValueKey(initialIndex + initialAlignment),
|
||||
itemPositionsListener: _itemPositionListener,
|
||||
addAutomaticKeepAlives: true,
|
||||
@@ -333,9 +334,49 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
physics: widget.scrollPhysics,
|
||||
itemScrollController: _scrollController,
|
||||
reverse: true,
|
||||
itemCount: messages.length +
|
||||
2 +
|
||||
(widget.parentMessage != null ? 1 : 0),
|
||||
itemCount:
|
||||
messages.length + 2 + (_isThreadConversation ? 1 : 0),
|
||||
separatorBuilder: (context, i) {
|
||||
if (i == messages.length) return Offstage();
|
||||
if (i == messages.length + 2) return Offstage();
|
||||
if (i == messages.length + 1) return Offstage();
|
||||
if (i == 0) return SizedBox(height: 30);
|
||||
final message = messages[i];
|
||||
final nextMessage = messages[i - 1];
|
||||
if (!Jiffy(message.createdAt.toLocal()).isSame(
|
||||
nextMessage.createdAt.toLocal(),
|
||||
Units.DAY,
|
||||
)) {
|
||||
final divider = widget.dateDividerBuilder != null
|
||||
? widget.dateDividerBuilder(
|
||||
nextMessage.createdAt.toLocal(),
|
||||
)
|
||||
: DateDivider(
|
||||
dateTime: nextMessage.createdAt.toLocal(),
|
||||
);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
||||
child: divider,
|
||||
);
|
||||
}
|
||||
final timeDiff =
|
||||
Jiffy(nextMessage.createdAt.toLocal()).diff(
|
||||
message.createdAt.toLocal(),
|
||||
Units.MINUTE,
|
||||
);
|
||||
|
||||
final isNextUserSame =
|
||||
message.user.id == nextMessage.user?.id;
|
||||
final isThread = message.replyCount > 0;
|
||||
final isDeleted = message.isDeleted;
|
||||
if (timeDiff >= 1 ||
|
||||
!isNextUserSame ||
|
||||
isThread ||
|
||||
isDeleted) {
|
||||
return SizedBox(height: 8);
|
||||
}
|
||||
return SizedBox(height: 2);
|
||||
},
|
||||
itemBuilder: (context, i) {
|
||||
if (i == messages.length + 2) {
|
||||
if (widget.parentMessageBuilder != null) {
|
||||
@@ -383,7 +424,6 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
);
|
||||
}
|
||||
final message = messages[i - 1];
|
||||
final nextMessage = (i - 1) > 0 ? messages[i - 2] : null;
|
||||
|
||||
Widget messageWidget;
|
||||
|
||||
@@ -419,27 +459,6 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
messageWidget = buildMessage(message, messages, i);
|
||||
}
|
||||
}
|
||||
|
||||
if (nextMessage != null &&
|
||||
!Jiffy(message.createdAt.toLocal()).isSame(
|
||||
nextMessage.createdAt.toLocal(), Units.DAY)) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
messageWidget,
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
||||
child: widget.dateDividerBuilder != null
|
||||
? widget.dateDividerBuilder(
|
||||
nextMessage.createdAt.toLocal())
|
||||
: DateDivider(
|
||||
dateTime: nextMessage.createdAt.toLocal(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return messageWidget;
|
||||
},
|
||||
),
|
||||
@@ -482,7 +501,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
|
||||
Future<void> _paginateData(
|
||||
StreamChannelState channel, QueryDirection direction) {
|
||||
if (widget.parentMessage == null) {
|
||||
if (!_isThreadConversation) {
|
||||
return channel.queryMessages(direction: direction);
|
||||
} else {
|
||||
return channel.getReplies(widget.parentMessage.id);
|
||||
@@ -672,14 +691,13 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
key: ValueKey<String>('BOTTOM-MESSAGE'),
|
||||
onVisibilityChanged: (visibility) {
|
||||
final isVisible = visibility.visibleBounds != Rect.zero;
|
||||
if (isVisible && !_bottomWasVisible) {
|
||||
if (isVisible) {
|
||||
final channel = streamChannel.channel;
|
||||
if (_upToDate &&
|
||||
channel.config?.readEvents == true &&
|
||||
channel.state.unreadCount > 0) {
|
||||
streamChannel.channel.markRead();
|
||||
}
|
||||
_bottomWasVisible = !isVisible;
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() => _showScrollToBottom = !isVisible);
|
||||
@@ -697,7 +715,12 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
return MessageWidget(
|
||||
showThreadReplyIndicator: false,
|
||||
showInChannelIndicator: false,
|
||||
showReplyIndicator: false,
|
||||
showReplyMessage: false,
|
||||
showResendMessage: false,
|
||||
showThreadReplyMessage: false,
|
||||
showCopyMessage: false,
|
||||
showDeleteMessage: false,
|
||||
showEditMessage: false,
|
||||
message: message,
|
||||
reverse: isMyMessage,
|
||||
showUsername: !isMyMessage,
|
||||
@@ -707,10 +730,8 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
right: 8.0,
|
||||
bottom: 16.0,
|
||||
),
|
||||
showSendingIndicator: DisplayWidget.hide,
|
||||
showSendingIndicator: false,
|
||||
onThreadTap: _onThreadTap,
|
||||
showEditMessage: false,
|
||||
showDeleteMessage: false,
|
||||
borderRadiusGeometry: BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
bottomLeft: Radius.circular(2),
|
||||
@@ -740,36 +761,73 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
|
||||
final userId = StreamChat.of(context).user.id;
|
||||
final isMyMessage = message.user.id == userId;
|
||||
final isNextUser =
|
||||
index - 2 >= 0 && message.user.id == messages[index - 2]?.user?.id;
|
||||
final nextMessage = index - 2 >= 0 ? messages[index - 2] : null;
|
||||
final isNextUserSame =
|
||||
nextMessage != null && message.user.id == nextMessage.user.id;
|
||||
|
||||
num timeDiff = 0;
|
||||
if (nextMessage != null) {
|
||||
timeDiff = Jiffy(nextMessage.createdAt.toLocal()).diff(
|
||||
message.createdAt.toLocal(),
|
||||
Units.MINUTE,
|
||||
);
|
||||
}
|
||||
|
||||
final channel = streamChannel.channel;
|
||||
final readList = channel.state?.read
|
||||
?.where((element) => element.user.id != userId)
|
||||
?.where((read) =>
|
||||
(read.lastRead.isAfter(message.createdAt) ||
|
||||
read.lastRead.isAtSameMomentAs(message.createdAt)) &&
|
||||
(index == 0 ||
|
||||
read.lastRead.isBefore(messages[index - 1].createdAt)))
|
||||
?.toList() ??
|
||||
final readList = channel.state?.read?.where((read) {
|
||||
if (read.user.id == userId) return false;
|
||||
return (read.lastRead.isAfter(message.createdAt) ||
|
||||
read.lastRead.isAtSameMomentAs(message.createdAt)) &&
|
||||
(index == 0 ||
|
||||
read.lastRead.isBefore(messages[index - 1].createdAt));
|
||||
})?.toList() ??
|
||||
[];
|
||||
|
||||
final allRead = readList.length >= (channel.memberCount ?? 0) - 1;
|
||||
final hasFileAttachment =
|
||||
message.attachments.any((it) => it.type == 'file');
|
||||
|
||||
final isThreadMessage =
|
||||
widget.parentMessage != null || message?.showInChannel == true;
|
||||
message?.parentId != null && message?.showInChannel == true;
|
||||
|
||||
final hasReplies = message.replyCount > 0;
|
||||
|
||||
final attachmentBorderRadius = hasFileAttachment ? 12.0 : 14.0;
|
||||
|
||||
final showTimeStamp = message.createdAt != null &&
|
||||
(!isThreadMessage || _isThreadConversation) &&
|
||||
!hasReplies &&
|
||||
(timeDiff >= 1 || !isNextUserSame);
|
||||
|
||||
final showUsername = !isMyMessage &&
|
||||
(!isThreadMessage || _isThreadConversation) &&
|
||||
!hasReplies &&
|
||||
(timeDiff >= 1 || !isNextUserSame);
|
||||
|
||||
final showUserAvatar = isMyMessage
|
||||
? DisplayWidget.gone
|
||||
: (timeDiff >= 1 || !isNextUserSame)
|
||||
? DisplayWidget.show
|
||||
: DisplayWidget.hide;
|
||||
|
||||
final showSendingIndicator =
|
||||
isMyMessage && (index == 0 || timeDiff >= 1 || !isNextUserSame);
|
||||
|
||||
bool showInChannelIndicator = !_isThreadConversation && isThreadMessage;
|
||||
bool showThreadReplyIndicator = !_isThreadConversation && hasReplies;
|
||||
|
||||
Widget child = MessageWidget(
|
||||
key: ValueKey<String>('MESSAGE-${message.id}'),
|
||||
message: message,
|
||||
reverse: isMyMessage,
|
||||
showReactions: !message.isDeleted,
|
||||
padding: EdgeInsets.only(
|
||||
left: 8.0,
|
||||
right: 8.0,
|
||||
bottom: index == 0 ? 30 : (isNextUser ? 2 : 7),
|
||||
top: 3,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
showInChannelIndicator: showInChannelIndicator,
|
||||
showThreadReplyIndicator: showThreadReplyIndicator,
|
||||
showUsername: showUsername,
|
||||
showTimestamp: showTimeStamp,
|
||||
showSendingIndicator: showSendingIndicator,
|
||||
showUserAvatar: showUserAvatar,
|
||||
onQuotedMessageTap: (quotedMessageId) async {
|
||||
final scrollToIndex = () {
|
||||
final index = messages.indexWhere((m) => m.id == quotedMessageId);
|
||||
@@ -790,35 +848,26 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
});
|
||||
}
|
||||
},
|
||||
showInChannelIndicator: widget.parentMessage == null,
|
||||
showThreadReplyIndicator: widget.parentMessage == null,
|
||||
showUsername: !isMyMessage && !isNextUser,
|
||||
showSendingIndicator: isMyMessage &&
|
||||
(index == 0 || message.status != MessageSendingStatus.SENT)
|
||||
? DisplayWidget.show
|
||||
: DisplayWidget.hide,
|
||||
showTimestamp: !isNextUser || readList?.isNotEmpty == true,
|
||||
showEditMessage: isMyMessage,
|
||||
showDeleteMessage: isMyMessage,
|
||||
showThreadReplyMessage: !isThreadMessage,
|
||||
borderSide: isMyMessage ? BorderSide.none : null,
|
||||
onThreadTap: _onThreadTap,
|
||||
onReplyTap: widget.onReplyTap,
|
||||
attachmentBorderRadiusGeometry: BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
bottomLeft: Radius.circular(!isNextUser ? 0 : 16),
|
||||
topRight: Radius.circular(16),
|
||||
bottomRight: Radius.circular(16),
|
||||
topLeft: Radius.circular(attachmentBorderRadius),
|
||||
bottomLeft: Radius.circular(
|
||||
timeDiff >= 1 || !isNextUserSame ? 0 : attachmentBorderRadius),
|
||||
topRight: Radius.circular(attachmentBorderRadius),
|
||||
bottomRight: Radius.circular(attachmentBorderRadius),
|
||||
),
|
||||
attachmentPadding: const EdgeInsets.all(2),
|
||||
borderRadiusGeometry: BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
bottomLeft: Radius.circular(!isNextUser ? 0 : 16),
|
||||
bottomLeft: Radius.circular(timeDiff >= 1 || !isNextUserSame ? 0 : 16),
|
||||
topRight: Radius.circular(16),
|
||||
bottomRight: Radius.circular(16),
|
||||
),
|
||||
showUserAvatar: isMyMessage
|
||||
? DisplayWidget.gone
|
||||
: (isNextUser ? DisplayWidget.hide : DisplayWidget.show),
|
||||
messageTheme: isMyMessage
|
||||
? StreamChatTheme.of(context).ownMessageTheme
|
||||
: StreamChatTheme.of(context).otherMessageTheme,
|
||||
@@ -827,7 +876,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
onShowMessage: widget.onShowMessage,
|
||||
);
|
||||
|
||||
if (!isThreadMessage) {
|
||||
if (!message.isDeleted && !message.isSystem && !message.isEphemeral) {
|
||||
child = Swipeable(
|
||||
onSwipeEnd: () => widget.onMessageSwiped(message),
|
||||
backgroundIcon: StreamSvgIcon.reply(
|
||||
@@ -890,7 +939,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
}
|
||||
});
|
||||
|
||||
if (widget.parentMessage != null) {
|
||||
if (_isThreadConversation) {
|
||||
streamChannel.getReplies(widget.parentMessage.id);
|
||||
}
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ class MessageReactionsModal extends StatelessWidget {
|
||||
showThreadReplyIndicator: false,
|
||||
showTimestamp: false,
|
||||
translateUserAvatar: false,
|
||||
showSendingIndicator: DisplayWidget.gone,
|
||||
showSendingIndicator: false,
|
||||
shape: messageShape,
|
||||
showInChannelIndicator: false,
|
||||
showReactionPickerIndicator: showReactions &&
|
||||
@@ -127,7 +127,6 @@ class MessageReactionsModal extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}),
|
||||
SizedBox(height: 8),
|
||||
if (message.latestReactions?.isNotEmpty == true)
|
||||
_buildReactionCard(context),
|
||||
],
|
||||
@@ -152,27 +151,22 @@ class MessageReactionsModal extends StatelessWidget {
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Message Reactions',
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
style: StreamChatTheme.of(context).textTheme.headlineBold,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 18,
|
||||
right: 18,
|
||||
bottom: 26,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
child: Wrap(
|
||||
spacing: 16,
|
||||
runSpacing: 22,
|
||||
runSpacing: 16,
|
||||
alignment: WrapAlignment.start,
|
||||
children: message.latestReactions
|
||||
.map((e) => _buildReaction(
|
||||
@@ -184,8 +178,8 @@ class MessageReactionsModal extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -227,24 +221,28 @@ class MessageReactionsModal extends StatelessWidget {
|
||||
),
|
||||
Positioned(
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
alignment: reverse
|
||||
? Alignment.centerRight
|
||||
: Alignment.centerLeft,
|
||||
child: ReactionBubble(
|
||||
reactions: [reaction],
|
||||
flipTail: !reverse,
|
||||
borderColor: messageTheme.reactionsBorderColor,
|
||||
backgroundColor:
|
||||
messageTheme.reactionsBackgroundColor,
|
||||
highlightOwnReactions: false,
|
||||
),
|
||||
),
|
||||
bottom: 4,
|
||||
bottom: 6,
|
||||
left: isCurrentUser ? 0 : null,
|
||||
right: isCurrentUser ? 0 : null,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
reaction.user.name,
|
||||
style: Theme.of(context).textTheme.subtitle2,
|
||||
style: StreamChatTheme.of(context).textTheme.footnoteBold,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:math';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:emojis/emoji.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -96,7 +95,7 @@ class MessageWidget extends StatefulWidget {
|
||||
final DisplayWidget showUserAvatar;
|
||||
|
||||
/// It controls the display behaviour of the sending indicator
|
||||
final DisplayWidget showSendingIndicator;
|
||||
final bool showSendingIndicator;
|
||||
|
||||
/// If true the widget will show the reactions
|
||||
final bool showReactions;
|
||||
@@ -106,9 +105,6 @@ class MessageWidget extends StatefulWidget {
|
||||
/// If true the widget will show the thread reply indicator
|
||||
final bool showThreadReplyIndicator;
|
||||
|
||||
/// If true the widget will show the reply indicator
|
||||
final bool showReplyIndicator;
|
||||
|
||||
/// If true the widget will show the show in channel indicator
|
||||
final bool showInChannelIndicator;
|
||||
|
||||
@@ -121,9 +117,6 @@ class MessageWidget extends StatefulWidget {
|
||||
/// Used in [MessageReactionsModal] and [MessageActionsModal]
|
||||
final bool showReactionPickerIndicator;
|
||||
|
||||
/// If true the widget will show the resendMessage indicator
|
||||
final bool showResendMessage;
|
||||
|
||||
final List<Read> readList;
|
||||
|
||||
final ShowMessageCallback onShowMessage;
|
||||
@@ -131,8 +124,14 @@ class MessageWidget extends StatefulWidget {
|
||||
/// If true show the users username next to the timestamp of the message
|
||||
final bool showUsername;
|
||||
final bool showTimestamp;
|
||||
final bool showDeleteMessage;
|
||||
|
||||
final bool showReplyMessage;
|
||||
final bool showThreadReplyMessage;
|
||||
final bool showEditMessage;
|
||||
final bool showCopyMessage;
|
||||
final bool showDeleteMessage;
|
||||
final bool showResendMessage;
|
||||
|
||||
final Map<String, AttachmentBuilder> attachmentBuilders;
|
||||
|
||||
/// Center user avatar with bottom of the message
|
||||
@@ -157,10 +156,9 @@ class MessageWidget extends StatefulWidget {
|
||||
this.onMentionTap,
|
||||
this.showReactionPickerIndicator = false,
|
||||
this.showUserAvatar = DisplayWidget.show,
|
||||
this.showSendingIndicator = DisplayWidget.show,
|
||||
this.showSendingIndicator = true,
|
||||
this.showThreadReplyIndicator = true,
|
||||
this.showInChannelIndicator = true,
|
||||
this.showReplyIndicator = true,
|
||||
this.onReplyTap,
|
||||
this.onThreadTap,
|
||||
this.showUsername = true,
|
||||
@@ -168,6 +166,10 @@ class MessageWidget extends StatefulWidget {
|
||||
this.showReactions = true,
|
||||
this.showDeleteMessage = true,
|
||||
this.showEditMessage = true,
|
||||
this.showReplyMessage = true,
|
||||
this.showThreadReplyMessage = true,
|
||||
this.showResendMessage = true,
|
||||
this.showCopyMessage = true,
|
||||
this.onUserAvatarTap,
|
||||
this.onLinkTap,
|
||||
this.onMessageActions,
|
||||
@@ -175,7 +177,6 @@ class MessageWidget extends StatefulWidget {
|
||||
this.editMessageInputBuilder,
|
||||
this.textBuilder,
|
||||
Map<String, AttachmentBuilder> customAttachmentBuilders,
|
||||
this.showResendMessage = true,
|
||||
this.readList,
|
||||
this.padding,
|
||||
this.textPadding = const EdgeInsets.symmetric(
|
||||
@@ -239,18 +240,19 @@ class MessageWidget extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MessageWidgetState extends State<MessageWidget> {
|
||||
bool get showThreadReplyIndicator =>
|
||||
widget.showThreadReplyIndicator && widget.message.replyCount > 0;
|
||||
bool get showThreadReplyIndicator => widget.showThreadReplyIndicator;
|
||||
|
||||
bool get showSendingIndicator => widget.showSendingIndicator;
|
||||
|
||||
bool get isDeleted => widget.message.isDeleted;
|
||||
|
||||
bool get showUsername => widget.showUsername;
|
||||
|
||||
bool get showTimeStamp =>
|
||||
widget.message.createdAt != null && widget.showTimestamp;
|
||||
bool get showTimeStamp => widget.showTimestamp;
|
||||
|
||||
bool get isMessageRead => widget.readList?.isNotEmpty == true;
|
||||
|
||||
bool get showInChannel =>
|
||||
widget.showInChannelIndicator && widget.message?.showInChannel == true;
|
||||
bool get showInChannel => widget.showInChannelIndicator;
|
||||
|
||||
bool get hasQuotedMessage => widget.message?.quotedMessage != null;
|
||||
|
||||
@@ -272,16 +274,15 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
showThreadReplyIndicator ||
|
||||
showUsername ||
|
||||
showTimeStamp ||
|
||||
showInChannel;
|
||||
showInChannel ||
|
||||
showSendingIndicator ||
|
||||
isDeleted;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final avatarWidth = widget.messageTheme.avatarTheme.constraints.maxWidth;
|
||||
var leftPadding =
|
||||
widget.showUserAvatar != DisplayWidget.gone ? avatarWidth + 8.5 : 4.5;
|
||||
|
||||
final isOnlyEmoji =
|
||||
widget.message.text.characters.every((c) => Emoji.byChar(c) != null);
|
||||
widget.showUserAvatar != DisplayWidget.gone ? avatarWidth + 8.5 : 0.5;
|
||||
|
||||
final hasFiles =
|
||||
widget.message.attachments?.any((element) => element.type == 'file') ==
|
||||
@@ -330,7 +331,7 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
child: PortalEntry(
|
||||
portal: Container(
|
||||
transform:
|
||||
Matrix4.translationValues(-16, 2, 0),
|
||||
Matrix4.translationValues(-12, 0, 0),
|
||||
child: _buildReactionIndicator(context),
|
||||
constraints:
|
||||
BoxConstraints(maxWidth: 22 * 6.0),
|
||||
@@ -372,6 +373,14 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
: Card(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
elevation: 0.0,
|
||||
margin: EdgeInsets.symmetric(
|
||||
horizontal:
|
||||
widget.showUserAvatar ==
|
||||
DisplayWidget
|
||||
.gone
|
||||
? 0
|
||||
: 4.0,
|
||||
),
|
||||
shape: widget.shape ??
|
||||
RoundedRectangleBorder(
|
||||
side: isOnlyEmoji &&
|
||||
@@ -395,7 +404,7 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment
|
||||
.start,
|
||||
.end,
|
||||
mainAxisSize:
|
||||
MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
@@ -423,10 +432,12 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
widget.reverse ? pi : 0),
|
||||
child: CustomPaint(
|
||||
painter: ReactionBubblePainter(
|
||||
widget.messageTheme
|
||||
.reactionsBackgroundColor,
|
||||
widget.messageTheme
|
||||
.reactionsBorderColor,
|
||||
StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.white,
|
||||
StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -440,7 +451,11 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
if (showBottomRow) SizedBox(height: 20.0),
|
||||
],
|
||||
),
|
||||
if (showBottomRow) _buildBottomRow(leftPadding),
|
||||
if (showBottomRow)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: leftPadding),
|
||||
child: _bottomRow,
|
||||
),
|
||||
if (isFailedState)
|
||||
Positioned(
|
||||
left: widget.reverse ? -3 : null,
|
||||
@@ -469,12 +484,12 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
Widget _buildQuotedMessage() {
|
||||
final isMyMessage =
|
||||
widget.message.user.id == StreamChat.of(context).user.id;
|
||||
final onTap = widget.message?.quotedMessage?.isDeleted != true &&
|
||||
widget.onQuotedMessageTap != null
|
||||
? () => widget.onQuotedMessageTap(widget.message.quotedMessageId)
|
||||
: null;
|
||||
return QuotedMessageWidget(
|
||||
onTap: () {
|
||||
if (widget.onQuotedMessageTap != null) {
|
||||
widget.onQuotedMessageTap(widget.message.quotedMessageId);
|
||||
}
|
||||
},
|
||||
onTap: onTap,
|
||||
message: widget.message.quotedMessage,
|
||||
messageTheme: isMyMessage
|
||||
? StreamChatTheme.of(context).otherMessageTheme
|
||||
@@ -483,127 +498,117 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomRow(double leftPadding) {
|
||||
final deleted = widget.message.isDeleted;
|
||||
var children = <Widget>[];
|
||||
if (deleted) {
|
||||
children.add(
|
||||
Row(
|
||||
Widget get _bottomRow {
|
||||
if (isDeleted) {
|
||||
return Transform(
|
||||
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
|
||||
alignment: Alignment.center,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
StreamSvgIcon.eye(
|
||||
color:
|
||||
StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5),
|
||||
color: StreamChatTheme.of(context).colorTheme.grey,
|
||||
size: 16.0,
|
||||
),
|
||||
SizedBox(width: 8.0),
|
||||
Text(
|
||||
'Only visible to you',
|
||||
style: StreamChatTheme.of(context).textTheme.footnote.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.5)),
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.footnote
|
||||
.copyWith(color: StreamChatTheme.of(context).colorTheme.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else if (showInChannel) {
|
||||
final onThreadTap = () async {
|
||||
try {
|
||||
}
|
||||
|
||||
var children = <Widget>[];
|
||||
|
||||
final threadParticipants = widget.message.threadParticipants;
|
||||
final showThreadParticipants = threadParticipants?.isNotEmpty == true;
|
||||
final replyCount = widget.message.replyCount;
|
||||
|
||||
var msg = 'Thread Reply';
|
||||
if (showThreadReplyIndicator && replyCount > 1) {
|
||||
msg = '$replyCount Thread Replies';
|
||||
}
|
||||
|
||||
final onThreadTap = () async {
|
||||
try {
|
||||
var message = widget.message;
|
||||
if (showInChannel) {
|
||||
final channel = StreamChannel.of(context);
|
||||
final message = await channel.getMessage(widget.message.parentId);
|
||||
return widget.onThreadTap(message);
|
||||
} catch (e, stk) {
|
||||
print(e);
|
||||
print(stk);
|
||||
return null;
|
||||
message = await channel.getMessage(widget.message.parentId);
|
||||
}
|
||||
};
|
||||
children.add(
|
||||
return widget.onThreadTap(message);
|
||||
} catch (e, stk) {
|
||||
print(e);
|
||||
print(stk);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
children.addAll([
|
||||
if (showSendingIndicator) _buildSendingIndicator(),
|
||||
if (showInChannel || showThreadReplyIndicator) ...[
|
||||
if (showThreadParticipants)
|
||||
SizedBox.fromSize(
|
||||
size: Size((threadParticipants.length * 8.0) + 10, 16),
|
||||
child: _buildThreadParticipantsIndicator(),
|
||||
),
|
||||
InkWell(
|
||||
onTap: widget.onThreadTap != null ? onThreadTap : null,
|
||||
child: Text('Thread Reply', style: widget.messageTheme?.replies),
|
||||
child: Text(msg, style: widget.messageTheme?.replies),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
final showSendingIndicator =
|
||||
widget.showSendingIndicator == DisplayWidget.show;
|
||||
final threadParticipants = widget.message.threadParticipants;
|
||||
final showThreadParticipants = threadParticipants?.isNotEmpty == true;
|
||||
final replyCount = widget.message.replyCount;
|
||||
final msg = replyCount != 0
|
||||
? '$replyCount ${replyCount > 1 ? 'Thread Replies' : 'Thread Reply'}'
|
||||
: 'Thread Reply';
|
||||
],
|
||||
if (showUsername)
|
||||
Text(
|
||||
widget.message.user.name,
|
||||
style: widget.messageTheme.replies.copyWith(
|
||||
color: widget.messageTheme.createdAt.color,
|
||||
),
|
||||
),
|
||||
if (showTimeStamp)
|
||||
Text(
|
||||
Jiffy(widget.message.createdAt.toLocal()).jm,
|
||||
style: widget.messageTheme.createdAt,
|
||||
),
|
||||
]);
|
||||
|
||||
final onThreadTap = () async {
|
||||
var message = widget.message;
|
||||
return widget.onThreadTap(message);
|
||||
};
|
||||
|
||||
children.addAll([
|
||||
if (showSendingIndicator) _buildSendingIndicator(),
|
||||
if (showThreadReplyIndicator) ...[
|
||||
if (showThreadParticipants)
|
||||
SizedBox.fromSize(
|
||||
size: Size((threadParticipants.length * 8.0) + 10, 16),
|
||||
child: _buildThreadParticipantsIndicator(),
|
||||
),
|
||||
InkWell(
|
||||
onTap: widget.onThreadTap != null ? onThreadTap : null,
|
||||
child: Text(msg, style: widget.messageTheme?.replies),
|
||||
),
|
||||
],
|
||||
if (showUsername)
|
||||
Text(
|
||||
widget.message.user.name,
|
||||
style: widget.messageTheme.replies.copyWith(
|
||||
color: widget.messageTheme.createdAt.color,
|
||||
),
|
||||
),
|
||||
if (showTimeStamp)
|
||||
Text(
|
||||
Jiffy(widget.message.createdAt.toLocal()).jm,
|
||||
style: widget.messageTheme.createdAt,
|
||||
),
|
||||
]);
|
||||
}
|
||||
if (widget.reverse) children = children.reversed.toList();
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(left: leftPadding),
|
||||
child: Flex(
|
||||
direction: Axis.horizontal,
|
||||
clipBehavior: Clip.none,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
if (!deleted && (showThreadReplyIndicator || showInChannel))
|
||||
Container(
|
||||
margin: EdgeInsets.only(
|
||||
bottom: widget.messageTheme.replies.fontSize / 2,
|
||||
),
|
||||
child: CustomPaint(
|
||||
size: const Size(16, 32),
|
||||
painter: _ThreadReplyPainter(
|
||||
context: context,
|
||||
color: widget.messageTheme.messageBorderColor,
|
||||
),
|
||||
),
|
||||
return Flex(
|
||||
direction: Axis.horizontal,
|
||||
clipBehavior: Clip.none,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
if (showThreadReplyIndicator || showInChannel)
|
||||
Container(
|
||||
margin: EdgeInsets.only(
|
||||
bottom: widget.messageTheme.replies.fontSize / 2,
|
||||
),
|
||||
...children.map(
|
||||
(child) => Transform(
|
||||
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
height: 16,
|
||||
child: Center(
|
||||
child: child,
|
||||
),
|
||||
child: CustomPaint(
|
||||
size: const Size(16, 32),
|
||||
painter: _ThreadReplyPainter(
|
||||
context: context,
|
||||
color: widget.messageTheme.messageBorderColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
].insertBetween(const SizedBox(width: 8.0)),
|
||||
),
|
||||
...children.map(
|
||||
(child) => Transform(
|
||||
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
height: 16,
|
||||
child: Center(
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
].insertBetween(const SizedBox(width: 8.0)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -698,14 +703,15 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
messageTheme: widget.messageTheme,
|
||||
messageShape: widget.shape ?? _getDefaultShape(context),
|
||||
reverse: widget.reverse,
|
||||
showDeleteMessage: widget.showDeleteMessage,
|
||||
showDeleteMessage: widget.showDeleteMessage || isDeleteFailed,
|
||||
message: widget.message,
|
||||
editMessageInputBuilder: widget.editMessageInputBuilder,
|
||||
onReplyTap: widget.onReplyTap,
|
||||
onThreadReplyTap: widget.onThreadTap,
|
||||
showResendMessage:
|
||||
widget.showResendMessage && (isSendFailed || isUpdateFailed),
|
||||
showCopyMessage: !isFailedState &&
|
||||
showCopyMessage: widget.showCopyMessage &&
|
||||
!isFailedState &&
|
||||
widget.message.text?.trim()?.isNotEmpty == true,
|
||||
showEditMessage: widget.showEditMessage &&
|
||||
!isDeleteFailed &&
|
||||
@@ -713,11 +719,12 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
?.any((element) => element.type == 'giphy') !=
|
||||
true,
|
||||
showReactions: widget.showReactions,
|
||||
showReply: widget.showReplyIndicator &&
|
||||
showReplyMessage: widget.showReplyMessage &&
|
||||
!isFailedState &&
|
||||
widget.onReplyTap != null,
|
||||
showThreadReply:
|
||||
widget.showThreadReplyIndicator && widget.onThreadTap != null,
|
||||
showThreadReplyMessage: widget.showThreadReplyMessage &&
|
||||
!isFailedState &&
|
||||
widget.onThreadTap != null,
|
||||
),
|
||||
);
|
||||
});
|
||||
@@ -754,9 +761,7 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
side: widget.attachmentBorderSide ??
|
||||
widget.borderSide ??
|
||||
BorderSide(
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? StreamChatTheme.of(context).colorTheme.white.withAlpha(24)
|
||||
: StreamChatTheme.of(context).colorTheme.black.withAlpha(24),
|
||||
color: StreamChatTheme.of(context).colorTheme.greyGainsboro,
|
||||
),
|
||||
borderRadius: widget.attachmentBorderRadiusGeometry ??
|
||||
widget.borderRadiusGeometry ??
|
||||
@@ -933,9 +938,7 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
bool get isOnlyEmoji =>
|
||||
widget.message.text.characters.isNotEmpty &&
|
||||
widget.message.text.characters.every((c) => Emoji.byChar(c) != null);
|
||||
bool get isOnlyEmoji => textIsOnlyEmoji(widget.message.text);
|
||||
|
||||
Color _getBackgroundColor() {
|
||||
if (hasQuotedMessage) {
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:emojis/emoji.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import 'attachment_error.dart';
|
||||
import 'extension.dart';
|
||||
import 'image_attachment.dart';
|
||||
import 'message_text.dart';
|
||||
import 'stream_chat_theme.dart';
|
||||
import 'user_avatar.dart';
|
||||
import 'utils.dart';
|
||||
import 'extension.dart';
|
||||
|
||||
typedef QuotedMessageAttachmentThumbnailBuilder = Widget Function(
|
||||
BuildContext,
|
||||
@@ -111,7 +110,7 @@ class QuotedMessageWidget extends StatelessWidget {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 8, bottom: 6, right: 4, left: 8),
|
||||
padding: const EdgeInsets.only(top: 8, right: 4, left: 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -126,8 +125,7 @@ class QuotedMessageWidget extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildMessage(BuildContext context) {
|
||||
final isOnlyEmoji =
|
||||
message.text.characters.every((c) => Emoji.byChar(c) != null);
|
||||
final isOnlyEmoji = textIsOnlyEmoji(message.text);
|
||||
var msg = _hasAttachments && !_containsText
|
||||
? message.copyWith(text: message.attachments.last?.title ?? '')
|
||||
: message;
|
||||
|
||||
@@ -27,7 +27,8 @@ class ReactionBubble extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final reactionIcons = StreamChatTheme.of(context).reactionIcons;
|
||||
final offset = reactions.length > 1 ? 16.0 : 2.0;
|
||||
final totalReactions = reactions.length;
|
||||
final offset = totalReactions > 1 ? 16.0 : 2.0;
|
||||
return Transform(
|
||||
transform: Matrix4.rotationY(reverse ? pi : 0),
|
||||
alignment: Alignment.center,
|
||||
@@ -37,7 +38,10 @@ class ReactionBubble extends StatelessWidget {
|
||||
Transform.translate(
|
||||
offset: Offset(reverse ? offset : -offset, 0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 4,
|
||||
horizontal: totalReactions > 1 ? 4 : 0,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: borderColor,
|
||||
@@ -134,16 +138,11 @@ class ReactionBubble extends StatelessWidget {
|
||||
borderColor,
|
||||
),
|
||||
);
|
||||
|
||||
if (!flipTail) {
|
||||
return tail;
|
||||
} else {
|
||||
return Transform(
|
||||
transform: Matrix4.rotationY(pi),
|
||||
alignment: Alignment.center,
|
||||
child: tail,
|
||||
);
|
||||
}
|
||||
return Transform(
|
||||
transform: Matrix4.rotationY(flipTail ? 0 : pi),
|
||||
alignment: Alignment.center,
|
||||
child: tail,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -58,62 +58,65 @@ class _ReactionPickerState extends State<ReactionPicker>
|
||||
return Transform.scale(
|
||||
scale: val,
|
||||
child: Material(
|
||||
color: widget.messageTheme.reactionsBackgroundColor,
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: reactionIcons.map((reactionIcon) {
|
||||
final ownReactionIndex = widget.message.ownReactions
|
||||
?.indexWhere((reaction) =>
|
||||
reaction.type == reactionIcon.type) ??
|
||||
-1;
|
||||
var index = reactionIcons.indexOf(reactionIcon);
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: reactionIcons.map((reactionIcon) {
|
||||
final ownReactionIndex = widget.message.ownReactions
|
||||
?.indexWhere((reaction) =>
|
||||
reaction.type == reactionIcon.type) ??
|
||||
-1;
|
||||
var index = reactionIcons.indexOf(reactionIcon);
|
||||
|
||||
return IconButton(
|
||||
iconSize: 24,
|
||||
icon: AnimatedBuilder(
|
||||
animation: animations[index],
|
||||
builder: (context, val) {
|
||||
return Transform(
|
||||
transform: Matrix4.identity()
|
||||
..scale(animations[index].value,
|
||||
animations[index].value)
|
||||
..rotateZ(1.0 - animations[index].value),
|
||||
child: StreamSvgIcon(
|
||||
assetName: reactionIcon.assetName,
|
||||
height: animations[index].value * 24.0,
|
||||
width: animations[index].value * 24.0,
|
||||
color: ownReactionIndex != -1
|
||||
? StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentBlue
|
||||
: Theme.of(context)
|
||||
.iconTheme
|
||||
.color
|
||||
.withOpacity(.5),
|
||||
),
|
||||
return IconButton(
|
||||
iconSize: 24,
|
||||
icon: AnimatedBuilder(
|
||||
animation: animations[index],
|
||||
builder: (context, val) {
|
||||
return Transform(
|
||||
transform: Matrix4.identity()
|
||||
..scale(animations[index].value,
|
||||
animations[index].value)
|
||||
..rotateZ(1.0 - animations[index].value),
|
||||
child: StreamSvgIcon(
|
||||
assetName: reactionIcon.assetName,
|
||||
height: animations[index].value * 24.0,
|
||||
width: animations[index].value * 24.0,
|
||||
color: ownReactionIndex != -1
|
||||
? StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentBlue
|
||||
: Theme.of(context)
|
||||
.iconTheme
|
||||
.color
|
||||
.withOpacity(.5),
|
||||
),
|
||||
);
|
||||
}),
|
||||
onPressed: () {
|
||||
if (ownReactionIndex != -1) {
|
||||
removeReaction(
|
||||
context,
|
||||
widget.message.ownReactions[ownReactionIndex],
|
||||
);
|
||||
}),
|
||||
onPressed: () {
|
||||
if (ownReactionIndex != -1) {
|
||||
removeReaction(
|
||||
context,
|
||||
widget.message.ownReactions[ownReactionIndex],
|
||||
);
|
||||
} else {
|
||||
sendReaction(
|
||||
context,
|
||||
reactionIcon.type,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
} else {
|
||||
sendReaction(
|
||||
context,
|
||||
reactionIcon.type,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -228,19 +228,9 @@ class StreamChatThemeData {
|
||||
inputBackground: colorTheme.white.withAlpha(12),
|
||||
),
|
||||
ownMessageTheme: MessageTheme(
|
||||
messageText: TextStyle(
|
||||
fontSize: 14.5,
|
||||
color: colorTheme.black,
|
||||
),
|
||||
createdAt: TextStyle(
|
||||
color: colorTheme.black.withOpacity(.5),
|
||||
fontSize: 12,
|
||||
),
|
||||
replies: TextStyle(
|
||||
color: accentColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 12,
|
||||
),
|
||||
messageText: textTheme.body,
|
||||
createdAt: textTheme.footnote.copyWith(color: colorTheme.grey),
|
||||
replies: textTheme.footnoteBold.copyWith(color: accentColor),
|
||||
messageBackgroundColor: colorTheme.greyGainsboro,
|
||||
reactionsBackgroundColor: colorTheme.white,
|
||||
reactionsBorderColor: colorTheme.greyWhisper,
|
||||
@@ -259,19 +249,9 @@ class StreamChatThemeData {
|
||||
otherMessageTheme: MessageTheme(
|
||||
reactionsBackgroundColor: colorTheme.greyGainsboro,
|
||||
reactionsBorderColor: colorTheme.white,
|
||||
messageText: TextStyle(
|
||||
fontSize: 14.5,
|
||||
color: colorTheme.black,
|
||||
),
|
||||
createdAt: TextStyle(
|
||||
color: colorTheme.black.withOpacity(.5),
|
||||
fontSize: 12,
|
||||
),
|
||||
replies: TextStyle(
|
||||
color: accentColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 12,
|
||||
),
|
||||
messageText: textTheme.body,
|
||||
createdAt: textTheme.footnote.copyWith(color: colorTheme.grey),
|
||||
replies: textTheme.footnoteBold.copyWith(color: accentColor),
|
||||
messageLinks: TextStyle(
|
||||
color: accentColor,
|
||||
),
|
||||
|
||||
@@ -470,7 +470,19 @@ class StreamSvgIcon extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.icon_SHARE({
|
||||
factory StreamSvgIcon.Icon_moon({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'icon_moon.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.Icon_SHARE({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
import 'back_button.dart';
|
||||
import 'channel_name.dart';
|
||||
@@ -81,6 +82,7 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
elevation: 1,
|
||||
leading: showBackButton
|
||||
? StreamBackButton(
|
||||
cid: StreamChannel.of(context).channel.cid,
|
||||
onPressed: onBackPressed,
|
||||
showUnreads: true,
|
||||
)
|
||||
|
||||
@@ -5,14 +5,22 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
class UnreadIndicator extends StatelessWidget {
|
||||
const UnreadIndicator({
|
||||
Key key,
|
||||
this.cid,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Channel cid used to retrieve unread count
|
||||
final String cid;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final client = StreamChat.of(context).client;
|
||||
return StreamBuilder<int>(
|
||||
stream: client.state.totalUnreadCountStream,
|
||||
initialData: client.state.totalUnreadCount,
|
||||
stream: cid != null
|
||||
? client.state.channels[cid].state.unreadCountStream
|
||||
: client.state.totalUnreadCountStream,
|
||||
initialData: cid != null
|
||||
? client.state.channels[cid].state.unreadCount
|
||||
: client.state.totalUnreadCount,
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData || snapshot.data == 0) {
|
||||
return SizedBox();
|
||||
|
||||
@@ -24,10 +24,6 @@ class UrlAttachment extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (urlAttachment.imageUrl != null)
|
||||
SizedBox(
|
||||
height: 16.0,
|
||||
),
|
||||
if (urlAttachment.imageUrl != null)
|
||||
Container(
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import 'package:emojis/emoji.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'stream_svg_icon.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
import 'stream_svg_icon.dart';
|
||||
|
||||
Future<void> launchURL(BuildContext context, String url) async {
|
||||
if (await canLaunch(url)) {
|
||||
@@ -215,3 +215,16 @@ StreamSvgIcon getFileTypeImage(String type) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
final _emojis = Emoji.all();
|
||||
|
||||
bool textIsOnlyEmoji(String text) {
|
||||
return text.trim().characters.isNotEmpty &&
|
||||
text.trim().characters.every((c) =>
|
||||
_emojis.firstWhere(
|
||||
(Emoji emoji) => emoji.char.contains(c),
|
||||
orElse: () => null,
|
||||
) !=
|
||||
null) &&
|
||||
text.characters.length < 4;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user