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> <array>
<string>remote-notification</string> <string>remote-notification</string>
</array> </array>
<key>UIUserInterfaceStyle</key>
<string>Light</string>
<key>UIViewControllerBasedStatusBarAppearance</key> <key>UIViewControllerBasedStatusBarAppearance</key>
<true/> <true/>
</dict> </dict>
+1 -2
View File
@@ -350,8 +350,7 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
return MaterialApp( return MaterialApp(
theme: ThemeData.light(), theme: ThemeData.light(),
darkTheme: ThemeData.dark(), darkTheme: ThemeData.dark(),
//TODO change to system once dark theme is implemented themeMode: ThemeMode.system,
themeMode: ThemeMode.light,
builder: (context, widget) { builder: (context, widget) {
return StreamChat( return StreamChat(
child: widget, child: widget,
+4 -1
View File
@@ -60,7 +60,10 @@ class MyApp extends StatelessWidget {
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
theme: ThemeData.light(), theme: ThemeData.light(),
darkTheme: ThemeData.dark(), darkTheme: ThemeData.dark(),
themeMode: ThemeMode.system, themeMode:
WidgetsBinding.instance.window.platformBrightness == Brightness.light
? ThemeMode.light
: ThemeMode.dark,
onGenerateRoute: AppRoutes.generateRoute, onGenerateRoute: AppRoutes.generateRoute,
initialRoute: initialRoute:
client.state.user == null ? Routes.CHOOSE_USER : Routes.HOME, client.state.user == null ? Routes.CHOOSE_USER : Routes.HOME,
+4 -3
View File
@@ -1,6 +1,6 @@
name: example name: example
description: A new Flutter project. description: A new Flutter project.
version: 1.0.102+105 version: 1.0.106+109
environment: environment:
sdk: ">=2.2.2 <3.0.0" sdk: ">=2.2.2 <3.0.0"
@@ -22,7 +22,7 @@ dev_dependencies:
sdk: flutter sdk: flutter
flutter_driver: flutter_driver:
sdk: flutter sdk: flutter
flutter_launcher_icons: ^0.8.0 flutter_launcher_icons: ^0.8.1
test: any test: any
flutter: flutter:
@@ -34,4 +34,5 @@ flutter:
flutter_icons: flutter_icons:
android: true android: true
ios: 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: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_info.dart';
import 'channel_name.dart'; import 'option_list_tile.dart';
import 'stream_chat.dart';
import 'stream_chat_theme.dart';
import 'user_avatar.dart';
class ChannelBottomSheet extends StatelessWidget { class ChannelBottomSheet extends StatefulWidget {
const ChannelBottomSheet({ VoidCallback onViewInfoTap;
Key key,
}) : super(key: key); ChannelBottomSheet({this.onViewInfoTap});
@override
_ChannelBottomSheetState createState() => _ChannelBottomSheetState();
}
class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final channel = StreamChannel.of(context).channel; var channel = StreamChannel.of(context).channel;
return SafeArea(
child: Padding( var members = channel.state.members;
padding: const EdgeInsets.all(8.0),
child: Column( var userAsMember =
mainAxisSize: MainAxisSize.min, members.firstWhere((e) => e.user.id == StreamChat.of(context).user.id);
crossAxisAlignment: CrossAxisAlignment.stretch, var isOwner = userAsMember.role == 'owner';
children: <Widget>[
Padding( return Material(
padding: const EdgeInsets.symmetric( color: StreamChatTheme.of(context).colorTheme.white,
vertical: 2.0, clipBehavior: Clip.antiAlias,
), shape: RoundedRectangleBorder(
child: Center( borderRadius: BorderRadius.only(
child: StreamChannel( topLeft: Radius.circular(16.0),
showLoading: false, topRight: Radius.circular(16.0),
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);
});
}
},
),
],
), ),
), ),
);
}
Widget _buildMembers(
BuildContext context,
AsyncSnapshot<List<Member>> snapshot,
) {
if (snapshot.data.isEmpty) {
return SizedBox();
}
return Container(
height: 83,
child: ListView( child: ListView(
padding: EdgeInsets.only( children: [
left: (MediaQuery.of(context).size.width / 2) - 48, SizedBox(
), height: 24.0,
scrollDirection: Axis.horizontal, ),
children: snapshot.data.map((m) { Center(
return Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(horizontal: 16.0),
horizontal: 8.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( UserAvatar(
showOnlineStatus: true, user: members
user: m.user, .firstWhere((e) => e.user.id != userAsMember.user.id)
borderRadius: BorderRadius.circular(32), .user,
constraints: BoxConstraints.tight( constraints: BoxConstraints(
Size.square(64), maxHeight: 64.0,
maxWidth: 64.0,
), ),
borderRadius: BorderRadius.circular(32.0),
), ),
Padding( SizedBox(
padding: const EdgeInsets.only(top: 5.0), height: 6.0,
child: Text( ),
m.user.name?.split(' ')?.elementAt(0), Text(
style: StreamChatTheme.of(context) members
.channelPreviewTheme .firstWhere((e) => e.user.id != userAsMember.user.id)
.title .user
.copyWith( .name,
fontSize: 12, style: StreamChatTheme.of(context).textTheme.footnoteBold,
), maxLines: 1,
), overflow: TextOverflow.ellipsis,
), ),
], ],
), ),
); if (!(channel.isDistinct && channel.memberCount == 2))
}).toList(), 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, width: 40,
), ),
), ),
contentPadding: const EdgeInsets.only(
left: 8,
right: 8,
),
title: Align( title: Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Container( child: Container(
@@ -394,6 +398,7 @@ class _ChannelListViewState extends State<ChannelListView>
), ),
), ),
subtitle: Row( subtitle: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Align( Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
@@ -567,7 +572,27 @@ class _ChannelListViewState extends State<ChannelListView>
context: context, context: context,
builder: (context) { builder: (context) {
return StreamChannel( 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, channel: channel,
); );
}, },
+17 -8
View File
@@ -130,11 +130,14 @@ class ChannelPreview extends StatelessWidget {
.lastMessageAt .lastMessageAt
.fontSize, .fontSize,
isMessageRead: channel.state.read isMessageRead: channel.state.read
.where((element) => element.lastRead ?.where((element) =>
element.user.id !=
channel.client.state.user.id)
?.where((element) => element.lastRead
.isAfter(channel .isAfter(channel
.state.lastMessage.createdAt)) .state.lastMessage.createdAt))
.length == ?.isNotEmpty ==
((channel.memberCount ?? 0) - 1), true,
), ),
); );
} }
@@ -162,12 +165,18 @@ class ChannelPreview extends StatelessWidget {
String stringDate; String stringDate;
final now = DateTime.now(); final now = DateTime.now();
if (now.year != lastMessageAt.year || var startOfDay = DateTime(now.year, now.month, now.day);
now.month != lastMessageAt.month ||
now.day != lastMessageAt.day) { if (lastMessageAt.millisecondsSinceEpoch >=
stringDate = Jiffy(lastMessageAt.toLocal()).format('dd/MM/yyyy'); startOfDay.millisecondsSinceEpoch) {
} else {
stringDate = Jiffy(lastMessageAt.toLocal()).format('HH:mm'); 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( return Text(
+2 -1
View File
@@ -19,6 +19,7 @@ class ChannelUnreadIndicator extends StatelessWidget {
if (!snapshot.hasData || snapshot.data == 0) { if (!snapshot.hasData || snapshot.data == 0) {
return SizedBox(); return SizedBox();
} }
return Material( return Material(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
@@ -33,7 +34,7 @@ class ChannelUnreadIndicator extends StatelessWidget {
), ),
child: Center( child: Center(
child: Text( child: Text(
'${snapshot.data}', '${snapshot.data > 99 ? '99+' : snapshot.data}',
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11,
color: Colors.white, color: Colors.white,
+3 -7
View File
@@ -1,6 +1,4 @@
import 'package:cached_network_image/cached_network_image.dart'; 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:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/full_screen_media.dart'; import 'package:stream_chat_flutter/src/full_screen_media.dart';
@@ -81,12 +79,10 @@ class ImageGroup extends StatelessWidget {
color: Colors.black38, color: Colors.black38,
child: Center( child: Center(
child: Text( child: Text(
'${images.length - 4} +', '+ ${images.length - 4}',
style: TextStyle( style: TextStyle(
color: StreamChatTheme.of(context) color: Colors.white,
.colorTheme fontSize: 26,
.white,
fontSize: 28,
), ),
), ),
), ),
+227 -240
View File
@@ -186,10 +186,10 @@ class _MessageListViewState extends State<MessageListView> {
bool _showScrollToBottom = false; bool _showScrollToBottom = false;
ItemPositionsListener _itemPositionListener; ItemPositionsListener _itemPositionListener;
int _messageListLength; int _messageListLength;
StreamChannelState streamChannel;
int get _initialIndex { int get _initialIndex {
if (widget.initialScrollIndex != null) return widget.initialScrollIndex; if (widget.initialScrollIndex != null) return widget.initialScrollIndex;
final streamChannel = StreamChannel.of(context);
if (streamChannel.initialMessageId != null) { if (streamChannel.initialMessageId != null) {
final messages = streamChannel.channel.state.messages; final messages = streamChannel.channel.state.messages;
final totalMessages = messages.length; final totalMessages = messages.length;
@@ -209,11 +209,10 @@ class _MessageListViewState extends State<MessageListView> {
} }
bool _isInitialMessage(String id) { bool _isInitialMessage(String id) {
final streamChannel = StreamChannel.of(context);
return streamChannel.initialMessageId == id; return streamChannel.initialMessageId == id;
} }
bool get _upToDate => StreamChannel.of(context).channel.state.isUpToDate; bool get _upToDate => streamChannel.channel.state.isUpToDate;
bool _topPaginationActive = false; bool _topPaginationActive = false;
bool _bottomPaginationActive = false; bool _bottomPaginationActive = false;
@@ -229,270 +228,259 @@ class _MessageListViewState extends State<MessageListView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final streamChannel = StreamChannel.of(context);
final messagesStream = widget.parentMessage != null final messagesStream = widget.parentMessage != null
? streamChannel.channel.state.threadsStream ? streamChannel.channel.state.threadsStream
.where((threads) => threads.containsKey(widget.parentMessage.id)) .where((threads) => threads.containsKey(widget.parentMessage.id))
.map((threads) => threads[widget.parentMessage.id]) .map((threads) => threads[widget.parentMessage.id])
: streamChannel.channel.state?.messagesStream; : streamChannel.channel.state?.messagesStream;
return WillPopScope( return StreamBuilder<List<Message>>(
onWillPop: () async { stream: messagesStream?.map((messages) => messages
if (!_upToDate) { ?.where((e) =>
await streamChannel.reloadChannel(); !e.isDeleted ||
} (e.isDeleted &&
return true; e.user.id == streamChannel.channel.client.state.user.id))
}, ?.toList()),
child: StreamBuilder<List<Message>>( builder: (context, snapshot) {
stream: messagesStream?.map((messages) => messages if (!snapshot.hasData) {
?.where((e) => return Center(
!e.isDeleted || child: const CircularProgressIndicator(),
(e.isDeleted && );
e.user.id == streamChannel.channel.client.state.user.id)) }
?.toList()),
builder: (context, snapshot) { final messageList = snapshot.data?.reversed?.toList() ?? [];
if (!snapshot.hasData) { if (messageList.isEmpty) {
if (_upToDate) {
return Center( 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() ?? []; final newMessagesListLength = messages.length;
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; if (_messageListLength != null) {
if (_bottomPaginationActive || (_inBetweenList && _upToDate)) {
if (_messageListLength != null) { if (_itemPositionListener.itemPositions.value?.isNotEmpty ==
if (_bottomPaginationActive || (_inBetweenList && _upToDate)) { true) {
if (_itemPositionListener.itemPositions.value?.isNotEmpty == final first = _itemPositionListener.itemPositions.value.first;
true) { final diff = newMessagesListLength - _messageListLength;
final first = _itemPositionListener.itemPositions.value.first; if (diff > 0) {
final diff = newMessagesListLength - _messageListLength; initialIndex = first.index + diff;
if (diff > 0) { initialAlignment = first.itemLeadingEdge;
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( return Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
LazyLoadScrollView( LazyLoadScrollView(
onStartOfPage: () async { onStartOfPage: () async {
_inBetweenList = false; _inBetweenList = false;
if (!_upToDate) { if (!_upToDate) {
_topPaginationActive = false; _topPaginationActive = false;
_bottomPaginationActive = true; _bottomPaginationActive = true;
return _paginateData(
streamChannel,
QueryDirection.bottom,
);
}
},
onEndOfPage: () async {
_inBetweenList = false;
_topPaginationActive = true;
_bottomPaginationActive = false;
return _paginateData( return _paginateData(
streamChannel, streamChannel,
QueryDirection.top, QueryDirection.bottom,
); );
}, }
onInBetweenOfPage: () { },
_inBetweenList = true; onEndOfPage: () async {
}, _inBetweenList = false;
child: ScrollablePositionedList.builder( _topPaginationActive = true;
key: ValueKey(initialIndex + initialAlignment), _bottomPaginationActive = false;
itemPositionsListener: _itemPositionListener, return _paginateData(
addAutomaticKeepAlives: true, streamChannel,
initialScrollIndex: initialIndex ?? 0, QueryDirection.top,
initialAlignment: initialAlignment ?? 0, );
physics: widget.scrollPhysics, },
itemScrollController: _scrollController, onInBetweenOfPage: () {
reverse: true, _inBetweenList = true;
itemCount: messages.length + },
2 + child: ScrollablePositionedList.builder(
(widget.parentMessage != null ? 1 : 0), key: ValueKey(initialIndex + initialAlignment),
itemBuilder: (context, i) { itemPositionsListener: _itemPositionListener,
if (i == messages.length + 2) { addAutomaticKeepAlives: true,
if (widget.parentMessageBuilder != null) { initialScrollIndex: initialIndex ?? 0,
return widget.parentMessageBuilder( initialAlignment: initialAlignment ?? 0,
context, physics: widget.scrollPhysics,
widget.parentMessage, itemScrollController: _scrollController,
); reverse: true,
} else { itemCount: messages.length +
return Column( 2 +
crossAxisAlignment: CrossAxisAlignment.stretch, (widget.parentMessage != null ? 1 : 0),
children: <Widget>[ itemBuilder: (context, i) {
buildParentMessage(widget.parentMessage), if (i == messages.length + 2) {
Container( if (widget.parentMessageBuilder != null) {
decoration: BoxDecoration( return widget.parentMessageBuilder(
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(
context, context,
message, widget.parentMessage,
messages,
streamChannel,
);
} else if (i == messages.length - 1) {
messageWidget = _buildTopMessage(
context,
message,
messages,
streamChannel,
); );
} else { } 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( return Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[ children: <Widget>[
messageWidget, buildParentMessage(widget.parentMessage),
Padding( Container(
padding: decoration: BoxDecoration(
const EdgeInsets.symmetric(vertical: 12.0), gradient: LinearGradient(
child: widget.dateDividerBuilder != null begin: Alignment.topCenter,
? widget.dateDividerBuilder( end: Alignment.bottomCenter,
nextMessage.createdAt.toLocal()) colors: [
: DateDivider( StreamChatTheme.of(context)
dateTime: nextMessage.createdAt.toLocal(), .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( if (widget.showScrollToBottom) _buildScrollToBottom(),
top: 20.0, Positioned(
child: ValueListenableBuilder<Iterable<ItemPosition>>( top: 20.0,
valueListenable: _itemPositionListener.itemPositions, child: ValueListenableBuilder<Iterable<ItemPosition>>(
builder: (context, values, _) { valueListenable: _itemPositionListener.itemPositions,
final items = _itemPositionListener.itemPositions?.value; builder: (context, values, _) {
if (items.isEmpty || messages.isEmpty) { final items = _itemPositionListener.itemPositions?.value;
return SizedBox(); if (items.isEmpty || messages.isEmpty) {
} return SizedBox();
}
var index = _getTopElement(values).index; var index = _getTopElement(values).index;
if (index > messages.length) { if (index > messages.length) {
return SizedBox(); return SizedBox();
} }
if (index == messages.length) { if (index == messages.length) {
index = max(index - 1, 0); index = max(index - 1, 0);
} }
return widget.dateDividerBuilder != null return widget.dateDividerBuilder != null
? widget.dateDividerBuilder( ? widget.dateDividerBuilder(
messages[index].createdAt.toLocal(), messages[index].createdAt.toLocal(),
) )
: DateDivider( : DateDivider(
dateTime: messages[index].createdAt.toLocal(), dateTime: messages[index].createdAt.toLocal(),
); );
}, },
),
), ),
], ),
); ],
}), );
); });
} }
Future<void> _paginateData( Future<void> _paginateData(
@@ -512,7 +500,6 @@ class _MessageListViewState extends State<MessageListView> {
} }
Widget _buildScrollToBottom() { Widget _buildScrollToBottom() {
final streamChannel = StreamChannel.of(context);
return StreamBuilder<Tuple2<bool, int>>( return StreamBuilder<Tuple2<bool, int>>(
stream: Rx.combineLatest2( stream: Rx.combineLatest2(
streamChannel.channel.state.isUpToDateStream, streamChannel.channel.state.isUpToDateStream,
@@ -758,7 +745,7 @@ class _MessageListViewState extends State<MessageListView> {
final isNextUser = final isNextUser =
index - 2 >= 0 && message.user.id == messages[index - 2]?.user?.id; 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 final readList = channel.state?.read
?.where((element) => element.user.id != userId) ?.where((element) => element.user.id != userId)
?.where((read) => ?.where((read) =>
@@ -864,7 +851,7 @@ class _MessageListViewState extends State<MessageListView> {
_itemPositionListener = _itemPositionListener =
widget.itemPositionListener ?? ItemPositionsListener.create(); widget.itemPositionListener ?? ItemPositionsListener.create();
final streamChannel = StreamChannel.of(context); streamChannel = StreamChannel.of(context);
initialIndex = _initialIndex; initialIndex = _initialIndex;
initialAlignment = _initialAlignment; initialAlignment = _initialAlignment;
@@ -907,16 +894,13 @@ class _MessageListViewState extends State<MessageListView> {
context, context,
MaterialPageRoute(builder: (_) { MaterialPageRoute(builder: (_) {
return StreamBuilder<Message>( return StreamBuilder<Message>(
stream: StreamChannel.of(context) stream: streamChannel.channel.state.messagesStream.map(
.channel (messages) =>
.state
.messagesStream
.map((messages) =>
messages.firstWhere((m) => m.id == message.id)), messages.firstWhere((m) => m.id == message.id)),
initialData: message, initialData: message,
builder: (_, snapshot) { builder: (_, snapshot) {
return StreamChannel( return StreamChannel(
channel: StreamChannel.of(context).channel, channel: streamChannel.channel,
child: widget.threadBuilder(context, snapshot.data), child: widget.threadBuilder(context, snapshot.data),
); );
}); });
@@ -928,6 +912,9 @@ class _MessageListViewState extends State<MessageListView> {
@override @override
void dispose() { void dispose() {
if (!_upToDate) {
streamChannel.reloadChannel();
}
_messageNewListener?.cancel(); _messageNewListener?.cancel();
super.dispose(); super.dispose();
} }
+6 -5
View File
@@ -855,12 +855,13 @@ class _MessageWidgetState extends State<MessageWidget> {
if (isMessageRead) { if (isMessageRead) {
child = Row( child = Row(
children: [ children: [
Text( if (StreamChannel.of(context).channel.memberCount > 2)
widget.readList.length.toString(), Text(
style: style.copyWith( widget.readList.length.toString(),
color: StreamChatTheme.of(context).colorTheme.accentBlue, style: style.copyWith(
color: StreamChatTheme.of(context).colorTheme.accentBlue,
),
), ),
),
SizedBox(width: 2), SizedBox(width: 2),
child, 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_item.dart';
export 'src/message_search_list_view.dart'; export 'src/message_search_list_view.dart';
export 'src/unread_indicator.dart'; export 'src/unread_indicator.dart';
export 'src/chat_info_screen.dart';