Merge branch 'feature/new-ui' into feature/mono-repo

This commit is contained in:
Salvatore Giordano
2021-01-11 16:29:15 +01:00
26 changed files with 704 additions and 514 deletions
@@ -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,
+15 -2
View File
@@ -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;
}