Merge pull request #31 from GetStream/nnbd-migration

stream chat v1 nnbd migration
This commit is contained in:
Salvatore Giordano
2021-05-17 15:23:44 +02:00
committed by GitHub
36 changed files with 562 additions and 488 deletions
@@ -6,7 +6,8 @@ class ImagePickerImpl extends ImagePickerRepository {
@override @override
Future<File> pickImage() async { Future<File> pickImage() async {
final picker = ImagePicker(); final picker = ImagePicker();
final pickedFile = await picker.getImage(source: ImageSource.gallery, maxWidth: 400); final pickedFile =
await picker.getImage(source: ImageSource.gallery, maxWidth: 400);
return File(pickedFile.path); return File(pickedFile.path);
} }
} }
@@ -46,7 +46,9 @@ class StreamApiLocalImpl extends StreamApiRepository {
} }
@override @override
Future<Channel> createGroupChat(String channelId, String name, List<String> members, {String image}) async { Future<Channel> createGroupChat(
String channelId, String name, List<String> members,
{String image}) async {
final channel = _client.channel('messaging', id: channelId, extraData: { final channel = _client.channel('messaging', id: channelId, extraData: {
'name': name, 'name': name,
'image': image, 'image': image,
@@ -58,13 +60,14 @@ class StreamApiLocalImpl extends StreamApiRepository {
@override @override
Future<Channel> createSimpleChat(String friendId) async { Future<Channel> createSimpleChat(String friendId) async {
final channel = final channel = _client.channel('messaging',
_client.channel('messaging', id: '${_client.state.user.id.hashCode}${friendId.hashCode}', extraData: { id: '${_client.state.user.id.hashCode}${friendId.hashCode}',
'members': [ extraData: {
friendId, 'members': [
_client.state.user.id, friendId,
], _client.state.user.id,
}); ],
});
await channel.watch(); await channel.watch();
return channel; return channel;
} }
+4 -2
View File
@@ -20,8 +20,10 @@ class AuthImpl extends AuthRepository {
try { try {
UserCredential userCredential; UserCredential userCredential;
final GoogleSignInAccount googleUser = await GoogleSignIn().signIn(); final GoogleSignInAccount googleUser = await GoogleSignIn().signIn();
final GoogleSignInAuthentication googleAuth = await googleUser.authentication; final GoogleSignInAuthentication googleAuth =
final GoogleAuthCredential googleAuthCredential = GoogleAuthProvider.credential( await googleUser.authentication;
final GoogleAuthCredential googleAuthCredential =
GoogleAuthProvider.credential(
accessToken: googleAuth.accessToken, accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken, idToken: googleAuth.idToken,
); );
@@ -62,7 +62,8 @@ class StreamApiImpl extends StreamApiRepository {
} }
@override @override
Future<Channel> createGroupChat(String id, String name, List<String> members, {String image}) async { Future<Channel> createGroupChat(String id, String name, List<String> members,
{String image}) async {
final channel = _client.channel('messaging', id: id, extraData: { final channel = _client.channel('messaging', id: id, extraData: {
'name': name, 'name': name,
'image': image, 'image': image,
@@ -74,13 +75,14 @@ class StreamApiImpl extends StreamApiRepository {
@override @override
Future<Channel> createSimpleChat(String friendId) async { Future<Channel> createSimpleChat(String friendId) async {
final channel = final channel = _client.channel('messaging',
_client.channel('messaging', id: '${_client.state.user.id.hashCode}${friendId.hashCode}', extraData: { id: '${_client.state.user.id.hashCode}${friendId.hashCode}',
'members': [ extraData: {
friendId, 'members': [
_client.state.user.id, friendId,
], _client.state.user.id,
}); ],
});
await channel.watch(); await channel.watch();
return channel; return channel;
} }
@@ -6,7 +6,9 @@ abstract class StreamApiRepository {
Future<String> getToken(String userId); Future<String> getToken(String userId);
Future<bool> connectIfExist(String userId); Future<bool> connectIfExist(String userId);
Future<ChatUser> connectUser(ChatUser user, String token); Future<ChatUser> connectUser(ChatUser user, String token);
Future<Channel> createGroupChat(String channelId, String name, List<String> members, {String image}); Future<Channel> createGroupChat(
String channelId, String name, List<String> members,
{String image});
Future<Channel> createSimpleChat(String friendId); Future<Channel> createSimpleChat(String friendId);
Future<void> logout(); Future<void> logout();
} }
+6 -3
View File
@@ -18,10 +18,13 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
List<RepositoryProvider> buildRepositories(StreamChatClient client) { List<RepositoryProvider> buildRepositories(StreamChatClient client) {
//TODO: Here you can use your local implementations of your repositories //TODO: Here you can use your local implementations of your repositories
return [ return [
RepositoryProvider<StreamApiRepository>(create: (_) => StreamApiImpl(client)), RepositoryProvider<StreamApiRepository>(
RepositoryProvider<PersistentStorageRepository>(create: (_) => PersistentStorageImpl()), create: (_) => StreamApiImpl(client)),
RepositoryProvider<PersistentStorageRepository>(
create: (_) => PersistentStorageImpl()),
RepositoryProvider<AuthRepository>(create: (_) => AuthImpl()), RepositoryProvider<AuthRepository>(create: (_) => AuthImpl()),
RepositoryProvider<UploadStorageRepository>(create: (_) => UploadStorageImpl()), RepositoryProvider<UploadStorageRepository>(
create: (_) => UploadStorageImpl()),
RepositoryProvider<ImagePickerRepository>(create: (_) => ImagePickerImpl()), RepositoryProvider<ImagePickerRepository>(create: (_) => ImagePickerImpl()),
RepositoryProvider<ProfileSignInUseCase>( RepositoryProvider<ProfileSignInUseCase>(
create: (context) => ProfileSignInUseCase( create: (context) => ProfileSignInUseCase(
@@ -25,7 +25,8 @@ class CreateGroupUseCase {
final channelId = Uuid().v4(); final channelId = Uuid().v4();
String image; String image;
if (input.imageFile != null) { if (input.imageFile != null) {
image = await _uploadStorageRepository.uploadPhoto(input.imageFile, 'channels/$channelId'); image = await _uploadStorageRepository.uploadPhoto(
input.imageFile, 'channels/$channelId');
} }
final channel = await _streamApiRepository.createGroupChat( final channel = await _streamApiRepository.createGroupChat(
channelId, channelId,
@@ -27,8 +27,10 @@ class ProfileSignInUseCase {
final token = await _streamApiRepository.getToken(auth.id); final token = await _streamApiRepository.getToken(auth.id);
String image; String image;
if (input.imageFile != null) { if (input.imageFile != null) {
image = await _uploadStorageRepository.uploadPhoto(input.imageFile, 'users/${auth.id}'); image = await _uploadStorageRepository.uploadPhoto(
input.imageFile, 'users/${auth.id}');
} }
await _streamApiRepository.connectUser(ChatUser(name: input.name, id: auth.id, image: image), token); await _streamApiRepository.connectUser(
ChatUser(name: input.name, id: auth.id, image: image), token);
} }
} }
+2 -1
View File
@@ -34,7 +34,8 @@ class MyApp extends StatelessWidget {
return StreamChat( return StreamChat(
child: child, child: child,
client: _streamChatClient, client: _streamChatClient,
streamChatThemeData: StreamChatThemeData.fromTheme(Theme.of(context)).copyWith( streamChatThemeData:
StreamChatThemeData.fromTheme(Theme.of(context)).copyWith(
ownMessageTheme: MessageTheme( ownMessageTheme: MessageTheme(
messageBackgroundColor: Theme.of(context).accentColor, messageBackgroundColor: Theme.of(context).accentColor,
messageText: TextStyle(color: Colors.white), messageText: TextStyle(color: Colors.white),
+3 -1
View File
@@ -18,5 +18,7 @@ Future pushAndReplaceToPage(BuildContext context, Widget widget) async {
Future popAllAndPush(BuildContext context, Widget widget) async { Future popAllAndPush(BuildContext context, Widget widget) async {
await Navigator.pushAndRemoveUntil( await Navigator.pushAndRemoveUntil(
context, MaterialPageRoute(builder: (BuildContext context) => widget), ModalRoute.withName('/')); context,
MaterialPageRoute(builder: (BuildContext context) => widget),
ModalRoute.withName('/'));
} }
@@ -90,7 +90,8 @@ class MyChannelPreview extends StatelessWidget {
children: <Widget>[ children: <Widget>[
Flexible( Flexible(
child: ChannelName( child: ChannelName(
textStyle: StreamChatTheme.of(context).channelPreviewTheme.title, textStyle:
StreamChatTheme.of(context).channelPreviewTheme.title,
), ),
), ),
StreamBuilder<List<Member>>( StreamBuilder<List<Member>>(
@@ -99,7 +100,8 @@ class MyChannelPreview extends StatelessWidget {
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData || if (!snapshot.hasData ||
snapshot.data.isEmpty || snapshot.data.isEmpty ||
!snapshot.data.any((Member e) => e.user.id == channel.client.state.user.id)) { !snapshot.data.any((Member e) =>
e.user.id == channel.client.state.user.id)) {
return SizedBox(); return SizedBox();
} }
return ChannelUnreadIndicator( return ChannelUnreadIndicator(
@@ -118,15 +120,21 @@ class MyChannelPreview extends StatelessWidget {
(m) => !m.isDeleted && m.shadowed != true, (m) => !m.isDeleted && m.shadowed != true,
orElse: () => null, orElse: () => null,
); );
if (lastMessage?.user?.id == StreamChat.of(context).user.id) { if (lastMessage?.user?.id ==
StreamChat.of(context).user.id) {
return Padding( return Padding(
padding: const EdgeInsets.only(right: 4.0), padding: const EdgeInsets.only(right: 4.0),
child: SendingIndicator( child: SendingIndicator(
message: lastMessage, message: lastMessage,
size: StreamChatTheme.of(context).channelPreviewTheme.indicatorIconSize, size: StreamChatTheme.of(context)
.channelPreviewTheme
.indicatorIconSize,
isMessageRead: channel.state.read isMessageRead: channel.state.read
?.where((element) => element.user.id != channel.client.state.user.id) ?.where((element) =>
?.where((element) => element.lastRead.isAfter(lastMessage.createdAt)) element.user.id !=
channel.client.state.user.id)
?.where((element) => element.lastRead
.isAfter(lastMessage.createdAt))
?.isNotEmpty == ?.isNotEmpty ==
true, true,
), ),
@@ -158,7 +166,8 @@ class MyChannelPreview extends StatelessWidget {
var startOfDay = DateTime(now.year, now.month, now.day); var startOfDay = DateTime(now.year, now.month, now.day);
if (lastMessageAt.millisecondsSinceEpoch >= startOfDay.millisecondsSinceEpoch) { if (lastMessageAt.millisecondsSinceEpoch >=
startOfDay.millisecondsSinceEpoch) {
stringDate = Jiffy(lastMessageAt.toLocal()).format('HH:mm'); stringDate = Jiffy(lastMessageAt.toLocal()).format('HH:mm');
} else if (lastMessageAt.millisecondsSinceEpoch >= } else if (lastMessageAt.millisecondsSinceEpoch >=
startOfDay.subtract(Duration(days: 1)).millisecondsSinceEpoch) { startOfDay.subtract(Duration(days: 1)).millisecondsSinceEpoch) {
@@ -187,8 +196,14 @@ class MyChannelPreview extends StatelessWidget {
), ),
Text( Text(
' Channel is muted', ' Channel is muted',
style: StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith( style: StreamChatTheme.of(context)
color: StreamChatTheme.of(context).channelPreviewTheme.subtitle.color, .channelPreviewTheme
.subtitle
.copyWith(
color: StreamChatTheme.of(context)
.channelPreviewTheme
.subtitle
.color,
), ),
), ),
], ],
@@ -198,7 +213,8 @@ class MyChannelPreview extends StatelessWidget {
channel: channel, channel: channel,
alternativeWidget: _buildLastMessage(context), alternativeWidget: _buildLastMessage(context),
style: StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith( style: StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith(
color: StreamChatTheme.of(context).channelPreviewTheme.subtitle.color, color:
StreamChatTheme.of(context).channelPreviewTheme.subtitle.color,
), ),
); );
} }
@@ -208,7 +224,9 @@ class MyChannelPreview extends StatelessWidget {
stream: channel.state.messagesStream, stream: channel.state.messagesStream,
initialData: channel.state.messages, initialData: channel.state.messages,
builder: (context, snapshot) { builder: (context, snapshot) {
final lastMessage = snapshot.data?.lastWhere((m) => m.shadowed != true && !m.isDeleted, orElse: () => null); final lastMessage = snapshot.data?.lastWhere(
(m) => m.shadowed != true && !m.isDeleted,
orElse: () => null);
if (lastMessage == null) { if (lastMessage == null) {
return SizedBox(); return SizedBox();
} }
@@ -224,7 +242,9 @@ class MyChannelPreview extends StatelessWidget {
} else if (e.type == 'giphy') { } else if (e.type == 'giphy') {
return '[GIF]'; return '[GIF]';
} }
return e == lastMessage.attachments.last ? (e.title ?? 'File') : '${e.title ?? 'File'} , '; return e == lastMessage.attachments.last
? (e.title ?? 'File')
: '${e.title ?? 'File'} , ';
}).where((e) => e != null), }).where((e) => e != null),
lastMessage.text ?? '', lastMessage.text ?? '',
]; ];
@@ -238,11 +258,21 @@ class MyChannelPreview extends StatelessWidget {
lastMessage.mentionedUsers, lastMessage.mentionedUsers,
lastMessage.attachments, lastMessage.attachments,
StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith( StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith(
color: StreamChatTheme.of(context).channelPreviewTheme.subtitle.color, color: StreamChatTheme.of(context)
fontStyle: (lastMessage.isSystem || lastMessage.isDeleted) ? FontStyle.italic : FontStyle.normal), .channelPreviewTheme
.subtitle
.color,
fontStyle: (lastMessage.isSystem || lastMessage.isDeleted)
? FontStyle.italic
: FontStyle.normal),
StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith( StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith(
color: StreamChatTheme.of(context).channelPreviewTheme.subtitle.color, color: StreamChatTheme.of(context)
fontStyle: (lastMessage.isSystem || lastMessage.isDeleted) ? FontStyle.italic : FontStyle.normal, .channelPreviewTheme
.subtitle
.color,
fontStyle: (lastMessage.isSystem || lastMessage.isDeleted)
? FontStyle.italic
: FontStyle.normal,
fontWeight: FontWeight.bold), fontWeight: FontWeight.bold),
), ),
maxLines: 1, maxLines: 1,
@@ -252,19 +282,27 @@ class MyChannelPreview extends StatelessWidget {
); );
} }
TextSpan _getDisplayText(String text, List<User> mentions, List<Attachment> attachments, TextStyle normalTextStyle, TextSpan _getDisplayText(
String text,
List<User> mentions,
List<Attachment> attachments,
TextStyle normalTextStyle,
TextStyle mentionsTextStyle) { TextStyle mentionsTextStyle) {
var textList = text.split(' '); var textList = text.split(' ');
var resList = <TextSpan>[]; var resList = <TextSpan>[];
for (var e in textList) { for (var e in textList) {
if (mentions != null && mentions.isNotEmpty && mentions.any((element) => '@${element.name}' == e)) { if (mentions != null &&
mentions.isNotEmpty &&
mentions.any((element) => '@${element.name}' == e)) {
resList.add(TextSpan( resList.add(TextSpan(
text: '$e ', text: '$e ',
style: mentionsTextStyle, style: mentionsTextStyle,
)); ));
} else if (attachments != null && } else if (attachments != null &&
attachments.isNotEmpty && attachments.isNotEmpty &&
attachments.where((e) => e.title != null).any((element) => element.title == e)) { attachments
.where((e) => e.title != null)
.any((element) => element.title == e)) {
resList.add(TextSpan( resList.add(TextSpan(
text: '$e ', text: '$e ',
style: normalTextStyle.copyWith(fontStyle: FontStyle.italic), style: normalTextStyle.copyWith(fontStyle: FontStyle.italic),
@@ -301,7 +339,9 @@ class ChannelUnreadIndicator extends StatelessWidget {
return Material( return Material(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
color: StreamChatTheme.of(context).channelPreviewTheme.unreadCounterColor, color: StreamChatTheme.of(context)
.channelPreviewTheme
.unreadCounterColor,
child: Padding( child: Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: 5.0, left: 5.0,
@@ -43,8 +43,10 @@ class ChatView extends StatelessWidget {
name = channel.extraData['name']; name = channel.extraData['name'];
image = channel.extraData['image']; image = channel.extraData['image'];
} else { } else {
final friend = final friend = channel.state.members
channel.state.members.where((element) => element.userId != currentUser.id).first.user; .where((element) => element.userId != currentUser.id)
.first
.user;
name = friend.name; name = friend.name;
image = friend.extraData['image']; image = friend.extraData['image'];
} }
@@ -13,21 +13,27 @@ class FriendsSelectionCubit extends Cubit<List<ChatUserState>> {
FriendsSelectionCubit(this._streamApiRepository) : super([]); FriendsSelectionCubit(this._streamApiRepository) : super([]);
final StreamApiRepository _streamApiRepository; final StreamApiRepository _streamApiRepository;
List<ChatUserState> get selectedUsers => state.where((element) => element.selected).toList(); List<ChatUserState> get selectedUsers =>
state.where((element) => element.selected).toList();
Future<void> init() async { Future<void> init() async {
final chatUsers = (await _streamApiRepository.getChatUsers()).map((e) => ChatUserState(e)).toList(); final chatUsers = (await _streamApiRepository.getChatUsers())
.map((e) => ChatUserState(e))
.toList();
emit(chatUsers); emit(chatUsers);
} }
void selectUser(ChatUserState chatUser) { void selectUser(ChatUserState chatUser) {
final index = state.indexWhere((element) => element.chatUser.id == chatUser.chatUser.id); final index = state
state[index] = ChatUserState(state[index].chatUser, selected: !chatUser.selected); .indexWhere((element) => element.chatUser.id == chatUser.chatUser.id);
state[index] =
ChatUserState(state[index].chatUser, selected: !chatUser.selected);
emit(List<ChatUserState>.from(state)); emit(List<ChatUserState>.from(state));
} }
Future<Channel> createFriendChannel(ChatUserState chatUserState) async { Future<Channel> createFriendChannel(ChatUserState chatUserState) async {
return await _streamApiRepository.createSimpleChat(chatUserState.chatUser.id); return await _streamApiRepository
.createSimpleChat(chatUserState.chatUser.id);
} }
} }
@@ -7,8 +7,11 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class FriendsSelectionView extends StatelessWidget { class FriendsSelectionView extends StatelessWidget {
void _createFriendChannel(BuildContext context, ChatUserState chatUserState) async { void _createFriendChannel(
final channel = await context.read<FriendsSelectionCubit>().createFriendChannel(chatUserState); BuildContext context, ChatUserState chatUserState) async {
final channel = await context
.read<FriendsSelectionCubit>()
.createFriendChannel(chatUserState);
pushAndReplaceToPage( pushAndReplaceToPage(
context, context,
Scaffold( Scaffold(
@@ -26,19 +29,23 @@ class FriendsSelectionView extends StatelessWidget {
final accentColor = Theme.of(context).accentColor; final accentColor = Theme.of(context).accentColor;
return MultiBlocProvider( return MultiBlocProvider(
providers: [ providers: [
BlocProvider(create: (context) => FriendsSelectionCubit(context.read())..init()), BlocProvider(
create: (context) => FriendsSelectionCubit(context.read())..init()),
BlocProvider(create: (_) => FriendsGroupCubit()), BlocProvider(create: (_) => FriendsGroupCubit()),
], ],
child: BlocBuilder<FriendsGroupCubit, bool>(builder: (context, isGroup) { child: BlocBuilder<FriendsGroupCubit, bool>(builder: (context, isGroup) {
return BlocBuilder<FriendsSelectionCubit, List<ChatUserState>>(builder: (context, snapshot) { return BlocBuilder<FriendsSelectionCubit, List<ChatUserState>>(
final selectedUsers = context.read<FriendsSelectionCubit>().selectedUsers; builder: (context, snapshot) {
final selectedUsers =
context.read<FriendsSelectionCubit>().selectedUsers;
return Scaffold( return Scaffold(
floatingActionButton: isGroup && selectedUsers.isNotEmpty floatingActionButton: isGroup && selectedUsers.isNotEmpty
? FloatingActionButton( ? FloatingActionButton(
child: Icon(Icons.arrow_right_alt_rounded), child: Icon(Icons.arrow_right_alt_rounded),
onPressed: () { onPressed: () {
pushAndReplaceToPage(context, GroupSelectionView(selectedUsers)); pushAndReplaceToPage(
context, GroupSelectionView(selectedUsers));
}) })
: null, : null,
backgroundColor: Theme.of(context).canvasColor, backgroundColor: Theme.of(context).canvasColor,
@@ -91,12 +98,14 @@ class FriendsSelectionView extends StatelessWidget {
backgroundColor: accentColor, backgroundColor: accentColor,
child: Icon(Icons.group_outlined), child: Icon(Icons.group_outlined),
), ),
title: Text('Create group', style: TextStyle(fontWeight: FontWeight.w700)), title: Text('Create group',
style: TextStyle(fontWeight: FontWeight.w700)),
subtitle: Text('Talk with 2 or more contacts'), subtitle: Text('Talk with 2 or more contacts'),
) )
else if (isGroup && selectedUsers.isEmpty) else if (isGroup && selectedUsers.isEmpty)
Padding( Padding(
padding: const EdgeInsets.only(top: 15.0, left: 20.0, bottom: 20), padding: const EdgeInsets.only(
top: 15.0, left: 20.0, bottom: 20),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -123,7 +132,8 @@ class FriendsSelectionView extends StatelessWidget {
itemBuilder: (context, index) { itemBuilder: (context, index) {
final chatUserState = selectedUsers[index]; final chatUserState = selectedUsers[index];
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 13.0), padding: const EdgeInsets.symmetric(
horizontal: 13.0),
child: Stack( child: Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
@@ -132,7 +142,8 @@ class FriendsSelectionView extends StatelessWidget {
children: [ children: [
CircleAvatar( CircleAvatar(
radius: 30, radius: 30,
backgroundImage: NetworkImage(chatUserState.chatUser.image), backgroundImage: NetworkImage(
chatUserState.chatUser.image),
), ),
Text(chatUserState.chatUser.name), Text(chatUserState.chatUser.name),
], ],
@@ -141,11 +152,14 @@ class FriendsSelectionView extends StatelessWidget {
bottom: 40, bottom: 40,
right: -4, right: -4,
child: InkWell( child: InkWell(
onTap: () => context.read<FriendsSelectionCubit>().selectUser(chatUserState), onTap: () => context
.read<FriendsSelectionCubit>()
.selectUser(chatUserState),
child: CircleAvatar( child: CircleAvatar(
radius: 9, radius: 9,
backgroundColor: accentColor, backgroundColor: accentColor,
child: Icon(Icons.close_rounded, size: 12), child: Icon(Icons.close_rounded,
size: 12),
), ),
), ),
), ),
@@ -163,7 +177,8 @@ class FriendsSelectionView extends StatelessWidget {
_createFriendChannel(context, chatUserState); _createFriendChannel(context, chatUserState);
}, },
leading: CircleAvatar( leading: CircleAvatar(
backgroundImage: NetworkImage(chatUserState.chatUser.image), backgroundImage:
NetworkImage(chatUserState.chatUser.image),
), ),
title: Text(chatUserState.chatUser.name), title: Text(chatUserState.chatUser.name),
trailing: isGroup trailing: isGroup
@@ -171,7 +186,9 @@ class FriendsSelectionView extends StatelessWidget {
value: chatUserState.selected, value: chatUserState.selected,
onChanged: (val) { onChanged: (val) {
print('select user for group'); print('select user for group');
context.read<FriendsSelectionCubit>().selectUser(chatUserState); context
.read<FriendsSelectionCubit>()
.selectUser(chatUserState);
}, },
) )
: null, : null,
@@ -21,7 +21,8 @@ class GroupSelectionView extends StatelessWidget {
context.read(), context.read(),
context.read(), context.read(),
), ),
child: BlocConsumer<GroupSelectionCubit, GroupSelectionState>(listener: (context, snapshot) { child: BlocConsumer<GroupSelectionCubit, GroupSelectionState>(
listener: (context, snapshot) {
if (snapshot.channel != null) { if (snapshot.channel != null) {
pushAndReplaceToPage( pushAndReplaceToPage(
context, context,
@@ -76,9 +77,12 @@ class GroupSelectionView extends StatelessWidget {
vertical: 20, vertical: 20,
), ),
child: TextField( child: TextField(
controller: context.read<GroupSelectionCubit>().nameTextController, controller:
context.read<GroupSelectionCubit>().nameTextController,
decoration: InputDecoration( decoration: InputDecoration(
fillColor: Theme.of(context).bottomNavigationBarTheme.backgroundColor, fillColor: Theme.of(context)
.bottomNavigationBarTheme
.backgroundColor,
hintText: 'Name of the group', hintText: 'Name of the group',
hintStyle: TextStyle( hintStyle: TextStyle(
fontSize: 13, fontSize: 13,
@@ -99,7 +103,8 @@ class GroupSelectionView extends StatelessWidget {
children: [ children: [
CircleAvatar( CircleAvatar(
radius: 30, radius: 30,
backgroundImage: NetworkImage(chatUserState.chatUser.image), backgroundImage:
NetworkImage(chatUserState.chatUser.image),
), ),
Text(chatUserState.chatUser.name), Text(chatUserState.chatUser.name),
], ],
+7 -3
View File
@@ -61,7 +61,9 @@ class HomeNavigationBar extends StatelessWidget {
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(25), borderRadius: BorderRadius.circular(25),
color: Theme.of(context).bottomNavigationBarTheme.backgroundColor, color: Theme.of(context)
.bottomNavigationBarTheme
.backgroundColor,
), ),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: MainAxisAlignment.spaceAround,
@@ -122,8 +124,10 @@ class _HomeNavItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final selectedColor = Theme.of(context).bottomNavigationBarTheme.selectedItemColor; final selectedColor =
final unselectedColor = Theme.of(context).bottomNavigationBarTheme.unselectedItemColor; Theme.of(context).bottomNavigationBarTheme.selectedItemColor;
final unselectedColor =
Theme.of(context).bottomNavigationBarTheme.unselectedItemColor;
final color = selected ? selectedColor : unselectedColor; final color = selected ? selectedColor : unselectedColor;
return GestureDetector( return GestureDetector(
onTap: onTap, onTap: onTap,
@@ -16,7 +16,8 @@ class SettingsView extends StatelessWidget {
return MultiBlocProvider( return MultiBlocProvider(
providers: [ providers: [
BlocProvider( BlocProvider(
create: (_) => SettingsSwitchCubit(context.read<AppThemeCubit>().isDark), create: (_) =>
SettingsSwitchCubit(context.read<AppThemeCubit>().isDark),
), ),
BlocProvider( BlocProvider(
create: (_) => SettingsLogoutCubit(context.read()), create: (_) => SettingsLogoutCubit(context.read()),
@@ -75,11 +76,14 @@ class SettingsView extends StatelessWidget {
), ),
), ),
Spacer(), Spacer(),
BlocBuilder<SettingsSwitchCubit, bool>(builder: (context, snapshot) { BlocBuilder<SettingsSwitchCubit, bool>(
builder: (context, snapshot) {
return Switch( return Switch(
value: snapshot, value: snapshot,
onChanged: (val) { onChanged: (val) {
context.read<SettingsSwitchCubit>().onChangeDarkMode(val); context
.read<SettingsSwitchCubit>()
.onChangeDarkMode(val);
context.read<AppThemeCubit>().updateTheme(val); context.read<AppThemeCubit>().updateTheme(val);
}, },
); );
@@ -13,7 +13,8 @@ class ProfileVerifyView extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (context) => ProfileVerifyCubit(context.read(), context.read()), create: (context) => ProfileVerifyCubit(context.read(), context.read()),
child: BlocConsumer<ProfileVerifyCubit, ProfileState>(listener: (context, snapshot) { child: BlocConsumer<ProfileVerifyCubit, ProfileState>(
listener: (context, snapshot) {
if (snapshot.success) { if (snapshot.success) {
pushAndReplaceToPage(context, HomeView()); pushAndReplaceToPage(context, HomeView());
} }
@@ -59,9 +60,12 @@ class ProfileVerifyView extends StatelessWidget {
vertical: 20, vertical: 20,
), ),
child: TextField( child: TextField(
controller: context.read<ProfileVerifyCubit>().nameController, controller:
context.read<ProfileVerifyCubit>().nameController,
decoration: InputDecoration( decoration: InputDecoration(
fillColor: Theme.of(context).bottomNavigationBarTheme.backgroundColor, fillColor: Theme.of(context)
.bottomNavigationBarTheme
.backgroundColor,
hintText: 'Or just how people now you', hintText: 'Or just how people now you',
hintStyle: TextStyle( hintStyle: TextStyle(
fontSize: 13, fontSize: 13,
@@ -11,7 +11,8 @@ class SignInView extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (context) => SignInCubit(context.read()), create: (context) => SignInCubit(context.read()),
child: BlocConsumer<SignInCubit, SignInState>(listener: (context, snapshot) { child:
BlocConsumer<SignInCubit, SignInState>(listener: (context, snapshot) {
if (snapshot == SignInState.none) { if (snapshot == SignInState.none) {
pushAndReplaceToPage(context, ProfileVerifyView()); pushAndReplaceToPage(context, ProfileVerifyView());
} else { } else {
@@ -59,7 +60,9 @@ class SignInView extends StatelessWidget {
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
color: Theme.of(context).bottomNavigationBarTheme.backgroundColor, color: Theme.of(context)
.bottomNavigationBarTheme
.backgroundColor,
child: InkWell( child: InkWell(
onTap: () { onTap: () {
context.read<SignInCubit>().signIn(); context.read<SignInCubit>().signIn();
-1
View File
@@ -49,7 +49,6 @@ class CupertinoCircleAvatar extends StatelessWidget {
} }
} }
class Divider extends StatelessWidget { class Divider extends StatelessWidget {
const Divider({ const Divider({
Key key, Key key,
@@ -17,13 +17,13 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
final TextEditingController _apiKeyController = TextEditingController(); final TextEditingController _apiKeyController = TextEditingController();
String _apiKeyError; String? _apiKeyError;
final TextEditingController _userIdController = TextEditingController(); final TextEditingController _userIdController = TextEditingController();
String _userIdError; String? _userIdError;
final TextEditingController _userTokenController = TextEditingController(); final TextEditingController _userTokenController = TextEditingController();
String _userTokenError; String? _userTokenError;
final TextEditingController _usernameController = TextEditingController(); final TextEditingController _usernameController = TextEditingController();
@@ -73,7 +73,7 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
} }
}, },
validator: (value) { validator: (value) {
if (value.isEmpty) { if (value!.isEmpty) {
setState(() { setState(() {
_apiKeyError = _apiKeyError =
'Please enter the Chat API Key'.toUpperCase(); 'Please enter the Chat API Key'.toUpperCase();
@@ -119,7 +119,7 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
} }
}, },
validator: (value) { validator: (value) {
if (value.isEmpty) { if (value!.isEmpty) {
setState(() { setState(() {
_userIdError = _userIdError =
'Please enter the User ID'.toUpperCase(); 'Please enter the User ID'.toUpperCase();
@@ -165,7 +165,7 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
}, },
controller: _userTokenController, controller: _userTokenController,
validator: (value) { validator: (value) {
if (value.isEmpty) { if (value!.isEmpty) {
setState(() { setState(() {
_userTokenError = _userTokenError =
'Please enter the user token'.toUpperCase(); 'Please enter the user token'.toUpperCase();
@@ -221,14 +221,22 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
), ),
), ),
Spacer(), Spacer(),
RaisedButton( ElevatedButton(
color: Theme.of(context).brightness == Brightness.light style: ButtonStyle(
? StreamChatTheme.of(context).colorTheme.accentBlue backgroundColor: MaterialStateProperty.all<Color>(
: Colors.white, Theme.of(context).brightness == Brightness.light
elevation: 0, ? StreamChatTheme.of(context)
padding: const EdgeInsets.symmetric(vertical: 16), .colorTheme
shape: RoundedRectangleBorder( .accentBlue
borderRadius: BorderRadius.circular(26), : Colors.white),
elevation: MaterialStateProperty.all<double>(0),
padding: MaterialStateProperty.all<EdgeInsets>(
const EdgeInsets.symmetric(vertical: 16)),
shape: MaterialStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(26),
),
),
), ),
child: Text( child: Text(
'Login', 'Login',
@@ -243,7 +251,7 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
if (loading) { if (loading) {
return; return;
} }
if (_formKey.currentState.validate()) { if (_formKey.currentState!.validate()) {
final apiKey = _apiKeyController.text; final apiKey = _apiKeyController.text;
final userId = _userIdController.text; final userId = _userIdController.text;
final userToken = _userTokenController.text; final userToken = _userTokenController.text;
@@ -6,16 +6,16 @@ class ChannelFileDisplayScreen extends StatefulWidget {
/// Sorting is based on field and direction, multiple sorting options can be provided. /// Sorting is based on field and direction, multiple sorting options can be provided.
/// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count. /// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count.
/// Direction can be ascending or descending. /// Direction can be ascending or descending.
final List<SortOption> sortOptions; final List<SortOption>? sortOptions;
/// Pagination parameters /// Pagination parameters
/// limit: the number of users to return (max is 30) /// limit: the number of users to return (max is 30)
/// offset: the offset (max is 1000) /// offset: the offset (max is 1000)
/// message_limit: how many messages should be included to each channel /// message_limit: how many messages should be included to each channel
final PaginationParams paginationParams; final PaginationParams? paginationParams;
/// The builder used when the file list is empty. /// The builder used when the file list is empty.
final WidgetBuilder emptyBuilder; final WidgetBuilder? emptyBuilder;
const ChannelFileDisplayScreen({ const ChannelFileDisplayScreen({
this.sortOptions, this.sortOptions,
@@ -34,16 +34,14 @@ class _ChannelFileDisplayScreenState extends State<ChannelFileDisplayScreen> {
super.initState(); super.initState();
final messageSearchBloc = MessageSearchBloc.of(context); final messageSearchBloc = MessageSearchBloc.of(context);
messageSearchBloc.search( messageSearchBloc.search(
filter: { filter: Filter.in_(
'cid': { 'cid',
r'$in': [StreamChannel.of(context).channel.cid] [StreamChannel.of(context).channel.cid!],
} ),
}, messageFilter: Filter.in_(
messageFilter: { 'attachments.type',
'attachments.type': { ['file'],
r'$in': ['file'], ),
},
},
sort: widget.sortOptions, sort: widget.sortOptions,
pagination: widget.paginationParams, pagination: widget.paginationParams,
); );
@@ -95,9 +93,9 @@ class _ChannelFileDisplayScreenState extends State<ChannelFileDisplayScreen> {
); );
} }
if (snapshot.data.isEmpty) { if (snapshot.data!.isEmpty) {
if (widget.emptyBuilder != null) { if (widget.emptyBuilder != null) {
return widget.emptyBuilder(context); return widget.emptyBuilder!(context);
} }
return Center( return Center(
child: Column( child: Column(
@@ -134,7 +132,7 @@ class _ChannelFileDisplayScreenState extends State<ChannelFileDisplayScreen> {
final media = <Attachment, Message>{}; final media = <Attachment, Message>{};
for (var item in snapshot.data) { for (var item in snapshot.data!) {
item.message.attachments.where((e) => e.type == 'file').forEach((e) { item.message.attachments.where((e) => e.type == 'file').forEach((e) {
media[e] = item.message; media[e] = item.message;
}); });
@@ -142,18 +140,16 @@ class _ChannelFileDisplayScreenState extends State<ChannelFileDisplayScreen> {
return LazyLoadScrollView( return LazyLoadScrollView(
onEndOfPage: () => messageSearchBloc.search( onEndOfPage: () => messageSearchBloc.search(
filter: { filter: Filter.in_(
'cid': { 'cid',
r'$in': [StreamChannel.of(context).channel.cid] [StreamChannel.of(context).channel.cid!],
} ),
}, messageFilter: Filter.in_(
messageFilter: { 'attachments.type',
'attachments.type': { ['file'],
r'$in': ['file'] ),
},
},
sort: widget.sortOptions, sort: widget.sortOptions,
pagination: widget.paginationParams.copyWith( pagination: widget.paginationParams!.copyWith(
offset: messageSearchBloc.messageResponses?.length ?? 0, offset: messageSearchBloc.messageResponses?.length ?? 0,
), ),
), ),
@@ -7,20 +7,23 @@ class ChannelMediaDisplayScreen extends StatefulWidget {
/// Sorting is based on field and direction, multiple sorting options can be provided. /// Sorting is based on field and direction, multiple sorting options can be provided.
/// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count. /// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count.
/// Direction can be ascending or descending. /// Direction can be ascending or descending.
final List<SortOption> sortOptions; final List<SortOption>? sortOptions;
/// Pagination parameters /// Pagination parameters
/// limit: the number of users to return (max is 30) /// limit: the number of users to return (max is 30)
/// offset: the offset (max is 1000) /// offset: the offset (max is 1000)
/// message_limit: how many messages should be included to each channel /// message_limit: how many messages should be included to each channel
final PaginationParams paginationParams; final PaginationParams? paginationParams;
/// The builder used when the file list is empty. /// The builder used when the file list is empty.
final WidgetBuilder emptyBuilder; final WidgetBuilder? emptyBuilder;
final ShowMessageCallback onShowMessage; final ShowMessageCallback? onShowMessage;
final MessageTheme messageTheme;
const ChannelMediaDisplayScreen({ const ChannelMediaDisplayScreen({
required this.messageTheme,
this.sortOptions, this.sortOptions,
this.paginationParams, this.paginationParams,
this.emptyBuilder, this.emptyBuilder,
@@ -33,23 +36,21 @@ class ChannelMediaDisplayScreen extends StatefulWidget {
} }
class _ChannelMediaDisplayScreenState extends State<ChannelMediaDisplayScreen> { class _ChannelMediaDisplayScreenState extends State<ChannelMediaDisplayScreen> {
Map<String, VideoPlayerController> controllerCache = {}; Map<String?, VideoPlayerController?> controllerCache = {};
@override @override
void initState() { void initState() {
super.initState(); super.initState();
final messageSearchBloc = MessageSearchBloc.of(context); final messageSearchBloc = MessageSearchBloc.of(context);
messageSearchBloc.search( messageSearchBloc.search(
filter: { filter: Filter.in_(
'cid': { 'cid',
r'$in': [StreamChannel.of(context).channel.cid], [StreamChannel.of(context).channel.cid!],
} ),
}, messageFilter: Filter.in_(
messageFilter: { 'attachments.type',
'attachments.type': { ['image', 'video'],
r'$in': ['image', 'video'] ),
},
},
sort: widget.sortOptions, sort: widget.sortOptions,
pagination: widget.paginationParams, pagination: widget.paginationParams,
); );
@@ -102,9 +103,9 @@ class _ChannelMediaDisplayScreenState extends State<ChannelMediaDisplayScreen> {
); );
} }
if (snapshot.data.isEmpty) { if (snapshot.data!.isEmpty) {
if (widget.emptyBuilder != null) { if (widget.emptyBuilder != null) {
return widget.emptyBuilder(context); return widget.emptyBuilder!(context);
} }
return Center( return Center(
child: Column( child: Column(
@@ -141,18 +142,18 @@ class _ChannelMediaDisplayScreenState extends State<ChannelMediaDisplayScreen> {
final media = <_AssetPackage>[]; final media = <_AssetPackage>[];
for (var item in snapshot.data) { for (var item in snapshot.data!) {
item.message.attachments item.message.attachments
.where((e) => .where((e) =>
(e.type == 'image' || e.type == 'video') && (e.type == 'image' || e.type == 'video') &&
e.ogScrapeUrl == null) e.ogScrapeUrl == null)
.forEach((e) { .forEach((e) {
VideoPlayerController controller; VideoPlayerController? controller;
if (e.type == 'video') { if (e.type == 'video') {
var cachedController = controllerCache[e.assetUrl]; var cachedController = controllerCache[e.assetUrl];
if (cachedController == null) { if (cachedController == null) {
controller = VideoPlayerController.network(e.assetUrl); controller = VideoPlayerController.network(e.assetUrl!);
controller.initialize(); controller.initialize();
controllerCache[e.assetUrl] = controller; controllerCache[e.assetUrl] = controller;
} else { } else {
@@ -165,18 +166,16 @@ class _ChannelMediaDisplayScreenState extends State<ChannelMediaDisplayScreen> {
return LazyLoadScrollView( return LazyLoadScrollView(
onEndOfPage: () => messageSearchBloc.search( onEndOfPage: () => messageSearchBloc.search(
filter: { filter: Filter.in_(
'cid': { 'cid',
r'$in': [StreamChannel.of(context).channel.cid] [StreamChannel.of(context).channel.cid!],
} ),
}, messageFilter: Filter.in_(
messageFilter: { 'attachments.type',
'attachments.type': { ['image', 'video'],
r'$in': ['image', 'video'] ),
},
},
sort: widget.sortOptions, sort: widget.sortOptions,
pagination: widget.paginationParams.copyWith( pagination: widget.paginationParams!.copyWith(
offset: messageSearchBloc.messageResponses?.length ?? 0, offset: messageSearchBloc.messageResponses?.length ?? 0,
), ),
), ),
@@ -199,8 +198,7 @@ class _ChannelMediaDisplayScreenState extends State<ChannelMediaDisplayScreen> {
media.map((e) => e.attachment).toList(), media.map((e) => e.attachment).toList(),
startIndex: position, startIndex: position,
message: media[position].message, message: media[position].message,
sentAt: media[position].message.createdAt, userName: media[position].message.user!.name,
userName: media[position].message.user.name,
onShowMessage: widget.onShowMessage, onShowMessage: widget.onShowMessage,
), ),
), ),
@@ -217,9 +215,10 @@ class _ChannelMediaDisplayScreenState extends State<ChannelMediaDisplayScreen> {
MediaQuery.of(context).size.width * 0.8, MediaQuery.of(context).size.width * 0.8,
MediaQuery.of(context).size.height * 0.3, MediaQuery.of(context).size.height * 0.3,
), ),
messageTheme: widget.messageTheme,
), ),
) )
: VideoPlayer(media[position].videoPlayer), : VideoPlayer(media[position].videoPlayer!),
), ),
); );
}, },
@@ -235,7 +234,7 @@ class _ChannelMediaDisplayScreenState extends State<ChannelMediaDisplayScreen> {
void dispose() { void dispose() {
super.dispose(); super.dispose();
for (var c in controllerCache.values) { for (var c in controllerCache.values) {
c.dispose(); c!.dispose();
} }
} }
} }
@@ -243,7 +242,7 @@ class _ChannelMediaDisplayScreenState extends State<ChannelMediaDisplayScreen> {
class _AssetPackage { class _AssetPackage {
Attachment attachment; Attachment attachment;
Message message; Message message;
VideoPlayerController videoPlayer; VideoPlayerController? videoPlayer;
_AssetPackage(this.attachment, this.message, this.videoPlayer); _AssetPackage(this.attachment, this.message, this.videoPlayer);
} }
@@ -1,3 +1,4 @@
import 'package:collection/collection.dart' show IterableExtension;
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart'; import 'package:jiffy/jiffy.dart';
@@ -11,16 +12,22 @@ import 'routes/routes.dart';
/// Detail screen for a 1:1 chat correspondence /// Detail screen for a 1:1 chat correspondence
class ChatInfoScreen extends StatefulWidget { class ChatInfoScreen extends StatefulWidget {
/// User in consideration /// User in consideration
final User user; final User? user;
const ChatInfoScreen({Key key, this.user}) : super(key: key); final MessageTheme messageTheme;
const ChatInfoScreen({
Key? key,
required this.messageTheme,
this.user,
}) : super(key: key);
@override @override
_ChatInfoScreenState createState() => _ChatInfoScreenState(); _ChatInfoScreenState createState() => _ChatInfoScreenState();
} }
class _ChatInfoScreenState extends State<ChatInfoScreen> { class _ChatInfoScreenState extends State<ChatInfoScreen> {
ValueNotifier<bool> mutedBool = ValueNotifier(false); ValueNotifier<bool?> mutedBool = ValueNotifier(false);
@override @override
void initState() { void initState() {
@@ -48,9 +55,9 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
if ([ if ([
'admin', 'admin',
'owner', 'owner',
].contains(channel.state.members ].contains(channel.state!.members
.firstWhere((m) => m.userId == channel.client.state.user.id, .firstWhereOrNull(
orElse: () => null) (m) => m.userId == channel.client.state.user!.id)
?.role)) ?.role))
_buildDeleteListTile(), _buildDeleteListTile(),
], ],
@@ -70,7 +77,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
Padding( Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: UserAvatar( child: UserAvatar(
user: widget.user, user: widget.user!,
constraints: BoxConstraints( constraints: BoxConstraints(
maxWidth: 72.0, maxWidth: 72.0,
maxHeight: 72.0, maxHeight: 72.0,
@@ -80,19 +87,19 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
), ),
), ),
Text( Text(
widget.user.name, widget.user!.name,
style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold), style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold),
), ),
SizedBox(height: 7.0), SizedBox(height: 7.0),
_buildConnectedTitleState(), _buildConnectedTitleState(),
SizedBox(height: 15.0), SizedBox(height: 15.0),
OptionListTile( OptionListTile(
title: '@${widget.user.id}', title: '@${widget.user!.id}',
tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow, tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
trailing: Padding( trailing: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0), padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text( child: Text(
widget.user.name, widget.user!.name,
style: TextStyle( style: TextStyle(
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
@@ -155,15 +162,15 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
), ),
trailing: snapshot.data == null trailing: snapshot.data == null
? CircularProgressIndicator() ? CircularProgressIndicator()
: ValueListenableBuilder<bool>( : ValueListenableBuilder<bool?>(
valueListenable: mutedBool, valueListenable: mutedBool,
builder: (context, value, _) { builder: (context, value, _) {
return CupertinoSwitch( return CupertinoSwitch(
value: value, value: value!,
onChanged: (val) { onChanged: (val) {
mutedBool.value = val; mutedBool.value = val;
if (snapshot.data) { if (snapshot.data!) {
channel.channel.unmute(); channel.channel.unmute();
} else { } else {
channel.channel.mute(); channel.channel.mute();
@@ -217,6 +224,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
channel: channel, channel: channel,
child: MessageSearchBloc( child: MessageSearchBloc(
child: ChannelMediaDisplayScreen( child: ChannelMediaDisplayScreen(
messageTheme: widget.messageTheme,
sortOptions: [ sortOptions: [
SortOption( SortOption(
'created_at', 'created_at',
@@ -387,7 +395,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
if (widget.user.online) if (widget.user!.online)
Material( Material(
type: MaterialType.circle, type: MaterialType.circle,
child: Container( child: Container(
@@ -404,7 +412,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
color: StreamChatTheme.of(context).colorTheme.white, color: StreamChatTheme.of(context).colorTheme.white,
), ),
alternativeWidget, alternativeWidget,
if (widget.user.online) if (widget.user!.online)
SizedBox( SizedBox(
width: 24.0, width: 24.0,
), ),
@@ -414,8 +422,8 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
} }
class _SharedGroupsScreen extends StatefulWidget { class _SharedGroupsScreen extends StatefulWidget {
final User mainUser; final User? mainUser;
final User otherUser; final User? otherUser;
_SharedGroupsScreen(this.mainUser, this.otherUser); _SharedGroupsScreen(this.mainUser, this.otherUser);
@@ -445,20 +453,10 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> {
), ),
body: StreamBuilder<List<Channel>>( body: StreamBuilder<List<Channel>>(
stream: chat.client.queryChannels( stream: chat.client.queryChannels(
filter: { filter: Filter.and([
r'$and': [ Filter.in_('members', [widget.otherUser!.id]),
{ Filter.in_('members', [widget.mainUser!.id]),
'members': { ]),
r'$in': [widget.otherUser.id],
},
},
{
'members': {
r'$in': [widget.mainUser.id],
},
}
],
},
), ),
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData) { if (!snapshot.hasData) {
@@ -467,7 +465,7 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> {
); );
} }
if (snapshot.data.isEmpty) { if (snapshot.data!.isEmpty) {
return Center( return Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@@ -501,11 +499,11 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> {
); );
} }
final channels = snapshot.data final channels = snapshot.data!
.where((c) => .where((c) =>
c.state.members.any((m) => c.state!.members.any((m) =>
m.userId != widget.mainUser.id && m.userId != widget.mainUser!.id &&
m.userId != widget.otherUser.id) || m.userId != widget.otherUser!.id) ||
!c.isDistinct) !c.isDistinct)
.toList(); .toList();
@@ -525,24 +523,24 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> {
Widget _buildListTile(Channel channel) { Widget _buildListTile(Channel channel) {
var extraData = channel.extraData; var extraData = channel.extraData;
var members = channel.state.members; var members = channel.state!.members;
var textStyle = TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold); var textStyle = TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold);
return Container( return Container(
height: 64.0, height: 64.0,
child: LayoutBuilder(builder: (context, constraints) { child: LayoutBuilder(builder: (context, constraints) {
String title; String? title;
if (extraData['name'] == null) { if (extraData['name'] == null) {
final otherMembers = members.where( final otherMembers = members.where(
(member) => member.userId != StreamChat.of(context).user.id); (member) => member.userId != StreamChat.of(context).user!.id);
if (otherMembers.isNotEmpty) { if (otherMembers.isNotEmpty) {
final maxWidth = constraints.maxWidth; final maxWidth = constraints.maxWidth;
final maxChars = maxWidth / textStyle.fontSize; final maxChars = maxWidth / textStyle.fontSize!;
var currentChars = 0; var currentChars = 0;
final currentMembers = <Member>[]; final currentMembers = <Member>[];
otherMembers.forEach((element) { otherMembers.forEach((element) {
final newLength = currentChars + element.user.name.length; final newLength = currentChars + element.user!.name.length;
if (newLength < maxChars) { if (newLength < maxChars) {
currentChars = newLength; currentChars = newLength;
currentMembers.add(element); currentMembers.add(element);
@@ -552,7 +550,7 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> {
final exceedingMembers = final exceedingMembers =
otherMembers.length - currentMembers.length; otherMembers.length - currentMembers.length;
title = title =
'${currentMembers.map((e) => e.user.name).join(', ')} ${exceedingMembers > 0 ? '+ $exceedingMembers' : ''}'; '${currentMembers.map((e) => e.user!.name).join(', ')} ${exceedingMembers > 0 ? '+ $exceedingMembers' : ''}';
} else { } else {
title = 'No title'; title = 'No title';
} }
@@ -575,7 +573,7 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> {
), ),
Expanded( Expanded(
child: Text( child: Text(
title, title!,
style: textStyle, style: textStyle,
)), )),
Padding( Padding(
@@ -6,18 +6,18 @@ typedef OnChipAdded<T> = void Function(T chip);
typedef OnChipRemoved<T> = void Function(T chip); typedef OnChipRemoved<T> = void Function(T chip);
class ChipsInputTextField<T> extends StatefulWidget { class ChipsInputTextField<T> extends StatefulWidget {
final TextEditingController controller; final TextEditingController? controller;
final FocusNode focusNode; final FocusNode? focusNode;
final ValueChanged<String> onInputChanged; final ValueChanged<String>? onInputChanged;
final ChipBuilder<T> chipBuilder; final ChipBuilder<T> chipBuilder;
final OnChipAdded<T> onChipAdded; final OnChipAdded<T>? onChipAdded;
final OnChipRemoved<T> onChipRemoved; final OnChipRemoved<T>? onChipRemoved;
final String hint; final String hint;
const ChipsInputTextField({ const ChipsInputTextField({
Key key, Key? key,
@required this.chipBuilder, required this.chipBuilder,
@required this.controller, required this.controller,
this.onInputChanged, this.onInputChanged,
this.focusNode, this.focusNode,
this.onChipAdded, this.onChipAdded,
@@ -35,7 +35,7 @@ class ChipInputTextFieldState<T> extends State<ChipsInputTextField<T>> {
void addItem(T item) { void addItem(T item) {
setState(() => _chips.add(item)); setState(() => _chips.add(item));
if (widget.onChipAdded != null) widget.onChipAdded(item); if (widget.onChipAdded != null) widget.onChipAdded!(item);
} }
void removeItem(T item) { void removeItem(T item) {
@@ -43,7 +43,7 @@ class ChipInputTextFieldState<T> extends State<ChipsInputTextField<T>> {
_chips.remove(item); _chips.remove(item);
if (_chips.isEmpty) resumeItemAddition(); if (_chips.isEmpty) resumeItemAddition();
}); });
if (widget.onChipRemoved != null) widget.onChipRemoved(item); if (widget.onChipRemoved != null) widget.onChipRemoved!(item);
} }
void pauseItemAddition() { void pauseItemAddition() {
@@ -6,11 +6,11 @@ import 'main.dart';
import 'routes/routes.dart'; import 'routes/routes.dart';
class GroupChatDetailsScreen extends StatefulWidget { class GroupChatDetailsScreen extends StatefulWidget {
final List<User> selectedUsers; final List<User>? selectedUsers;
const GroupChatDetailsScreen({ const GroupChatDetailsScreen({
Key key, Key? key,
@required this.selectedUsers, required this.selectedUsers,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -20,14 +20,14 @@ class GroupChatDetailsScreen extends StatefulWidget {
class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> { class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
final _selectedUsers = <User>[]; final _selectedUsers = <User>[];
TextEditingController _groupNameController; TextEditingController? _groupNameController;
bool _isGroupNameEmpty = true; bool _isGroupNameEmpty = true;
int get _totalUsers => _selectedUsers.length; int get _totalUsers => _selectedUsers.length;
void _groupNameListener() { void _groupNameListener() {
final name = _groupNameController.text; final name = _groupNameController!.text;
if (mounted) { if (mounted) {
setState(() { setState(() {
_isGroupNameEmpty = name.isEmpty; _isGroupNameEmpty = name.isEmpty;
@@ -38,7 +38,7 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_selectedUsers.addAll(widget.selectedUsers); _selectedUsers.addAll(widget.selectedUsers!);
_groupNameController = TextEditingController() _groupNameController = TextEditingController()
..addListener(_groupNameListener); ..addListener(_groupNameListener);
} }
@@ -124,13 +124,13 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
? null ? null
: () async { : () async {
try { try {
final groupName = _groupNameController.text; final groupName = _groupNameController!.text;
final client = StreamChat.of(context).client; final client = StreamChat.of(context).client;
final channel = client.channel('messaging', final channel = client.channel('messaging',
id: Uuid().v4(), id: Uuid().v4(),
extraData: { extraData: {
'members': [ 'members': [
client.state.user.id, client.state.user!.id,
..._selectedUsers.map((e) => e.id), ..._selectedUsers.map((e) => e.id),
], ],
'name': groupName, 'name': groupName,
@@ -306,7 +306,7 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
FlatButton( TextButton(
child: Text( child: Text(
'OK', 'OK',
style: StreamChatTheme.of(context) style: StreamChatTheme.of(context)
@@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:collection/collection.dart' show IterableExtension;
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart'; import 'package:jiffy/jiffy.dart';
@@ -14,34 +15,41 @@ import 'main.dart';
import 'routes/routes.dart'; import 'routes/routes.dart';
class GroupInfoScreen extends StatefulWidget { class GroupInfoScreen extends StatefulWidget {
final MessageTheme messageTheme;
const GroupInfoScreen({
Key? key,
required this.messageTheme,
}) : super(key: key);
@override @override
_GroupInfoScreenState createState() => _GroupInfoScreenState(); _GroupInfoScreenState createState() => _GroupInfoScreenState();
} }
class _GroupInfoScreenState extends State<GroupInfoScreen> { class _GroupInfoScreenState extends State<GroupInfoScreen> {
TextEditingController _nameController; TextEditingController? _nameController;
TextEditingController _searchController; TextEditingController? _searchController;
String _userNameQuery = ''; String _userNameQuery = '';
Timer _debounce; Timer? _debounce;
Function modalSetStateCallback; Function? modalSetStateCallback;
final FocusNode _focusNode = FocusNode(); final FocusNode _focusNode = FocusNode();
bool listExpanded = false; bool listExpanded = false;
ValueNotifier<bool> mutedBool = ValueNotifier(false); ValueNotifier<bool?> mutedBool = ValueNotifier(false);
void _userNameListener() { void _userNameListener() {
if (_searchController.text == _userNameQuery) { if (_searchController!.text == _userNameQuery) {
return; return;
} }
if (_debounce?.isActive ?? false) _debounce.cancel(); if (_debounce?.isActive ?? false) _debounce!.cancel();
_debounce = Timer(const Duration(milliseconds: 350), () { _debounce = Timer(const Duration(milliseconds: 350), () {
if (mounted && modalSetStateCallback != null) { if (mounted && modalSetStateCallback != null) {
modalSetStateCallback(() { modalSetStateCallback!(() {
_userNameQuery = _searchController.text; _userNameQuery = _searchController!.text;
}); });
} }
}); });
@@ -55,7 +63,7 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
TextEditingValue(text: channel.channel.extraData['name'] ?? '')); TextEditingValue(text: channel.channel.extraData['name'] ?? ''));
_searchController = TextEditingController()..addListener(_userNameListener); _searchController = TextEditingController()..addListener(_userNameListener);
_nameController.addListener(() { _nameController!.addListener(() {
setState(() {}); setState(() {});
}); });
mutedBool = ValueNotifier(StreamChannel.of(context).channel.isMuted); mutedBool = ValueNotifier(StreamChannel.of(context).channel.isMuted);
@@ -66,7 +74,7 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
var channel = StreamChannel.of(context); var channel = StreamChannel.of(context);
return StreamBuilder<List<Member>>( return StreamBuilder<List<Member>>(
stream: channel.channel.state.membersStream, stream: channel.channel.state!.membersStream,
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData) { if (!snapshot.hasData) {
return Container( return Container(
@@ -75,9 +83,8 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
); );
} }
var userMember = snapshot.data.firstWhere( var userMember = snapshot.data!.firstWhereOrNull(
(e) => e.user.id == StreamChat.of(context).user.id, (e) => e.user!.id == StreamChat.of(context).user!.id,
orElse: () => null,
); );
var isOwner = userMember?.role == 'owner'; var isOwner = userMember?.role == 'owner';
@@ -111,9 +118,9 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
_getChannelName( _getChannelName(
2 * MediaQuery.of(context).size.width / 3, 2 * MediaQuery.of(context).size.width / 3,
members: snapshot.data, members: snapshot.data,
extraData: state.data.channel.extraData, extraData: state.data!.channel!.extraData,
maxFontSize: 16.0, maxFontSize: 16.0,
), )!,
style: TextStyle( style: TextStyle(
color: StreamChatTheme.of(context).colorTheme.black, color: StreamChatTheme.of(context).colorTheme.black,
fontSize: 16, fontSize: 16,
@@ -126,7 +133,7 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
height: 3.0, height: 3.0,
), ),
Text( Text(
'${channel.channel.memberCount} Members, ${snapshot?.data?.where((e) => e.user.online)?.length ?? 0} Online', '${channel.channel.memberCount} Members, ${snapshot.data?.where((e) => e.user!.online).length ?? 0} Online',
style: TextStyle( style: TextStyle(
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
@@ -158,7 +165,7 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
), ),
body: ListView( body: ListView(
children: [ children: [
_buildMembers(snapshot.data), _buildMembers(snapshot.data!),
Container( Container(
height: 8.0, height: 8.0,
color: StreamChatTheme.of(context).colorTheme.greyGainsboro, color: StreamChatTheme.of(context).colorTheme.greyGainsboro,
@@ -197,9 +204,8 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
return Material( return Material(
child: InkWell( child: InkWell(
onTap: () { onTap: () {
final userMember = groupMembers.firstWhere( final userMember = groupMembers.firstWhereOrNull(
(e) => e.user.id == StreamChat.of(context).user.id, (e) => e.user!.id == StreamChat.of(context).user!.id,
orElse: () => null,
); );
_showUserInfoModal(member.user, userMember?.role == 'owner'); _showUserInfoModal(member.user, userMember?.role == 'owner');
}, },
@@ -213,7 +219,7 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 8.0, vertical: 12.0), horizontal: 8.0, vertical: 12.0),
child: UserAvatar( child: UserAvatar(
user: member.user, user: member.user!,
constraints: BoxConstraints( constraints: BoxConstraints(
maxHeight: 40.0, maxWidth: 40.0), maxHeight: 40.0, maxWidth: 40.0),
), ),
@@ -224,14 +230,14 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( Text(
member.user.name, member.user!.name,
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold),
), ),
SizedBox( SizedBox(
height: 1.0, height: 1.0,
), ),
Text( Text(
_getLastSeen(member.user), _getLastSeen(member.user!),
style: TextStyle( style: TextStyle(
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
@@ -371,7 +377,7 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
), ),
), ),
if ((channelName == null) || if ((channelName == null) ||
(channelName != _nameController.text.trim())) (channelName != _nameController!.text.trim()))
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@@ -379,12 +385,12 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
child: StreamSvgIcon.closeSmall(), child: StreamSvgIcon.closeSmall(),
onTap: () { onTap: () {
setState(() { setState(() {
_nameController.text = _getChannelName( _nameController!.text = _getChannelName(
2 * MediaQuery.of(context).size.width / 3, 2 * MediaQuery.of(context).size.width / 3,
members: channel.state.members, members: channel.state!.members,
extraData: channel.extraData, extraData: channel.extraData,
maxFontSize: 16.0, maxFontSize: 16.0,
); )!;
_focusNode.unfocus(); _focusNode.unfocus();
}); });
}, },
@@ -399,10 +405,10 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
), ),
onTap: () { onTap: () {
StreamChannel.of(context).channel.update({ StreamChannel.of(context).channel.update({
'name': _nameController.text.trim(), 'name': _nameController!.text.trim(),
}).catchError((err) { }).catchError((err) {
setState(() { setState(() {
_nameController.text = channelName; _nameController!.text = channelName;
_focusNode.unfocus(); _focusNode.unfocus();
}); });
}); });
@@ -457,15 +463,15 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
), ),
trailing: snapshot.data == null trailing: snapshot.data == null
? CircularProgressIndicator() ? CircularProgressIndicator()
: ValueListenableBuilder<bool>( : ValueListenableBuilder<bool?>(
valueListenable: mutedBool, valueListenable: mutedBool,
builder: (context, value, _) { builder: (context, value, _) {
return CupertinoSwitch( return CupertinoSwitch(
value: value, value: value!,
onChanged: (val) { onChanged: (val) {
mutedBool.value = val; mutedBool.value = val;
if (snapshot.data) { if (snapshot.data!) {
channel.channel.unmute(); channel.channel.unmute();
} else { } else {
channel.channel.mute(); channel.channel.mute();
@@ -502,6 +508,7 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
channel: channel, channel: channel,
child: MessageSearchBloc( child: MessageSearchBloc(
child: ChannelMediaDisplayScreen( child: ChannelMediaDisplayScreen(
messageTheme: widget.messageTheme,
sortOptions: [ sortOptions: [
SortOption( SortOption(
'created_at', 'created_at',
@@ -609,7 +616,7 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
); );
if (res == true) { if (res == true) {
final channel = StreamChannel.of(context).channel; final channel = StreamChannel.of(context).channel;
await channel.removeMembers([StreamChat.of(context).user.id]); await channel.removeMembers([StreamChat.of(context).user!.id]);
Navigator.pop(context); Navigator.pop(context);
} }
}, },
@@ -647,7 +654,7 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
child: UserListView( child: UserListView(
selectedUsers: {}, selectedUsers: {},
onUserTap: (user, _) async { onUserTap: (user, _) async {
_searchController.clear(); _searchController!.clear();
await channel.addMembers([user.id]); await channel.addMembers([user.id]);
Navigator.pop(context); Navigator.pop(context);
@@ -657,18 +664,17 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
pagination: PaginationParams( pagination: PaginationParams(
limit: 25, limit: 25,
), ),
filter: { filter: Filter.and(
if (_searchController.text.isNotEmpty) [
'name': { if (_searchController!.text.isNotEmpty)
r'$autocomplete': _userNameQuery, Filter.autoComplete('name', _userNameQuery),
}, Filter.notIn('id', [
'id': { StreamChat.of(context).user!.id,
r'$nin': [ ...channel.state!.members.map(((e) => e.userId!)
StreamChat.of(context).user.id, as Object Function(Member)),
...channel.state.members.map((e) => e.userId), ]),
], ],
}, ),
},
sort: [ sort: [
SortOption( SortOption(
'name', 'name',
@@ -778,7 +784,7 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
); );
} }
void _showUserInfoModal(User user, bool isUserAdmin) { void _showUserInfoModal(User? user, bool isUserAdmin) {
var channel = StreamChannel.of(context).channel; var channel = StreamChannel.of(context).channel;
showModalBottomSheet( showModalBottomSheet(
@@ -798,7 +804,7 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
), ),
Center( Center(
child: Text( child: Text(
user.name, user!.name,
style: TextStyle( style: TextStyle(
fontSize: 16.0, fontSize: 16.0,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@@ -808,7 +814,7 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
SizedBox( SizedBox(
height: 5.0, height: 5.0,
), ),
_buildConnectedTitleState(user), _buildConnectedTitleState(user)!,
Center( Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
@@ -822,7 +828,7 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
), ),
), ),
), ),
if (StreamChat.of(context).user.id != user.id) if (StreamChat.of(context).user!.id != user.id)
_buildModalListTile( _buildModalListTile(
context, context,
StreamSvgIcon.user( StreamSvgIcon.user(
@@ -836,7 +842,7 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
var c = client.channel('messaging', extraData: { var c = client.channel('messaging', extraData: {
'members': [ 'members': [
user.id, user.id,
StreamChat.of(context).user.id, StreamChat.of(context).user!.id,
], ],
}); });
@@ -848,6 +854,7 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
builder: (context) => StreamChannel( builder: (context) => StreamChannel(
channel: c, channel: c,
child: ChatInfoScreen( child: ChatInfoScreen(
messageTheme: widget.messageTheme,
user: user, user: user,
), ),
), ),
@@ -855,7 +862,7 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
); );
}, },
), ),
if (StreamChat.of(context).user.id != user.id) if (StreamChat.of(context).user!.id != user.id)
_buildModalListTile( _buildModalListTile(
context, context,
StreamSvgIcon.message( StreamSvgIcon.message(
@@ -869,7 +876,7 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
var c = client.channel('messaging', extraData: { var c = client.channel('messaging', extraData: {
'members': [ 'members': [
user.id, user.id,
StreamChat.of(context).user.id, StreamChat.of(context).user!.id,
], ],
}); });
@@ -887,7 +894,7 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
}, },
), ),
if (!channel.isDistinct && if (!channel.isDistinct &&
StreamChat.of(context).user.id != user.id && StreamChat.of(context).user!.id != user.id &&
isUserAdmin) isUserAdmin)
_buildModalListTile( _buildModalListTile(
context, context,
@@ -899,7 +906,7 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
// TODO: Add make owner implementation (Remaining from backend) // TODO: Add make owner implementation (Remaining from backend)
}), }),
if (!channel.isDistinct && if (!channel.isDistinct &&
StreamChat.of(context).user.id != user.id && StreamChat.of(context).user!.id != user.id &&
isUserAdmin) isUserAdmin)
_buildModalListTile( _buildModalListTile(
context, context,
@@ -934,7 +941,7 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
); );
} }
Widget _buildConnectedTitleState(User user) { Widget? _buildConnectedTitleState(User? user) {
var alternativeWidget; var alternativeWidget;
final otherMember = user; final otherMember = user;
@@ -966,7 +973,7 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
Widget _buildModalListTile( Widget _buildModalListTile(
BuildContext context, Widget leading, String title, VoidCallback onTap, BuildContext context, Widget leading, String title, VoidCallback onTap,
{Color color}) { {Color? color}) {
color ??= StreamChatTheme.of(context).colorTheme.black; color ??= StreamChatTheme.of(context).colorTheme.black;
return Material( return Material(
@@ -1003,24 +1010,24 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
); );
} }
String _getChannelName( String? _getChannelName(
double width, { double width, {
List<Member> members, List<Member>? members,
Map extraData, required Map extraData,
double maxFontSize, double? maxFontSize,
}) { }) {
String title; String? title;
var client = StreamChat.of(context); var client = StreamChat.of(context);
if (extraData['name'] == null) { if (extraData['name'] == null) {
final otherMembers = final otherMembers =
members.where((member) => member.user.id != client.user.id); members!.where((member) => member.user!.id != client.user!.id);
if (otherMembers.isNotEmpty) { if (otherMembers.isNotEmpty) {
final maxWidth = width; final maxWidth = width;
final maxChars = maxWidth / maxFontSize; final maxChars = maxWidth / maxFontSize!;
var currentChars = 0; var currentChars = 0;
final currentMembers = <Member>[]; final currentMembers = <Member>[];
otherMembers.forEach((element) { otherMembers.forEach((element) {
final newLength = currentChars + element.user.name.length; final newLength = currentChars + element.user!.name.length;
if (newLength < maxChars) { if (newLength < maxChars) {
currentChars = newLength; currentChars = newLength;
currentMembers.add(element); currentMembers.add(element);
@@ -1029,7 +1036,7 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
final exceedingMembers = otherMembers.length - currentMembers.length; final exceedingMembers = otherMembers.length - currentMembers.length;
title = title =
'${currentMembers.map((e) => e.user.name).join(', ')} ${exceedingMembers > 0 ? '+ $exceedingMembers' : ''}'; '${currentMembers.map((e) => e.user!.name).join(', ')} ${exceedingMembers > 0 ? '+ $exceedingMembers' : ''}';
} else { } else {
title = 'No title'; title = 'No title';
} }
+88 -92
View File
@@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:collection/collection.dart' show IterableExtension;
import 'package:example/chat_info_screen.dart'; import 'package:example/chat_info_screen.dart';
import 'package:example/choose_user_page.dart'; import 'package:example/choose_user_page.dart';
import 'package:example/group_info_screen.dart'; import 'package:example/group_info_screen.dart';
@@ -38,15 +39,15 @@ class MyApp extends StatefulWidget {
} }
class _MyAppState extends State<MyApp> with TickerProviderStateMixin { class _MyAppState extends State<MyApp> with TickerProviderStateMixin {
InitData _initData; InitData? _initData;
bool _animCompleted = false; bool _animCompleted = false;
Animation<double> _animation, _scaleAnimation; Animation<double>? _animation, _scaleAnimation;
AnimationController _animationController, _scaleAnimationController; AnimationController? _animationController, _scaleAnimationController;
Animation<Color> _colorAnimation; Animation<Color?>? _colorAnimation;
int timeOfStartMs; late int timeOfStartMs;
Future<InitData> _initConnection() async { Future<InitData> _initConnection() async {
String apiKey, userId, token; String? apiKey, userId, token;
if (!kIsWeb) { if (!kIsWeb) {
final secureStorage = FlutterSecureStorage(); final secureStorage = FlutterSecureStorage();
@@ -57,7 +58,7 @@ class _MyAppState extends State<MyApp> with TickerProviderStateMixin {
final client = StreamChatClient( final client = StreamChatClient(
apiKey ?? kDefaultStreamApiKey, apiKey ?? kDefaultStreamApiKey,
logLevel: Level.SEVERE, logLevel: Level.INFO,
)..chatPersistenceClient = chatPersistentClient; )..chatPersistenceClient = chatPersistentClient;
if (userId != null) { if (userId != null) {
@@ -84,7 +85,7 @@ class _MyAppState extends State<MyApp> with TickerProviderStateMixin {
begin: 1.0, begin: 1.0,
end: 1.5, end: 1.5,
).animate(CurvedAnimation( ).animate(CurvedAnimation(
parent: _scaleAnimationController, parent: _scaleAnimationController!,
curve: Curves.easeInOutBack, curve: Curves.easeInOutBack,
)); ));
@@ -98,21 +99,21 @@ class _MyAppState extends State<MyApp> with TickerProviderStateMixin {
begin: 0.0, begin: 0.0,
end: 1000.0, end: 1000.0,
).animate(CurvedAnimation( ).animate(CurvedAnimation(
parent: _animationController, parent: _animationController!,
curve: Curves.easeInOut, curve: Curves.easeInOut,
)); ));
_colorAnimation = ColorTween( _colorAnimation = ColorTween(
begin: Color(0xff005FFF), begin: Color(0xff005FFF),
end: Color(0xff005FFF), end: Color(0xff005FFF),
).animate(CurvedAnimation( ).animate(CurvedAnimation(
parent: _animationController, parent: _animationController!,
curve: Curves.easeInOut, curve: Curves.easeInOut,
)); ));
_colorAnimation = ColorTween( _colorAnimation = ColorTween(
begin: Color(0xff005FFF), begin: Color(0xff005FFF),
end: Colors.transparent, end: Colors.transparent,
).animate(CurvedAnimation( ).animate(CurvedAnimation(
parent: _animationController, parent: _animationController!,
curve: Curves.easeInOut, curve: Curves.easeInOut,
)); ));
} }
@@ -132,22 +133,22 @@ class _MyAppState extends State<MyApp> with TickerProviderStateMixin {
var now = DateTime.now().millisecondsSinceEpoch; var now = DateTime.now().millisecondsSinceEpoch;
if (now - timeOfStartMs > 1500) { if (now - timeOfStartMs > 1500) {
SchedulerBinding.instance.addPostFrameCallback((timeStamp) { SchedulerBinding.instance!.addPostFrameCallback((timeStamp) {
_scaleAnimationController.forward().whenComplete(() { _scaleAnimationController?.forward().whenComplete(() {
_animationController.forward(); _animationController?.forward();
}); });
}); });
} else { } else {
Future.delayed(Duration(milliseconds: 1500)).then((value) { Future.delayed(Duration(milliseconds: 1500)).then((value) {
_scaleAnimationController.forward().whenComplete(() { _scaleAnimationController?.forward().whenComplete(() {
_animationController.forward(); _animationController?.forward();
}); });
}); });
} }
if (!kIsWeb) { if (!kIsWeb) {
_initData.client.state?.totalUnreadCountStream?.listen((count) { _initData!.client.state.totalUnreadCountStream.listen((count) {
if (count > 0) { if (count! > 0) {
FlutterAppBadger.updateBadgeCount(count); FlutterAppBadger.updateBadgeCount(count);
} else { } else {
FlutterAppBadger.removeBadge(); FlutterAppBadger.removeBadge();
@@ -156,7 +157,7 @@ class _MyAppState extends State<MyApp> with TickerProviderStateMixin {
} }
}, },
); );
_animationController.addStatusListener((status) { _animationController?.addStatusListener((status) {
if (status == AnimationStatus.completed) { if (status == AnimationStatus.completed) {
setState(() { setState(() {
_animCompleted = true; _animCompleted = true;
@@ -174,20 +175,20 @@ class _MyAppState extends State<MyApp> with TickerProviderStateMixin {
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
AnimatedBuilder( AnimatedBuilder(
animation: _scaleAnimation, animation: _scaleAnimation!,
builder: (context, _) { builder: (context, _) {
return Transform.scale( return Transform.scale(
scale: _scaleAnimation.value, scale: _scaleAnimation!.value,
child: AnimatedBuilder( child: AnimatedBuilder(
animation: _colorAnimation, animation: _colorAnimation!,
builder: (context, snapshot) { builder: (context, snapshot) {
return Container( return Container(
alignment: Alignment.center, alignment: Alignment.center,
constraints: BoxConstraints.expand(), constraints: BoxConstraints.expand(),
color: _colorAnimation == null color: _colorAnimation == null
? Color(0xff005FFF) ? Color(0xff005FFF)
: _colorAnimation.value, : _colorAnimation!.value,
child: !_animationController.isAnimating child: !_animationController!.isAnimating
? Lottie.asset( ? Lottie.asset(
'assets/floating_boat.json', 'assets/floating_boat.json',
alignment: Alignment.center, alignment: Alignment.center,
@@ -199,16 +200,16 @@ class _MyAppState extends State<MyApp> with TickerProviderStateMixin {
}, },
), ),
AnimatedBuilder( AnimatedBuilder(
animation: _animation, animation: _animation!,
builder: (context, snapshot) { builder: (context, snapshot) {
return Transform.scale( return Transform.scale(
scale: _animation.value, scale: _animation!.value,
child: Container( child: Container(
width: 1.0, width: 1.0,
height: 1.0, height: 1.0,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white color: Colors.white
.withOpacity(1 - _animationController.value), .withOpacity(1 - _animationController!.value),
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
), ),
@@ -227,19 +228,19 @@ class _MyAppState extends State<MyApp> with TickerProviderStateMixin {
children: [ children: [
if (_initData != null) if (_initData != null)
PreferenceBuilder<int>( PreferenceBuilder<int>(
preference: _initData.preferences.getInt( preference: _initData!.preferences.getInt(
'theme', 'theme',
defaultValue: 0, defaultValue: 0,
), ),
builder: (context, snapshot) => MaterialApp( builder: (context, snapshot) => MaterialApp(
builder: (context, child) { builder: (context, child) {
return StreamChat( return StreamChat(
client: _initData.client, client: _initData!.client,
onBackgroundEventReceived: (e) => onBackgroundEventReceived: (e) => showLocalNotification(
showLocalNotification(e, _initData.client.state.user.id), e, _initData!.client.state.user!.id),
child: Builder( child: Builder(
builder: (context) => AnnotatedRegion<SystemUiOverlayStyle>( builder: (context) => AnnotatedRegion<SystemUiOverlayStyle>(
child: child, child: child!,
value: SystemUiOverlayStyle( value: SystemUiOverlayStyle(
systemNavigationBarColor: systemNavigationBarColor:
StreamChatTheme.of(context).colorTheme.white, StreamChatTheme.of(context).colorTheme.white,
@@ -260,7 +261,7 @@ class _MyAppState extends State<MyApp> with TickerProviderStateMixin {
1: ThemeMode.light, 1: ThemeMode.light,
}[snapshot], }[snapshot],
onGenerateRoute: AppRoutes.generateRoute, onGenerateRoute: AppRoutes.generateRoute,
initialRoute: _initData.client.state.user == null initialRoute: _initData!.client.state.user == null
? Routes.CHOOSE_USER ? Routes.CHOOSE_USER
: Routes.HOME, : Routes.HOME,
), ),
@@ -319,7 +320,7 @@ class _HomePageState extends State<HomePage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final user = StreamChat.of(context).user; final user = StreamChat.of(context).user!;
return Scaffold( return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow, backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
appBar: ChannelListHeader( appBar: ChannelListHeader(
@@ -495,19 +496,15 @@ class _HomePageState extends State<HomePage> {
class UserMentionPage extends StatelessWidget { class UserMentionPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final user = StreamChat.of(context).user; final user = StreamChat.of(context).user!;
return MessageSearchBloc( return MessageSearchBloc(
child: MessageSearchListView( child: MessageSearchListView(
filters: { filters: Filter.in_('members', [user.id]),
'members': { messageFilters: Filter.custom(
r'$in': [user.id], operator: r'$contains',
}, key: 'mentioned_users.id',
}, value: user.id,
messageFilters: { ),
'mentioned_users.id': {
r'$contains': user.id,
},
},
sortOptions: [ sortOptions: [
SortOption( SortOption(
'created_at', 'created_at',
@@ -559,8 +556,8 @@ class UserMentionPage extends StatelessWidget {
final client = StreamChat.of(context).client; final client = StreamChat.of(context).client;
final message = messageResponse.message; final message = messageResponse.message;
final channel = client.channel( final channel = client.channel(
messageResponse.channel.type, messageResponse.channel!.type,
id: messageResponse.channel.id, id: messageResponse.channel!.id,
); );
if (channel.state == null) { if (channel.state == null) {
await channel.watch(); await channel.watch();
@@ -585,20 +582,20 @@ class ChannelListPage extends StatefulWidget {
} }
class _ChannelListPageState extends State<ChannelListPage> { class _ChannelListPageState extends State<ChannelListPage> {
TextEditingController _controller; TextEditingController? _controller;
String _channelQuery = ''; String _channelQuery = '';
bool _isSearchActive = false; bool _isSearchActive = false;
Timer _debounce; Timer? _debounce;
void _channelQueryListener() { void _channelQueryListener() {
if (_debounce?.isActive ?? false) _debounce.cancel(); if (_debounce?.isActive ?? false) _debounce!.cancel();
_debounce = Timer(const Duration(milliseconds: 350), () { _debounce = Timer(const Duration(milliseconds: 350), () {
if (mounted) { if (mounted) {
setState(() { setState(() {
_channelQuery = _controller.text; _channelQuery = _controller!.text;
_isSearchActive = _channelQuery.isNotEmpty; _isSearchActive = _channelQuery.isNotEmpty;
}); });
} }
@@ -624,7 +621,7 @@ class _ChannelListPageState extends State<ChannelListPage> {
return WillPopScope( return WillPopScope(
onWillPop: () async { onWillPop: () async {
if (_isSearchActive) { if (_isSearchActive) {
_controller.clear(); _controller!.clear();
setState(() => _isSearchActive = false); setState(() => _isSearchActive = false);
return false; return false;
} }
@@ -651,11 +648,7 @@ class _ChannelListPageState extends State<ChannelListPage> {
? MessageSearchListView( ? MessageSearchListView(
showErrorTile: true, showErrorTile: true,
messageQuery: _channelQuery, messageQuery: _channelQuery,
filters: { filters: Filter.in_('members', [user!.id]),
'members': {
r'$in': [user.id]
},
},
sortOptions: [ sortOptions: [
SortOption( SortOption(
'created_at', 'created_at',
@@ -699,8 +692,8 @@ class _ChannelListPageState extends State<ChannelListPage> {
final client = StreamChat.of(context).client; final client = StreamChat.of(context).client;
final message = messageResponse.message; final message = messageResponse.message;
final channel = client.channel( final channel = client.channel(
messageResponse.channel.type, messageResponse.channel!.type,
id: messageResponse.channel.id, id: messageResponse.channel!.id,
); );
if (channel.state == null) { if (channel.state == null) {
await channel.watch(); await channel.watch();
@@ -720,11 +713,7 @@ class _ChannelListPageState extends State<ChannelListPage> {
Navigator.pushNamed(context, Routes.NEW_CHAT); Navigator.pushNamed(context, Routes.NEW_CHAT);
}, },
swipeToAction: true, swipeToAction: true,
filter: { filter: Filter.in_('members', [user!.id]),
'members': {
r'$in': [user.id],
},
},
options: { options: {
'presence': true, 'presence': true,
}, },
@@ -741,10 +730,12 @@ class _ChannelListPageState extends State<ChannelListPage> {
builder: (context) => StreamChannel( builder: (context) => StreamChannel(
channel: channel, channel: channel,
child: ChatInfoScreen( child: ChatInfoScreen(
user: channel.state.members messageTheme: StreamChatTheme.of(context)
.ownMessageTheme,
user: channel.state!.members
.where((m) => .where((m) =>
m.userId != m.userId !=
channel.client.state.user.id) channel.client.state.user!.id)
.first .first
.user, .user,
), ),
@@ -757,7 +748,10 @@ class _ChannelListPageState extends State<ChannelListPage> {
MaterialPageRoute( MaterialPageRoute(
builder: (context) => StreamChannel( builder: (context) => StreamChannel(
channel: channel, channel: channel,
child: GroupInfoScreen(), child: GroupInfoScreen(
messageTheme: StreamChatTheme.of(context)
.ownMessageTheme,
),
), ),
), ),
); );
@@ -774,8 +768,8 @@ class _ChannelListPageState extends State<ChannelListPage> {
} }
class ChannelPageArgs { class ChannelPageArgs {
final Channel channel; final Channel? channel;
final Message initialMessage; final Message? initialMessage;
const ChannelPageArgs({ const ChannelPageArgs({
this.channel, this.channel,
@@ -784,12 +778,12 @@ class ChannelPageArgs {
} }
class ChannelPage extends StatefulWidget { class ChannelPage extends StatefulWidget {
final int initialScrollIndex; final int? initialScrollIndex;
final double initialAlignment; final double? initialAlignment;
final bool highlightInitialMessage; final bool highlightInitialMessage;
const ChannelPage({ const ChannelPage({
Key key, Key? key,
this.initialScrollIndex, this.initialScrollIndex,
this.initialAlignment, this.initialAlignment,
this.highlightInitialMessage = false, this.highlightInitialMessage = false,
@@ -800,8 +794,8 @@ class ChannelPage extends StatefulWidget {
} }
class _ChannelPageState extends State<ChannelPage> { class _ChannelPageState extends State<ChannelPage> {
Message _quotedMessage; Message? _quotedMessage;
FocusNode _focusNode; FocusNode? _focusNode;
@override @override
void initState() { void initState() {
@@ -811,14 +805,14 @@ class _ChannelPageState extends State<ChannelPage> {
@override @override
void dispose() { void dispose() {
_focusNode.dispose(); _focusNode!.dispose();
super.dispose(); super.dispose();
} }
void _reply(Message message) { void _reply(Message message) {
setState(() => _quotedMessage = message); setState(() => _quotedMessage = message);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) { WidgetsBinding.instance!.addPostFrameCallback((timeStamp) {
_focusNode.requestFocus(); _focusNode!.requestFocus();
}); });
} }
@@ -833,9 +827,8 @@ class _ChannelPageState extends State<ChannelPage> {
if (channel.memberCount == 2 && channel.isDistinct) { if (channel.memberCount == 2 && channel.isDistinct) {
final currentUser = StreamChat.of(context).user; final currentUser = StreamChat.of(context).user;
final otherUser = channel.state.members.firstWhere( final otherUser = channel.state!.members.firstWhereOrNull(
(element) => element.user.id != currentUser.id, (element) => element.user!.id != currentUser!.id,
orElse: () => null,
); );
if (otherUser != null) { if (otherUser != null) {
final pop = await Navigator.push( final pop = await Navigator.push(
@@ -843,6 +836,7 @@ class _ChannelPageState extends State<ChannelPage> {
MaterialPageRoute( MaterialPageRoute(
builder: (context) => StreamChannel( builder: (context) => StreamChannel(
child: ChatInfoScreen( child: ChatInfoScreen(
messageTheme: StreamChatTheme.of(context).ownMessageTheme,
user: otherUser.user, user: otherUser.user,
), ),
channel: channel, channel: channel,
@@ -859,7 +853,9 @@ class _ChannelPageState extends State<ChannelPage> {
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => StreamChannel( builder: (context) => StreamChannel(
child: GroupInfoScreen(), child: GroupInfoScreen(
messageTheme: StreamChatTheme.of(context).ownMessageTheme,
),
channel: channel, channel: channel,
), ),
), ),
@@ -936,7 +932,7 @@ class _ChannelPageState extends State<ChannelPage> {
quotedMessage: _quotedMessage, quotedMessage: _quotedMessage,
onQuotedMessageCleared: () { onQuotedMessageCleared: () {
setState(() => _quotedMessage = null); setState(() => _quotedMessage = null);
_focusNode.unfocus(); _focusNode!.unfocus();
}, },
), ),
], ],
@@ -946,12 +942,12 @@ class _ChannelPageState extends State<ChannelPage> {
} }
class ThreadPage extends StatefulWidget { class ThreadPage extends StatefulWidget {
final Message parent; final Message? parent;
final int initialScrollIndex; final int? initialScrollIndex;
final double initialAlignment; final double? initialAlignment;
ThreadPage({ ThreadPage({
Key key, Key? key,
this.parent, this.parent,
this.initialScrollIndex, this.initialScrollIndex,
this.initialAlignment, this.initialAlignment,
@@ -962,7 +958,7 @@ class ThreadPage extends StatefulWidget {
} }
class _ThreadPageState extends State<ThreadPage> { class _ThreadPageState extends State<ThreadPage> {
Message _quotedMessage; Message? _quotedMessage;
FocusNode _focusNode = FocusNode(); FocusNode _focusNode = FocusNode();
@override @override
@@ -973,7 +969,7 @@ class _ThreadPageState extends State<ThreadPage> {
void _reply(Message message) { void _reply(Message message) {
setState(() => _quotedMessage = message); setState(() => _quotedMessage = message);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) { WidgetsBinding.instance!.addPostFrameCallback((timeStamp) {
_focusNode.requestFocus(); _focusNode.requestFocus();
}); });
} }
@@ -983,7 +979,7 @@ class _ThreadPageState extends State<ThreadPage> {
return Scaffold( return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow, backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
appBar: ThreadHeader( appBar: ThreadHeader(
parent: widget.parent, parent: widget.parent!,
), ),
body: Column( body: Column(
children: <Widget>[ children: <Widget>[
@@ -996,7 +992,7 @@ class _ThreadPageState extends State<ThreadPage> {
onReplyTap: _reply, onReplyTap: _reply,
), ),
), ),
if (widget.parent.type != 'deleted') if (widget.parent!.type != 'deleted')
MessageInput( MessageInput(
parentMessage: widget.parent, parentMessage: widget.parent,
focusNode: _focusNode, focusNode: _focusNode,
@@ -1021,8 +1017,8 @@ class InitData {
class HolePainter extends CustomPainter { class HolePainter extends CustomPainter {
HolePainter({ HolePainter({
@required this.color, required this.color,
@required this.holeSize, required this.holeSize,
}); });
Color color; Color color;
@@ -16,9 +16,9 @@ class _NewChatScreenState extends State<NewChatScreen> {
final _chipInputTextFieldStateKey = final _chipInputTextFieldStateKey =
GlobalKey<ChipInputTextFieldState<User>>(); GlobalKey<ChipInputTextFieldState<User>>();
TextEditingController _controller; late TextEditingController _controller;
ChipInputTextFieldState get _chipInputTextFieldState => ChipInputTextFieldState? get _chipInputTextFieldState =>
_chipInputTextFieldStateKey.currentState; _chipInputTextFieldStateKey.currentState;
String _userNameQuery = ''; String _userNameQuery = '';
@@ -30,14 +30,14 @@ class _NewChatScreenState extends State<NewChatScreen> {
bool _isSearchActive = false; bool _isSearchActive = false;
Channel channel; Channel? channel;
Timer _debounce; Timer? _debounce;
bool _showUserList = true; bool _showUserList = true;
void _userNameListener() { void _userNameListener() {
if (_debounce?.isActive ?? false) _debounce.cancel(); if (_debounce?.isActive ?? false) _debounce!.cancel();
_debounce = Timer(const Duration(milliseconds: 350), () { _debounce = Timer(const Duration(milliseconds: 350), () {
if (mounted) if (mounted)
setState(() { setState(() {
@@ -70,13 +70,13 @@ class _NewChatScreenState extends State<NewChatScreen> {
'state': false, 'state': false,
'watch': false, 'watch': false,
}, },
filter: { filter: Filter.raw(value: {
'members': [ 'members': [
..._selectedUsers.map((e) => e.id), ..._selectedUsers.map((e) => e.id),
chatState.user.id, chatState.user!.id,
], ],
'distinct': true, 'distinct': true,
}, }),
messageLimit: 0, messageLimit: 0,
paginationParams: PaginationParams( paginationParams: PaginationParams(
limit: 1, limit: 1,
@@ -86,14 +86,14 @@ class _NewChatScreenState extends State<NewChatScreen> {
final _channelExisted = res.length == 1; final _channelExisted = res.length == 1;
if (_channelExisted) { if (_channelExisted) {
channel = res.first; channel = res.first;
await channel.watch(); await channel!.watch();
} else { } else {
channel = chatState.client.channel( channel = chatState.client.channel(
'messaging', 'messaging',
extraData: { extraData: {
'members': [ 'members': [
..._selectedUsers.map((e) => e.id), ..._selectedUsers.map((e) => e.id),
chatState.user.id, chatState.user!.id,
], ],
}, },
); );
@@ -110,9 +110,9 @@ class _NewChatScreenState extends State<NewChatScreen> {
void dispose() { void dispose() {
_searchFocusNode.dispose(); _searchFocusNode.dispose();
_messageInputFocusNode.dispose(); _messageInputFocusNode.dispose();
_controller?.clear(); _controller.clear();
_controller?.removeListener(_userNameListener); _controller.removeListener(_userNameListener);
_controller?.dispose(); _controller.dispose();
super.dispose(); super.dispose();
} }
@@ -158,7 +158,7 @@ class _NewChatScreenState extends State<NewChatScreen> {
message: statusString, message: statusString,
child: StreamChannel( child: StreamChannel(
showLoading: false, showLoading: false,
channel: channel, channel: channel!,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -169,7 +169,7 @@ class _NewChatScreenState extends State<NewChatScreen> {
chipBuilder: (context, user) { chipBuilder: (context, user) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
_chipInputTextFieldState.removeItem(user); _chipInputTextFieldState?.removeItem(user);
_searchFocusNode.requestFocus(); _searchFocusNode.requestFocus();
}, },
child: Stack( child: Stack(
@@ -299,24 +299,21 @@ class _NewChatScreenState extends State<NewChatScreen> {
_controller.clear(); _controller.clear();
if (!_selectedUsers.contains(user)) { if (!_selectedUsers.contains(user)) {
_chipInputTextFieldState _chipInputTextFieldState
..addItem(user) ?..addItem(user)
..pauseItemAddition(); ..pauseItemAddition();
} else { } else {
_chipInputTextFieldState.removeItem(user); _chipInputTextFieldState!.removeItem(user);
} }
}, },
pagination: PaginationParams( pagination: PaginationParams(
limit: 25, limit: 25,
), ),
filter: { filter: Filter.and([
if (_userNameQuery.isNotEmpty) if (_userNameQuery.isNotEmpty)
'name': { Filter.autoComplete('name', _userNameQuery),
r'$autocomplete': _userNameQuery, Filter.notEqual(
}, 'id', StreamChat.of(context).user!.id),
'id': { ]),
r'$ne': StreamChat.of(context).user.id,
},
},
sort: [ sort: [
SortOption( SortOption(
'name', 'name',
@@ -370,7 +367,7 @@ class _NewChatScreenState extends State<NewChatScreen> {
), ),
) )
: FutureBuilder<bool>( : FutureBuilder<bool>(
future: channel.initialized, future: channel!.initialized,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.data == true) { if (snapshot.data == true) {
return MessageListView(); return MessageListView();
@@ -394,7 +391,7 @@ class _NewChatScreenState extends State<NewChatScreen> {
MessageInput( MessageInput(
focusNode: _messageInputFocusNode, focusNode: _messageInputFocusNode,
preMessageSending: (message) async { preMessageSending: (message) async {
await channel.watch(); await channel!.watch();
return message; return message;
}, },
onMessageSent: (m) { onMessageSent: (m) {
@@ -12,7 +12,7 @@ class NewGroupChatScreen extends StatefulWidget {
} }
class _NewGroupChatScreenState extends State<NewGroupChatScreen> { class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
TextEditingController _controller; TextEditingController? _controller;
String _userNameQuery = ''; String _userNameQuery = '';
@@ -20,14 +20,14 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
bool _isSearchActive = false; bool _isSearchActive = false;
Timer _debounce; Timer? _debounce;
void _userNameListener() { void _userNameListener() {
if (_debounce?.isActive ?? false) _debounce.cancel(); if (_debounce?.isActive ?? false) _debounce!.cancel();
_debounce = Timer(const Duration(milliseconds: 350), () { _debounce = Timer(const Duration(milliseconds: 350), () {
if (mounted) { if (mounted) {
setState(() { setState(() {
_userNameQuery = _controller.text; _userNameQuery = _controller!.text;
_isSearchActive = _userNameQuery.isNotEmpty; _isSearchActive = _userNameQuery.isNotEmpty;
}); });
} }
@@ -80,7 +80,7 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
setState(() { setState(() {
_selectedUsers _selectedUsers
..clear() ..clear()
..addAll(updatedList); ..addAll(updatedList as Iterable<User>);
}); });
} }
}, },
@@ -244,15 +244,11 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
pagination: PaginationParams( pagination: PaginationParams(
limit: 25, limit: 25,
), ),
filter: { filter: Filter.and([
if (_userNameQuery.isNotEmpty) if (_userNameQuery.isNotEmpty)
'name': { Filter.autoComplete('name', _userNameQuery),
r'$autocomplete': _userNameQuery, Filter.notEqual('id', StreamChat.of(context).user!.id),
}, ]),
'id': {
r'$ne': StreamChat.of(context).user.id,
}
},
sort: [ sort: [
SortOption( SortOption(
'name', 'name',
@@ -315,8 +311,8 @@ class _HeaderDelegate extends SliverPersistentHeaderDelegate {
final double height; final double height;
const _HeaderDelegate({ const _HeaderDelegate({
@required this.child, required this.child,
@required this.height, required this.height,
}); });
@override @override
@@ -7,7 +7,7 @@ void showLocalNotification(Event event, String currentUserId) async {
EventType.messageNew, EventType.messageNew,
EventType.notificationMessageNew, EventType.notificationMessageNew,
].contains(event.type) || ].contains(event.type) ||
event.user.id == currentUserId) { event.user!.id == currentUserId) {
return; return;
} }
if (event.message == null) return; if (event.message == null) return;
@@ -21,9 +21,9 @@ void showLocalNotification(Event event, String currentUserId) async {
); );
await flutterLocalNotificationsPlugin.initialize(initializationSettings); await flutterLocalNotificationsPlugin.initialize(initializationSettings);
await flutterLocalNotificationsPlugin.show( await flutterLocalNotificationsPlugin.show(
event.message.id.hashCode, event.message!.id.hashCode,
event.message.user.name, event.message!.user!.name,
event.message.text, event.message!.text,
NotificationDetails( NotificationDetails(
android: AndroidNotificationDetails( android: AndroidNotificationDetails(
'message channel', 'message channel',
@@ -12,7 +12,7 @@ import '../group_info_screen.dart';
class AppRoutes { class AppRoutes {
/// Add entry for new route here /// Add entry for new route here
static Route<dynamic> generateRoute(RouteSettings settings) { static Route<dynamic>? generateRoute(RouteSettings settings) {
final args = settings.arguments; final args = settings.arguments;
switch (settings.name) { switch (settings.name) {
case Routes.APP: case Routes.APP:
@@ -44,7 +44,7 @@ class AppRoutes {
builder: (_) { builder: (_) {
final arg = args as ChannelPageArgs; final arg = args as ChannelPageArgs;
return StreamChannel( return StreamChannel(
channel: arg.channel, channel: arg.channel!,
initialMessageId: arg.initialMessage?.id, initialMessageId: arg.initialMessage?.id,
child: ChannelPage( child: ChannelPage(
highlightInitialMessage: arg.initialMessage != null, highlightInitialMessage: arg.initialMessage != null,
@@ -68,22 +68,25 @@ class AppRoutes {
settings: const RouteSettings(name: Routes.NEW_GROUP_CHAT_DETAILS), settings: const RouteSettings(name: Routes.NEW_GROUP_CHAT_DETAILS),
builder: (_) { builder: (_) {
return GroupChatDetailsScreen( return GroupChatDetailsScreen(
selectedUsers: args, selectedUsers: args as List<User>?,
); );
}); });
case Routes.CHAT_INFO_SCREEN: case Routes.CHAT_INFO_SCREEN:
return MaterialPageRoute( return MaterialPageRoute(
settings: const RouteSettings(name: Routes.CHAT_INFO_SCREEN), settings: const RouteSettings(name: Routes.CHAT_INFO_SCREEN),
builder: (_) { builder: (context) {
return ChatInfoScreen( return ChatInfoScreen(
user: args, user: args as User?,
messageTheme: StreamChatTheme.of(context).ownMessageTheme,
); );
}); });
case Routes.GROUP_INFO_SCREEN: case Routes.GROUP_INFO_SCREEN:
return MaterialPageRoute( return MaterialPageRoute(
settings: const RouteSettings(name: Routes.GROUP_INFO_SCREEN), settings: const RouteSettings(name: Routes.GROUP_INFO_SCREEN),
builder: (_) { builder: (context) {
return GroupInfoScreen(); return GroupInfoScreen(
messageTheme: StreamChatTheme.of(context).ownMessageTheme,
);
}); });
// Default case, should not reach here. // Default case, should not reach here.
default: default:
@@ -2,15 +2,15 @@ import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class SearchTextField extends StatelessWidget { class SearchTextField extends StatelessWidget {
final TextEditingController controller; final TextEditingController? controller;
final ValueChanged<String> onChanged; final ValueChanged<String>? onChanged;
final String hintText; final String hintText;
final VoidCallback onTap; final VoidCallback? onTap;
final bool showCloseButton; final bool showCloseButton;
const SearchTextField({ const SearchTextField({
Key key, Key? key,
@required this.controller, required this.controller,
this.onChanged, this.onChanged,
this.onTap, this.onTap,
this.hintText = 'Search', this.hintText = 'Search',
@@ -76,11 +76,11 @@ class SearchTextField extends StatelessWidget {
), ),
splashRadius: 24, splashRadius: 24,
onPressed: () { onPressed: () {
if (controller.text.isNotEmpty) { if (controller!.text.isNotEmpty) {
Future.microtask( Future.microtask(
() => [ () => [
controller.clear(), controller!.clear(),
if (onChanged != null) onChanged(''), if (onChanged != null) onChanged!(''),
], ],
); );
} }
@@ -5,7 +5,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class StreamVersion extends StatelessWidget { class StreamVersion extends StatelessWidget {
const StreamVersion({ const StreamVersion({
Key key, Key? key,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -20,13 +20,13 @@ class StreamVersion extends StatelessWidget {
return SizedBox(); return SizedBox();
} }
final pubspec = snapshot.data; final pubspec = snapshot.data!;
final yaml = loadYaml(pubspec); final yaml = loadYaml(pubspec);
final streamChatDep = final streamChatDep =
yaml['packages']['stream_chat_flutter']['version']; yaml['packages']['stream_chat_flutter']['version'];
return Text( return Text(
'Stream SDK v ${streamChatDep}', 'Stream SDK v $streamChatDep',
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: StreamChatTheme.of(context).colorTheme.greyGainsboro, color: StreamChatTheme.of(context).colorTheme.greyGainsboro,
+11 -10
View File
@@ -4,10 +4,10 @@ publish_to: 'none'
version: 1.5.4+1 version: 1.5.4+1
environment: environment:
sdk: ">=2.2.2 <3.0.0" sdk: '>=2.12.0 <3.0.0'
dependencies: dependencies:
flutter_app_badger: ^1.1.2 flutter_app_badger: ^1.2.0
flutter: flutter:
sdk: flutter sdk: flutter
stream_chat_flutter: stream_chat_flutter:
@@ -20,13 +20,14 @@ dependencies:
url: https://github.com/GetStream/stream-chat-flutter.git url: https://github.com/GetStream/stream-chat-flutter.git
ref: develop ref: develop
path: packages/stream_chat_persistence path: packages/stream_chat_persistence
flutter_local_notifications: ^2.0.2 flutter_local_notifications: ^5.0.0+4
flutter_svg: ^0.19.3 flutter_svg: ^0.22.0
flutter_secure_storage: ^3.3.5 flutter_secure_storage: ^4.2.0
yaml: ^2.2.1 yaml: ^3.1.0
uuid: ^2.2.2 uuid: ^3.0.4
streaming_shared_preferences: ^1.0.2 streaming_shared_preferences: ^2.0.0
lottie: ^0.7.0+1 lottie: ^1.0.1
collection: ^1.15.0-nullsafety.4
dependency_overrides: dependency_overrides:
stream_chat: stream_chat:
@@ -41,7 +42,7 @@ dependency_overrides:
path: packages/stream_chat_flutter_core path: packages/stream_chat_flutter_core
dev_dependencies: dev_dependencies:
flutter_launcher_icons: ^0.8.1 flutter_launcher_icons: ^0.9.0
test: any test: any
flutter: flutter:
@@ -1,30 +0,0 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility that Flutter provides. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:stream_chat_v1/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}