Merge branch 'feature/new-ui' of https://github.com/GetStream/stream-chat-flutter into feature/flag

 Conflicts:
	lib/src/message_actions_modal.dart
	lib/src/message_list_view.dart
	lib/src/message_widget.dart
This commit is contained in:
Deven Joshi
2021-01-11 21:38:12 +05:30
18 changed files with 458 additions and 320 deletions
+53 -10
View File
@@ -1,13 +1,16 @@
import 'dart:io'; import 'dart:io';
import 'package:example/routes/app_routes.dart';
import 'package:example/routes/routes.dart';
import 'package:example/stream_version.dart'; import 'package:example/stream_version.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:streaming_shared_preferences/streaming_shared_preferences.dart';
import 'choose_user_page.dart'; import 'choose_user_page.dart';
import 'main.dart';
import 'notifications_service.dart'; import 'notifications_service.dart';
class AdvancedOptionsPage extends StatefulWidget { class AdvancedOptionsPage extends StatefulWidget {
@@ -312,17 +315,57 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) { builder: (context) {
return MaterialApp( return FutureBuilder<StreamingSharedPreferences>(
theme: ThemeData.light(), future: StreamingSharedPreferences.instance,
darkTheme: ThemeData.dark(), builder: (context, snapshot) {
themeMode: ThemeMode.system, if (!snapshot.hasData) {
builder: (context, widget) { return SizedBox();
return StreamChat( }
child: widget, return PreferenceBuilder<int>(
client: client, preference: snapshot.data.getInt(
'theme',
defaultValue: 0,
),
builder: (context, snapshot) => MaterialApp(
builder: (context, child) {
return StreamChat(
client: client,
child: Builder(
builder: (context) =>
AnnotatedRegion<
SystemUiOverlayStyle>(
child: child,
value: SystemUiOverlayStyle(
systemNavigationBarColor:
StreamChatTheme.of(context)
.colorTheme
.white,
systemNavigationBarIconBrightness:
Theme.of(context)
.brightness ==
Brightness.dark
? Brightness.light
: Brightness.dark,
),
),
),
);
},
debugShowCheckedModeBanner: false,
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
themeMode: {
-1: ThemeMode.dark,
0: ThemeMode.system,
1: ThemeMode.light,
}[snapshot],
onGenerateRoute: AppRoutes.generateRoute,
initialRoute: client.state.user == null
? Routes.CHOOSE_USER
: Routes.HOME,
),
); );
}, },
home: ChannelListPage(),
); );
}, },
), ),
+1 -1
View File
@@ -120,7 +120,7 @@ class ChannelPage extends StatelessWidget {
top: !details.isLastUser ? Radius.circular(16) : Radius.zero, top: !details.isLastUser ? Radius.circular(16) : Radius.zero,
bottom: !details.isNextUser ? Radius.circular(16) : Radius.zero, bottom: !details.isNextUser ? Radius.circular(16) : Radius.zero,
), ),
showSendingIndicator: DisplayWidget.gone, showSendingIndicator: false,
reverse: false, reverse: false,
showUserAvatar: showUserAvatar:
details.isNextUser ? DisplayWidget.hide : DisplayWidget.show, details.isNextUser ? DisplayWidget.hide : DisplayWidget.show,
+153 -116
View File
@@ -10,6 +10,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:streaming_shared_preferences/streaming_shared_preferences.dart';
import 'notifications_service.dart'; import 'notifications_service.dart';
import 'routes/app_routes.dart'; import 'routes/app_routes.dart';
@@ -52,32 +53,50 @@ class MyApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return FutureBuilder<StreamingSharedPreferences>(
builder: (context, child) { future: StreamingSharedPreferences.instance,
return StreamChat( builder: (context, snapshot) {
client: client, if (!snapshot.hasData) {
child: Builder( return SizedBox();
builder: (context) => AnnotatedRegion<SystemUiOverlayStyle>( }
child: child, return PreferenceBuilder<int>(
value: SystemUiOverlayStyle( preference: snapshot.data.getInt(
systemNavigationBarColor: 'theme',
StreamChatTheme.of(context).colorTheme.white, defaultValue: 0,
systemNavigationBarIconBrightness: ),
Theme.of(context).brightness == Brightness.dark builder: (context, snapshot) => MaterialApp(
? Brightness.light builder: (context, child) {
: Brightness.dark, return StreamChat(
), client: client,
), child: Builder(
builder: (context) => AnnotatedRegion<SystemUiOverlayStyle>(
child: child,
value: SystemUiOverlayStyle(
systemNavigationBarColor:
StreamChatTheme.of(context).colorTheme.white,
systemNavigationBarIconBrightness:
Theme.of(context).brightness == Brightness.dark
? Brightness.light
: Brightness.dark,
),
),
),
);
},
debugShowCheckedModeBanner: false,
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
themeMode: {
-1: ThemeMode.dark,
0: ThemeMode.system,
1: ThemeMode.light,
}[snapshot],
onGenerateRoute: AppRoutes.generateRoute,
initialRoute:
client.state.user == null ? Routes.CHOOSE_USER : Routes.HOME,
), ),
); );
}, },
debugShowCheckedModeBanner: false,
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
themeMode: ThemeMode.system,
onGenerateRoute: AppRoutes.generateRoute,
initialRoute:
client.state.user == null ? Routes.CHOOSE_USER : Routes.HOME,
); );
} }
} }
@@ -166,113 +185,131 @@ class _HomePageState extends State<HomePage> {
Drawer _buildDrawer(BuildContext context, User user) { Drawer _buildDrawer(BuildContext context, User user) {
return Drawer( return Drawer(
child: SafeArea( child: Container(
child: Padding( color: StreamChatTheme.of(context).colorTheme.white,
padding: EdgeInsets.only( child: SafeArea(
top: MediaQuery.of(context).viewPadding.top + 8, child: Padding(
), padding: EdgeInsets.only(
child: Column( top: MediaQuery.of(context).viewPadding.top + 8,
children: [ ),
Padding( child: Column(
padding: const EdgeInsets.only( children: [
bottom: 20.0, Padding(
left: 8, padding: const EdgeInsets.only(
), bottom: 20.0,
child: Row( left: 8,
children: [ ),
UserAvatar( child: Row(
user: user, children: [
showOnlineStatus: false, UserAvatar(
constraints: BoxConstraints.tight(Size.fromRadius(20)), user: user,
), showOnlineStatus: false,
Padding( constraints: BoxConstraints.tight(Size.fromRadius(20)),
padding: const EdgeInsets.only(left: 16.0), ),
child: Text( Padding(
user.name, padding: const EdgeInsets.only(left: 16.0),
style: TextStyle( child: Text(
fontSize: 16, user.name,
fontWeight: FontWeight.bold, style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
), ),
), ),
],
),
),
ListTile(
leading: StreamSvgIcon.penWrite(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(.5),
),
onTap: () {
Navigator.popAndPushNamed(
context,
Routes.NEW_CHAT,
);
},
title: Text(
'New direct message',
style: TextStyle(
fontSize: 14.5,
), ),
],
),
),
ListTile(
leading: StreamSvgIcon.penWrite(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(.5),
),
onTap: () {
Navigator.popAndPushNamed(
context,
Routes.NEW_CHAT,
);
},
title: Text(
'New direct message',
style: TextStyle(
fontSize: 14.5,
), ),
), ),
), ListTile(
ListTile( leading: StreamSvgIcon.contacts(
leading: StreamSvgIcon.contacts( color: StreamChatTheme.of(context)
color: StreamChatTheme.of(context) .colorTheme
.colorTheme .black
.black .withOpacity(.5),
.withOpacity(.5), ),
), onTap: () {
onTap: () { Navigator.popAndPushNamed(
Navigator.popAndPushNamed( context,
context, Routes.NEW_GROUP_CHAT,
Routes.NEW_GROUP_CHAT, );
); },
}, title: Text(
title: Text( 'New group',
'New group', style: TextStyle(
style: TextStyle( fontSize: 14.5,
fontSize: 14.5, ),
), ),
), ),
), Expanded(
Expanded( child: Container(
child: Container( alignment: Alignment.bottomCenter,
alignment: Alignment.bottomCenter, child: ListTile(
child: ListTile( onTap: () async {
onTap: () async { Navigator.pop(context);
Navigator.pop(context);
final secureStorage = FlutterSecureStorage(); final secureStorage = FlutterSecureStorage();
await secureStorage.deleteAll(); await secureStorage.deleteAll();
StreamChat.of(context).client.disconnect( StreamChat.of(context).client.disconnect(
clearUser: true, clearUser: true,
);
await Navigator.pushReplacementNamed(
context,
Routes.CHOOSE_USER,
);
},
leading: StreamSvgIcon.user(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(.5),
),
title: Text(
'Sign out',
style: TextStyle(
fontSize: 14.5,
),
),
trailing: IconButton(
icon: StreamSvgIcon.Icon_moon(
size: 24,
),
color: StreamChatTheme.of(context).colorTheme.grey,
onPressed: () async {
final sp = await StreamingSharedPreferences.instance;
sp.setInt(
'theme',
Theme.of(context).brightness == Brightness.dark
? 1
: -1,
); );
},
await Navigator.pushReplacementNamed(
context,
Routes.CHOOSE_USER,
);
},
leading: StreamSvgIcon.user(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(.5),
),
title: Text(
'Sign out',
style: TextStyle(
fontSize: 14.5,
), ),
), ),
), ),
), ),
), ],
], ),
), ),
), ),
), ),
+1
View File
@@ -16,6 +16,7 @@ dependencies:
flutter_secure_storage: ^3.3.5 flutter_secure_storage: ^3.3.5
yaml: ^2.2.1 yaml: ^2.2.1
uuid: ^2.2.2 uuid: ^2.2.2
streaming_shared_preferences: ^1.0.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
+7 -1
View File
@@ -9,11 +9,15 @@ class StreamBackButton extends StatelessWidget {
Key key, Key key,
this.onPressed, this.onPressed,
this.showUnreads = false, this.showUnreads = false,
this.cid,
}) : super(key: key); }) : super(key: key);
final VoidCallback onPressed; final VoidCallback onPressed;
final bool showUnreads; final bool showUnreads;
/// Channel cid used to retrieve unread count
final String cid;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Stack( return Stack(
@@ -45,7 +49,9 @@ class StreamBackButton extends StatelessWidget {
Positioned( Positioned(
top: 7, top: 7,
right: 7, right: 7,
child: UnreadIndicator(), child: UnreadIndicator(
cid: cid,
),
), ),
], ],
); );
+1 -4
View File
@@ -63,10 +63,7 @@ class DeletedMessage extends StatelessWidget {
'Message deleted', 'Message deleted',
style: messageTheme.messageText.copyWith( style: messageTheme.messageText.copyWith(
fontStyle: FontStyle.italic, fontStyle: FontStyle.italic,
color: (Theme.of(context).brightness == Brightness.dark color: messageTheme.createdAt.color,
? StreamChatTheme.of(context).colorTheme.white
: StreamChatTheme.of(context).colorTheme.black)
.withOpacity(.5),
), ),
), ),
), ),
+1 -1
View File
@@ -116,7 +116,7 @@ class _ImageFooterState extends State<ImageFooter> {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
IconButton( IconButton(
icon: StreamSvgIcon.icon_SHARE( icon: StreamSvgIcon.Icon_SHARE(
size: 24.0, size: 24.0,
color: StreamChatTheme.of(context).colorTheme.black, color: StreamChatTheme.of(context).colorTheme.black,
), ),
+8 -8
View File
@@ -32,8 +32,8 @@ class MessageActionsModal extends StatefulWidget {
final bool showCopyMessage; final bool showCopyMessage;
final bool showEditMessage; final bool showEditMessage;
final bool showResendMessage; final bool showResendMessage;
final bool showReply; final bool showReplyMessage;
final bool showThreadReply; final bool showThreadReplyMessage;
final bool showFlagButton; final bool showFlagButton;
final bool reverse; final bool reverse;
final ShapeBorder messageShape; final ShapeBorder messageShape;
@@ -49,9 +49,9 @@ class MessageActionsModal extends StatefulWidget {
this.onReplyTap, this.onReplyTap,
this.onThreadReplyTap, this.onThreadReplyTap,
this.showCopyMessage = true, this.showCopyMessage = true,
this.showReply = true, this.showReplyMessage = true,
this.showResendMessage = true, this.showResendMessage = true,
this.showThreadReply = true, this.showThreadReplyMessage = true,
this.showFlagButton = true, this.showFlagButton = true,
this.showUserAvatar = DisplayWidget.show, this.showUserAvatar = DisplayWidget.show,
this.editMessageInputBuilder, this.editMessageInputBuilder,
@@ -148,8 +148,8 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
showReactions: false, showReactions: false,
showUsername: false, showUsername: false,
showThreadReplyIndicator: false, showThreadReplyIndicator: false,
showReplyIndicator: false, showReplyMessage: false,
showUserAvatar: widget.showUserAvatar, showUserAvatar: showUserAvatar,
showTimestamp: false, showTimestamp: false,
translateUserAvatar: false, translateUserAvatar: false,
showReactionPickerIndicator: showReactionPickerIndicator:
@@ -200,14 +200,14 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
.greyWhisper, .greyWhisper,
context: context, context: context,
tiles: [ tiles: [
if (widget.showReply && if (widget.showReplyMessage &&
(widget.message.status == (widget.message.status ==
MessageSendingStatus.SENT || MessageSendingStatus.SENT ||
widget.message.status == widget.message.status ==
null) && null) &&
widget.message.parentId == null) widget.message.parentId == null)
_buildReplyButton(context), _buildReplyButton(context),
if (widget.showThreadReply && if (widget.showThreadReplyMessage &&
(widget.message.status == (widget.message.status ==
MessageSendingStatus.SENT || MessageSendingStatus.SENT ||
widget.message.status == widget.message.status ==
+62 -41
View File
@@ -187,7 +187,6 @@ class MessageListView extends StatefulWidget {
class _MessageListViewState extends State<MessageListView> { class _MessageListViewState extends State<MessageListView> {
ItemScrollController _scrollController; ItemScrollController _scrollController;
bool _bottomWasVisible = false;
Function _onThreadTap; Function _onThreadTap;
bool _showScrollToBottom = false; bool _showScrollToBottom = false;
ItemPositionsListener _itemPositionListener; ItemPositionsListener _itemPositionListener;
@@ -220,6 +219,8 @@ class _MessageListViewState extends State<MessageListView> {
bool get _upToDate => streamChannel.channel.state.isUpToDate; bool get _upToDate => streamChannel.channel.state.isUpToDate;
bool get _isThreadConversation => widget.parentMessage != null;
bool _topPaginationActive = false; bool _topPaginationActive = false;
bool _bottomPaginationActive = false; bool _bottomPaginationActive = false;
@@ -234,7 +235,7 @@ class _MessageListViewState extends State<MessageListView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final messagesStream = widget.parentMessage != null final messagesStream = _isThreadConversation
? streamChannel.channel.state.threadsStream ? streamChannel.channel.state.threadsStream
.where((threads) => threads.containsKey(widget.parentMessage.id)) .where((threads) => threads.containsKey(widget.parentMessage.id))
.map((threads) => threads[widget.parentMessage.id]) .map((threads) => threads[widget.parentMessage.id])
@@ -333,9 +334,8 @@ class _MessageListViewState extends State<MessageListView> {
physics: widget.scrollPhysics, physics: widget.scrollPhysics,
itemScrollController: _scrollController, itemScrollController: _scrollController,
reverse: true, reverse: true,
itemCount: messages.length + itemCount:
2 + messages.length + 2 + (_isThreadConversation ? 1 : 0),
(widget.parentMessage != null ? 1 : 0),
separatorBuilder: (context, i) { separatorBuilder: (context, i) {
if (i == messages.length) return Offstage(); if (i == messages.length) return Offstage();
if (i == messages.length + 2) return Offstage(); if (i == messages.length + 2) return Offstage();
@@ -368,7 +368,11 @@ class _MessageListViewState extends State<MessageListView> {
final isNextUserSame = final isNextUserSame =
message.user.id == nextMessage.user?.id; message.user.id == nextMessage.user?.id;
final isThread = message.replyCount > 0; final isThread = message.replyCount > 0;
if (timeDiff >= 1 || !isNextUserSame || isThread) { final isDeleted = message.isDeleted;
if (timeDiff >= 1 ||
!isNextUserSame ||
isThread ||
isDeleted) {
return SizedBox(height: 8); return SizedBox(height: 8);
} }
return SizedBox(height: 2); return SizedBox(height: 2);
@@ -497,7 +501,7 @@ class _MessageListViewState extends State<MessageListView> {
Future<void> _paginateData( Future<void> _paginateData(
StreamChannelState channel, QueryDirection direction) { StreamChannelState channel, QueryDirection direction) {
if (widget.parentMessage == null) { if (!_isThreadConversation) {
return channel.queryMessages(direction: direction); return channel.queryMessages(direction: direction);
} else { } else {
return channel.getReplies(widget.parentMessage.id); return channel.getReplies(widget.parentMessage.id);
@@ -687,14 +691,13 @@ class _MessageListViewState extends State<MessageListView> {
key: ValueKey<String>('BOTTOM-MESSAGE'), key: ValueKey<String>('BOTTOM-MESSAGE'),
onVisibilityChanged: (visibility) { onVisibilityChanged: (visibility) {
final isVisible = visibility.visibleBounds != Rect.zero; final isVisible = visibility.visibleBounds != Rect.zero;
if (isVisible && !_bottomWasVisible) { if (isVisible) {
final channel = streamChannel.channel; final channel = streamChannel.channel;
if (_upToDate && if (_upToDate &&
channel.config?.readEvents == true && channel.config?.readEvents == true &&
channel.state.unreadCount > 0) { channel.state.unreadCount > 0) {
streamChannel.channel.markRead(); streamChannel.channel.markRead();
} }
_bottomWasVisible = !isVisible;
} }
if (mounted) { if (mounted) {
setState(() => _showScrollToBottom = !isVisible); setState(() => _showScrollToBottom = !isVisible);
@@ -712,7 +715,12 @@ class _MessageListViewState extends State<MessageListView> {
return MessageWidget( return MessageWidget(
showThreadReplyIndicator: false, showThreadReplyIndicator: false,
showInChannelIndicator: false, showInChannelIndicator: false,
showReplyIndicator: false, showReplyMessage: false,
showResendMessage: false,
showThreadReplyMessage: false,
showCopyMessage: false,
showDeleteMessage: false,
showEditMessage: false,
message: message, message: message,
reverse: isMyMessage, reverse: isMyMessage,
showUsername: !isMyMessage, showUsername: !isMyMessage,
@@ -722,10 +730,8 @@ class _MessageListViewState extends State<MessageListView> {
right: 8.0, right: 8.0,
bottom: 16.0, bottom: 16.0,
), ),
showSendingIndicator: DisplayWidget.hide, showSendingIndicator: false,
onThreadTap: _onThreadTap, onThreadTap: _onThreadTap,
showEditMessage: false,
showDeleteMessage: false,
borderRadiusGeometry: BorderRadius.only( borderRadiusGeometry: BorderRadius.only(
topLeft: Radius.circular(16), topLeft: Radius.circular(16),
bottomLeft: Radius.circular(2), bottomLeft: Radius.circular(2),
@@ -768,32 +774,60 @@ class _MessageListViewState extends State<MessageListView> {
} }
final channel = streamChannel.channel; final channel = streamChannel.channel;
final readList = channel.state?.read final readList = channel.state?.read?.where((read) {
?.where((element) => element.user.id != userId) if (read.user.id == userId) return false;
?.where((read) => return (read.lastRead.isAfter(message.createdAt) ||
(read.lastRead.isAfter(message.createdAt) || read.lastRead.isAtSameMomentAs(message.createdAt)) &&
read.lastRead.isAtSameMomentAs(message.createdAt)) && (index == 0 ||
(index == 0 || read.lastRead.isBefore(messages[index - 1].createdAt));
read.lastRead.isBefore(messages[index - 1].createdAt))) })?.toList() ??
?.toList() ??
[]; [];
final allRead = readList.length >= (channel.memberCount ?? 0) - 1; final allRead = readList.length >= (channel.memberCount ?? 0) - 1;
final isThreadMessage =
widget.parentMessage != null || message?.showInChannel == true;
final hasFileAttachment = final hasFileAttachment =
message.attachments.any((it) => it.type == 'file'); message.attachments.any((it) => it.type == 'file');
final isThreadMessage =
message?.parentId != null && message?.showInChannel == true;
final hasReplies = message.replyCount > 0;
final attachmentBorderRadius = hasFileAttachment ? 12.0 : 14.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( Widget child = MessageWidget(
key: ValueKey<String>('MESSAGE-${message.id}'), key: ValueKey<String>('MESSAGE-${message.id}'),
message: message, message: message,
reverse: isMyMessage, reverse: isMyMessage,
showReactions: !message.isDeleted, showReactions: !message.isDeleted,
padding: const EdgeInsets.symmetric(horizontal: 8.0), padding: const EdgeInsets.symmetric(horizontal: 8.0),
showInChannelIndicator: showInChannelIndicator,
showThreadReplyIndicator: showThreadReplyIndicator,
showUsername: showUsername,
showTimestamp: showTimeStamp,
showSendingIndicator: showSendingIndicator,
showUserAvatar: showUserAvatar,
onQuotedMessageTap: (quotedMessageId) async { onQuotedMessageTap: (quotedMessageId) async {
final scrollToIndex = () { final scrollToIndex = () {
final index = messages.indexWhere((m) => m.id == quotedMessageId); final index = messages.indexWhere((m) => m.id == quotedMessageId);
@@ -814,17 +848,9 @@ class _MessageListViewState extends State<MessageListView> {
}); });
} }
}, },
showInChannelIndicator: widget.parentMessage == null,
showThreadReplyIndicator: widget.parentMessage == null,
showUsername: !isMyMessage && (timeDiff >= 1 || !isNextUserSame),
showSendingIndicator:
isMyMessage && (index == 0 || timeDiff >= 1 || !isNextUserSame)
? DisplayWidget.show
: DisplayWidget.hide,
showTimestamp:
!isNextUserSame || readList?.isNotEmpty == true || timeDiff >= 1,
showEditMessage: isMyMessage, showEditMessage: isMyMessage,
showDeleteMessage: isMyMessage, showDeleteMessage: isMyMessage,
showThreadReplyMessage: !isThreadMessage,
showFlagButton: !isMyMessage, showFlagButton: !isMyMessage,
borderSide: isMyMessage ? BorderSide.none : null, borderSide: isMyMessage ? BorderSide.none : null,
onThreadTap: _onThreadTap, onThreadTap: _onThreadTap,
@@ -843,11 +869,6 @@ class _MessageListViewState extends State<MessageListView> {
topRight: Radius.circular(16), topRight: Radius.circular(16),
bottomRight: Radius.circular(16), bottomRight: Radius.circular(16),
), ),
showUserAvatar: isMyMessage
? DisplayWidget.gone
: (timeDiff >= 1 || !isNextUserSame
? DisplayWidget.show
: DisplayWidget.hide),
messageTheme: isMyMessage messageTheme: isMyMessage
? StreamChatTheme.of(context).ownMessageTheme ? StreamChatTheme.of(context).ownMessageTheme
: StreamChatTheme.of(context).otherMessageTheme, : StreamChatTheme.of(context).otherMessageTheme,
@@ -856,7 +877,7 @@ class _MessageListViewState extends State<MessageListView> {
onShowMessage: widget.onShowMessage, onShowMessage: widget.onShowMessage,
); );
if (!isThreadMessage) { if (!message.isDeleted && !message.isSystem && !message.isEphemeral) {
child = Swipeable( child = Swipeable(
onSwipeEnd: () => widget.onMessageSwiped(message), onSwipeEnd: () => widget.onMessageSwiped(message),
backgroundIcon: StreamSvgIcon.reply( backgroundIcon: StreamSvgIcon.reply(
@@ -919,7 +940,7 @@ class _MessageListViewState extends State<MessageListView> {
} }
}); });
if (widget.parentMessage != null) { if (_isThreadConversation) {
streamChannel.getReplies(widget.parentMessage.id); streamChannel.getReplies(widget.parentMessage.id);
} }
+1 -1
View File
@@ -116,7 +116,7 @@ class MessageReactionsModal extends StatelessWidget {
showThreadReplyIndicator: false, showThreadReplyIndicator: false,
showTimestamp: false, showTimestamp: false,
translateUserAvatar: false, translateUserAvatar: false,
showSendingIndicator: DisplayWidget.gone, showSendingIndicator: false,
shape: messageShape, shape: messageShape,
showInChannelIndicator: false, showInChannelIndicator: false,
showReactionPickerIndicator: showReactions && showReactionPickerIndicator: showReactions &&
+123 -126
View File
@@ -1,7 +1,6 @@
import 'dart:math'; import 'dart:math';
import 'dart:ui'; import 'dart:ui';
import 'package:emojis/emoji.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -96,7 +95,7 @@ class MessageWidget extends StatefulWidget {
final DisplayWidget showUserAvatar; final DisplayWidget showUserAvatar;
/// It controls the display behaviour of the sending indicator /// It controls the display behaviour of the sending indicator
final DisplayWidget showSendingIndicator; final bool showSendingIndicator;
/// If true the widget will show the reactions /// If true the widget will show the reactions
final bool showReactions; final bool showReactions;
@@ -106,9 +105,6 @@ class MessageWidget extends StatefulWidget {
/// If true the widget will show the thread reply indicator /// If true the widget will show the thread reply indicator
final bool showThreadReplyIndicator; 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 /// If true the widget will show the show in channel indicator
final bool showInChannelIndicator; final bool showInChannelIndicator;
@@ -121,9 +117,6 @@ class MessageWidget extends StatefulWidget {
/// Used in [MessageReactionsModal] and [MessageActionsModal] /// Used in [MessageReactionsModal] and [MessageActionsModal]
final bool showReactionPickerIndicator; final bool showReactionPickerIndicator;
/// If true the widget will show the resendMessage indicator
final bool showResendMessage;
final List<Read> readList; final List<Read> readList;
final ShowMessageCallback onShowMessage; final ShowMessageCallback onShowMessage;
@@ -131,8 +124,14 @@ class MessageWidget extends StatefulWidget {
/// If true show the users username next to the timestamp of the message /// If true show the users username next to the timestamp of the message
final bool showUsername; final bool showUsername;
final bool showTimestamp; final bool showTimestamp;
final bool showDeleteMessage;
final bool showReplyMessage;
final bool showThreadReplyMessage;
final bool showEditMessage; final bool showEditMessage;
final bool showCopyMessage;
final bool showDeleteMessage;
final bool showResendMessage;
final bool showFlagButton; final bool showFlagButton;
final Map<String, AttachmentBuilder> attachmentBuilders; final Map<String, AttachmentBuilder> attachmentBuilders;
@@ -158,10 +157,9 @@ class MessageWidget extends StatefulWidget {
this.onMentionTap, this.onMentionTap,
this.showReactionPickerIndicator = false, this.showReactionPickerIndicator = false,
this.showUserAvatar = DisplayWidget.show, this.showUserAvatar = DisplayWidget.show,
this.showSendingIndicator = DisplayWidget.show, this.showSendingIndicator = true,
this.showThreadReplyIndicator = true, this.showThreadReplyIndicator = true,
this.showInChannelIndicator = true, this.showInChannelIndicator = true,
this.showReplyIndicator = true,
this.onReplyTap, this.onReplyTap,
this.onThreadTap, this.onThreadTap,
this.showUsername = true, this.showUsername = true,
@@ -169,6 +167,10 @@ class MessageWidget extends StatefulWidget {
this.showReactions = true, this.showReactions = true,
this.showDeleteMessage = true, this.showDeleteMessage = true,
this.showEditMessage = true, this.showEditMessage = true,
this.showReplyMessage = true,
this.showThreadReplyMessage = true,
this.showResendMessage = true,
this.showCopyMessage = true,
this.showFlagButton = true, this.showFlagButton = true,
this.onUserAvatarTap, this.onUserAvatarTap,
this.onLinkTap, this.onLinkTap,
@@ -177,7 +179,6 @@ class MessageWidget extends StatefulWidget {
this.editMessageInputBuilder, this.editMessageInputBuilder,
this.textBuilder, this.textBuilder,
Map<String, AttachmentBuilder> customAttachmentBuilders, Map<String, AttachmentBuilder> customAttachmentBuilders,
this.showResendMessage = true,
this.readList, this.readList,
this.padding, this.padding,
this.textPadding = const EdgeInsets.symmetric( this.textPadding = const EdgeInsets.symmetric(
@@ -241,18 +242,19 @@ class MessageWidget extends StatefulWidget {
} }
class _MessageWidgetState extends State<MessageWidget> { class _MessageWidgetState extends State<MessageWidget> {
bool get showThreadReplyIndicator => bool get showThreadReplyIndicator => widget.showThreadReplyIndicator;
widget.showThreadReplyIndicator && widget.message.replyCount > 0;
bool get showSendingIndicator => widget.showSendingIndicator;
bool get isDeleted => widget.message.isDeleted;
bool get showUsername => widget.showUsername; bool get showUsername => widget.showUsername;
bool get showTimeStamp => bool get showTimeStamp => widget.showTimestamp;
widget.message.createdAt != null && widget.showTimestamp;
bool get isMessageRead => widget.readList?.isNotEmpty == true; bool get isMessageRead => widget.readList?.isNotEmpty == true;
bool get showInChannel => bool get showInChannel => widget.showInChannelIndicator;
widget.showInChannelIndicator && widget.message?.showInChannel == true;
bool get hasQuotedMessage => widget.message?.quotedMessage != null; bool get hasQuotedMessage => widget.message?.quotedMessage != null;
@@ -274,7 +276,9 @@ class _MessageWidgetState extends State<MessageWidget> {
showThreadReplyIndicator || showThreadReplyIndicator ||
showUsername || showUsername ||
showTimeStamp || showTimeStamp ||
showInChannel; showInChannel ||
showSendingIndicator ||
isDeleted;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -282,9 +286,6 @@ class _MessageWidgetState extends State<MessageWidget> {
var leftPadding = var leftPadding =
widget.showUserAvatar != DisplayWidget.gone ? avatarWidth + 8.5 : 0.5; widget.showUserAvatar != DisplayWidget.gone ? avatarWidth + 8.5 : 0.5;
final isOnlyEmoji =
widget.message.text.characters.every((c) => Emoji.byChar(c) != null);
final hasFiles = final hasFiles =
widget.message.attachments?.any((element) => element.type == 'file') == widget.message.attachments?.any((element) => element.type == 'file') ==
true; true;
@@ -452,7 +453,11 @@ class _MessageWidgetState extends State<MessageWidget> {
if (showBottomRow) SizedBox(height: 20.0), if (showBottomRow) SizedBox(height: 20.0),
], ],
), ),
if (showBottomRow) _buildBottomRow(leftPadding), if (showBottomRow)
Padding(
padding: EdgeInsets.only(left: leftPadding),
child: _bottomRow,
),
if (isFailedState) if (isFailedState)
Positioned( Positioned(
left: widget.reverse ? -3 : null, left: widget.reverse ? -3 : null,
@@ -481,12 +486,12 @@ class _MessageWidgetState extends State<MessageWidget> {
Widget _buildQuotedMessage() { Widget _buildQuotedMessage() {
final isMyMessage = final isMyMessage =
widget.message.user.id == StreamChat.of(context).user.id; 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( return QuotedMessageWidget(
onTap: () { onTap: onTap,
if (widget.onQuotedMessageTap != null) {
widget.onQuotedMessageTap(widget.message.quotedMessageId);
}
},
message: widget.message.quotedMessage, message: widget.message.quotedMessage,
messageTheme: isMyMessage messageTheme: isMyMessage
? StreamChatTheme.of(context).otherMessageTheme ? StreamChatTheme.of(context).otherMessageTheme
@@ -495,12 +500,12 @@ class _MessageWidgetState extends State<MessageWidget> {
); );
} }
Widget _buildBottomRow(double leftPadding) { Widget get _bottomRow {
final deleted = widget.message.isDeleted; if (isDeleted) {
var children = <Widget>[]; return Transform(
if (deleted) { transform: Matrix4.rotationY(widget.reverse ? pi : 0),
children.add( alignment: Alignment.center,
Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
StreamSvgIcon.eye( StreamSvgIcon.eye(
@@ -518,102 +523,94 @@ class _MessageWidgetState extends State<MessageWidget> {
], ],
), ),
); );
} 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 channel = StreamChannel.of(context);
final message = await channel.getMessage(widget.message.parentId); message = await channel.getMessage(widget.message.parentId);
return widget.onThreadTap(message);
} catch (e, stk) {
print(e);
print(stk);
return null;
} }
}; return widget.onThreadTap(message);
children.add( } 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( InkWell(
onTap: widget.onThreadTap != null ? onThreadTap : null, onTap: widget.onThreadTap != null ? onThreadTap : null,
child: Text('Thread Reply', style: widget.messageTheme?.replies), child: Text(msg, style: widget.messageTheme?.replies),
), ),
); ],
} else { if (showUsername)
final showSendingIndicator = Text(
widget.showSendingIndicator == DisplayWidget.show; widget.message.user.name,
final threadParticipants = widget.message.threadParticipants; style: widget.messageTheme.replies.copyWith(
final showThreadParticipants = threadParticipants?.isNotEmpty == true; color: widget.messageTheme.createdAt.color,
final replyCount = widget.message.replyCount; ),
final msg = replyCount != 0 ),
? '$replyCount ${replyCount > 1 ? 'Thread Replies' : 'Thread Reply'}' if (showTimeStamp)
: 'Thread Reply'; 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(); if (widget.reverse) children = children.reversed.toList();
return Padding( return Flex(
padding: EdgeInsets.only(left: leftPadding), direction: Axis.horizontal,
child: Flex( clipBehavior: Clip.none,
direction: Axis.horizontal, crossAxisAlignment: CrossAxisAlignment.end,
clipBehavior: Clip.none, children: [
crossAxisAlignment: CrossAxisAlignment.end, if (showThreadReplyIndicator || showInChannel)
children: [ Container(
if (!deleted && (showThreadReplyIndicator || showInChannel)) margin: EdgeInsets.only(
Container( bottom: widget.messageTheme.replies.fontSize / 2,
margin: EdgeInsets.only(
bottom: widget.messageTheme.replies.fontSize / 2,
),
child: CustomPaint(
size: const Size(16, 32),
painter: _ThreadReplyPainter(
context: context,
color: widget.messageTheme.messageBorderColor,
),
),
), ),
...children.map( child: CustomPaint(
(child) => Transform( size: const Size(16, 32),
transform: Matrix4.rotationY(widget.reverse ? pi : 0), painter: _ThreadReplyPainter(
alignment: Alignment.center, context: context,
child: Container( color: widget.messageTheme.messageBorderColor,
height: 16,
child: Center(
child: child,
),
), ),
), ),
), ),
].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)),
); );
} }
@@ -709,14 +706,15 @@ class _MessageWidgetState extends State<MessageWidget> {
messageTheme: widget.messageTheme, messageTheme: widget.messageTheme,
messageShape: widget.shape ?? _getDefaultShape(context), messageShape: widget.shape ?? _getDefaultShape(context),
reverse: widget.reverse, reverse: widget.reverse,
showDeleteMessage: widget.showDeleteMessage, showDeleteMessage: widget.showDeleteMessage || isDeleteFailed,
message: widget.message, message: widget.message,
editMessageInputBuilder: widget.editMessageInputBuilder, editMessageInputBuilder: widget.editMessageInputBuilder,
onReplyTap: widget.onReplyTap, onReplyTap: widget.onReplyTap,
onThreadReplyTap: widget.onThreadTap, onThreadReplyTap: widget.onThreadTap,
showResendMessage: showResendMessage:
widget.showResendMessage && (isSendFailed || isUpdateFailed), widget.showResendMessage && (isSendFailed || isUpdateFailed),
showCopyMessage: !isFailedState && showCopyMessage: widget.showCopyMessage &&
!isFailedState &&
widget.message.text?.trim()?.isNotEmpty == true, widget.message.text?.trim()?.isNotEmpty == true,
showEditMessage: widget.showEditMessage && showEditMessage: widget.showEditMessage &&
!isDeleteFailed && !isDeleteFailed &&
@@ -724,11 +722,12 @@ class _MessageWidgetState extends State<MessageWidget> {
?.any((element) => element.type == 'giphy') != ?.any((element) => element.type == 'giphy') !=
true, true,
showReactions: widget.showReactions, showReactions: widget.showReactions,
showReply: widget.showReplyIndicator && showReplyMessage: widget.showReplyMessage &&
!isFailedState && !isFailedState &&
widget.onReplyTap != null, widget.onReplyTap != null,
showThreadReply: showThreadReplyMessage: widget.showThreadReplyMessage &&
widget.showThreadReplyIndicator && widget.onThreadTap != null, !isFailedState &&
widget.onThreadTap != null,
showFlagButton: widget.showFlagButton, showFlagButton: widget.showFlagButton,
), ),
); );
@@ -943,9 +942,7 @@ class _MessageWidgetState extends State<MessageWidget> {
); );
} }
bool get isOnlyEmoji => bool get isOnlyEmoji => textIsOnlyEmoji(widget.message.text);
widget.message.text.characters.isNotEmpty &&
widget.message.text.characters.every((c) => Emoji.byChar(c) != null);
Color _getBackgroundColor() { Color _getBackgroundColor() {
if (hasQuotedMessage) { if (hasQuotedMessage) {
+2 -4
View File
@@ -1,18 +1,17 @@
import 'dart:math'; import 'dart:math';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:emojis/emoji.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat/stream_chat.dart';
import 'package:video_player/video_player.dart'; import 'package:video_player/video_player.dart';
import 'attachment_error.dart'; import 'attachment_error.dart';
import 'extension.dart';
import 'image_attachment.dart'; import 'image_attachment.dart';
import 'message_text.dart'; import 'message_text.dart';
import 'stream_chat_theme.dart'; import 'stream_chat_theme.dart';
import 'user_avatar.dart'; import 'user_avatar.dart';
import 'utils.dart'; import 'utils.dart';
import 'extension.dart';
typedef QuotedMessageAttachmentThumbnailBuilder = Widget Function( typedef QuotedMessageAttachmentThumbnailBuilder = Widget Function(
BuildContext, BuildContext,
@@ -126,8 +125,7 @@ class QuotedMessageWidget extends StatelessWidget {
} }
Widget _buildMessage(BuildContext context) { Widget _buildMessage(BuildContext context) {
final isOnlyEmoji = final isOnlyEmoji = textIsOnlyEmoji(message.text);
message.text.characters.every((c) => Emoji.byChar(c) != null);
var msg = _hasAttachments && !_containsText var msg = _hasAttachments && !_containsText
? message.copyWith(text: message.attachments.last?.title ?? '') ? message.copyWith(text: message.attachments.last?.title ?? '')
: message; : message;
+13 -1
View File
@@ -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, double size,
Color color, Color color,
}) { }) {
+2
View File
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.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 'back_button.dart';
import 'channel_name.dart'; import 'channel_name.dart';
@@ -81,6 +82,7 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
elevation: 1, elevation: 1,
leading: showBackButton leading: showBackButton
? StreamBackButton( ? StreamBackButton(
cid: StreamChannel.of(context).channel.cid,
onPressed: onBackPressed, onPressed: onBackPressed,
showUnreads: true, showUnreads: true,
) )
+10 -2
View File
@@ -5,14 +5,22 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class UnreadIndicator extends StatelessWidget { class UnreadIndicator extends StatelessWidget {
const UnreadIndicator({ const UnreadIndicator({
Key key, Key key,
this.cid,
}) : super(key: key); }) : super(key: key);
/// Channel cid used to retrieve unread count
final String cid;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final client = StreamChat.of(context).client; final client = StreamChat.of(context).client;
return StreamBuilder<int>( return StreamBuilder<int>(
stream: client.state.totalUnreadCountStream, stream: cid != null
initialData: client.state.totalUnreadCount, ? client.state.channels[cid].state.unreadCountStream
: client.state.totalUnreadCountStream,
initialData: cid != null
? client.state.channels[cid].state.unreadCount
: client.state.totalUnreadCount,
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data == 0) { if (!snapshot.hasData || snapshot.data == 0) {
return SizedBox(); return SizedBox();
+15 -2
View File
@@ -1,10 +1,10 @@
import 'package:emojis/emoji.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat/stream_chat.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'stream_svg_icon.dart';
import '../stream_chat_flutter.dart'; import '../stream_chat_flutter.dart';
import 'stream_svg_icon.dart';
Future<void> launchURL(BuildContext context, String url) async { Future<void> launchURL(BuildContext context, String url) async {
if (await canLaunch(url)) { if (await canLaunch(url)) {
@@ -289,3 +289,16 @@ StreamSvgIcon getFileTypeImage(String type) {
break; 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;
}
+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 12.79C20.8427 14.4922 20.2039 16.1144 19.1582 17.4668C18.1126 18.8192 16.7035 19.8458 15.0957 20.4265C13.4879 21.0073 11.748 21.1181 10.0795 20.7461C8.41102 20.3741 6.88299 19.5345 5.67423 18.3258C4.46546 17.117 3.62594 15.589 3.25391 13.9205C2.88188 12.252 2.99272 10.5121 3.57346 8.9043C4.1542 7.29651 5.18083 5.88737 6.53321 4.84175C7.8856 3.79614 9.5078 3.15731 11.21 3C10.2134 4.34827 9.73385 6.00945 9.85853 7.68141C9.98322 9.35338 10.7039 10.9251 11.8894 12.1106C13.0749 13.2961 14.6466 14.0168 16.3186 14.1415C17.9906 14.2662 19.6517 13.7866 21 12.79Z" stroke="#7A7A7A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 760 B

+2 -2
View File
@@ -70,8 +70,8 @@ void main() {
showEditMessage: false, showEditMessage: false,
showCopyMessage: false, showCopyMessage: false,
showDeleteMessage: false, showDeleteMessage: false,
showReply: false, showReplyMessage: false,
showThreadReply: false, showThreadReplyMessage: false,
message: Message( message: Message(
text: 'test', text: 'test',
user: User( user: User(