Merge branch 'feature/new-ui' of github.com:GetStream/stream-chat-flutter into feat/messg-reply

 Conflicts:
	lib/src/message_list_view.dart
This commit is contained in:
Sahil Kumar
2021-01-01 09:31:41 +05:30
20 changed files with 554 additions and 442 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

-2
View File
@@ -58,8 +58,6 @@
<array>
<string>remote-notification</string>
</array>
<key>UIUserInterfaceStyle</key>
<string>Light</string>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
</dict>
+1 -2
View File
@@ -350,8 +350,7 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
return MaterialApp(
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
//TODO change to system once dark theme is implemented
themeMode: ThemeMode.light,
themeMode: ThemeMode.system,
builder: (context, widget) {
return StreamChat(
child: widget,
+4 -1
View File
@@ -60,7 +60,10 @@ class MyApp extends StatelessWidget {
debugShowCheckedModeBanner: false,
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
themeMode: ThemeMode.system,
themeMode:
WidgetsBinding.instance.window.platformBrightness == Brightness.light
? ThemeMode.light
: ThemeMode.dark,
onGenerateRoute: AppRoutes.generateRoute,
initialRoute:
client.state.user == null ? Routes.CHOOSE_USER : Routes.HOME,
+4 -3
View File
@@ -1,6 +1,6 @@
name: example
description: A new Flutter project.
version: 1.0.102+105
version: 1.0.106+109
environment:
sdk: ">=2.2.2 <3.0.0"
@@ -22,7 +22,7 @@ dev_dependencies:
sdk: flutter
flutter_driver:
sdk: flutter
flutter_launcher_icons: ^0.8.0
flutter_launcher_icons: ^0.8.1
test: any
flutter:
@@ -34,4 +34,5 @@ flutter:
flutter_icons:
android: true
ios: true
image_path: "assets/icon.png"
image_path_android: "assets/android_icon.png"
image_path_ios: "assets/ios_icon.png"
+187 -172
View File
@@ -1,193 +1,208 @@
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/utils.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import '../stream_chat_flutter.dart';
import 'channel_info.dart';
import 'channel_name.dart';
import 'stream_chat.dart';
import 'stream_chat_theme.dart';
import 'user_avatar.dart';
import 'option_list_tile.dart';
class ChannelBottomSheet extends StatelessWidget {
const ChannelBottomSheet({
Key key,
}) : super(key: key);
class ChannelBottomSheet extends StatefulWidget {
VoidCallback onViewInfoTap;
ChannelBottomSheet({this.onViewInfoTap});
@override
_ChannelBottomSheetState createState() => _ChannelBottomSheetState();
}
class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
@override
Widget build(BuildContext context) {
final channel = StreamChannel.of(context).channel;
return SafeArea(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(
vertical: 2.0,
),
child: Center(
child: StreamChannel(
showLoading: false,
channel: channel,
child: ChannelName(
textStyle:
StreamChatTheme.of(context).channelPreviewTheme.title,
),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(
vertical: 2.0,
),
child: Center(
child: ChannelInfo(
channel: channel,
textStyle:
StreamChatTheme.of(context).channelPreviewTheme.subtitle,
),
),
),
Padding(
padding: const EdgeInsets.symmetric(
vertical: 15.0,
),
child: Center(
child: StreamBuilder<List<Member>>(
stream: channel.state.membersStream.map((event) => event
.where((m) => m.userId != StreamChat.of(context).user.id)
.toList()),
initialData: channel.state.members
.where((m) => m.userId != StreamChat.of(context).user.id)
.toList(),
builder: _buildMembers,
),
),
),
Divider(),
if (channel.isGroup && !channel.isDistinct)
ListTile(
leading: StreamSvgIcon.userRemove(
size: 24,
color: StreamChatTheme.of(context).colorTheme.grey,
),
title: Text(
'Leave Group',
style: TextStyle(fontWeight: FontWeight.bold),
),
onTap: () async {
final confirm = await showConfirmationDialog(
context,
title: 'Leave Group',
okText: 'LEAVE',
question: 'Are you sure you want to leave this group?',
cancelText: 'CANCEL',
icon: StreamSvgIcon.userRemove(
color: Colors.red,
),
);
if (confirm == true) {
await channel
.removeMembers([StreamChat.of(context).user.id]);
Navigator.pop(context);
}
},
),
if ([
'admin',
'owner',
].contains(channel.state.members
.firstWhere((m) => m.userId == channel.client.state.user.id,
orElse: () => null)
?.role))
ListTile(
leading: StreamSvgIcon.delete(
color: StreamChatTheme.of(context).colorTheme.accentRed,
size: 24,
),
title: Text(
'Delete chat',
style: TextStyle(
color: StreamChatTheme.of(context).colorTheme.accentRed,
),
),
onTap: () async {
final res = await showConfirmationDialog(
context,
title: 'Delete Conversation',
okText: 'DELETE',
question:
'Are you sure you want to delete this conversation?',
cancelText: 'CANCEL',
icon: StreamSvgIcon.delete(
color: StreamChatTheme.of(context).colorTheme.accentRed,
),
);
var channel = StreamChannel.of(context).channel;
if (res == true) {
await channel.delete().then((value) {
Navigator.pop(context);
});
}
},
),
],
var channel = StreamChannel.of(context).channel;
var members = channel.state.members;
var userAsMember =
members.firstWhere((e) => e.user.id == StreamChat.of(context).user.id);
var isOwner = userAsMember.role == 'owner';
return Material(
color: StreamChatTheme.of(context).colorTheme.white,
clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16.0),
topRight: Radius.circular(16.0),
),
),
);
}
Widget _buildMembers(
BuildContext context,
AsyncSnapshot<List<Member>> snapshot,
) {
if (snapshot.data.isEmpty) {
return SizedBox();
}
return Container(
height: 83,
child: ListView(
padding: EdgeInsets.only(
left: (MediaQuery.of(context).size.width / 2) - 48,
),
scrollDirection: Axis.horizontal,
children: snapshot.data.map((m) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
children: [
SizedBox(
height: 24.0,
),
Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: ChannelName(
textStyle: StreamChatTheme.of(context).textTheme.headlineBold,
),
),
child: Column(
children: <Widget>[
),
SizedBox(
height: 5.0,
),
Center(
child: ChannelInfo(
showTypingIndicator: false,
channel: StreamChannel.of(context).channel,
textStyle:
StreamChatTheme.of(context).channelPreviewTheme.subtitle,
),
),
SizedBox(
height: 17.0,
),
if (channel.isDistinct && channel.memberCount == 2)
Column(
children: [
UserAvatar(
showOnlineStatus: true,
user: m.user,
borderRadius: BorderRadius.circular(32),
constraints: BoxConstraints.tight(
Size.square(64),
user: members
.firstWhere((e) => e.user.id != userAsMember.user.id)
.user,
constraints: BoxConstraints(
maxHeight: 64.0,
maxWidth: 64.0,
),
borderRadius: BorderRadius.circular(32.0),
),
Padding(
padding: const EdgeInsets.only(top: 5.0),
child: Text(
m.user.name?.split(' ')?.elementAt(0),
style: StreamChatTheme.of(context)
.channelPreviewTheme
.title
.copyWith(
fontSize: 12,
),
),
SizedBox(
height: 6.0,
),
Text(
members
.firstWhere((e) => e.user.id != userAsMember.user.id)
.user
.name,
style: StreamChatTheme.of(context).textTheme.footnoteBold,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
);
}).toList(),
if (!(channel.isDistinct && channel.memberCount == 2))
Container(
height: 94.0,
alignment: Alignment.center,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: members.length,
shrinkWrap: true,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Column(
children: [
UserAvatar(
user: members[index].user,
constraints: BoxConstraints(
maxHeight: 64.0,
maxWidth: 64.0,
),
borderRadius: BorderRadius.circular(32.0),
),
SizedBox(
height: 6.0,
),
Text(
members[index].user.name,
style: StreamChatTheme.of(context)
.textTheme
.footnoteBold,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
);
},
),
),
SizedBox(
height: 24.0,
),
OptionListTile(
leading: StreamSvgIcon.user(
color: StreamChatTheme.of(context).colorTheme.grey,
),
title: 'View Info',
onTap: widget.onViewInfoTap,
),
if (!channel.isDistinct)
OptionListTile(
leading: StreamSvgIcon.userRemove(
color: StreamChatTheme.of(context).colorTheme.grey,
),
title: 'Leave Group',
onTap: () async {
_showLeaveDialog();
},
),
if (isOwner)
OptionListTile(
leading: StreamSvgIcon.delete(
color: StreamChatTheme.of(context).colorTheme.accentRed,
),
title: 'Delete Conversation',
titleColor: StreamChatTheme.of(context).colorTheme.accentRed,
onTap: () async {
_showDeleteDialog();
},
),
OptionListTile(
leading: StreamSvgIcon.close_small(
color: StreamChatTheme.of(context).colorTheme.grey,
),
title: 'Cancel',
onTap: () {
Navigator.pop(context);
},
),
],
),
);
}
void _showDeleteDialog() async {
final res = await showConfirmationDialog(
context,
title: 'Delete Conversation',
okText: 'DELETE',
question: 'Are you sure you want to delete this conversation?',
cancelText: 'CANCEL',
icon: StreamSvgIcon.delete(
color: StreamChatTheme.of(context).colorTheme.accentRed,
),
);
var channel = StreamChannel.of(context).channel;
if (res == true) {
await channel.delete();
Navigator.pop(context);
}
}
void _showLeaveDialog() async {
final res = await showConfirmationDialog(
context,
title: 'Leave conversation',
okText: 'LEAVE',
question: 'Are you sure you want to leave this conversation?',
cancelText: 'CANCEL',
icon: StreamSvgIcon.userRemove(
color: StreamChatTheme.of(context).colorTheme.accentRed,
),
);
var channel = StreamChannel.of(context).channel;
if (res == true) {
await channel.removeMembers([StreamChat.of(context).user.id]);
Navigator.pop(context);
}
}
}
+26 -1
View File
@@ -380,6 +380,10 @@ class _ChannelListViewState extends State<ChannelListView>
width: 40,
),
),
contentPadding: const EdgeInsets.only(
left: 8,
right: 8,
),
title: Align(
alignment: Alignment.centerLeft,
child: Container(
@@ -394,6 +398,7 @@ class _ChannelListViewState extends State<ChannelListView>
),
),
subtitle: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Align(
alignment: Alignment.centerLeft,
@@ -567,7 +572,27 @@ class _ChannelListViewState extends State<ChannelListView>
context: context,
builder: (context) {
return StreamChannel(
child: ChannelBottomSheet(),
child: ChannelBottomSheet(
onViewInfoTap: () {
if (channel.memberCount == 2 &&
channel.isDistinct) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StreamChannel(
channel: channel,
child: ChatInfoScreen(
user:
channel.state.members.first.user,
),
),
),
);
}
// TODO: Add group screen
},
),
channel: channel,
);
},
+17 -8
View File
@@ -130,11 +130,14 @@ class ChannelPreview extends StatelessWidget {
.lastMessageAt
.fontSize,
isMessageRead: channel.state.read
.where((element) => element.lastRead
?.where((element) =>
element.user.id !=
channel.client.state.user.id)
?.where((element) => element.lastRead
.isAfter(channel
.state.lastMessage.createdAt))
.length ==
((channel.memberCount ?? 0) - 1),
?.isNotEmpty ==
true,
),
);
}
@@ -162,12 +165,18 @@ class ChannelPreview extends StatelessWidget {
String stringDate;
final now = DateTime.now();
if (now.year != lastMessageAt.year ||
now.month != lastMessageAt.month ||
now.day != lastMessageAt.day) {
stringDate = Jiffy(lastMessageAt.toLocal()).format('dd/MM/yyyy');
} else {
var startOfDay = DateTime(now.year, now.month, now.day);
if (lastMessageAt.millisecondsSinceEpoch >=
startOfDay.millisecondsSinceEpoch) {
stringDate = Jiffy(lastMessageAt.toLocal()).format('HH:mm');
} else if (lastMessageAt.millisecondsSinceEpoch >=
startOfDay.subtract(Duration(days: 1)).millisecondsSinceEpoch) {
stringDate = 'Yesterday';
} else if (startOfDay.difference(lastMessageAt).inDays < 7) {
stringDate = Jiffy(lastMessageAt.toLocal()).EEEE;
} else {
stringDate = Jiffy(lastMessageAt.toLocal()).format('dd/MM/yyyy');
}
return Text(
+2 -1
View File
@@ -19,6 +19,7 @@ class ChannelUnreadIndicator extends StatelessWidget {
if (!snapshot.hasData || snapshot.data == 0) {
return SizedBox();
}
return Material(
borderRadius: BorderRadius.circular(8),
color: StreamChatTheme.of(context)
@@ -33,7 +34,7 @@ class ChannelUnreadIndicator extends StatelessWidget {
),
child: Center(
child: Text(
'${snapshot.data}',
'${snapshot.data > 99 ? '99+' : snapshot.data}',
style: TextStyle(
fontSize: 11,
color: Colors.white,
+3 -7
View File
@@ -1,6 +1,4 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:carousel_slider/carousel_options.dart';
import 'package:carousel_slider/carousel_slider.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/full_screen_media.dart';
@@ -81,12 +79,10 @@ class ImageGroup extends StatelessWidget {
color: Colors.black38,
child: Center(
child: Text(
'${images.length - 4} +',
'+ ${images.length - 4}',
style: TextStyle(
color: StreamChatTheme.of(context)
.colorTheme
.white,
fontSize: 28,
color: Colors.white,
fontSize: 26,
),
),
),
+227 -240
View File
@@ -186,10 +186,10 @@ class _MessageListViewState extends State<MessageListView> {
bool _showScrollToBottom = false;
ItemPositionsListener _itemPositionListener;
int _messageListLength;
StreamChannelState streamChannel;
int get _initialIndex {
if (widget.initialScrollIndex != null) return widget.initialScrollIndex;
final streamChannel = StreamChannel.of(context);
if (streamChannel.initialMessageId != null) {
final messages = streamChannel.channel.state.messages;
final totalMessages = messages.length;
@@ -209,11 +209,10 @@ class _MessageListViewState extends State<MessageListView> {
}
bool _isInitialMessage(String id) {
final streamChannel = StreamChannel.of(context);
return streamChannel.initialMessageId == id;
}
bool get _upToDate => StreamChannel.of(context).channel.state.isUpToDate;
bool get _upToDate => streamChannel.channel.state.isUpToDate;
bool _topPaginationActive = false;
bool _bottomPaginationActive = false;
@@ -229,270 +228,259 @@ class _MessageListViewState extends State<MessageListView> {
@override
Widget build(BuildContext context) {
final streamChannel = StreamChannel.of(context);
final messagesStream = widget.parentMessage != null
? streamChannel.channel.state.threadsStream
.where((threads) => threads.containsKey(widget.parentMessage.id))
.map((threads) => threads[widget.parentMessage.id])
: streamChannel.channel.state?.messagesStream;
return WillPopScope(
onWillPop: () async {
if (!_upToDate) {
await streamChannel.reloadChannel();
}
return true;
},
child: StreamBuilder<List<Message>>(
stream: messagesStream?.map((messages) => messages
?.where((e) =>
!e.isDeleted ||
(e.isDeleted &&
e.user.id == streamChannel.channel.client.state.user.id))
?.toList()),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return StreamBuilder<List<Message>>(
stream: messagesStream?.map((messages) => messages
?.where((e) =>
!e.isDeleted ||
(e.isDeleted &&
e.user.id == streamChannel.channel.client.state.user.id))
?.toList()),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(
child: const CircularProgressIndicator(),
);
}
final messageList = snapshot.data?.reversed?.toList() ?? [];
if (messageList.isEmpty) {
if (_upToDate) {
return Center(
child: const CircularProgressIndicator(),
child: Text(
'No chats here yet...',
style: StreamChatTheme.of(context)
.textTheme
.footnote
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(.5)),
),
);
}
} else {
messages = messageList;
}
final messageList = snapshot.data?.reversed?.toList() ?? [];
if (messageList.isEmpty) {
if (_upToDate) {
return Center(
child: Text(
'No chats here yet...',
style: StreamChatTheme.of(context)
.textTheme
.footnote
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(.5)),
),
);
}
} else {
messages = messageList;
}
final newMessagesListLength = messages.length;
final newMessagesListLength = messages.length;
if (_messageListLength != null) {
if (_bottomPaginationActive || (_inBetweenList && _upToDate)) {
if (_itemPositionListener.itemPositions.value?.isNotEmpty ==
true) {
final first = _itemPositionListener.itemPositions.value.first;
final diff = newMessagesListLength - _messageListLength;
if (diff > 0) {
initialIndex = first.index + diff;
initialAlignment = first.itemLeadingEdge;
}
if (_messageListLength != null) {
if (_bottomPaginationActive || (_inBetweenList && _upToDate)) {
if (_itemPositionListener.itemPositions.value?.isNotEmpty ==
true) {
final first = _itemPositionListener.itemPositions.value.first;
final diff = newMessagesListLength - _messageListLength;
if (diff > 0) {
initialIndex = first.index + diff;
initialAlignment = first.itemLeadingEdge;
}
} else if (!_topPaginationActive && _upToDate) {
// Reset the index in-case we send any new message
initialIndex = 0;
initialAlignment = 0;
}
} else if (!_topPaginationActive && _upToDate) {
// Reset the index in-case we send any new message
initialIndex = 0;
initialAlignment = 0;
}
}
_messageListLength = newMessagesListLength;
_messageListLength = newMessagesListLength;
return Stack(
alignment: Alignment.center,
children: [
LazyLoadScrollView(
onStartOfPage: () async {
_inBetweenList = false;
if (!_upToDate) {
_topPaginationActive = false;
_bottomPaginationActive = true;
return _paginateData(
streamChannel,
QueryDirection.bottom,
);
}
},
onEndOfPage: () async {
_inBetweenList = false;
_topPaginationActive = true;
_bottomPaginationActive = false;
return Stack(
alignment: Alignment.center,
children: [
LazyLoadScrollView(
onStartOfPage: () async {
_inBetweenList = false;
if (!_upToDate) {
_topPaginationActive = false;
_bottomPaginationActive = true;
return _paginateData(
streamChannel,
QueryDirection.top,
QueryDirection.bottom,
);
},
onInBetweenOfPage: () {
_inBetweenList = true;
},
child: ScrollablePositionedList.builder(
key: ValueKey(initialIndex + initialAlignment),
itemPositionsListener: _itemPositionListener,
addAutomaticKeepAlives: true,
initialScrollIndex: initialIndex ?? 0,
initialAlignment: initialAlignment ?? 0,
physics: widget.scrollPhysics,
itemScrollController: _scrollController,
reverse: true,
itemCount: messages.length +
2 +
(widget.parentMessage != null ? 1 : 0),
itemBuilder: (context, i) {
if (i == messages.length + 2) {
if (widget.parentMessageBuilder != null) {
return widget.parentMessageBuilder(
context,
widget.parentMessage,
);
} else {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
buildParentMessage(widget.parentMessage),
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
StreamChatTheme.of(context)
.colorTheme
.whiteSmoke,
StreamChatTheme.of(context)
.colorTheme
.whiteSnow,
],
),
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'${widget.parentMessage.replyCount} ${widget.parentMessage.replyCount == 1 ? 'Reply' : 'Replies'}',
textAlign: TextAlign.center,
style: StreamChatTheme.of(context)
.channelTheme
.channelHeaderTheme
.lastMessageAt,
),
),
),
],
);
}
}
if (i == messages.length + 1) {
return _buildLoadingIndicator(
streamChannel,
QueryDirection.top,
);
}
if (i == 0) {
return _buildLoadingIndicator(
streamChannel,
QueryDirection.bottom,
);
}
final message = messages[i - 1];
final nextMessage = (i - 1) > 0 ? messages[i - 2] : null;
Widget messageWidget;
if (i == 1) {
messageWidget = _buildBottomMessage(
}
},
onEndOfPage: () async {
_inBetweenList = false;
_topPaginationActive = true;
_bottomPaginationActive = false;
return _paginateData(
streamChannel,
QueryDirection.top,
);
},
onInBetweenOfPage: () {
_inBetweenList = true;
},
child: ScrollablePositionedList.builder(
key: ValueKey(initialIndex + initialAlignment),
itemPositionsListener: _itemPositionListener,
addAutomaticKeepAlives: true,
initialScrollIndex: initialIndex ?? 0,
initialAlignment: initialAlignment ?? 0,
physics: widget.scrollPhysics,
itemScrollController: _scrollController,
reverse: true,
itemCount: messages.length +
2 +
(widget.parentMessage != null ? 1 : 0),
itemBuilder: (context, i) {
if (i == messages.length + 2) {
if (widget.parentMessageBuilder != null) {
return widget.parentMessageBuilder(
context,
message,
messages,
streamChannel,
);
} else if (i == messages.length - 1) {
messageWidget = _buildTopMessage(
context,
message,
messages,
streamChannel,
widget.parentMessage,
);
} else {
if (widget.messageBuilder != null) {
messageWidget = Builder(
key: ValueKey<String>('MESSAGE-${message.id}'),
builder: (context) => widget.messageBuilder(
context,
MessageDetails(
context,
message,
messages,
i,
),
messages),
);
} else {
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(),
),
buildParentMessage(widget.parentMessage),
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
StreamChatTheme.of(context)
.colorTheme
.whiteSmoke,
StreamChatTheme.of(context)
.colorTheme
.whiteSnow,
],
),
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'${widget.parentMessage.replyCount} ${widget.parentMessage.replyCount == 1 ? 'Reply' : 'Replies'}',
textAlign: TextAlign.center,
style: StreamChatTheme.of(context)
.channelTheme
.channelHeaderTheme
.lastMessageAt,
),
),
),
],
);
}
}
if (i == messages.length + 1) {
return _buildLoadingIndicator(
streamChannel,
QueryDirection.top,
);
}
if (i == 0) {
return _buildLoadingIndicator(
streamChannel,
QueryDirection.bottom,
);
}
final message = messages[i - 1];
final nextMessage = (i - 1) > 0 ? messages[i - 2] : null;
return messageWidget;
},
),
Widget messageWidget;
if (i == 1) {
messageWidget = _buildBottomMessage(
context,
message,
messages,
streamChannel,
);
} else if (i == messages.length - 1) {
messageWidget = _buildTopMessage(
context,
message,
messages,
streamChannel,
);
} else {
if (widget.messageBuilder != null) {
messageWidget = Builder(
key: ValueKey<String>('MESSAGE-${message.id}'),
builder: (context) => widget.messageBuilder(
context,
MessageDetails(
context,
message,
messages,
i,
),
messages),
);
} else {
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;
},
),
if (widget.showScrollToBottom) _buildScrollToBottom(),
Positioned(
top: 20.0,
child: ValueListenableBuilder<Iterable<ItemPosition>>(
valueListenable: _itemPositionListener.itemPositions,
builder: (context, values, _) {
final items = _itemPositionListener.itemPositions?.value;
if (items.isEmpty || messages.isEmpty) {
return SizedBox();
}
),
if (widget.showScrollToBottom) _buildScrollToBottom(),
Positioned(
top: 20.0,
child: ValueListenableBuilder<Iterable<ItemPosition>>(
valueListenable: _itemPositionListener.itemPositions,
builder: (context, values, _) {
final items = _itemPositionListener.itemPositions?.value;
if (items.isEmpty || messages.isEmpty) {
return SizedBox();
}
var index = _getTopElement(values).index;
var index = _getTopElement(values).index;
if (index > messages.length) {
return SizedBox();
}
if (index > messages.length) {
return SizedBox();
}
if (index == messages.length) {
index = max(index - 1, 0);
}
if (index == messages.length) {
index = max(index - 1, 0);
}
return widget.dateDividerBuilder != null
? widget.dateDividerBuilder(
messages[index].createdAt.toLocal(),
)
: DateDivider(
dateTime: messages[index].createdAt.toLocal(),
);
},
),
return widget.dateDividerBuilder != null
? widget.dateDividerBuilder(
messages[index].createdAt.toLocal(),
)
: DateDivider(
dateTime: messages[index].createdAt.toLocal(),
);
},
),
],
);
}),
);
),
],
);
});
}
Future<void> _paginateData(
@@ -512,7 +500,6 @@ class _MessageListViewState extends State<MessageListView> {
}
Widget _buildScrollToBottom() {
final streamChannel = StreamChannel.of(context);
return StreamBuilder<Tuple2<bool, int>>(
stream: Rx.combineLatest2(
streamChannel.channel.state.isUpToDateStream,
@@ -758,7 +745,7 @@ class _MessageListViewState extends State<MessageListView> {
final isNextUser =
index - 2 >= 0 && message.user.id == messages[index - 2]?.user?.id;
final channel = StreamChannel.of(context).channel;
final channel = streamChannel.channel;
final readList = channel.state?.read
?.where((element) => element.user.id != userId)
?.where((read) =>
@@ -864,7 +851,7 @@ class _MessageListViewState extends State<MessageListView> {
_itemPositionListener =
widget.itemPositionListener ?? ItemPositionsListener.create();
final streamChannel = StreamChannel.of(context);
streamChannel = StreamChannel.of(context);
initialIndex = _initialIndex;
initialAlignment = _initialAlignment;
@@ -907,16 +894,13 @@ class _MessageListViewState extends State<MessageListView> {
context,
MaterialPageRoute(builder: (_) {
return StreamBuilder<Message>(
stream: StreamChannel.of(context)
.channel
.state
.messagesStream
.map((messages) =>
stream: streamChannel.channel.state.messagesStream.map(
(messages) =>
messages.firstWhere((m) => m.id == message.id)),
initialData: message,
builder: (_, snapshot) {
return StreamChannel(
channel: StreamChannel.of(context).channel,
channel: streamChannel.channel,
child: widget.threadBuilder(context, snapshot.data),
);
});
@@ -928,6 +912,9 @@ class _MessageListViewState extends State<MessageListView> {
@override
void dispose() {
if (!_upToDate) {
streamChannel.reloadChannel();
}
_messageNewListener?.cancel();
super.dispose();
}
+6 -5
View File
@@ -855,12 +855,13 @@ class _MessageWidgetState extends State<MessageWidget> {
if (isMessageRead) {
child = Row(
children: [
Text(
widget.readList.length.toString(),
style: style.copyWith(
color: StreamChatTheme.of(context).colorTheme.accentBlue,
if (StreamChannel.of(context).channel.memberCount > 2)
Text(
widget.readList.length.toString(),
style: style.copyWith(
color: StreamChatTheme.of(context).colorTheme.accentBlue,
),
),
),
SizedBox(width: 2),
child,
],
+76
View File
@@ -0,0 +1,76 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
class OptionListTile extends StatelessWidget {
final String title;
final StreamSvgIcon leading;
final Widget trailing;
final VoidCallback onTap;
final Color titleColor;
OptionListTile({
this.title,
this.leading,
this.trailing,
this.onTap,
this.titleColor,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
Container(
color: StreamChatTheme.of(context).colorTheme.greyGainsboro,
height: 1.0,
),
Material(
color: StreamChatTheme.of(context).colorTheme.white,
child: Container(
height: 63.0,
child: InkWell(
onTap: onTap,
child: Row(
children: [
if (leading != null)
Expanded(
child: Center(child: leading),
),
if (leading == null)
SizedBox(
width: 16.0,
),
Expanded(
flex: 4,
child: Text(
title,
style: titleColor == null
? StreamChatTheme.of(context).textTheme.bodyBold
: StreamChatTheme.of(context)
.textTheme
.bodyBold
.copyWith(
color: titleColor,
),
),
),
Expanded(
flex: 2,
child: Padding(
padding: const EdgeInsets.only(right: 16.0),
child: Align(
alignment: Alignment.centerRight,
child: trailing ?? Container(),
),
),
),
],
),
),
),
),
],
);
}
}
+1
View File
@@ -43,3 +43,4 @@ export 'src/message_search_bloc.dart';
export 'src/message_search_item.dart';
export 'src/message_search_list_view.dart';
export 'src/unread_indicator.dart';
export 'src/chat_info_screen.dart';