diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
index 8ec56ee7..91797672 100644
Binary files a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
index 2f7fdaa7..103da98b 100644
Binary files a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
index 2c20055d..1718335e 100644
Binary files a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
index 89492aaa..a111f507 100644
Binary files a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index dd30e06e..fc254c25 100644
Binary files a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/example/assets/android_icon.png b/example/assets/android_icon.png
new file mode 100644
index 00000000..3b85d6c3
Binary files /dev/null and b/example/assets/android_icon.png differ
diff --git a/example/assets/icon.png b/example/assets/ios_icon.png
similarity index 100%
rename from example/assets/icon.png
rename to example/assets/ios_icon.png
diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist
index 53d6d58a..7e63f46d 100644
--- a/example/ios/Runner/Info.plist
+++ b/example/ios/Runner/Info.plist
@@ -58,8 +58,6 @@
remote-notification
- UIUserInterfaceStyle
- Light
UIViewControllerBasedStatusBarAppearance
diff --git a/example/lib/advanced_options_page.dart b/example/lib/advanced_options_page.dart
index 483e0902..e24bd5da 100644
--- a/example/lib/advanced_options_page.dart
+++ b/example/lib/advanced_options_page.dart
@@ -350,8 +350,7 @@ class _AdvancedOptionsPageState extends State {
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,
diff --git a/example/lib/main.dart b/example/lib/main.dart
index 17f497a4..84084dba 100644
--- a/example/lib/main.dart
+++ b/example/lib/main.dart
@@ -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,
diff --git a/example/pubspec.yaml b/example/pubspec.yaml
index 944be796..4f76c695 100644
--- a/example/pubspec.yaml
+++ b/example/pubspec.yaml
@@ -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"
diff --git a/lib/src/channel_bottom_sheet.dart b/lib/src/channel_bottom_sheet.dart
index f4894b02..d2c7eb04 100644
--- a/lib/src/channel_bottom_sheet.dart
+++ b/lib/src/channel_bottom_sheet.dart
@@ -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 {
@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: [
- 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>(
- 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> 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: [
+ ),
+ 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);
+ }
+ }
}
diff --git a/lib/src/channel_list_view.dart b/lib/src/channel_list_view.dart
index 777fae57..1988b9e1 100644
--- a/lib/src/channel_list_view.dart
+++ b/lib/src/channel_list_view.dart
@@ -380,6 +380,10 @@ class _ChannelListViewState extends State
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
),
),
subtitle: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Align(
alignment: Alignment.centerLeft,
@@ -567,7 +572,27 @@ class _ChannelListViewState extends State
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,
);
},
diff --git a/lib/src/channel_preview.dart b/lib/src/channel_preview.dart
index 7b86bf56..7f976688 100644
--- a/lib/src/channel_preview.dart
+++ b/lib/src/channel_preview.dart
@@ -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(
diff --git a/lib/src/channel_unread_indicator.dart b/lib/src/channel_unread_indicator.dart
index 8080b1e9..c15db13a 100644
--- a/lib/src/channel_unread_indicator.dart
+++ b/lib/src/channel_unread_indicator.dart
@@ -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,
diff --git a/lib/src/image_group.dart b/lib/src/image_group.dart
index 7bce22f9..e9b52f88 100644
--- a/lib/src/image_group.dart
+++ b/lib/src/image_group.dart
@@ -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,
),
),
),
diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart
index 14a49255..7cd68eb3 100644
--- a/lib/src/message_list_view.dart
+++ b/lib/src/message_list_view.dart
@@ -186,10 +186,10 @@ class _MessageListViewState extends State {
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 {
}
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 {
@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>(
- 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>(
+ 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: [
- 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('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: [
- 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('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: [
+ 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>(
- 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>(
+ 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 _paginateData(
@@ -512,7 +500,6 @@ class _MessageListViewState extends State {
}
Widget _buildScrollToBottom() {
- final streamChannel = StreamChannel.of(context);
return StreamBuilder>(
stream: Rx.combineLatest2(
streamChannel.channel.state.isUpToDateStream,
@@ -758,7 +745,7 @@ class _MessageListViewState extends State {
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 {
_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 {
context,
MaterialPageRoute(builder: (_) {
return StreamBuilder(
- 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 {
@override
void dispose() {
+ if (!_upToDate) {
+ streamChannel.reloadChannel();
+ }
_messageNewListener?.cancel();
super.dispose();
}
diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart
index a344068f..36364416 100644
--- a/lib/src/message_widget.dart
+++ b/lib/src/message_widget.dart
@@ -855,12 +855,13 @@ class _MessageWidgetState extends State {
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,
],
diff --git a/lib/src/option_list_tile.dart b/lib/src/option_list_tile.dart
new file mode 100644
index 00000000..d551d33e
--- /dev/null
+++ b/lib/src/option_list_tile.dart
@@ -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(),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/stream_chat_flutter.dart b/lib/stream_chat_flutter.dart
index 1cef26a6..d2dfc692 100644
--- a/lib/stream_chat_flutter.dart
+++ b/lib/stream_chat_flutter.dart
@@ -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';