@@ -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
|
||||||
|
|||||||
+99
-102
@@ -166,116 +166,113 @@ class _HomePageState extends State<HomePage> {
|
|||||||
|
|
||||||
Drawer _buildDrawer(BuildContext context, User user) {
|
Drawer _buildDrawer(BuildContext context, User user) {
|
||||||
return Drawer(
|
return Drawer(
|
||||||
child: Container(
|
child: SafeArea(
|
||||||
color: StreamChatTheme.of(context).colorTheme.white,
|
child: Padding(
|
||||||
child: SafeArea(
|
padding: EdgeInsets.only(
|
||||||
child: Padding(
|
top: MediaQuery.of(context).viewPadding.top + 8,
|
||||||
padding: EdgeInsets.only(
|
),
|
||||||
top: MediaQuery.of(context).viewPadding.top + 8,
|
child: Column(
|
||||||
),
|
children: [
|
||||||
child: Column(
|
Padding(
|
||||||
children: [
|
padding: const EdgeInsets.only(
|
||||||
Padding(
|
bottom: 20.0,
|
||||||
padding: const EdgeInsets.only(
|
left: 8,
|
||||||
bottom: 20.0,
|
|
||||||
left: 8,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
UserAvatar(
|
|
||||||
user: user,
|
|
||||||
showOnlineStatus: false,
|
|
||||||
constraints: BoxConstraints.tight(Size.fromRadius(20)),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 16.0),
|
|
||||||
child: Text(
|
|
||||||
user.name,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
ListTile(
|
child: Row(
|
||||||
leading: StreamSvgIcon.penWrite(
|
children: [
|
||||||
color: StreamChatTheme.of(context)
|
UserAvatar(
|
||||||
.colorTheme
|
user: user,
|
||||||
.black
|
showOnlineStatus: false,
|
||||||
.withOpacity(.5),
|
constraints: BoxConstraints.tight(Size.fromRadius(20)),
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.popAndPushNamed(
|
|
||||||
context,
|
|
||||||
Routes.NEW_CHAT,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
title: Text(
|
|
||||||
'New direct message',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14.5,
|
|
||||||
),
|
),
|
||||||
),
|
Padding(
|
||||||
),
|
padding: const EdgeInsets.only(left: 16.0),
|
||||||
ListTile(
|
child: Text(
|
||||||
leading: StreamSvgIcon.contacts(
|
user.name,
|
||||||
color: StreamChatTheme.of(context)
|
|
||||||
.colorTheme
|
|
||||||
.black
|
|
||||||
.withOpacity(.5),
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.popAndPushNamed(
|
|
||||||
context,
|
|
||||||
Routes.NEW_GROUP_CHAT,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
title: Text(
|
|
||||||
'New group',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14.5,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Container(
|
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
child: ListTile(
|
|
||||||
onTap: () async {
|
|
||||||
Navigator.pop(context);
|
|
||||||
|
|
||||||
final secureStorage = FlutterSecureStorage();
|
|
||||||
await secureStorage.deleteAll();
|
|
||||||
|
|
||||||
StreamChat.of(context).client.disconnect(
|
|
||||||
clearUser: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
await Navigator.pushReplacementNamed(
|
|
||||||
context,
|
|
||||||
Routes.CHOOSE_USER,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
leading: StreamSvgIcon.user(
|
|
||||||
color: StreamChatTheme.of(context)
|
|
||||||
.colorTheme
|
|
||||||
.black
|
|
||||||
.withOpacity(.5),
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
'Sign out',
|
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14.5,
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: StreamSvgIcon.penWrite(
|
||||||
|
color: StreamChatTheme.of(context)
|
||||||
|
.colorTheme
|
||||||
|
.black
|
||||||
|
.withOpacity(.5),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.popAndPushNamed(
|
||||||
|
context,
|
||||||
|
Routes.NEW_CHAT,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
title: Text(
|
||||||
|
'New direct message',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: StreamSvgIcon.contacts(
|
||||||
|
color: StreamChatTheme.of(context)
|
||||||
|
.colorTheme
|
||||||
|
.black
|
||||||
|
.withOpacity(.5),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.popAndPushNamed(
|
||||||
|
context,
|
||||||
|
Routes.NEW_GROUP_CHAT,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
title: Text(
|
||||||
|
'New group',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: ListTile(
|
||||||
|
onTap: () async {
|
||||||
|
Navigator.pop(context);
|
||||||
|
|
||||||
|
final secureStorage = FlutterSecureStorage();
|
||||||
|
await secureStorage.deleteAll();
|
||||||
|
|
||||||
|
StreamChat.of(context).client.disconnect(
|
||||||
|
clearUser: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
await Navigator.pushReplacementNamed(
|
||||||
|
context,
|
||||||
|
Routes.CHOOSE_USER,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
leading: StreamSvgIcon.user(
|
||||||
|
color: StreamChatTheme.of(context)
|
||||||
|
.colorTheme
|
||||||
|
.black
|
||||||
|
.withOpacity(.5),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
'Sign out',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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') {
|
||||||
|
|||||||
+13
-12
@@ -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,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:math';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
@@ -92,8 +93,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 ||
|
||||||
@@ -143,7 +145,6 @@ class MessageActionsModal extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
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 +154,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 (showReply &&
|
||||||
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 (showThreadReply &&
|
||||||
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 +221,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 +246,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 +274,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 +297,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 +320,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 +417,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);
|
||||||
|
|||||||
@@ -324,7 +324,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,
|
||||||
@@ -336,6 +336,43 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
itemCount: messages.length +
|
itemCount: messages.length +
|
||||||
2 +
|
2 +
|
||||||
(widget.parentMessage != null ? 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;
|
||||||
|
if (timeDiff >= 1 || !isNextUserSame || isThread) {
|
||||||
|
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 +420,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 +455,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;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -740,8 +755,17 @@ 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
|
||||||
@@ -759,17 +783,17 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
final isThreadMessage =
|
final isThreadMessage =
|
||||||
widget.parentMessage != null || message?.showInChannel == true;
|
widget.parentMessage != null || message?.showInChannel == true;
|
||||||
|
|
||||||
|
final hasFileAttachment =
|
||||||
|
message.attachments.any((it) => it.type == 'file');
|
||||||
|
|
||||||
|
final attachmentBorderRadius = hasFileAttachment ? 12.0 : 14.0;
|
||||||
|
|
||||||
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,
|
|
||||||
right: 8.0,
|
|
||||||
bottom: index == 0 ? 30 : (isNextUser ? 2 : 7),
|
|
||||||
top: 3,
|
|
||||||
),
|
|
||||||
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);
|
||||||
@@ -792,33 +816,37 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
},
|
},
|
||||||
showInChannelIndicator: widget.parentMessage == null,
|
showInChannelIndicator: widget.parentMessage == null,
|
||||||
showThreadReplyIndicator: widget.parentMessage == null,
|
showThreadReplyIndicator: widget.parentMessage == null,
|
||||||
showUsername: !isMyMessage && !isNextUser,
|
showUsername: !isMyMessage && (timeDiff >= 1 || !isNextUserSame),
|
||||||
showSendingIndicator: isMyMessage &&
|
showSendingIndicator:
|
||||||
(index == 0 || message.status != MessageSendingStatus.SENT)
|
isMyMessage && (index == 0 || timeDiff >= 1 || !isNextUserSame)
|
||||||
? DisplayWidget.show
|
? DisplayWidget.show
|
||||||
: DisplayWidget.hide,
|
: DisplayWidget.hide,
|
||||||
showTimestamp: !isNextUser || readList?.isNotEmpty == true,
|
showTimestamp:
|
||||||
|
!isNextUserSame || readList?.isNotEmpty == true || timeDiff >= 1,
|
||||||
showEditMessage: isMyMessage,
|
showEditMessage: isMyMessage,
|
||||||
showDeleteMessage: isMyMessage,
|
showDeleteMessage: isMyMessage,
|
||||||
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
|
showUserAvatar: isMyMessage
|
||||||
? DisplayWidget.gone
|
? DisplayWidget.gone
|
||||||
: (isNextUser ? DisplayWidget.hide : DisplayWidget.show),
|
: (timeDiff >= 1 || !isNextUserSame
|
||||||
|
? DisplayWidget.show
|
||||||
|
: DisplayWidget.hide),
|
||||||
messageTheme: isMyMessage
|
messageTheme: isMyMessage
|
||||||
? StreamChatTheme.of(context).ownMessageTheme
|
? StreamChatTheme.of(context).ownMessageTheme
|
||||||
: StreamChatTheme.of(context).otherMessageTheme,
|
: StreamChatTheme.of(context).otherMessageTheme,
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
+23
-17
@@ -278,7 +278,7 @@ class _MessageWidgetState extends State<MessageWidget> {
|
|||||||
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 =
|
final isOnlyEmoji =
|
||||||
widget.message.text.characters.every((c) => Emoji.byChar(c) != null);
|
widget.message.text.characters.every((c) => Emoji.byChar(c) != null);
|
||||||
@@ -330,7 +330,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 +372,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 +403,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 +431,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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -492,18 +502,16 @@ class _MessageWidgetState extends State<MessageWidget> {
|
|||||||
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)),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -754,9 +762,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 ??
|
||||||
|
|||||||
@@ -111,7 +111,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,
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user