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