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
@@ -5,39 +5,27 @@
<testcase classname="fastlane.lanes" name="0: Verifying fastlane version" time="0.00047"> <testcase classname="fastlane.lanes" name="0: Verifying fastlane version" time="0.000341">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="1: default_platform" time="0.000176"> <testcase classname="fastlane.lanes" name="1: default_platform" time="0.000173">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="2: is_ci" time="0.000168"> <testcase classname="fastlane.lanes" name="2: is_ci" time="0.000178">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="3: Switch to ios match_me lane" time="0.000157"> <testcase classname="fastlane.lanes" name="3: is_ci" time="0.0002">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="4: is_ci" time="0.000161"> <testcase classname="fastlane.lanes" name="4: match" time="41.729101">
</testcase>
<testcase classname="fastlane.lanes" name="5: match" time="32.299713">
</testcase>
<testcase classname="fastlane.lanes" name="6: gym" time="9.737819">
<failure message="/Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/actions/actions_helper.rb:67:in `execute_action'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/runner.rb:255:in `block in execute_action'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/runner.rb:229:in `chdir'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/runner.rb:229:in `execute_action'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/runner.rb:157:in `trigger_action_by_name'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/fast_file.rb:159:in `method_missing'&#10;Fastfile:28:in `block (2 levels) in parsing_binding'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/lane.rb:33:in `call'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/runner.rb:49:in `block in execute'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/runner.rb:45:in `chdir'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/runner.rb:45:in `execute'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/lane_manager.rb:47:in `cruise_lane'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/command_line_handler.rb:36:in `handle'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/commands_generator.rb:108:in `block (2 levels) in run'&#10;/Library/Ruby/Gems/2.6.0/gems/commander-fastlane-4.4.6/lib/commander/command.rb:178:in `call'&#10;/Library/Ruby/Gems/2.6.0/gems/commander-fastlane-4.4.6/lib/commander/command.rb:153:in `run'&#10;/Library/Ruby/Gems/2.6.0/gems/commander-fastlane-4.4.6/lib/commander/runner.rb:476:in `run_active_command'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb:76:in `run!'&#10;/Library/Ruby/Gems/2.6.0/gems/commander-fastlane-4.4.6/lib/commander/delegates.rb:15:in `run!'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/commands_generator.rb:352:in `run'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/commands_generator.rb:41:in `start'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/cli_tools_distributor.rb:119:in `take_off'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/bin/fastlane:23:in `&lt;top (required)&gt;'&#10;/usr/local/bin/fastlane:23:in `load'&#10;/usr/local/bin/fastlane:23:in `&lt;top (required)&gt;'&#10;/Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/cli/exec.rb:74:in `load'&#10;/Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/cli/exec.rb:74:in `kernel_load'&#10;/Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/cli/exec.rb:28:in `run'&#10;/Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/cli.rb:465:in `exec'&#10;/Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/vendor/thor/lib/thor/command.rb:27:in `run'&#10;/Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/vendor/thor/lib/thor/invocation.rb:126:in `invoke_command'&#10;/Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/vendor/thor/lib/thor.rb:387:in `dispatch'&#10;/Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/cli.rb:27:in `dispatch'&#10;/Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/vendor/thor/lib/thor/base.rb:466:in `start'&#10;/Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/cli.rb:18:in `start'&#10;/Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/exe/bundle:30:in `block in &lt;top (required)&gt;'&#10;/Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/friendly_errors.rb:124:in `with_friendly_errors'&#10;/Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/exe/bundle:22:in `&lt;top (required)&gt;'&#10;/usr/local/bin/bundle:23:in `load'&#10;/usr/local/bin/bundle:23:in `&lt;main&gt;'&#10;&#10;Error building the application - see the log above" />
</testcase> </testcase>
@@ -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(),
); );
}, },
), ),
@@ -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,
@@ -169,12 +169,18 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
} }
Widget _buildMembers(List<Member> members) { Widget _buildMembers(List<Member> members) {
int groupMemberListLength; final groupMembers = members
..sort((prev, curr) {
if (curr.role == 'owner') return 1;
return 0;
});
int groupMembersLength;
if (listExpanded) { if (listExpanded) {
groupMemberListLength = members.length; groupMembersLength = groupMembers.length;
} else { } else {
groupMemberListLength = members.length > 6 ? 6 : members.length; groupMembersLength = groupMembers.length > 6 ? 6 : groupMembers.length;
} }
return Column( return Column(
@@ -182,15 +188,15 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
ListView.builder( ListView.builder(
shrinkWrap: true, shrinkWrap: true,
physics: NeverScrollableScrollPhysics(), physics: NeverScrollableScrollPhysics(),
itemCount: groupMemberListLength, itemCount: groupMembersLength,
itemBuilder: (context, position) { itemBuilder: (context, index) {
final member = groupMembers[index];
return Material( return Material(
child: InkWell( child: InkWell(
onTap: () { onTap: () {
var userMember = members.firstWhere( var userMember = groupMembers.firstWhere(
(e) => e.user.id == StreamChat.of(context).user.id); (e) => e.user.id == StreamChat.of(context).user.id);
_showUserInfoModal( _showUserInfoModal(member.user, userMember.role == 'owner');
members[position].user, userMember.role == 'owner');
}, },
child: Container( child: Container(
height: 65.0, height: 65.0,
@@ -202,7 +208,7 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 8.0, vertical: 12.0), horizontal: 8.0, vertical: 12.0),
child: UserAvatar( child: UserAvatar(
user: members[position].user, user: member.user,
constraints: BoxConstraints( constraints: BoxConstraints(
maxHeight: 40.0, maxWidth: 40.0), maxHeight: 40.0, maxWidth: 40.0),
), ),
@@ -213,14 +219,14 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( Text(
members[position].user.name, member.user.name,
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold),
), ),
SizedBox( SizedBox(
height: 1.0, height: 1.0,
), ),
Text( Text(
_getLastSeen(members[position].user), _getLastSeen(member.user),
style: TextStyle( style: TextStyle(
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
@@ -233,7 +239,7 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Text( child: Text(
members[position].role == 'owner' ? 'Owner' : '', member.role == 'owner' ? 'Owner' : '',
style: TextStyle( style: TextStyle(
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
@@ -257,7 +263,7 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
); );
}, },
), ),
if (groupMemberListLength != members.length) if (groupMembersLength != groupMembers.length)
InkWell( InkWell(
onTap: () { onTap: () {
setState(() { setState(() {
@@ -287,7 +293,7 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( Text(
'${members.length - groupMemberListLength} more', '${members.length - groupMembersLength} more',
style: TextStyle( style: TextStyle(
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
+57 -23
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,
); );
} }
} }
@@ -271,6 +290,21 @@ class _HomePageState extends State<HomePage> {
fontSize: 14.5, 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,
);
},
),
), ),
), ),
), ),
@@ -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:
@@ -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,
),
), ),
], ],
); );
@@ -142,7 +142,9 @@ class _ChannelMediaDisplayScreenState extends State<ChannelMediaDisplayScreen> {
for (var item in snapshot.data) { for (var item in snapshot.data) {
item.message.attachments item.message.attachments
.where((e) => e.type == 'image' || e.type == 'video') .where((e) =>
(e.type == 'image' || e.type == 'video') &&
e.ogScrapeUrl == null)
.forEach((e) { .forEach((e) {
VideoPlayerController controller; VideoPlayerController controller;
if (e.type == 'video') { 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 /// It shows a date divider depending on the date difference
class DateDivider extends StatelessWidget { class DateDivider extends StatelessWidget {
final DateTime dateTime; final DateTime dateTime;
final bool uppercase;
const DateDivider({ const DateDivider({
Key key, Key key,
@required this.dateTime, @required this.dateTime,
this.uppercase = false,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -18,38 +20,37 @@ class DateDivider extends StatelessWidget {
String dayInfo; String dayInfo;
if (Jiffy(createdAt).isSame(now, Units.DAY)) { if (Jiffy(createdAt).isSame(now, Units.DAY)) {
dayInfo = 'TODAY'; dayInfo = 'Today';
} else if (Jiffy(createdAt) } else if (Jiffy(createdAt)
.isSame(now.subtract(Duration(days: 1)), Units.DAY)) { .isSame(now.subtract(Duration(days: 1)), Units.DAY)) {
dayInfo = 'YESTERDAY'; dayInfo = 'Yesterday';
} else if (Jiffy(createdAt).isAfter( } else if (Jiffy(createdAt).isAfter(
now.subtract(Duration(days: 7)), now.subtract(Duration(days: 7)),
Units.DAY, Units.DAY,
)) { )) {
dayInfo = createdAt.format('EEEE').toUpperCase(); dayInfo = createdAt.format('EEEE');
} else if (Jiffy(createdAt).isAfter( } else if (Jiffy(createdAt).isAfter(
Jiffy(now).subtract(years: 1), Jiffy(now).subtract(years: 1),
Units.DAY, Units.DAY,
)) { )) {
dayInfo = createdAt.format('MMMM d').toUpperCase(); dayInfo = createdAt.format('MMMM d');
} else { } else {
dayInfo = createdAt.format('MMMM d').toUpperCase(); dayInfo = createdAt.format('MMMM d');
} }
if (uppercase) dayInfo = dayInfo.toUpperCase();
return Center( return Center(
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 1),
decoration: BoxDecoration( decoration: BoxDecoration(
color: StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5), color: StreamChatTheme.of(context).colorTheme.overlayDark,
borderRadius: BorderRadius.circular( borderRadius: BorderRadius.circular(8),
8,
),
), ),
child: Text( child: Text(
dayInfo, dayInfo,
style: StreamChatTheme.of(context).textTheme.footnoteBold.copyWith( style: StreamChatTheme.of(context).textTheme.footnote.copyWith(
color: StreamChatTheme.of(context).colorTheme.white, color: StreamChatTheme.of(context).colorTheme.white,
fontWeight: FontWeight.bold,
), ),
), ),
), ),
@@ -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),
), ),
), ),
), ),
@@ -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,
), ),
@@ -24,8 +24,8 @@ class MessageActionsModal extends StatelessWidget {
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 reverse; final bool reverse;
final ShapeBorder messageShape; final ShapeBorder messageShape;
final DisplayWidget showUserAvatar; final DisplayWidget showUserAvatar;
@@ -40,9 +40,9 @@ class MessageActionsModal extends StatelessWidget {
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.showUserAvatar = DisplayWidget.show, this.showUserAvatar = DisplayWidget.show,
this.editMessageInputBuilder, this.editMessageInputBuilder,
this.messageShape, this.messageShape,
@@ -92,8 +92,9 @@ class MessageActionsModal extends StatelessWidget {
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0), padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: reverse
crossAxisAlignment: CrossAxisAlignment.stretch, ? CrossAxisAlignment.end
: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
if (showReactions && if (showReactions &&
(message.status == MessageSendingStatus.SENT || (message.status == MessageSendingStatus.SENT ||
@@ -128,7 +129,7 @@ class MessageActionsModal extends StatelessWidget {
showReactions: false, showReactions: false,
showUsername: false, showUsername: false,
showThreadReplyIndicator: false, showThreadReplyIndicator: false,
showReplyIndicator: false, showReplyMessage: false,
showUserAvatar: showUserAvatar, showUserAvatar: showUserAvatar,
showTimestamp: false, showTimestamp: false,
translateUserAvatar: false, translateUserAvatar: false,
@@ -137,13 +138,12 @@ class MessageActionsModal extends StatelessWidget {
MessageSendingStatus.SENT || MessageSendingStatus.SENT ||
message.status == null), message.status == null),
showInChannelIndicator: false, showInChannelIndicator: false,
showSendingIndicator: DisplayWidget.gone, showSendingIndicator: false,
shape: messageShape, shape: messageShape,
), ),
), ),
); );
}), }),
SizedBox(height: 8),
TweenAnimationBuilder<double>( TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0), tween: Tween(begin: 0.0, end: 1.0),
duration: Duration(milliseconds: 300), duration: Duration(milliseconds: 300),
@@ -153,47 +153,56 @@ class MessageActionsModal extends StatelessWidget {
transform: Matrix4.identity() transform: Matrix4.identity()
..scale(val) ..scale(val)
..rotateZ(-1.0 + val), ..rotateZ(-1.0 + val),
alignment: Alignment.topRight, alignment: reverse
? Alignment.topRight
: Alignment.topLeft,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: EdgeInsets.only(
horizontal: 48.0, right: reverse ? 16 : 0,
left: reverse ? 0 : 48,
), ),
child: Material( child: SizedBox(
color: StreamChatTheme.of(context) width: MediaQuery.of(context).size.width * 0.75,
.colorTheme child: Material(
.whiteSnow, color: StreamChatTheme.of(context)
clipBehavior: Clip.hardEdge, .colorTheme
shape: RoundedRectangleBorder( .whiteSnow,
borderRadius: BorderRadius.circular(16), clipBehavior: Clip.hardEdge,
), shape: RoundedRectangleBorder(
child: Column( borderRadius: BorderRadius.circular(16),
crossAxisAlignment: ),
CrossAxisAlignment.stretch, child: Column(
children: ListTile.divideTiles( crossAxisAlignment:
context: context, CrossAxisAlignment.stretch,
tiles: [ children: ListTile.divideTiles(
if (showReply && color: StreamChatTheme.of(context)
(message.status == .colorTheme
MessageSendingStatus.SENT || .greyWhisper,
message.status == null) && context: context,
message.parentId == null) tiles: [
_buildReplyButton(context), if (showReplyMessage &&
if (showThreadReply && (message.status ==
(message.status == MessageSendingStatus.SENT ||
MessageSendingStatus.SENT || message.status == null) &&
message.status == null) && message.parentId == null)
message.parentId == null) _buildReplyButton(context),
_buildThreadReplyButton(context), if (showThreadReplyMessage &&
if (showResendMessage) (message.status ==
_buildResendMessage(context), MessageSendingStatus.SENT ||
if (showEditMessage) message.status == null) &&
_buildEditMessage(context), message.parentId == null)
if (showDeleteMessage) _buildThreadReplyButton(context),
_buildDeleteButton(context), if (showResendMessage)
if (showCopyMessage) _buildResendMessage(context),
_buildCopyButton(context), if (showEditMessage)
], _buildEditMessage(context),
).toList(), if (showCopyMessage)
_buildCopyButton(context),
if (showDeleteMessage)
_buildDeleteButton(context),
],
).toList(),
),
), ),
), ),
), ),
@@ -211,12 +220,18 @@ class MessageActionsModal extends StatelessWidget {
Widget _buildReplyButton(BuildContext context) { Widget _buildReplyButton(BuildContext context) {
return ListTile( return ListTile(
title: Text( dense: true,
'Reply', title: Row(
style: Theme.of(context).textTheme.headline6, children: [
), StreamSvgIcon.reply(
leading: StreamSvgIcon.reply( color: StreamChatTheme.of(context).primaryIconTheme.color,
color: StreamChatTheme.of(context).primaryIconTheme.color, ),
const SizedBox(width: 16),
Text(
'Reply',
style: StreamChatTheme.of(context).textTheme.headline,
),
],
), ),
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
@@ -230,13 +245,21 @@ class MessageActionsModal extends StatelessWidget {
Widget _buildDeleteButton(BuildContext context) { Widget _buildDeleteButton(BuildContext context) {
final isDeleteFailed = message.status == MessageSendingStatus.FAILED_DELETE; final isDeleteFailed = message.status == MessageSendingStatus.FAILED_DELETE;
return ListTile( return ListTile(
title: Text( dense: true,
isDeleteFailed ? 'Retry deleting message' : 'Delete message', title: Row(
style: children: [
Theme.of(context).textTheme.headline6.copyWith(color: Colors.red), StreamSvgIcon.delete(
), color: Colors.red,
leading: 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: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
@@ -250,12 +273,19 @@ class MessageActionsModal extends StatelessWidget {
Widget _buildCopyButton(BuildContext context) { Widget _buildCopyButton(BuildContext context) {
return ListTile( return ListTile(
title: Text( dense: true,
'Copy message', title: Row(
style: Theme.of(context).textTheme.headline6, children: [
), StreamSvgIcon.copy(
leading: StreamSvgIcon.copy( size: 24,
color: StreamChatTheme.of(context).primaryIconTheme.color, color: StreamChatTheme.of(context).primaryIconTheme.color,
),
const SizedBox(width: 16),
Text(
'Copy Message',
style: StreamChatTheme.of(context).textTheme.headline,
),
],
), ),
onTap: () async { onTap: () async {
await Clipboard.setData(ClipboardData(text: message.text)); await Clipboard.setData(ClipboardData(text: message.text));
@@ -266,12 +296,18 @@ class MessageActionsModal extends StatelessWidget {
Widget _buildEditMessage(BuildContext context) { Widget _buildEditMessage(BuildContext context) {
return ListTile( return ListTile(
title: Text( dense: true,
'Edit message', title: Row(
style: Theme.of(context).textTheme.headline6, children: [
), StreamSvgIcon.edit(
leading: StreamSvgIcon.edit( color: StreamChatTheme.of(context).primaryIconTheme.color,
color: StreamChatTheme.of(context).primaryIconTheme.color, ),
const SizedBox(width: 16),
Text(
'Edit Message',
style: StreamChatTheme.of(context).textTheme.headline,
),
],
), ),
onTap: () async { onTap: () async {
Navigator.pop(context); Navigator.pop(context);
@@ -283,12 +319,18 @@ class MessageActionsModal extends StatelessWidget {
Widget _buildResendMessage(BuildContext context) { Widget _buildResendMessage(BuildContext context) {
final isUpdateFailed = message.status == MessageSendingStatus.FAILED_UPDATE; final isUpdateFailed = message.status == MessageSendingStatus.FAILED_UPDATE;
return ListTile( return ListTile(
title: Text( dense: true,
isUpdateFailed ? 'Resend edited message' : 'Resend', title: Row(
style: Theme.of(context).textTheme.headline6, children: [
), StreamSvgIcon.circle_up(
leading: StreamSvgIcon.circle_up( color: StreamChatTheme.of(context).colorTheme.accentBlue,
color: StreamChatTheme.of(context).colorTheme.accentBlue, ),
const SizedBox(width: 16),
Text(
isUpdateFailed ? 'Resend Edited Message' : 'Resend',
style: StreamChatTheme.of(context).textTheme.headline,
),
],
), ),
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
@@ -374,12 +416,18 @@ class MessageActionsModal extends StatelessWidget {
Widget _buildThreadReplyButton(BuildContext context) { Widget _buildThreadReplyButton(BuildContext context) {
return ListTile( return ListTile(
title: Text( dense: true,
'Thread reply', title: Row(
style: Theme.of(context).textTheme.headline6, children: [
), StreamSvgIcon.thread(
leading: StreamSvgIcon.thread( color: StreamChatTheme.of(context).primaryIconTheme.color,
color: StreamChatTheme.of(context).primaryIconTheme.color, ),
const SizedBox(width: 16),
Text(
'Thread Reply',
style: StreamChatTheme.of(context).textTheme.headline,
),
],
), ),
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
@@ -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])
@@ -324,7 +325,7 @@ class _MessageListViewState extends State<MessageListView> {
onInBetweenOfPage: () { onInBetweenOfPage: () {
_inBetweenList = true; _inBetweenList = true;
}, },
child: ScrollablePositionedList.builder( child: ScrollablePositionedList.separated(
key: ValueKey(initialIndex + initialAlignment), key: ValueKey(initialIndex + initialAlignment),
itemPositionsListener: _itemPositionListener, itemPositionsListener: _itemPositionListener,
addAutomaticKeepAlives: true, addAutomaticKeepAlives: true,
@@ -333,9 +334,49 @@ 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) {
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) { itemBuilder: (context, i) {
if (i == messages.length + 2) { if (i == messages.length + 2) {
if (widget.parentMessageBuilder != null) { if (widget.parentMessageBuilder != null) {
@@ -383,7 +424,6 @@ class _MessageListViewState extends State<MessageListView> {
); );
} }
final message = messages[i - 1]; final message = messages[i - 1];
final nextMessage = (i - 1) > 0 ? messages[i - 2] : null;
Widget messageWidget; Widget messageWidget;
@@ -419,27 +459,6 @@ class _MessageListViewState extends State<MessageListView> {
messageWidget = buildMessage(message, messages, i); 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; return messageWidget;
}, },
), ),
@@ -482,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);
@@ -672,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);
@@ -697,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,
@@ -707,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),
@@ -740,36 +761,73 @@ class _MessageListViewState extends State<MessageListView> {
final userId = StreamChat.of(context).user.id; final userId = StreamChat.of(context).user.id;
final isMyMessage = message.user.id == userId; final isMyMessage = message.user.id == userId;
final isNextUser = final nextMessage = index - 2 >= 0 ? messages[index - 2] : null;
index - 2 >= 0 && message.user.id == messages[index - 2]?.user?.id; 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 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 hasFileAttachment =
message.attachments.any((it) => it.type == 'file');
final isThreadMessage = 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( 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: EdgeInsets.only( padding: const EdgeInsets.symmetric(horizontal: 8.0),
left: 8.0, showInChannelIndicator: showInChannelIndicator,
right: 8.0, showThreadReplyIndicator: showThreadReplyIndicator,
bottom: index == 0 ? 30 : (isNextUser ? 2 : 7), showUsername: showUsername,
top: 3, 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);
@@ -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, showEditMessage: isMyMessage,
showDeleteMessage: isMyMessage, showDeleteMessage: isMyMessage,
showThreadReplyMessage: !isThreadMessage,
borderSide: isMyMessage ? BorderSide.none : null, borderSide: isMyMessage ? BorderSide.none : null,
onThreadTap: _onThreadTap, onThreadTap: _onThreadTap,
onReplyTap: widget.onReplyTap, onReplyTap: widget.onReplyTap,
attachmentBorderRadiusGeometry: BorderRadius.only( attachmentBorderRadiusGeometry: BorderRadius.only(
topLeft: Radius.circular(16), topLeft: Radius.circular(attachmentBorderRadius),
bottomLeft: Radius.circular(!isNextUser ? 0 : 16), bottomLeft: Radius.circular(
topRight: Radius.circular(16), timeDiff >= 1 || !isNextUserSame ? 0 : attachmentBorderRadius),
bottomRight: Radius.circular(16), topRight: Radius.circular(attachmentBorderRadius),
bottomRight: Radius.circular(attachmentBorderRadius),
), ),
attachmentPadding: const EdgeInsets.all(2), attachmentPadding: const EdgeInsets.all(2),
borderRadiusGeometry: BorderRadius.only( borderRadiusGeometry: BorderRadius.only(
topLeft: Radius.circular(16), topLeft: Radius.circular(16),
bottomLeft: Radius.circular(!isNextUser ? 0 : 16), bottomLeft: Radius.circular(timeDiff >= 1 || !isNextUserSame ? 0 : 16),
topRight: Radius.circular(16), topRight: Radius.circular(16),
bottomRight: Radius.circular(16), bottomRight: Radius.circular(16),
), ),
showUserAvatar: isMyMessage
? DisplayWidget.gone
: (isNextUser ? DisplayWidget.hide : DisplayWidget.show),
messageTheme: isMyMessage messageTheme: isMyMessage
? StreamChatTheme.of(context).ownMessageTheme ? StreamChatTheme.of(context).ownMessageTheme
: StreamChatTheme.of(context).otherMessageTheme, : StreamChatTheme.of(context).otherMessageTheme,
@@ -827,7 +876,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(
@@ -890,7 +939,7 @@ class _MessageListViewState extends State<MessageListView> {
} }
}); });
if (widget.parentMessage != null) { if (_isThreadConversation) {
streamChannel.getReplies(widget.parentMessage.id); streamChannel.getReplies(widget.parentMessage.id);
} }
@@ -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 &&
@@ -127,7 +127,6 @@ class MessageReactionsModal extends StatelessWidget {
), ),
); );
}), }),
SizedBox(height: 8),
if (message.latestReactions?.isNotEmpty == true) if (message.latestReactions?.isNotEmpty == true)
_buildReactionCard(context), _buildReactionCard(context),
], ],
@@ -152,27 +151,22 @@ class MessageReactionsModal extends StatelessWidget {
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
), ),
child: Column( margin: EdgeInsets.zero,
mainAxisSize: MainAxisSize.min, child: Padding(
children: [ padding: const EdgeInsets.all(16),
Padding( child: Column(
padding: const EdgeInsets.all(16.0), mainAxisSize: MainAxisSize.min,
child: Text( children: [
Text(
'Message Reactions', 'Message Reactions',
style: Theme.of(context).textTheme.headline6, style: StreamChatTheme.of(context).textTheme.headlineBold,
), ),
), const SizedBox(height: 16),
Flexible( Flexible(
child: SingleChildScrollView( child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.only(
left: 18,
right: 18,
bottom: 26,
),
child: Wrap( child: Wrap(
spacing: 16, spacing: 16,
runSpacing: 22, runSpacing: 16,
alignment: WrapAlignment.start, alignment: WrapAlignment.start,
children: message.latestReactions children: message.latestReactions
.map((e) => _buildReaction( .map((e) => _buildReaction(
@@ -184,8 +178,8 @@ class MessageReactionsModal extends StatelessWidget {
), ),
), ),
), ),
), ],
], ),
), ),
), ),
); );
@@ -227,24 +221,28 @@ class MessageReactionsModal extends StatelessWidget {
), ),
Positioned( Positioned(
child: Align( child: Align(
alignment: Alignment.centerLeft, alignment: reverse
? Alignment.centerRight
: Alignment.centerLeft,
child: ReactionBubble( child: ReactionBubble(
reactions: [reaction], reactions: [reaction],
flipTail: !reverse,
borderColor: messageTheme.reactionsBorderColor, borderColor: messageTheme.reactionsBorderColor,
backgroundColor: backgroundColor:
messageTheme.reactionsBackgroundColor, messageTheme.reactionsBackgroundColor,
highlightOwnReactions: false, highlightOwnReactions: false,
), ),
), ),
bottom: 4, bottom: 6,
left: isCurrentUser ? 0 : null, left: isCurrentUser ? 0 : null,
right: isCurrentUser ? 0 : null, right: isCurrentUser ? 0 : null,
), ),
], ],
), ),
const SizedBox(height: 8),
Text( Text(
reaction.user.name, reaction.user.name,
style: Theme.of(context).textTheme.subtitle2, style: StreamChatTheme.of(context).textTheme.footnoteBold,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
], ],
@@ -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 Map<String, AttachmentBuilder> attachmentBuilders; final Map<String, AttachmentBuilder> attachmentBuilders;
/// Center user avatar with bottom of the message /// Center user avatar with bottom of the message
@@ -157,10 +156,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,
@@ -168,6 +166,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.onUserAvatarTap, this.onUserAvatarTap,
this.onLinkTap, this.onLinkTap,
this.onMessageActions, this.onMessageActions,
@@ -175,7 +177,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(
@@ -239,18 +240,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;
@@ -272,16 +274,15 @@ 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) {
final avatarWidth = widget.messageTheme.avatarTheme.constraints.maxWidth; final avatarWidth = widget.messageTheme.avatarTheme.constraints.maxWidth;
var leftPadding = var leftPadding =
widget.showUserAvatar != DisplayWidget.gone ? avatarWidth + 8.5 : 4.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') ==
@@ -330,7 +331,7 @@ class _MessageWidgetState extends State<MessageWidget> {
child: PortalEntry( child: PortalEntry(
portal: Container( portal: Container(
transform: transform:
Matrix4.translationValues(-16, 2, 0), Matrix4.translationValues(-12, 0, 0),
child: _buildReactionIndicator(context), child: _buildReactionIndicator(context),
constraints: constraints:
BoxConstraints(maxWidth: 22 * 6.0), BoxConstraints(maxWidth: 22 * 6.0),
@@ -372,6 +373,14 @@ class _MessageWidgetState extends State<MessageWidget> {
: Card( : Card(
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
elevation: 0.0, elevation: 0.0,
margin: EdgeInsets.symmetric(
horizontal:
widget.showUserAvatar ==
DisplayWidget
.gone
? 0
: 4.0,
),
shape: widget.shape ?? shape: widget.shape ??
RoundedRectangleBorder( RoundedRectangleBorder(
side: isOnlyEmoji && side: isOnlyEmoji &&
@@ -395,7 +404,7 @@ class _MessageWidgetState extends State<MessageWidget> {
child: Column( child: Column(
crossAxisAlignment: crossAxisAlignment:
CrossAxisAlignment CrossAxisAlignment
.start, .end,
mainAxisSize: mainAxisSize:
MainAxisSize.min, MainAxisSize.min,
children: <Widget>[ children: <Widget>[
@@ -423,10 +432,12 @@ class _MessageWidgetState extends State<MessageWidget> {
widget.reverse ? pi : 0), widget.reverse ? pi : 0),
child: CustomPaint( child: CustomPaint(
painter: ReactionBubblePainter( painter: ReactionBubblePainter(
widget.messageTheme StreamChatTheme.of(context)
.reactionsBackgroundColor, .colorTheme
widget.messageTheme .white,
.reactionsBorderColor, StreamChatTheme.of(context)
.colorTheme
.white,
), ),
), ),
), ),
@@ -440,7 +451,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,
@@ -469,12 +484,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
@@ -483,127 +498,117 @@ 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(
color: color: StreamChatTheme.of(context).colorTheme.grey,
StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5),
size: 16.0, size: 16.0,
), ),
SizedBox(width: 8.0), SizedBox(width: 8.0),
Text( Text(
'Only visible to you', 'Only visible to you',
style: StreamChatTheme.of(context).textTheme.footnote.copyWith( style: StreamChatTheme.of(context)
color: StreamChatTheme.of(context) .textTheme
.colorTheme .footnote
.black .copyWith(color: StreamChatTheme.of(context).colorTheme.grey),
.withOpacity(0.5)),
), ),
], ],
), ),
); );
} 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)),
); );
} }
@@ -698,14 +703,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 &&
@@ -713,11 +719,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,
), ),
); );
}); });
@@ -754,9 +761,7 @@ class _MessageWidgetState extends State<MessageWidget> {
side: widget.attachmentBorderSide ?? side: widget.attachmentBorderSide ??
widget.borderSide ?? widget.borderSide ??
BorderSide( BorderSide(
color: Theme.of(context).brightness == Brightness.dark color: StreamChatTheme.of(context).colorTheme.greyGainsboro,
? StreamChatTheme.of(context).colorTheme.white.withAlpha(24)
: StreamChatTheme.of(context).colorTheme.black.withAlpha(24),
), ),
borderRadius: widget.attachmentBorderRadiusGeometry ?? borderRadius: widget.attachmentBorderRadiusGeometry ??
widget.borderRadiusGeometry ?? widget.borderRadiusGeometry ??
@@ -933,9 +938,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) {
@@ -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,
@@ -111,7 +110,7 @@ class QuotedMessageWidget extends StatelessWidget {
return InkWell( return InkWell(
onTap: onTap, onTap: onTap,
child: Padding( 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( child: Row(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -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;
@@ -27,7 +27,8 @@ class ReactionBubble extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final reactionIcons = StreamChatTheme.of(context).reactionIcons; 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( return Transform(
transform: Matrix4.rotationY(reverse ? pi : 0), transform: Matrix4.rotationY(reverse ? pi : 0),
alignment: Alignment.center, alignment: Alignment.center,
@@ -37,7 +38,10 @@ class ReactionBubble extends StatelessWidget {
Transform.translate( Transform.translate(
offset: Offset(reverse ? offset : -offset, 0), offset: Offset(reverse ? offset : -offset, 0),
child: Container( child: Container(
padding: const EdgeInsets.symmetric(vertical: 4), padding: EdgeInsets.symmetric(
vertical: 4,
horizontal: totalReactions > 1 ? 4 : 0,
),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all( border: Border.all(
color: borderColor, color: borderColor,
@@ -134,16 +138,11 @@ class ReactionBubble extends StatelessWidget {
borderColor, borderColor,
), ),
); );
return Transform(
if (!flipTail) { transform: Matrix4.rotationY(flipTail ? 0 : pi),
return tail; alignment: Alignment.center,
} else { child: tail,
return Transform( );
transform: Matrix4.rotationY(pi),
alignment: Alignment.center,
child: tail,
);
}
} }
} }
@@ -58,62 +58,65 @@ class _ReactionPickerState extends State<ReactionPicker>
return Transform.scale( return Transform.scale(
scale: val, scale: val,
child: Material( child: Material(
color: widget.messageTheme.reactionsBackgroundColor, color: StreamChatTheme.of(context).colorTheme.white,
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24), borderRadius: BorderRadius.circular(24),
), ),
child: Row( child: Padding(
crossAxisAlignment: CrossAxisAlignment.start, padding: const EdgeInsets.symmetric(horizontal: 8.0),
mainAxisAlignment: MainAxisAlignment.end, child: Row(
mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start,
children: reactionIcons.map((reactionIcon) { mainAxisAlignment: MainAxisAlignment.end,
final ownReactionIndex = widget.message.ownReactions mainAxisSize: MainAxisSize.min,
?.indexWhere((reaction) => children: reactionIcons.map((reactionIcon) {
reaction.type == reactionIcon.type) ?? final ownReactionIndex = widget.message.ownReactions
-1; ?.indexWhere((reaction) =>
var index = reactionIcons.indexOf(reactionIcon); reaction.type == reactionIcon.type) ??
-1;
var index = reactionIcons.indexOf(reactionIcon);
return IconButton( return IconButton(
iconSize: 24, iconSize: 24,
icon: AnimatedBuilder( icon: AnimatedBuilder(
animation: animations[index], animation: animations[index],
builder: (context, val) { builder: (context, val) {
return Transform( return Transform(
transform: Matrix4.identity() transform: Matrix4.identity()
..scale(animations[index].value, ..scale(animations[index].value,
animations[index].value) animations[index].value)
..rotateZ(1.0 - animations[index].value), ..rotateZ(1.0 - animations[index].value),
child: StreamSvgIcon( child: StreamSvgIcon(
assetName: reactionIcon.assetName, assetName: reactionIcon.assetName,
height: animations[index].value * 24.0, height: animations[index].value * 24.0,
width: animations[index].value * 24.0, width: animations[index].value * 24.0,
color: ownReactionIndex != -1 color: ownReactionIndex != -1
? StreamChatTheme.of(context) ? StreamChatTheme.of(context)
.colorTheme .colorTheme
.accentBlue .accentBlue
: Theme.of(context) : Theme.of(context)
.iconTheme .iconTheme
.color .color
.withOpacity(.5), .withOpacity(.5),
), ),
);
}),
onPressed: () {
if (ownReactionIndex != -1) {
removeReaction(
context,
widget.message.ownReactions[ownReactionIndex],
); );
}), } else {
onPressed: () { sendReaction(
if (ownReactionIndex != -1) { context,
removeReaction( reactionIcon.type,
context, );
widget.message.ownReactions[ownReactionIndex], }
); },
} else { );
sendReaction( }).toList(),
context, ),
reactionIcon.type,
);
}
},
);
}).toList(),
), ),
), ),
); );
@@ -228,19 +228,9 @@ class StreamChatThemeData {
inputBackground: colorTheme.white.withAlpha(12), inputBackground: colorTheme.white.withAlpha(12),
), ),
ownMessageTheme: MessageTheme( ownMessageTheme: MessageTheme(
messageText: TextStyle( messageText: textTheme.body,
fontSize: 14.5, createdAt: textTheme.footnote.copyWith(color: colorTheme.grey),
color: colorTheme.black, replies: textTheme.footnoteBold.copyWith(color: accentColor),
),
createdAt: TextStyle(
color: colorTheme.black.withOpacity(.5),
fontSize: 12,
),
replies: TextStyle(
color: accentColor,
fontWeight: FontWeight.w600,
fontSize: 12,
),
messageBackgroundColor: colorTheme.greyGainsboro, messageBackgroundColor: colorTheme.greyGainsboro,
reactionsBackgroundColor: colorTheme.white, reactionsBackgroundColor: colorTheme.white,
reactionsBorderColor: colorTheme.greyWhisper, reactionsBorderColor: colorTheme.greyWhisper,
@@ -259,19 +249,9 @@ class StreamChatThemeData {
otherMessageTheme: MessageTheme( otherMessageTheme: MessageTheme(
reactionsBackgroundColor: colorTheme.greyGainsboro, reactionsBackgroundColor: colorTheme.greyGainsboro,
reactionsBorderColor: colorTheme.white, reactionsBorderColor: colorTheme.white,
messageText: TextStyle( messageText: textTheme.body,
fontSize: 14.5, createdAt: textTheme.footnote.copyWith(color: colorTheme.grey),
color: colorTheme.black, replies: textTheme.footnoteBold.copyWith(color: accentColor),
),
createdAt: TextStyle(
color: colorTheme.black.withOpacity(.5),
fontSize: 12,
),
replies: TextStyle(
color: accentColor,
fontWeight: FontWeight.w600,
fontSize: 12,
),
messageLinks: TextStyle( messageLinks: TextStyle(
color: accentColor, 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, double size,
Color color, Color color,
}) { }) {
@@ -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,
) )
@@ -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();
@@ -24,10 +24,6 @@ class UrlAttachment extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
if (urlAttachment.imageUrl != null)
SizedBox(
height: 16.0,
),
if (urlAttachment.imageUrl != null) if (urlAttachment.imageUrl != null)
Container( Container(
clipBehavior: Clip.antiAliasWithSaveLayer, clipBehavior: Clip.antiAliasWithSaveLayer,
+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)) {
@@ -215,3 +215,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;
}
@@ -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

@@ -42,10 +42,10 @@ void main() {
await tester.pump(Duration(milliseconds: 1000)); await tester.pump(Duration(milliseconds: 1000));
expect(find.byKey(Key('MessageWidget')), findsOneWidget); expect(find.byKey(Key('MessageWidget')), findsOneWidget);
expect(find.text('Thread reply'), findsOneWidget); expect(find.text('Thread Reply'), findsOneWidget);
expect(find.text('Edit message'), findsOneWidget); expect(find.text('Edit Message'), findsOneWidget);
expect(find.text('Delete message'), findsOneWidget); expect(find.text('Delete Message'), findsOneWidget);
expect(find.text('Copy message'), findsOneWidget); expect(find.text('Copy Message'), findsOneWidget);
}, },
); );
testWidgets( testWidgets(
@@ -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(