diff --git a/.github/workflows/build_nightly.yml b/.github/workflows/build_nightly.yml index 44716c9..011d2fa 100644 --- a/.github/workflows/build_nightly.yml +++ b/.github/workflows/build_nightly.yml @@ -69,3 +69,24 @@ jobs: with: name: android-stream-chat-v1 path: packages/stream_chat_v1/build/app/outputs/apk/release/app-release.apk + build_and_deploy_web: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: config git + run: | + git config --global user.email "$(git log --format='%ae' HEAD^!)" + git config --global user.name "$(git log --format='%an' HEAD^!)" + git fetch origin gh-pages:gh-pages + - uses: subosito/flutter-action@v1.4.0 + with: + channel: 'stable' + - run: flutter pub get + - name: Copy production config + run: echo "${{ secrets.PRODUCTION_CONFIG }}" > lib/app_config.dart + - uses: erickzanardo/flutter-gh-pages@v3 + with: + webRenderer: canvaskit + workingDir: packages/stream_chat_v1 + + diff --git a/packages/chatty/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/chatty/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 1d526a1..919434a 100644 --- a/packages/chatty/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/packages/chatty/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> diff --git a/packages/chatty/lib/data/auth_repository.dart b/packages/chatty/lib/data/auth_repository.dart index 2ac77e8..db5849e 100644 --- a/packages/chatty/lib/data/auth_repository.dart +++ b/packages/chatty/lib/data/auth_repository.dart @@ -1,7 +1,7 @@ import 'package:stream_chatter/domain/models/auth_user.dart'; abstract class AuthRepository { - Future getAuthUser(); + Future getAuthUser(); Future signIn(); Future logout(); } diff --git a/packages/chatty/lib/data/image_picker_repository.dart b/packages/chatty/lib/data/image_picker_repository.dart index f5632f0..08d7ab8 100644 --- a/packages/chatty/lib/data/image_picker_repository.dart +++ b/packages/chatty/lib/data/image_picker_repository.dart @@ -1,5 +1,5 @@ import 'dart:io'; abstract class ImagePickerRepository { - Future pickImage(); + Future pickImage(); } diff --git a/packages/chatty/lib/data/local/image_picker_impl.dart b/packages/chatty/lib/data/local/image_picker_impl.dart index 21a1b15..2bd82dd 100644 --- a/packages/chatty/lib/data/local/image_picker_impl.dart +++ b/packages/chatty/lib/data/local/image_picker_impl.dart @@ -4,9 +4,17 @@ import 'package:stream_chatter/data/image_picker_repository.dart'; class ImagePickerImpl extends ImagePickerRepository { @override - Future pickImage() async { + Future pickImage() async { final picker = ImagePicker(); - final pickedFile = await picker.getImage(source: ImageSource.gallery, maxWidth: 400); + final pickedFile = await picker.getImage( + source: ImageSource.gallery, + maxWidth: 400, + ); + + if (pickedFile == null) { + return null; + } + return File(pickedFile.path); } } diff --git a/packages/chatty/lib/data/local/stream_api_local_impl.dart b/packages/chatty/lib/data/local/stream_api_local_impl.dart index f772a26..4a84e35 100644 --- a/packages/chatty/lib/data/local/stream_api_local_impl.dart +++ b/packages/chatty/lib/data/local/stream_api_local_impl.dart @@ -8,7 +8,7 @@ class StreamApiLocalImpl extends StreamApiRepository { final StreamChatClient _client; @override - Future connectUser(ChatUser user, String token) async { + Future connectUser(ChatUser user, String? token) async { Map extraData = {}; if (user.image != null) { extraData['image'] = user.image; @@ -18,7 +18,7 @@ class StreamApiLocalImpl extends StreamApiRepository { } await _client.disconnect(); await _client.connectUser( - User(id: user.id, extraData: extraData), + User(id: user.id!, extraData: extraData as Map), token, ); return user; @@ -28,12 +28,12 @@ class StreamApiLocalImpl extends StreamApiRepository { Future> getChatUsers() async { final result = await _client.queryUsers(); final chatUsers = result.users - .where((element) => element.id != _client.state.user.id) + .where((element) => element.id != _client.state.user!.id) .map( (e) => ChatUser( id: e.id, name: e.name, - image: e.extraData['image'], + image: e.extraData['image'] as String?, ), ) .toList(); @@ -46,25 +46,28 @@ class StreamApiLocalImpl extends StreamApiRepository { } @override - Future createGroupChat(String channelId, String name, List members, {String image}) async { + Future createGroupChat( + String channelId, String? name, List? members, + {String? image}) async { final channel = _client.channel('messaging', id: channelId, extraData: { - 'name': name, - 'image': image, - 'members': [_client.state.user.id, ...members], + 'name': name!, + 'image': image!, + 'members': [_client.state.user!.id, ...members!], }); await channel.watch(); return channel; } @override - Future createSimpleChat(String friendId) async { - final channel = - _client.channel('messaging', id: '${_client.state.user.id.hashCode}${friendId.hashCode}', extraData: { - 'members': [ - friendId, - _client.state.user.id, - ], - }); + Future createSimpleChat(String? friendId) async { + final channel = _client.channel('messaging', + id: '${_client.state.user!.id.hashCode}${friendId.hashCode}', + extraData: { + 'members': [ + friendId, + _client.state.user!.id, + ], + }); await channel.watch(); return channel; } @@ -81,6 +84,6 @@ class StreamApiLocalImpl extends StreamApiRepository { User(id: userId), token, ); - return _client.state.user.name != null && _client.state.user.name != userId; + return _client.state.user!.name != userId; } } diff --git a/packages/chatty/lib/data/local/upload_storage_local_impl.dart b/packages/chatty/lib/data/local/upload_storage_local_impl.dart index 8a3b804..a1b0f63 100644 --- a/packages/chatty/lib/data/local/upload_storage_local_impl.dart +++ b/packages/chatty/lib/data/local/upload_storage_local_impl.dart @@ -4,7 +4,7 @@ import 'package:stream_chatter/data/upload_storage_repository.dart'; class UploadStorageLocalImpl extends UploadStorageRepository { @override - Future uploadPhoto(File file, String path) async { + Future uploadPhoto(File? file, String path) async { return 'https://lh3.googleusercontent.com/a-/AOh14GjhqGZ-V7tNXS1pOIp9vbBij4OS9JbzxXgxgy1t=s600-k-no-rp-mo'; } } diff --git a/packages/chatty/lib/data/prod/auth_impl.dart b/packages/chatty/lib/data/prod/auth_impl.dart index 90547bb..cc8e139 100644 --- a/packages/chatty/lib/data/prod/auth_impl.dart +++ b/packages/chatty/lib/data/prod/auth_impl.dart @@ -7,7 +7,7 @@ class AuthImpl extends AuthRepository { FirebaseAuth _auth = FirebaseAuth.instance; @override - Future getAuthUser() async { + Future getAuthUser() async { final user = _auth.currentUser; if (user != null) { return AuthUser(user.uid); @@ -19,14 +19,21 @@ class AuthImpl extends AuthRepository { Future signIn() async { try { UserCredential userCredential; - final GoogleSignInAccount googleUser = await GoogleSignIn().signIn(); - final GoogleSignInAuthentication googleAuth = await googleUser.authentication; - final GoogleAuthCredential googleAuthCredential = GoogleAuthProvider.credential( + final GoogleSignInAccount? googleUser = await GoogleSignIn().signIn(); + + if (googleUser == null) { + throw Exception('login error'); + } + + final GoogleSignInAuthentication googleAuth = + await googleUser.authentication; + final GoogleAuthCredential googleAuthCredential = + GoogleAuthProvider.credential( accessToken: googleAuth.accessToken, idToken: googleAuth.idToken, - ); + ) as GoogleAuthCredential; userCredential = await _auth.signInWithCredential(googleAuthCredential); - final user = userCredential.user; + final user = userCredential.user!; return AuthUser(user.uid); } catch (e) { print(e); diff --git a/packages/chatty/lib/data/prod/persistent_storage_impl.dart b/packages/chatty/lib/data/prod/persistent_storage_impl.dart index 5d247d5..d701846 100644 --- a/packages/chatty/lib/data/prod/persistent_storage_impl.dart +++ b/packages/chatty/lib/data/prod/persistent_storage_impl.dart @@ -13,6 +13,6 @@ class PersistentStorageImpl extends PersistentStorageRepository { @override Future updateDarkMode(bool isDarkMode) async { final preference = await SharedPreferences.getInstance(); - return await preference.setBool(_isDarkMode, isDarkMode); + await preference.setBool(_isDarkMode, isDarkMode); } } diff --git a/packages/chatty/lib/data/prod/stream_api_impl.dart b/packages/chatty/lib/data/prod/stream_api_impl.dart index 1bcab14..fa65e76 100644 --- a/packages/chatty/lib/data/prod/stream_api_impl.dart +++ b/packages/chatty/lib/data/prod/stream_api_impl.dart @@ -11,7 +11,7 @@ class StreamApiImpl extends StreamApiRepository { final StreamChatClient _client; @override - Future connectUser(ChatUser user, String token) async { + Future connectUser(ChatUser user, String? token) async { Map extraData = {}; if (user.image != null) { extraData['image'] = user.image; @@ -21,7 +21,7 @@ class StreamApiImpl extends StreamApiRepository { } await _client.disconnect(); await _client.connectUser( - User(id: user.id, extraData: extraData), + User(id: user.id!, extraData: extraData as Map), token, ); return user; @@ -31,12 +31,12 @@ class StreamApiImpl extends StreamApiRepository { Future> getChatUsers() async { final result = await _client.queryUsers(); final chatUsers = result.users - .where((element) => element.id != _client.state.user.id) + .where((element) => element.id != _client.state.user!.id) .map( (e) => ChatUser( id: e.id, name: e.name, - image: e.extraData['image'], + image: e.extraData['image'] as String?, ), ) .toList(); @@ -44,10 +44,10 @@ class StreamApiImpl extends StreamApiRepository { } @override - Future getToken(String userId) async { + Future getToken(String userId) async { //TODO: use your own implementation in Production final response = await http.post( - 'your_backend_url', + Uri.parse('your_backend_url'), body: jsonEncode({'id': userId}), headers: { 'Content-Type': 'application/json; charset=UTF-8', @@ -62,25 +62,27 @@ class StreamApiImpl extends StreamApiRepository { } @override - Future createGroupChat(String id, String name, List members, {String image}) async { + Future createGroupChat(String id, String? name, List? members, + {String? image}) async { final channel = _client.channel('messaging', id: id, extraData: { - 'name': name, - 'image': image, - 'members': [_client.state.user.id, ...members], + 'name': name!, + 'image': image!, + 'members': [_client.state.user!.id, ...members!], }); await channel.watch(); return channel; } @override - Future createSimpleChat(String friendId) async { - final channel = - _client.channel('messaging', id: '${_client.state.user.id.hashCode}${friendId.hashCode}', extraData: { - 'members': [ - friendId, - _client.state.user.id, - ], - }); + Future createSimpleChat(String? friendId) async { + final channel = _client.channel('messaging', + id: '${_client.state.user!.id.hashCode}${friendId.hashCode}', + extraData: { + 'members': [ + friendId, + _client.state.user!.id, + ], + }); await channel.watch(); return channel; } @@ -97,6 +99,6 @@ class StreamApiImpl extends StreamApiRepository { User(id: userId), token, ); - return _client.state.user.name != null && _client.state.user.name != userId; + return _client.state.user!.name != userId; } } diff --git a/packages/chatty/lib/data/prod/upload_storage_impl.dart b/packages/chatty/lib/data/prod/upload_storage_impl.dart index 37f6830..a476f43 100644 --- a/packages/chatty/lib/data/prod/upload_storage_impl.dart +++ b/packages/chatty/lib/data/prod/upload_storage_impl.dart @@ -4,9 +4,9 @@ import 'package:stream_chatter/data/upload_storage_repository.dart'; class UploadStorageImpl extends UploadStorageRepository { @override - Future uploadPhoto(File file, String path) async { + Future uploadPhoto(File? file, String path) async { final ref = firebase_storage.FirebaseStorage.instance.ref(path); - final uploadTask = ref.putFile(file); + final uploadTask = ref.putFile(file!); await uploadTask; return await ref.getDownloadURL(); } diff --git a/packages/chatty/lib/data/stream_api_repository.dart b/packages/chatty/lib/data/stream_api_repository.dart index 4ae31cb..b722932 100644 --- a/packages/chatty/lib/data/stream_api_repository.dart +++ b/packages/chatty/lib/data/stream_api_repository.dart @@ -3,10 +3,12 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; abstract class StreamApiRepository { Future> getChatUsers(); - Future getToken(String userId); + Future getToken(String userId); Future connectIfExist(String userId); - Future connectUser(ChatUser user, String token); - Future createGroupChat(String channelId, String name, List members, {String image}); - Future createSimpleChat(String friendId); + Future connectUser(ChatUser user, String? token); + Future createGroupChat( + String channelId, String? name, List? members, + {String? image}); + Future createSimpleChat(String? friendId); Future logout(); } diff --git a/packages/chatty/lib/data/upload_storage_repository.dart b/packages/chatty/lib/data/upload_storage_repository.dart index 6876138..88590ee 100644 --- a/packages/chatty/lib/data/upload_storage_repository.dart +++ b/packages/chatty/lib/data/upload_storage_repository.dart @@ -1,5 +1,5 @@ import 'dart:io'; abstract class UploadStorageRepository { - Future uploadPhoto(File file, String path); + Future uploadPhoto(File? file, String path); } diff --git a/packages/chatty/lib/dependencies.dart b/packages/chatty/lib/dependencies.dart index f6d6414..d60194c 100644 --- a/packages/chatty/lib/dependencies.dart +++ b/packages/chatty/lib/dependencies.dart @@ -18,10 +18,13 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; List buildRepositories(StreamChatClient client) { //TODO: Here you can use your local implementations of your repositories return [ - RepositoryProvider(create: (_) => StreamApiImpl(client)), - RepositoryProvider(create: (_) => PersistentStorageImpl()), + RepositoryProvider( + create: (_) => StreamApiImpl(client)), + RepositoryProvider( + create: (_) => PersistentStorageImpl()), RepositoryProvider(create: (_) => AuthImpl()), - RepositoryProvider(create: (_) => UploadStorageImpl()), + RepositoryProvider( + create: (_) => UploadStorageImpl()), RepositoryProvider(create: (_) => ImagePickerImpl()), RepositoryProvider( create: (context) => ProfileSignInUseCase( diff --git a/packages/chatty/lib/domain/models/chat_user.dart b/packages/chatty/lib/domain/models/chat_user.dart index 05a0a61..9ad376b 100644 --- a/packages/chatty/lib/domain/models/chat_user.dart +++ b/packages/chatty/lib/domain/models/chat_user.dart @@ -1,6 +1,6 @@ class ChatUser { const ChatUser({this.name, this.image, this.id}); - final String name; - final String image; - final String id; + final String? name; + final String? image; + final String? id; } diff --git a/packages/chatty/lib/domain/usecases/create_group_usecase.dart b/packages/chatty/lib/domain/usecases/create_group_usecase.dart index 82122a2..4b0dc78 100644 --- a/packages/chatty/lib/domain/usecases/create_group_usecase.dart +++ b/packages/chatty/lib/domain/usecases/create_group_usecase.dart @@ -7,9 +7,9 @@ import 'package:uuid/uuid.dart'; class CreateGroupInput { CreateGroupInput({this.imageFile, this.name, this.members}); - final File imageFile; - final String name; - final List members; + final File? imageFile; + final String? name; + final List? members; } class CreateGroupUseCase { @@ -23,9 +23,10 @@ class CreateGroupUseCase { Future createGroup(CreateGroupInput input) async { final channelId = Uuid().v4(); - String image; + String? image; 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( channelId, diff --git a/packages/chatty/lib/domain/usecases/profile_sign_in_usecase.dart b/packages/chatty/lib/domain/usecases/profile_sign_in_usecase.dart index 8450581..19969d6 100644 --- a/packages/chatty/lib/domain/usecases/profile_sign_in_usecase.dart +++ b/packages/chatty/lib/domain/usecases/profile_sign_in_usecase.dart @@ -5,10 +5,12 @@ import 'package:stream_chatter/data/stream_api_repository.dart'; import 'package:stream_chatter/data/upload_storage_repository.dart'; import 'package:stream_chatter/domain/models/chat_user.dart'; +import '../exceptions/auth_exception.dart'; + class ProfileInput { ProfileInput({this.imageFile, this.name}); - final File imageFile; - final String name; + final File? imageFile; + final String? name; } class ProfileSignInUseCase { @@ -24,11 +26,21 @@ class ProfileSignInUseCase { Future verify(ProfileInput input) async { final auth = await _authRepository.getAuthUser(); - final token = await _streamApiRepository.getToken(auth.id); - String image; - if (input.imageFile != null) { - image = await _uploadStorageRepository.uploadPhoto(input.imageFile, 'users/${auth.id}'); + if (auth == null) { + throw AuthException(AuthErrorCode.not_auth); } - await _streamApiRepository.connectUser(ChatUser(name: input.name, id: auth.id, image: image), token); + final token = await _streamApiRepository.getToken(auth.id); + String? image; + if (input.imageFile != null) { + image = await _uploadStorageRepository.uploadPhoto( + input.imageFile, 'users/${auth.id}'); + } + await _streamApiRepository.connectUser( + ChatUser( + name: input.name, + id: auth.id, + image: image, + ), + token); } } diff --git a/packages/chatty/lib/main.dart b/packages/chatty/lib/main.dart index 479c1c8..817010d 100644 --- a/packages/chatty/lib/main.dart +++ b/packages/chatty/lib/main.dart @@ -34,7 +34,8 @@ class MyApp extends StatelessWidget { return StreamChat( child: child, client: _streamChatClient, - streamChatThemeData: StreamChatThemeData.fromTheme(Theme.of(context)).copyWith( + streamChatThemeData: + StreamChatThemeData.fromTheme(Theme.of(context)).copyWith( ownMessageTheme: MessageTheme( messageBackgroundColor: Theme.of(context).accentColor, messageText: TextStyle(color: Colors.white), diff --git a/packages/chatty/lib/navigator_utils.dart b/packages/chatty/lib/navigator_utils.dart index 2d5c27f..731c4c1 100644 --- a/packages/chatty/lib/navigator_utils.dart +++ b/packages/chatty/lib/navigator_utils.dart @@ -18,5 +18,7 @@ Future pushAndReplaceToPage(BuildContext context, Widget widget) async { Future popAllAndPush(BuildContext context, Widget widget) async { await Navigator.pushAndRemoveUntil( - context, MaterialPageRoute(builder: (BuildContext context) => widget), ModalRoute.withName('/')); + context, + MaterialPageRoute(builder: (BuildContext context) => widget), + ModalRoute.withName('/')); } diff --git a/packages/chatty/lib/ui/common/avatar_image_view.dart b/packages/chatty/lib/ui/common/avatar_image_view.dart index 76ee22a..08a2fbf 100644 --- a/packages/chatty/lib/ui/common/avatar_image_view.dart +++ b/packages/chatty/lib/ui/common/avatar_image_view.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; class AvatarImageView extends StatelessWidget { - const AvatarImageView({Key key, this.onTap, this.child}) : super(key: key); - final Widget child; - final VoidCallback onTap; + const AvatarImageView({Key? key, this.onTap, this.child}) : super(key: key); + final Widget? child; + final VoidCallback? onTap; @override Widget build(BuildContext context) { diff --git a/packages/chatty/lib/ui/common/loading_view.dart b/packages/chatty/lib/ui/common/loading_view.dart index 9e1cded..6ff3f12 100644 --- a/packages/chatty/lib/ui/common/loading_view.dart +++ b/packages/chatty/lib/ui/common/loading_view.dart @@ -5,8 +5,8 @@ class LoadingView extends StatelessWidget { final Widget child; const LoadingView({ - Key key, - @required this.child, + Key? key, + required this.child, this.isLoading = false, }) : super(key: key); diff --git a/packages/chatty/lib/ui/common/my_channel_preview.dart b/packages/chatty/lib/ui/common/my_channel_preview.dart index b561e45..2e168b5 100644 --- a/packages/chatty/lib/ui/common/my_channel_preview.dart +++ b/packages/chatty/lib/ui/common/my_channel_preview.dart @@ -1,3 +1,4 @@ +import 'package:collection/collection.dart' show IterableExtension; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:jiffy/jiffy.dart'; @@ -30,22 +31,22 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// Modify it to change the widget appearance. class MyChannelPreview extends StatelessWidget { /// Function called when tapping this widget - final void Function(Channel) onTap; + final void Function(Channel)? onTap; /// Function called when long pressing this widget - final void Function(Channel) onLongPress; + final void Function(Channel)? onLongPress; /// Channel displayed final Channel channel; /// The function called when the image is tapped - final VoidCallback onImageTap; + final VoidCallback? onImageTap; - final String heroTag; + final String? heroTag; MyChannelPreview({ - @required this.channel, - Key key, + required this.channel, + Key? key, this.onTap, this.onLongPress, this.onImageTap, @@ -59,24 +60,24 @@ class MyChannelPreview extends StatelessWidget { initialData: channel.isMuted, builder: (context, snapshot) { return Opacity( - opacity: snapshot.data ? 0.5 : 1, + opacity: snapshot.data! ? 0.5 : 1, child: ListTile( contentPadding: const EdgeInsets.symmetric( horizontal: 8, ), onTap: () { if (onTap != null) { - onTap(channel); + onTap!(channel); } }, onLongPress: () { if (onLongPress != null) { - onLongPress(channel); + onLongPress!(channel); } }, leading: Material( child: Hero( - tag: heroTag, + tag: heroTag!, child: StreamChannel( channel: channel, child: ChannelImage( @@ -90,16 +91,18 @@ class MyChannelPreview extends StatelessWidget { children: [ Flexible( child: ChannelName( - textStyle: StreamChatTheme.of(context).channelPreviewTheme.title, + textStyle: + StreamChatTheme.of(context).channelPreviewTheme.title, ), ), StreamBuilder>( - stream: channel.state.membersStream, - initialData: channel.state.members, + stream: channel.state!.membersStream, + initialData: channel.state!.members, builder: (context, snapshot) { if (!snapshot.hasData || - snapshot.data.isEmpty || - !snapshot.data.any((Member e) => e.user.id == channel.client.state.user.id)) { + snapshot.data!.isEmpty || + !snapshot.data!.any((Member e) => + e.user!.id == channel.client.state.user!.id)) { return SizedBox(); } return ChannelUnreadIndicator( @@ -114,20 +117,26 @@ class MyChannelPreview extends StatelessWidget { Flexible(child: _buildSubtitle(context)), Builder( builder: (context) { - final lastMessage = channel.state.messages.lastWhere( + final lastMessage = + channel.state!.messages.lastWhereOrNull( (m) => !m.isDeleted && m.shadowed != true, - orElse: () => null, ); - if (lastMessage?.user?.id == StreamChat.of(context).user.id) { + if (lastMessage?.user?.id == + StreamChat.of(context).user!.id) { return Padding( padding: const EdgeInsets.only(right: 4.0), child: SendingIndicator( - message: lastMessage, - size: StreamChatTheme.of(context).channelPreviewTheme.indicatorIconSize, - isMessageRead: channel.state.read - ?.where((element) => element.user.id != channel.client.state.user.id) - ?.where((element) => element.lastRead.isAfter(lastMessage.createdAt)) - ?.isNotEmpty == + message: lastMessage!, + size: StreamChatTheme.of(context) + .channelPreviewTheme + .indicatorIconSize, + isMessageRead: channel.state!.read + ?.where((element) => + element.user.id != + channel.client.state.user!.id) + .where((element) => element.lastRead + .isAfter(lastMessage.createdAt)) + .isNotEmpty == true, ), ); @@ -144,21 +153,22 @@ class MyChannelPreview extends StatelessWidget { } Widget _buildDate(BuildContext context) { - return StreamBuilder( + return StreamBuilder( stream: channel.lastMessageAtStream, initialData: channel.lastMessageAt, builder: (context, snapshot) { if (!snapshot.hasData) { return SizedBox(); } - final lastMessageAt = snapshot.data.toLocal(); + final lastMessageAt = snapshot.data!.toLocal(); String stringDate; final now = DateTime.now(); 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'); } else if (lastMessageAt.millisecondsSinceEpoch >= startOfDay.subtract(Duration(days: 1)).millisecondsSinceEpoch) { @@ -187,8 +197,14 @@ class MyChannelPreview extends StatelessWidget { ), Text( ' Channel is muted', - style: StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith( - color: StreamChatTheme.of(context).channelPreviewTheme.subtitle.color, + style: StreamChatTheme.of(context) + .channelPreviewTheme + .subtitle! + .copyWith( + color: StreamChatTheme.of(context) + .channelPreviewTheme + .subtitle! + .color, ), ), ], @@ -197,52 +213,64 @@ class MyChannelPreview extends StatelessWidget { return TypingIndicator( channel: channel, alternativeWidget: _buildLastMessage(context), - style: StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith( - color: StreamChatTheme.of(context).channelPreviewTheme.subtitle.color, + style: StreamChatTheme.of(context).channelPreviewTheme.subtitle!.copyWith( + color: + StreamChatTheme.of(context).channelPreviewTheme.subtitle!.color, ), ); } Widget _buildLastMessage(BuildContext context) { - return StreamBuilder>( - stream: channel.state.messagesStream, - initialData: channel.state.messages, + return StreamBuilder?>( + stream: channel.state!.messagesStream, + initialData: channel.state!.messages, builder: (context, snapshot) { - final lastMessage = snapshot.data?.lastWhere((m) => m.shadowed != true && !m.isDeleted, orElse: () => null); + final lastMessage = snapshot.data + ?.lastWhereOrNull((m) => m.shadowed != true && !m.isDeleted); if (lastMessage == null) { return SizedBox(); } var text = lastMessage.text; - if (lastMessage.attachments != null) { - final parts = [ - ...lastMessage.attachments.map((e) { - if (e.type == 'image') { - return '📷'; - } else if (e.type == 'video') { - return '🎬'; - } else if (e.type == 'giphy') { - return '[GIF]'; - } - return e == lastMessage.attachments.last ? (e.title ?? 'File') : '${e.title ?? 'File'} , '; - }).where((e) => e != null), - lastMessage.text ?? '', - ]; + final parts = [ + ...lastMessage.attachments.map((e) { + if (e.type == 'image') { + return '📷'; + } else if (e.type == 'video') { + return '🎬'; + } else if (e.type == 'giphy') { + return '[GIF]'; + } + return e == lastMessage.attachments.last + ? (e.title ?? 'File') + : '${e.title ?? 'File'} , '; + }), + lastMessage.text ?? '', + ]; - text = parts.join(' '); - } + text = parts.join(' '); return Text.rich( _getDisplayText( text, lastMessage.mentionedUsers, lastMessage.attachments, - StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith( - color: StreamChatTheme.of(context).channelPreviewTheme.subtitle.color, - fontStyle: (lastMessage.isSystem || lastMessage.isDeleted) ? FontStyle.italic : FontStyle.normal), - StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith( - color: StreamChatTheme.of(context).channelPreviewTheme.subtitle.color, - fontStyle: (lastMessage.isSystem || lastMessage.isDeleted) ? FontStyle.italic : FontStyle.normal, + StreamChatTheme.of(context).channelPreviewTheme.subtitle!.copyWith( + color: StreamChatTheme.of(context) + .channelPreviewTheme + .subtitle! + .color, + fontStyle: (lastMessage.isSystem || lastMessage.isDeleted) + ? FontStyle.italic + : FontStyle.normal), + StreamChatTheme.of(context).channelPreviewTheme.subtitle!.copyWith( + color: StreamChatTheme.of(context) + .channelPreviewTheme + .subtitle! + .color, + fontStyle: (lastMessage.isSystem || lastMessage.isDeleted) + ? FontStyle.italic + : FontStyle.normal, fontWeight: FontWeight.bold), ), maxLines: 1, @@ -252,19 +280,25 @@ class MyChannelPreview extends StatelessWidget { ); } - TextSpan _getDisplayText(String text, List mentions, List attachments, TextStyle normalTextStyle, + TextSpan _getDisplayText( + String text, + List mentions, + List attachments, + TextStyle normalTextStyle, TextStyle mentionsTextStyle) { var textList = text.split(' '); var resList = []; for (var e in textList) { - if (mentions != null && mentions.isNotEmpty && mentions.any((element) => '@${element.name}' == e)) { + if (mentions.isNotEmpty && + mentions.any((element) => '@${element.name}' == e)) { resList.add(TextSpan( text: '$e ', style: mentionsTextStyle, )); - } else if (attachments != null && - attachments.isNotEmpty && - attachments.where((e) => e.title != null).any((element) => element.title == e)) { + } else if (attachments.isNotEmpty && + attachments + .where((e) => e.title != null) + .any((element) => element.title == e)) { resList.add(TextSpan( text: '$e ', style: normalTextStyle.copyWith(fontStyle: FontStyle.italic), @@ -283,8 +317,8 @@ class MyChannelPreview extends StatelessWidget { class ChannelUnreadIndicator extends StatelessWidget { const ChannelUnreadIndicator({ - Key key, - @required this.channel, + Key? key, + required this.channel, }) : super(key: key); final Channel channel; @@ -292,8 +326,8 @@ class ChannelUnreadIndicator extends StatelessWidget { @override Widget build(BuildContext context) { return StreamBuilder( - stream: channel.state.unreadCountStream, - initialData: channel.state.unreadCount, + stream: channel.state!.unreadCountStream, + initialData: channel.state!.unreadCount, builder: (context, snapshot) { if (!snapshot.hasData || snapshot.data == 0) { return SizedBox(); @@ -301,7 +335,9 @@ class ChannelUnreadIndicator extends StatelessWidget { return Material( borderRadius: BorderRadius.circular(8), - color: StreamChatTheme.of(context).channelPreviewTheme.unreadCounterColor, + color: StreamChatTheme.of(context) + .channelPreviewTheme + .unreadCounterColor, child: Padding( padding: const EdgeInsets.only( left: 5.0, @@ -311,7 +347,7 @@ class ChannelUnreadIndicator extends StatelessWidget { ), child: Center( child: Text( - '${snapshot.data > 99 ? '99+' : snapshot.data}', + '${snapshot.data! > 99 ? '99+' : snapshot.data}', style: TextStyle( fontSize: 11, color: Colors.white, diff --git a/packages/chatty/lib/ui/home/chat/chat_view.dart b/packages/chatty/lib/ui/home/chat/chat_view.dart index b9f4ea7..0bed83a 100644 --- a/packages/chatty/lib/ui/home/chat/chat_view.dart +++ b/packages/chatty/lib/ui/home/chat/chat_view.dart @@ -23,11 +23,10 @@ class ChatView extends StatelessWidget { ), body: ChannelsBloc( child: ChannelListView( - filter: { - 'members': { - '\$in': [StreamChat.of(context).user?.id], - } - }, + filter: Filter.in_( + 'members', + [StreamChat.of(context).user!.id], + ), sort: [SortOption('last_message_at')], channelPreviewBuilder: (context, channel) { return Container( @@ -36,20 +35,22 @@ class ChatView extends StatelessWidget { channel: channel, heroTag: channel.id, onImageTap: () { - String name; - String image; + String? name; + String? image; final currentUser = StreamChat.of(context).client.state.user; if (channel.isGroup) { name = channel.extraData['name']; image = channel.extraData['image']; } else { - final friend = - channel.state.members.where((element) => element.userId != currentUser.id).first.user; + final friend = channel.state!.members + .where((element) => element.userId != currentUser!.id) + .first + .user!; name = friend.name; - image = friend.extraData['image']; + image = friend.extraData['image'] as String?; } - return Navigator.of(context).push( + Navigator.of(context).push( PageRouteBuilder( barrierColor: Colors.black45, barrierDismissible: true, @@ -108,15 +109,15 @@ class ChannelPage extends StatelessWidget { class ChatDetailView extends StatelessWidget { const ChatDetailView({ - Key key, + Key? key, this.image, this.name, this.channelId, }) : super(key: key); - final String image; - final String name; - final String channelId; + final String? image; + final String? name; + final String? channelId; @override Widget build(BuildContext context) { @@ -133,10 +134,10 @@ class ChatDetailView extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Hero( - tag: channelId, + tag: channelId!, child: ClipOval( child: Image.network( - image, + image!, height: 180, width: 180, fit: BoxFit.cover, @@ -144,7 +145,7 @@ class ChatDetailView extends StatelessWidget { ), ), Text( - name, + name!, style: TextStyle( fontWeight: FontWeight.w700, fontSize: 22, diff --git a/packages/chatty/lib/ui/home/chat/selection/friends_selection_cubit.dart b/packages/chatty/lib/ui/home/chat/selection/friends_selection_cubit.dart index 3f35080..d43c222 100644 --- a/packages/chatty/lib/ui/home/chat/selection/friends_selection_cubit.dart +++ b/packages/chatty/lib/ui/home/chat/selection/friends_selection_cubit.dart @@ -13,21 +13,27 @@ class FriendsSelectionCubit extends Cubit> { FriendsSelectionCubit(this._streamApiRepository) : super([]); final StreamApiRepository _streamApiRepository; - List get selectedUsers => state.where((element) => element.selected).toList(); + List get selectedUsers => + state.where((element) => element.selected).toList(); Future init() async { - final chatUsers = (await _streamApiRepository.getChatUsers()).map((e) => ChatUserState(e)).toList(); + final chatUsers = (await _streamApiRepository.getChatUsers()) + .map((e) => ChatUserState(e)) + .toList(); emit(chatUsers); } void selectUser(ChatUserState chatUser) { - final index = state.indexWhere((element) => element.chatUser.id == chatUser.chatUser.id); - state[index] = ChatUserState(state[index].chatUser, selected: !chatUser.selected); + final index = state + .indexWhere((element) => element.chatUser.id == chatUser.chatUser.id); + state[index] = + ChatUserState(state[index].chatUser, selected: !chatUser.selected); emit(List.from(state)); } Future createFriendChannel(ChatUserState chatUserState) async { - return await _streamApiRepository.createSimpleChat(chatUserState.chatUser.id); + return await _streamApiRepository + .createSimpleChat(chatUserState.chatUser.id); } } diff --git a/packages/chatty/lib/ui/home/chat/selection/friends_selection_view.dart b/packages/chatty/lib/ui/home/chat/selection/friends_selection_view.dart index 4cafcef..3fdb50b 100644 --- a/packages/chatty/lib/ui/home/chat/selection/friends_selection_view.dart +++ b/packages/chatty/lib/ui/home/chat/selection/friends_selection_view.dart @@ -7,8 +7,11 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; class FriendsSelectionView extends StatelessWidget { - void _createFriendChannel(BuildContext context, ChatUserState chatUserState) async { - final channel = await context.read().createFriendChannel(chatUserState); + void _createFriendChannel( + BuildContext context, ChatUserState chatUserState) async { + final channel = await context + .read() + .createFriendChannel(chatUserState); pushAndReplaceToPage( context, Scaffold( @@ -26,19 +29,23 @@ class FriendsSelectionView extends StatelessWidget { final accentColor = Theme.of(context).accentColor; return MultiBlocProvider( providers: [ - BlocProvider(create: (context) => FriendsSelectionCubit(context.read())..init()), + BlocProvider( + create: (context) => FriendsSelectionCubit(context.read())..init()), BlocProvider(create: (_) => FriendsGroupCubit()), ], child: BlocBuilder(builder: (context, isGroup) { - return BlocBuilder>(builder: (context, snapshot) { - final selectedUsers = context.read().selectedUsers; + return BlocBuilder>( + builder: (context, snapshot) { + final selectedUsers = + context.read().selectedUsers; return Scaffold( floatingActionButton: isGroup && selectedUsers.isNotEmpty ? FloatingActionButton( child: Icon(Icons.arrow_right_alt_rounded), onPressed: () { - pushAndReplaceToPage(context, GroupSelectionView(selectedUsers)); + pushAndReplaceToPage( + context, GroupSelectionView(selectedUsers)); }) : null, backgroundColor: Theme.of(context).canvasColor, @@ -91,12 +98,14 @@ class FriendsSelectionView extends StatelessWidget { backgroundColor: accentColor, 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'), ) else if (isGroup && selectedUsers.isEmpty) 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( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -123,7 +132,8 @@ class FriendsSelectionView extends StatelessWidget { itemBuilder: (context, index) { final chatUserState = selectedUsers[index]; return Padding( - padding: const EdgeInsets.symmetric(horizontal: 13.0), + padding: const EdgeInsets.symmetric( + horizontal: 13.0), child: Stack( clipBehavior: Clip.none, children: [ @@ -132,20 +142,24 @@ class FriendsSelectionView extends StatelessWidget { children: [ CircleAvatar( radius: 30, - backgroundImage: NetworkImage(chatUserState.chatUser.image), + backgroundImage: NetworkImage( + chatUserState.chatUser.image!), ), - Text(chatUserState.chatUser.name), + Text(chatUserState.chatUser.name!), ], ), Positioned( bottom: 40, right: -4, child: InkWell( - onTap: () => context.read().selectUser(chatUserState), + onTap: () => context + .read() + .selectUser(chatUserState), child: CircleAvatar( radius: 9, backgroundColor: accentColor, - child: Icon(Icons.close_rounded, size: 12), + child: Icon(Icons.close_rounded, + size: 12), ), ), ), @@ -163,15 +177,18 @@ class FriendsSelectionView extends StatelessWidget { _createFriendChannel(context, chatUserState); }, leading: CircleAvatar( - backgroundImage: NetworkImage(chatUserState.chatUser.image), + backgroundImage: + NetworkImage(chatUserState.chatUser.image!), ), - title: Text(chatUserState.chatUser.name), + title: Text(chatUserState.chatUser.name!), trailing: isGroup ? Checkbox( value: chatUserState.selected, onChanged: (val) { print('select user for group'); - context.read().selectUser(chatUserState); + context + .read() + .selectUser(chatUserState); }, ) : null, diff --git a/packages/chatty/lib/ui/home/chat/selection/group_selection_cubit.dart b/packages/chatty/lib/ui/home/chat/selection/group_selection_cubit.dart index 79ac5eb..db72b1a 100644 --- a/packages/chatty/lib/ui/home/chat/selection/group_selection_cubit.dart +++ b/packages/chatty/lib/ui/home/chat/selection/group_selection_cubit.dart @@ -13,8 +13,8 @@ class GroupSelectionState { this.channel, this.isLoading = false, }); - final File file; - final Channel channel; + final File? file; + final Channel? channel; final bool isLoading; } diff --git a/packages/chatty/lib/ui/home/chat/selection/group_selection_view.dart b/packages/chatty/lib/ui/home/chat/selection/group_selection_view.dart index 7f336f9..42f65e5 100644 --- a/packages/chatty/lib/ui/home/chat/selection/group_selection_view.dart +++ b/packages/chatty/lib/ui/home/chat/selection/group_selection_view.dart @@ -21,13 +21,14 @@ class GroupSelectionView extends StatelessWidget { context.read(), context.read(), ), - child: BlocConsumer(listener: (context, snapshot) { + child: BlocConsumer( + listener: (context, snapshot) { if (snapshot.channel != null) { pushAndReplaceToPage( context, Scaffold( body: StreamChannel( - channel: snapshot.channel, + channel: snapshot.channel!, child: ChannelPage(), ), ), @@ -59,9 +60,9 @@ class GroupSelectionView extends StatelessWidget { children: [ AvatarImageView( onTap: context.read().pickImage, - child: snapshot?.file != null + child: snapshot.file != null ? Image.file( - snapshot?.file, + snapshot.file!, fit: BoxFit.cover, ) : Icon( @@ -76,9 +77,12 @@ class GroupSelectionView extends StatelessWidget { vertical: 20, ), child: TextField( - controller: context.read().nameTextController, + controller: + context.read().nameTextController, decoration: InputDecoration( - fillColor: Theme.of(context).bottomNavigationBarTheme.backgroundColor, + fillColor: Theme.of(context) + .bottomNavigationBarTheme + .backgroundColor, hintText: 'Name of the group', hintStyle: TextStyle( fontSize: 13, @@ -99,9 +103,10 @@ class GroupSelectionView extends StatelessWidget { children: [ CircleAvatar( radius: 30, - backgroundImage: NetworkImage(chatUserState.chatUser.image), + backgroundImage: + NetworkImage(chatUserState.chatUser.image!), ), - Text(chatUserState.chatUser.name), + Text(chatUserState.chatUser.name!), ], ), ); diff --git a/packages/chatty/lib/ui/home/home_view.dart b/packages/chatty/lib/ui/home/home_view.dart index 5199415..0492889 100644 --- a/packages/chatty/lib/ui/home/home_view.dart +++ b/packages/chatty/lib/ui/home/home_view.dart @@ -36,7 +36,7 @@ class HomeView extends StatelessWidget { class HomeNavigationBar extends StatelessWidget { const HomeNavigationBar({ - Key key, + Key? key, }) : super(key: key); @override @@ -61,7 +61,9 @@ class HomeNavigationBar extends StatelessWidget { child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(25), - color: Theme.of(context).bottomNavigationBarTheme.backgroundColor, + color: Theme.of(context) + .bottomNavigationBarTheme + .backgroundColor, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, @@ -108,22 +110,24 @@ class HomeNavigationBar extends StatelessWidget { class _HomeNavItem extends StatelessWidget { const _HomeNavItem({ - Key key, + Key? key, this.iconData, this.text, this.onTap, this.selected = false, }) : super(key: key); - final IconData iconData; - final String text; - final VoidCallback onTap; + final IconData? iconData; + final String? text; + final VoidCallback? onTap; final bool selected; @override Widget build(BuildContext context) { - final selectedColor = Theme.of(context).bottomNavigationBarTheme.selectedItemColor; - final unselectedColor = Theme.of(context).bottomNavigationBarTheme.unselectedItemColor; + final selectedColor = + Theme.of(context).bottomNavigationBarTheme.selectedItemColor; + final unselectedColor = + Theme.of(context).bottomNavigationBarTheme.unselectedItemColor; final color = selected ? selectedColor : unselectedColor; return GestureDetector( onTap: onTap, @@ -131,7 +135,7 @@ class _HomeNavItem extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Icon(iconData, color: color), - Text(text, style: TextStyle(color: color)), + Text(text!, style: TextStyle(color: color)), ], ), ); diff --git a/packages/chatty/lib/ui/home/settings/settings_view.dart b/packages/chatty/lib/ui/home/settings/settings_view.dart index 77ce96a..4347749 100644 --- a/packages/chatty/lib/ui/home/settings/settings_view.dart +++ b/packages/chatty/lib/ui/home/settings/settings_view.dart @@ -10,13 +10,14 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; class SettingsView extends StatelessWidget { @override Widget build(BuildContext context) { - final user = StreamChat.of(context).client.state.user; - final image = user?.extraData['image']; + final user = StreamChat.of(context).client.state.user!; + final image = user.extraData['image']; final textColor = Theme.of(context).appBarTheme.color; return MultiBlocProvider( providers: [ BlocProvider( - create: (_) => SettingsSwitchCubit(context.read().isDark), + create: (_) => + SettingsSwitchCubit(context.read().isDark), ), BlocProvider( create: (_) => SettingsLogoutCubit(context.read()), @@ -47,7 +48,7 @@ class SettingsView extends StatelessWidget { onTap: () => null, child: image != null ? Image.network( - image, + image as String, fit: BoxFit.cover, ) : Icon( @@ -75,11 +76,14 @@ class SettingsView extends StatelessWidget { ), ), Spacer(), - BlocBuilder(builder: (context, snapshot) { + BlocBuilder( + builder: (context, snapshot) { return Switch( value: snapshot, onChanged: (val) { - context.read().onChangeDarkMode(val); + context + .read() + .onChangeDarkMode(val); context.read().updateTheme(val); }, ); diff --git a/packages/chatty/lib/ui/profile_verify/profile_verify_cubit.dart b/packages/chatty/lib/ui/profile_verify/profile_verify_cubit.dart index cc82668..2f32aa7 100644 --- a/packages/chatty/lib/ui/profile_verify/profile_verify_cubit.dart +++ b/packages/chatty/lib/ui/profile_verify/profile_verify_cubit.dart @@ -11,7 +11,7 @@ class ProfileState { this.success = false, this.loading = false, }); - final File file; + final File? file; final bool success; final bool loading; } diff --git a/packages/chatty/lib/ui/profile_verify/profile_verify_view.dart b/packages/chatty/lib/ui/profile_verify/profile_verify_view.dart index 3a475c3..6b6c2bd 100644 --- a/packages/chatty/lib/ui/profile_verify/profile_verify_view.dart +++ b/packages/chatty/lib/ui/profile_verify/profile_verify_view.dart @@ -13,7 +13,8 @@ class ProfileVerifyView extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => ProfileVerifyCubit(context.read(), context.read()), - child: BlocConsumer(listener: (context, snapshot) { + child: BlocConsumer( + listener: (context, snapshot) { if (snapshot.success) { pushAndReplaceToPage(context, HomeView()); } @@ -38,7 +39,7 @@ class ProfileVerifyView extends StatelessWidget { onTap: context.read().pickImage, child: snapshot.file != null ? Image.file( - snapshot.file, + snapshot.file!, fit: BoxFit.cover, ) : Icon( @@ -59,9 +60,12 @@ class ProfileVerifyView extends StatelessWidget { vertical: 20, ), child: TextField( - controller: context.read().nameController, + controller: + context.read().nameController, decoration: InputDecoration( - fillColor: Theme.of(context).bottomNavigationBarTheme.backgroundColor, + fillColor: Theme.of(context) + .bottomNavigationBarTheme + .backgroundColor, hintText: 'Or just how people now you', hintStyle: TextStyle( fontSize: 13, diff --git a/packages/chatty/lib/ui/sign_in/sign_in_cubit.dart b/packages/chatty/lib/ui/sign_in/sign_in_cubit.dart index 10d23b2..ee341f2 100644 --- a/packages/chatty/lib/ui/sign_in/sign_in_cubit.dart +++ b/packages/chatty/lib/ui/sign_in/sign_in_cubit.dart @@ -20,10 +20,8 @@ class SignInCubit extends Cubit { emit(SignInState.existing_user); } } catch (ex) { - final result = await _loginUseCase.signIn(); - if (result != null) { - emit(SignInState.none); - } + _loginUseCase.signIn(); + emit(SignInState.none); } } } diff --git a/packages/chatty/lib/ui/sign_in/sign_in_view.dart b/packages/chatty/lib/ui/sign_in/sign_in_view.dart index 41ff317..d8f2a9e 100644 --- a/packages/chatty/lib/ui/sign_in/sign_in_view.dart +++ b/packages/chatty/lib/ui/sign_in/sign_in_view.dart @@ -11,7 +11,8 @@ class SignInView extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => SignInCubit(context.read()), - child: BlocConsumer(listener: (context, snapshot) { + child: + BlocConsumer(listener: (context, snapshot) { if (snapshot == SignInState.none) { pushAndReplaceToPage(context, ProfileVerifyView()); } else { @@ -59,7 +60,9 @@ class SignInView extends StatelessWidget { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), - color: Theme.of(context).bottomNavigationBarTheme.backgroundColor, + color: Theme.of(context) + .bottomNavigationBarTheme + .backgroundColor, child: InkWell( onTap: () { context.read().signIn(); diff --git a/packages/chatty/lib/ui/themes.dart b/packages/chatty/lib/ui/themes.dart index c99082d..ef9412b 100644 --- a/packages/chatty/lib/ui/themes.dart +++ b/packages/chatty/lib/ui/themes.dart @@ -44,7 +44,7 @@ class Themes { selectedItemColor: primaryColor, unselectedItemColor: Colors.grey[300], ), - textSelectionColor: Colors.white, + textSelectionTheme: TextSelectionThemeData(selectionColor: Colors.white), // switch active color toggleableActiveColor: primaryColor, canvasColor: backgroundDarkColor, diff --git a/packages/chatty/pubspec.yaml b/packages/chatty/pubspec.yaml index 6327bfd..c0f4552 100644 --- a/packages/chatty/pubspec.yaml +++ b/packages/chatty/pubspec.yaml @@ -4,22 +4,23 @@ publish_to: 'none' version: 1.0.0+1 environment: - sdk: ">=2.7.0 <3.0.0" + sdk: '>=2.12.0 <3.0.0' dependencies: flutter: sdk: flutter - flutter_bloc: 6.1.2 - stream_chat_flutter: 1.3.0-beta - uuid: 2.2.2 + flutter_bloc: ^7.0.0 + stream_chat_flutter: ^2.0.0-nullsafety.3 + uuid: ^3.0.4 - firebase_core: 0.7.0 - google_sign_in: 4.5.9 - firebase_auth: 0.20.0+1 - firebase_storage: 7.0.0 - shared_preferences: 0.5.12+4 + firebase_core: ^1.2.0 + google_sign_in: ^5.0.3 + firebase_auth: ^1.2.0 + firebase_storage: ^8.1.0 + shared_preferences: ^2.0.5 http: any + collection: ^1.15.0-nullsafety.4 dev_dependencies: flutter_test: diff --git a/packages/imessage/lib/channel_image.dart b/packages/imessage/lib/channel_image.dart index 65b23ff..d7b6343 100644 --- a/packages/imessage/lib/channel_image.dart +++ b/packages/imessage/lib/channel_image.dart @@ -4,7 +4,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart' show Channel; import 'package:imessage/utils.dart'; class ChannelImage extends StatelessWidget { - const ChannelImage({Key key, @required this.channel, @required this.size}) + const ChannelImage({Key? key, required this.channel, required this.size}) : super(key: key); final Channel channel; @@ -14,7 +14,7 @@ class ChannelImage extends StatelessWidget { Widget build(BuildContext context) { final avatarUrl = channel.extraData.containsKey('image') && (channel.extraData['image'] as String).isNotEmpty - ? channel.extraData['image'] as String + ? channel.extraData['image'] as String? : 'https://4.bp.blogspot.com/-Jx21kNqFSTU/UXemtqPhZCI/AAAAAAAAh74/BMGSzpU6F48/s1600/funny-cat-pictures-047-001.jpg'; return CupertinoCircleAvatar( diff --git a/packages/imessage/lib/channel_list_view.dart b/packages/imessage/lib/channel_list_view.dart index 89c03b5..9421b6d 100644 --- a/packages/imessage/lib/channel_list_view.dart +++ b/packages/imessage/lib/channel_list_view.dart @@ -6,7 +6,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart' show Channel, StreamChannel; class ChannelListView extends StatelessWidget { - const ChannelListView({Key key, @required this.channels}) : super(key: key); + const ChannelListView({Key? key, required this.channels}) : super(key: key); final List channels; @override Widget build(BuildContext context) { @@ -36,10 +36,10 @@ class ChannelListView extends StatelessWidget { child, ) => SharedAxisTransition( - child: child, animation: animation, secondaryAnimation: secondaryAnimation, transitionType: SharedAxisTransitionType.horizontal, + child: child, ), ), ); diff --git a/packages/imessage/lib/channel_name_text.dart b/packages/imessage/lib/channel_name_text.dart index e2e8d2b..169bb9d 100644 --- a/packages/imessage/lib/channel_name_text.dart +++ b/packages/imessage/lib/channel_name_text.dart @@ -3,8 +3,8 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart' show Channel; class ChannelNameText extends StatelessWidget { const ChannelNameText({ - Key key, - @required this.channel, + Key? key, + required this.channel, this.size = 17, }) : super(key: key); @@ -14,7 +14,7 @@ class ChannelNameText extends StatelessWidget { @override Widget build(BuildContext context) { return Text( - channel.extraData['name'] as String ?? 'No name', + channel.extraData['name'] as String? ?? 'No name', style: TextStyle( fontSize: size, fontWeight: FontWeight.bold, diff --git a/packages/imessage/lib/channel_page_appbar.dart b/packages/imessage/lib/channel_page_appbar.dart index f991829..5f44995 100644 --- a/packages/imessage/lib/channel_page_appbar.dart +++ b/packages/imessage/lib/channel_page_appbar.dart @@ -2,7 +2,7 @@ import 'package:flutter/cupertino.dart'; class ChannelPageAppBar extends StatelessWidget { const ChannelPageAppBar({ - Key key, + Key? key, }) : super(key: key); @override diff --git a/packages/imessage/lib/channel_preview.dart b/packages/imessage/lib/channel_preview.dart index 8b8c039..98a16c4 100644 --- a/packages/imessage/lib/channel_preview.dart +++ b/packages/imessage/lib/channel_preview.dart @@ -11,19 +11,20 @@ class ChannelPreview extends StatelessWidget { final Channel channel; const ChannelPreview({ - Key key, - @required this.onTap, - @required this.channel, + Key? key, + required this.onTap, + required this.channel, }) : super(key: key); @override Widget build(BuildContext context) { - final lastMessage = - channel.state.messages.isNotEmpty ? channel.state.messages.last : null; + final lastMessage = channel.state!.messages.isNotEmpty + ? channel.state!.messages.last + : null; final prefix = lastMessage?.attachments != null ? lastMessage?.attachments //TODO: ugly - ?.map((e) { + .map((e) { if (e.type == 'image') { return '📷 '; } else if (e.type == 'video') { @@ -31,8 +32,8 @@ class ChannelPreview extends StatelessWidget { } return null; }) - ?.where((e) => e != null) - ?.join(' ') + .where((e) => e != null) + .join(' ') : ''; return GestureDetector( onTap: onTap, @@ -76,9 +77,9 @@ class ChannelPreview extends StatelessWidget { child: Row( children: [ Text( - isSameWeek(channel.lastMessageAt) - ? formatDateSameWeek(channel.lastMessageAt) - : formatDate(channel.lastMessageAt), + isSameWeek(channel.lastMessageAt!) + ? formatDateSameWeek(channel.lastMessageAt!) + : formatDate(channel.lastMessageAt!), style: TextStyle( fontSize: 15, color: CupertinoColors.systemGrey, diff --git a/packages/imessage/lib/cutom_painter.dart b/packages/imessage/lib/cutom_painter.dart index 4437fc3..0ed6213 100644 --- a/packages/imessage/lib/cutom_painter.dart +++ b/packages/imessage/lib/cutom_painter.dart @@ -2,10 +2,10 @@ import 'package:flutter/cupertino.dart'; class ChatBubble extends CustomPainter { final Color color; - final Alignment alignment; + final Alignment? alignment; ChatBubble({ - @required this.color, + required this.color, this.alignment, }); diff --git a/packages/imessage/lib/main.dart b/packages/imessage/lib/main.dart index f235f41..fc7283d 100644 --- a/packages/imessage/lib/main.dart +++ b/packages/imessage/lib/main.dart @@ -1,18 +1,7 @@ import 'package:flutter/cupertino.dart'; import 'package:intl/date_symbol_data_local.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart' - show - Channel, - ChannelListController, - ChannelListCore, - ChannelsBloc, - LazyLoadScrollView, - Level, - PaginationParams, - SortOption, - StreamChatClient, - StreamChatCore, - User; + hide ChannelListView; import 'package:imessage/channel_list_view.dart'; @@ -37,7 +26,7 @@ Future main() async { class IMessage extends StatelessWidget { final StreamChatClient client; - IMessage({@required this.client}); + IMessage({required this.client}); @override Widget build(BuildContext context) { initializeDateFormatting('en_US', null); @@ -52,70 +41,69 @@ class IMessage extends StatelessWidget { class ChatLoader extends StatelessWidget { ChatLoader({ - Key key, + Key? key, }) : super(key: key); final channelListController = ChannelListController(); @override Widget build(BuildContext context) { - final user = StreamChatCore.of(context).user; + final user = StreamChatCore.of(context).user!; return CupertinoPageScaffold( - child: ChannelsBloc( - child: ChannelListCore( - channelListController: channelListController, - filter: { - 'members': { - r'$in': [user.id], - }, - 'type': { - r'$eq': 'messaging', - }, - }, - sort: [SortOption('last_message_at')], - pagination: PaginationParams( - limit: 20, - ), - emptyBuilder: (BuildContext context) { - return Center( - child: Text('Looks like you are not in any channels'), - ); - }, - loadingBuilder: (BuildContext context) { - return Center( - child: SizedBox( - height: 100.0, - width: 100.0, - child: CupertinoActivityIndicator(), - ), - ); - }, - errorBuilder: (BuildContext context, dynamic error) { - return Center( - child: Text( - 'Oh no, something went wrong. Please check your config.'), - ); - }, - listBuilder: ( - BuildContext context, - List channels, - ) => - LazyLoadScrollView( - onEndOfPage: () async { - channelListController.paginateData(); - }, - child: CustomScrollView( - slivers: [ - CupertinoSliverRefreshControl(onRefresh: () async { - channelListController.loadData(); - }), - ChannelPageAppBar(), - SliverPadding( - sliver: ChannelListView(channels: channels), - padding: const EdgeInsets.only(top: 16), - ) - ], - ), - )))); + child: ChannelsBloc( + child: ChannelListCore( + channelListController: channelListController, + filter: Filter.and([ + Filter.in_('members', [user.id]), + Filter.equal('type', 'messaging'), + ]), + sort: [SortOption('last_message_at')], + pagination: PaginationParams( + limit: 20, + ), + emptyBuilder: (BuildContext context) { + return Center( + child: Text('Looks like you are not in any channels'), + ); + }, + loadingBuilder: (BuildContext context) { + return Center( + child: SizedBox( + height: 100.0, + width: 100.0, + child: CupertinoActivityIndicator(), + ), + ); + }, + errorBuilder: (BuildContext context, dynamic error) { + return Center( + child: Text( + 'Oh no, something went wrong. Please check your config.'), + ); + }, + listBuilder: ( + BuildContext context, + List channels, + ) => + LazyLoadScrollView( + onEndOfPage: () async { + return channelListController.paginateData!(); + }, + child: CustomScrollView( + slivers: [ + CupertinoSliverRefreshControl(onRefresh: () async { + return channelListController.loadData!(); + }), + ChannelPageAppBar(), + SliverPadding( + sliver: ChannelListView(channels: channels), + padding: const EdgeInsets.only(top: 16), + ) + ], + ), + ), + ), + ), + ); } } diff --git a/packages/imessage/lib/message_header.dart b/packages/imessage/lib/message_header.dart index 31ae34b..e8ee94b 100644 --- a/packages/imessage/lib/message_header.dart +++ b/packages/imessage/lib/message_header.dart @@ -4,7 +4,7 @@ import 'package:imessage/utils.dart'; class MessageHeader extends StatelessWidget { final String rawTimeStamp; - const MessageHeader({Key key, @required this.rawTimeStamp}) : super(key: key); + const MessageHeader({Key? key, required this.rawTimeStamp}) : super(key: key); @override Widget build(BuildContext context) { diff --git a/packages/imessage/lib/message_input.dart b/packages/imessage/lib/message_input.dart index 9d9d831..7836498 100644 --- a/packages/imessage/lib/message_input.dart +++ b/packages/imessage/lib/message_input.dart @@ -3,11 +3,11 @@ import 'dart:io'; import 'package:flutter/cupertino.dart'; import 'package:image_picker/image_picker.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart' - show Attachment, AttachmentFile, Message, MultipartFile, StreamChannel; + show Attachment, AttachmentFile, Message, StreamChannel; class MessageInput extends StatefulWidget { const MessageInput({ - Key key, + Key? key, }) : super(key: key); @override @@ -16,7 +16,6 @@ class MessageInput extends StatefulWidget { class _MessageInputState extends State { final textController = TextEditingController(); - File _image; final picker = ImagePicker(); @override @@ -39,16 +38,25 @@ class _MessageInputState extends State { GestureDetector( onTap: () async { final pickedFile = - await picker.getImage(source: ImageSource.gallery); + await (picker.getImage(source: ImageSource.gallery)); + if (pickedFile == null) { + return; + } final bytes = await File(pickedFile.path).readAsBytes(); final channel = StreamChannel.of(context).channel; - final message = - Message(text: textController.value.text, attachments: [ - Attachment( - type: 'image', - file: AttachmentFile(bytes: bytes, path: pickedFile.path), - ), - ]); + final message = Message( + text: textController.value.text, + attachments: [ + Attachment( + type: 'image', + file: AttachmentFile( + bytes: bytes, + path: pickedFile.path, + size: bytes.length, + ), + ), + ], + ); await channel.sendMessage(message); }, child: Padding( @@ -68,10 +76,11 @@ class _MessageInputState extends State { }, placeholder: 'Text Message', prefix: Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - "") //trick to add padding around placeholder iMessage text - ), + padding: const EdgeInsets.all(8.0), + child: Text( + '', + ), //trick to add padding around placeholder iMessage text + ), suffix: GestureDetector( onTap: () async { if (textController.value.text.isNotEmpty) { diff --git a/packages/imessage/lib/message_list_view.dart b/packages/imessage/lib/message_list_view.dart index e12572c..b76f506 100644 --- a/packages/imessage/lib/message_list_view.dart +++ b/packages/imessage/lib/message_list_view.dart @@ -7,12 +7,12 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart' show Message, StreamChatCore; class MessageListView extends StatelessWidget { - const MessageListView({Key key, this.messages}) : super(key: key); - final List messages; + const MessageListView({Key? key, this.messages}) : super(key: key); + final List? messages; @override Widget build(BuildContext context) { - final entries = groupBy(messages, + final entries = groupBy(messages!, (Message message) => message.createdAt.toString().substring(0, 10)) .entries .toList(); @@ -64,8 +64,8 @@ class MessageListView extends StatelessWidget { } bool isReceived(Message message, BuildContext context) { - final currentUserId = StreamChatCore.of(context).user.id; - return message.user.id == currentUserId; + final currentUserId = StreamChatCore.of(context).user!.id; + return message.user!.id == currentUserId; } bool isSameDay(Message message) => diff --git a/packages/imessage/lib/message_page.dart b/packages/imessage/lib/message_page.dart index 01b555e..f64eb66 100644 --- a/packages/imessage/lib/message_page.dart +++ b/packages/imessage/lib/message_page.dart @@ -53,7 +53,7 @@ class MessagePage extends StatelessWidget { }, messageListBuilder: (context, messages) => LazyLoadScrollView( onStartOfPage: () async { - messageListController.paginateData(); + await messageListController.paginateData!(); }, child: MessageListView( messages: messages, diff --git a/packages/imessage/lib/message_widget.dart b/packages/imessage/lib/message_widget.dart index 494e9d2..8fa6638 100644 --- a/packages/imessage/lib/message_widget.dart +++ b/packages/imessage/lib/message_widget.dart @@ -1,8 +1,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/cupertino.dart'; import 'package:imessage/cutom_painter.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart' - show Message, AttachmentUploadStateBuilder; +import 'package:stream_chat_flutter/stream_chat_flutter.dart' show Message; class MessageWidget extends StatelessWidget { final Alignment alignment; @@ -11,16 +10,16 @@ class MessageWidget extends StatelessWidget { final Color messageColor; const MessageWidget( - {Key key, - @required this.alignment, - @required this.message, - @required this.color, - @required this.messageColor}) + {Key? key, + required this.alignment, + required this.message, + required this.color, + required this.messageColor}) : super(key: key); @override Widget build(BuildContext context) { - if (message.attachments?.isNotEmpty == true && + if (message.attachments.isNotEmpty == true && message.attachments.first.type == 'image') { return MessageImage( color: color, message: message, messageColor: messageColor); @@ -36,10 +35,10 @@ class MessageWidget extends StatelessWidget { class MessageImage extends StatelessWidget { const MessageImage({ - Key key, - @required this.color, - @required this.message, - @required this.messageColor, + Key? key, + required this.color, + required this.message, + required this.messageColor, }) : super(key: key); final Color color; @@ -61,23 +60,23 @@ class MessageImage extends StatelessWidget { children: [ if (message.attachments.first.file != null) Image.memory( - message.attachments.first.file.bytes, + message.attachments.first.file!.bytes!, fit: BoxFit.cover, ) else CachedNetworkImage( imageUrl: message.attachments.first.thumbUrl ?? message.attachments.first.imageUrl ?? - message.attachments.first.assetUrl, + message.attachments.first.assetUrl!, ), - if (message.attachments.first?.title != null) + if (message.attachments.first.title != null) Padding( padding: const EdgeInsets.all(8.0), - child: Text(message.attachments.first.title, + child: Text(message.attachments.first.title!, style: TextStyle(color: messageColor)), ), message.attachments.first.pretext != null - ? Text(message.attachments.first.pretext) + ? Text(message.attachments.first.pretext!) : Container() ], ), @@ -92,7 +91,7 @@ class MessageImage extends StatelessWidget { child: Container( color: color, child: CachedNetworkImage( - imageUrl: message.attachments.first.thumbUrl, + imageUrl: message.attachments.first.thumbUrl!, )), ); } @@ -101,11 +100,11 @@ class MessageImage extends StatelessWidget { class MessageText extends StatelessWidget { const MessageText({ - Key key, - @required this.alignment, - @required this.color, - @required this.message, - @required this.messageColor, + Key? key, + required this.alignment, + required this.color, + required this.message, + required this.messageColor, }) : super(key: key); final Alignment alignment; @@ -133,7 +132,7 @@ class MessageText extends StatelessWidget { child: Padding( padding: const EdgeInsets.all(4.0), child: Text( - message.text, + message.text!, style: TextStyle(color: messageColor), ), ), diff --git a/packages/imessage/lib/utils.dart b/packages/imessage/lib/utils.dart index f78e450..6487b35 100644 --- a/packages/imessage/lib/utils.dart +++ b/packages/imessage/lib/utils.dart @@ -26,44 +26,45 @@ bool isSameWeek(DateTime timestamp) => DateTime.now().difference(timestamp).inDays < 7; class CupertinoCircleAvatar extends StatelessWidget { - final String url; - final double size; - const CupertinoCircleAvatar({Key key, this.url, this.size}) : super(key: key); + final String? url; + final double? size; + const CupertinoCircleAvatar({Key? key, this.url, this.size}) + : super(key: key); @override Widget build(BuildContext context) { return ClipRRect( - borderRadius: BorderRadius.circular(size / 2), + borderRadius: BorderRadius.circular(size! / 2), child: CachedNetworkImage( - imageUrl: url, + imageUrl: url!, height: size, width: size, fit: BoxFit.cover, errorWidget: (context, url, error) { //TODO: this crash the app when getting 404 and in debug mode, see :https://github.com/Baseflow/flutter_cached_network_image/issues/504 return CachedNetworkImage( - imageUrl: - "https://4.bp.blogspot.com/-Jx21kNqFSTU/UXemtqPhZCI/AAAAAAAAh74/BMGSzpU6F48/s1600/funny-cat-pictures-047-001.jpg"); + imageUrl: + 'https://4.bp.blogspot.com/-Jx21kNqFSTU/UXemtqPhZCI/AAAAAAAAh74/BMGSzpU6F48/s1600/funny-cat-pictures-047-001.jp', + ); }), ); } } - class Divider extends StatelessWidget { const Divider({ - Key key, + Key? key, }) : super(key: key); @override Widget build(BuildContext context) { return Expanded( child: Align( + alignment: Alignment.bottomCenter, child: Container( height: 1, color: CupertinoColors.systemGrey5, ), - alignment: Alignment.bottomCenter, ), ); } diff --git a/packages/imessage/pubspec.yaml b/packages/imessage/pubspec.yaml index 62e7507..9d05f4c 100644 --- a/packages/imessage/pubspec.yaml +++ b/packages/imessage/pubspec.yaml @@ -18,16 +18,16 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: ">=2.7.0 <3.0.0" + sdk: '>=2.12.0 <3.0.0' dependencies: flutter: sdk: flutter - intl: ^0.16.1 - stream_chat_flutter: ^1.3.0-beta - animations: ^1.0.0+5 - collection: ^1.14.13 - cached_network_image: ^2.0.0-rc + intl: ^0.17.0 + stream_chat_flutter: ^2.0.0-nullsafety.3 + animations: ^2.0.0 + collection: ^1.15.0 + cached_network_image: ^3.0.0 # The following adds the Cupertino Icons font to your application. diff --git a/packages/stream_chat_v1/android/gradle/wrapper/gradle-wrapper.properties b/packages/stream_chat_v1/android/gradle/wrapper/gradle-wrapper.properties index 4a4c204..c0a81d1 100644 --- a/packages/stream_chat_v1/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/stream_chat_v1/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/packages/stream_chat_v1/ios/Gemfile.lock b/packages/stream_chat_v1/ios/Gemfile.lock index d4fa45b..06d690f 100644 --- a/packages/stream_chat_v1/ios/Gemfile.lock +++ b/packages/stream_chat_v1/ios/Gemfile.lock @@ -2,21 +2,21 @@ GEM remote: https://rubygems.org/ specs: CFPropertyList (3.0.3) - addressable (2.7.0) + addressable (2.8.0) public_suffix (>= 2.0.2, < 5.0) artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.1.1) - aws-partitions (1.437.0) - aws-sdk-core (3.113.1) + aws-partitions (1.473.0) + aws-sdk-core (3.115.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.239.0) aws-sigv4 (~> 1.1) jmespath (~> 1.0) - aws-sdk-kms (1.43.0) + aws-sdk-kms (1.44.0) aws-sdk-core (~> 3, >= 3.112.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.93.0) + aws-sdk-s3 (1.96.1) aws-sdk-core (~> 3, >= 3.112.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.1) @@ -26,29 +26,40 @@ GEM claide (1.0.3) colored (1.2) colored2 (3.1.2) - commander-fastlane (4.4.6) - highline (~> 1.7.2) + commander (4.6.0) + highline (~> 2.0.0) declarative (0.0.20) - declarative-option (0.1.0) digest-crc (0.6.3) rake (>= 12.0.0, < 14.0.0) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) dotenv (2.7.6) emoji_regex (3.2.2) - excon (0.79.0) - faraday (1.3.0) + excon (0.83.0) + faraday (1.5.0) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0.1) faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.1) + faraday-patron (~> 1.0) multipart-post (>= 1.2, < 3) - ruby2_keywords + ruby2_keywords (>= 0.0.4) faraday-cookie_jar (0.0.7) faraday (>= 0.8.0) http-cookie (~> 1.0.0) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.0) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) faraday-net_http (1.0.1) + faraday-net_http_persistent (1.1.0) + faraday-patron (1.0.0) faraday_middleware (1.0.0) faraday (~> 1.0) - fastimage (2.2.3) - fastlane (2.179.0) + fastimage (2.2.4) + fastlane (2.187.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.3, < 3.0.0) artifactory (~> 3.0) @@ -56,7 +67,7 @@ GEM babosa (>= 1.0.3, < 2.0.0) bundler (>= 1.12.0, < 3.0.0) colored - commander-fastlane (>= 4.4.6, < 5.0.0) + commander (~> 4.6) dotenv (>= 2.1.1, < 3.0.0) emoji_regex (>= 0.1, < 4.0) excon (>= 0.71.0, < 1.0.0) @@ -65,9 +76,10 @@ GEM faraday_middleware (~> 1.0) fastimage (>= 2.1.0, < 3.0.0) gh_inspector (>= 1.1.2, < 2.0.0) - google-api-client (>= 0.37.0, < 0.39.0) - google-cloud-storage (>= 1.15.0, < 2.0.0) - highline (>= 1.7.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.1) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-storage (~> 1.31) + highline (~> 2.0) json (< 3.0.0) jwt (>= 2.1.0, < 3) mini_magick (>= 4.9.4, < 5.0.0) @@ -77,7 +89,6 @@ GEM rubyzip (>= 2.0.0, < 3.0.0) security (= 0.1.3) simctl (~> 1.6.3) - slack-notifier (>= 2.0.0, < 3.0.0) terminal-notifier (>= 2.0.0, < 3.0.0) terminal-table (>= 1.4.5, < 2.0.0) tty-screen (>= 0.6.3, < 1.0.0) @@ -86,61 +97,56 @@ GEM xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.3.0) xcpretty-travis-formatter (>= 0.0.3) - fastlane-plugin-firebase_app_distribution (0.2.3) + fastlane-plugin-firebase_app_distribution (0.2.9) gh_inspector (1.1.3) - google-api-client (0.38.0) + google-apis-androidpublisher_v3 (0.8.0) + google-apis-core (>= 0.4, < 2.a) + google-apis-core (0.4.0) addressable (~> 2.5, >= 2.5.1) - googleauth (~> 0.9) - httpclient (>= 2.8.1, < 3.0) + googleauth (>= 0.16.2, < 2.a) + httpclient (>= 2.8.1, < 3.a) mini_mime (~> 1.0) representable (~> 3.0) - retriable (>= 2.0, < 4.0) - signet (~> 0.12) - google-apis-core (0.3.0) - addressable (~> 2.5, >= 2.5.1) - googleauth (~> 0.14) - httpclient (>= 2.8.1, < 3.0) - mini_mime (~> 1.0) - representable (~> 3.0) - retriable (>= 2.0, < 4.0) + retriable (>= 2.0, < 4.a) rexml - signet (~> 0.14) webrick - google-apis-iamcredentials_v1 (0.2.0) - google-apis-core (~> 0.1) - google-apis-storage_v1 (0.3.0) - google-apis-core (~> 0.1) + google-apis-iamcredentials_v1 (0.6.0) + google-apis-core (>= 0.4, < 2.a) + google-apis-playcustomapp_v1 (0.5.0) + google-apis-core (>= 0.4, < 2.a) + google-apis-storage_v1 (0.6.0) + google-apis-core (>= 0.4, < 2.a) google-cloud-core (1.6.0) google-cloud-env (~> 1.0) google-cloud-errors (~> 1.0) google-cloud-env (1.5.0) faraday (>= 0.17.3, < 2.0) google-cloud-errors (1.1.0) - google-cloud-storage (1.31.0) + google-cloud-storage (1.34.0) addressable (~> 2.5) digest-crc (~> 0.4) google-apis-iamcredentials_v1 (~> 0.1) google-apis-storage_v1 (~> 0.1) - google-cloud-core (~> 1.2) - googleauth (~> 0.9) + google-cloud-core (~> 1.6) + googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) - googleauth (0.16.0) + googleauth (0.16.2) faraday (>= 0.17.3, < 2.0) jwt (>= 1.4, < 3.0) memoist (~> 0.16) multi_json (~> 1.11) os (>= 0.9, < 2.0) signet (~> 0.14) - highline (1.7.10) - http-cookie (1.0.3) + highline (2.0.3) + http-cookie (1.0.4) domain_name (~> 0.5) httpclient (2.8.3) jmespath (1.4.0) json (2.5.1) - jwt (2.2.2) + jwt (2.2.3) memoist (0.16.2) mini_magick (4.11.0) - mini_mime (1.0.3) + mini_mime (1.1.0) multi_json (1.15.0) multipart-post (2.0.0) nanaimo (0.3.0) @@ -148,16 +154,16 @@ GEM os (1.1.1) plist (3.6.0) public_suffix (4.0.6) - rake (13.0.3) - representable (3.0.4) + rake (13.0.4) + representable (3.1.1) declarative (< 0.1.0) - declarative-option (< 0.2.0) + trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.2.4) + rexml (3.2.5) rouge (2.0.7) ruby2_keywords (0.0.4) - rubyzip (2.3.0) + rubyzip (2.3.2) security (0.1.3) signet (0.15.0) addressable (~> 2.3) @@ -167,10 +173,10 @@ GEM simctl (1.6.8) CFPropertyList naturally - slack-notifier (2.3.2) terminal-notifier (2.0.0) terminal-table (1.8.0) unicode-display_width (~> 1.1, >= 1.1.1) + trailblazer-option (0.1.1) tty-cursor (0.7.1) tty-screen (0.8.1) tty-spinner (0.9.3) @@ -182,12 +188,13 @@ GEM unicode-display_width (1.7.0) webrick (1.7.0) word_wrap (1.0.0) - xcodeproj (1.19.0) + xcodeproj (1.20.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) nanaimo (~> 0.3.0) + rexml (~> 3.2.4) xcpretty (0.3.0) rouge (~> 2.0.7) xcpretty-travis-formatter (1.0.1) diff --git a/packages/stream_chat_v1/ios/Runner.xcodeproj/project.pbxproj b/packages/stream_chat_v1/ios/Runner.xcodeproj/project.pbxproj index 8cd3ed0..417fb0b 100644 --- a/packages/stream_chat_v1/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/stream_chat_v1/ios/Runner.xcodeproj/project.pbxproj @@ -245,8 +245,10 @@ "${BUILT_PRODUCTS_DIR}/DKImagePickerController/DKImagePickerController.framework", "${BUILT_PRODUCTS_DIR}/DKPhotoGallery/DKPhotoGallery.framework", "${BUILT_PRODUCTS_DIR}/FMDB/FMDB.framework", + "${BUILT_PRODUCTS_DIR}/Reachability/Reachability.framework", "${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework", "${BUILT_PRODUCTS_DIR}/SwiftyGif/SwiftyGif.framework", + "${BUILT_PRODUCTS_DIR}/connectivity_plus/connectivity_plus.framework", "${BUILT_PRODUCTS_DIR}/file_picker/file_picker.framework", "${BUILT_PRODUCTS_DIR}/flutter_app_badger/flutter_app_badger.framework", "${BUILT_PRODUCTS_DIR}/flutter_keyboard_visibility/flutter_keyboard_visibility.framework", @@ -273,8 +275,10 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DKImagePickerController.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DKPhotoGallery.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FMDB.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Reachability.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyGif.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/connectivity_plus.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_picker.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_app_badger.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_keyboard_visibility.framework", @@ -447,7 +451,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = io.getstream.flutter; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = "match AdHoc io.getstream.flutter"; + PROVISIONING_PROFILE_SPECIFIER = "match AdHoc io.getstream.flutter 1620032657"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -586,7 +590,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = io.getstream.flutter; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = "match AdHoc io.getstream.flutter"; + PROVISIONING_PROFILE_SPECIFIER = "match AdHoc io.getstream.flutter 1620032657"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -620,7 +624,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = io.getstream.flutter; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = "match AdHoc io.getstream.flutter"; + PROVISIONING_PROFILE_SPECIFIER = "match AdHoc io.getstream.flutter 1620032657"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; diff --git a/packages/stream_chat_v1/ios/fastlane/Fastfile b/packages/stream_chat_v1/ios/fastlane/Fastfile index 91aacbf..2d46198 100644 --- a/packages/stream_chat_v1/ios/fastlane/Fastfile +++ b/packages/stream_chat_v1/ios/fastlane/Fastfile @@ -1,4 +1,4 @@ -fastlane_version "2.179.0" +fastlane_version "2.187.0" default_platform :ios before_all do @@ -62,7 +62,7 @@ platform :ios do settings_to_override = { :BUNDLE_IDENTIFIER => "io.getstream.flutter", - :PROVISIONING_PROFILE_SPECIFIER => "match AppStore io.getstream.flutter" + :PROVISIONING_PROFILE_SPECIFIER => "match AppStore io.getstream.flutter 1620032657" } gym( diff --git a/packages/stream_chat_v1/ios/fastlane/beta_gym_export_options.plist b/packages/stream_chat_v1/ios/fastlane/beta_gym_export_options.plist index e88559a..3721528 100644 --- a/packages/stream_chat_v1/ios/fastlane/beta_gym_export_options.plist +++ b/packages/stream_chat_v1/ios/fastlane/beta_gym_export_options.plist @@ -7,7 +7,7 @@ provisioningProfiles io.getstream.flutter - match AdHoc io.getstream.flutter + match AdHoc io.getstream.flutter 1620032657 \ No newline at end of file diff --git a/packages/stream_chat_v1/ios/fastlane/report.xml b/packages/stream_chat_v1/ios/fastlane/report.xml index 42d2518..55d4f11 100644 --- a/packages/stream_chat_v1/ios/fastlane/report.xml +++ b/packages/stream_chat_v1/ios/fastlane/report.xml @@ -5,12 +5,12 @@ - + - + @@ -20,24 +20,22 @@ - + - + - + - - - + diff --git a/packages/stream_chat_v1/lib/advanced_options_page.dart b/packages/stream_chat_v1/lib/advanced_options_page.dart index a654f3d..3fd9a95 100644 --- a/packages/stream_chat_v1/lib/advanced_options_page.dart +++ b/packages/stream_chat_v1/lib/advanced_options_page.dart @@ -1,3 +1,4 @@ +import 'package:example/home_page.dart'; import 'package:example/routes/routes.dart'; import 'package:example/stream_version.dart'; import 'package:flutter/material.dart'; @@ -17,13 +18,13 @@ class _AdvancedOptionsPageState extends State { final _formKey = GlobalKey(); final TextEditingController _apiKeyController = TextEditingController(); - String _apiKeyError; + String? _apiKeyError; final TextEditingController _userIdController = TextEditingController(); - String _userIdError; + String? _userIdError; final TextEditingController _userTokenController = TextEditingController(); - String _userTokenError; + String? _userTokenError; final TextEditingController _usernameController = TextEditingController(); @@ -32,22 +33,20 @@ class _AdvancedOptionsPageState extends State { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow, + backgroundColor: StreamChatTheme.of(context).colorTheme.appBg, appBar: AppBar( - backgroundColor: StreamChatTheme.of(context).colorTheme.white, + backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg, elevation: 1, centerTitle: true, brightness: Theme.of(context).brightness, title: Text( 'Advanced Options', - style: StreamChatTheme.of(context) - .textTheme - .headlineBold - .copyWith(color: StreamChatTheme.of(context).colorTheme.black), + style: StreamChatTheme.of(context).textTheme.headlineBold.copyWith( + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis), ), leading: IconButton( icon: StreamSvgIcon.left( - color: StreamChatTheme.of(context).colorTheme.black, + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis, ), onPressed: () { Navigator.pop(context); @@ -73,7 +72,7 @@ class _AdvancedOptionsPageState extends State { } }, validator: (value) { - if (value.isEmpty) { + if (value!.isEmpty) { setState(() { _apiKeyError = 'Please enter the Chat API Key'.toUpperCase(); @@ -84,7 +83,9 @@ class _AdvancedOptionsPageState extends State { }, style: TextStyle( fontSize: 14, - color: StreamChatTheme.of(context).colorTheme.black, + color: StreamChatTheme.of(context) + .colorTheme + .textHighEmphasis, ), decoration: InputDecoration( errorStyle: TextStyle(height: 0, fontSize: 0), @@ -92,15 +93,16 @@ class _AdvancedOptionsPageState extends State { fontSize: 14, fontWeight: FontWeight.bold, color: _apiKeyError != null - ? StreamChatTheme.of(context).colorTheme.accentRed - : StreamChatTheme.of(context).colorTheme.grey, + ? StreamChatTheme.of(context).colorTheme.accentError + : StreamChatTheme.of(context) + .colorTheme + .textLowEmphasis, ), border: UnderlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide.none, ), - fillColor: - StreamChatTheme.of(context).colorTheme.whiteSmoke, + fillColor: StreamChatTheme.of(context).colorTheme.inputBg, filled: true, labelText: _apiKeyError != null ? 'CHAT API KEY: $_apiKeyError' @@ -119,7 +121,7 @@ class _AdvancedOptionsPageState extends State { } }, validator: (value) { - if (value.isEmpty) { + if (value!.isEmpty) { setState(() { _userIdError = 'Please enter the User ID'.toUpperCase(); @@ -130,7 +132,9 @@ class _AdvancedOptionsPageState extends State { }, style: TextStyle( fontSize: 14, - color: StreamChatTheme.of(context).colorTheme.black, + color: StreamChatTheme.of(context) + .colorTheme + .textHighEmphasis, ), textInputAction: TextInputAction.next, decoration: InputDecoration( @@ -139,15 +143,16 @@ class _AdvancedOptionsPageState extends State { fontWeight: FontWeight.bold, fontSize: 14, color: _userIdError != null - ? StreamChatTheme.of(context).colorTheme.accentRed - : StreamChatTheme.of(context).colorTheme.grey, + ? StreamChatTheme.of(context).colorTheme.accentError + : StreamChatTheme.of(context) + .colorTheme + .textLowEmphasis, ), border: UnderlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide.none, ), - fillColor: - StreamChatTheme.of(context).colorTheme.whiteSmoke, + fillColor: StreamChatTheme.of(context).colorTheme.inputBg, filled: true, labelText: _userIdError != null ? 'USER ID: $_userIdError' @@ -165,7 +170,7 @@ class _AdvancedOptionsPageState extends State { }, controller: _userTokenController, validator: (value) { - if (value.isEmpty) { + if (value!.isEmpty) { setState(() { _userTokenError = 'Please enter the user token'.toUpperCase(); @@ -176,7 +181,9 @@ class _AdvancedOptionsPageState extends State { }, style: TextStyle( fontSize: 14, - color: StreamChatTheme.of(context).colorTheme.black, + color: StreamChatTheme.of(context) + .colorTheme + .textHighEmphasis, ), textInputAction: TextInputAction.next, decoration: InputDecoration( @@ -185,15 +192,16 @@ class _AdvancedOptionsPageState extends State { fontWeight: FontWeight.bold, fontSize: 14, color: _userTokenError != null - ? StreamChatTheme.of(context).colorTheme.accentRed - : StreamChatTheme.of(context).colorTheme.grey, + ? StreamChatTheme.of(context).colorTheme.accentError + : StreamChatTheme.of(context) + .colorTheme + .textLowEmphasis, ), border: UnderlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide.none, ), - fillColor: - StreamChatTheme.of(context).colorTheme.whiteSmoke, + fillColor: StreamChatTheme.of(context).colorTheme.inputBg, filled: true, labelText: _userTokenError != null ? 'USER TOKEN: $_userTokenError' @@ -208,34 +216,45 @@ class _AdvancedOptionsPageState extends State { labelStyle: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, - color: StreamChatTheme.of(context).colorTheme.grey, + color: StreamChatTheme.of(context) + .colorTheme + .textLowEmphasis, ), border: UnderlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide.none, ), - fillColor: - StreamChatTheme.of(context).colorTheme.whiteSmoke, + fillColor: StreamChatTheme.of(context).colorTheme.inputBg, filled: true, labelText: 'Username (optional)', ), ), Spacer(), - RaisedButton( - color: Theme.of(context).brightness == Brightness.light - ? StreamChatTheme.of(context).colorTheme.accentBlue - : Colors.white, - elevation: 0, - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(26), + ElevatedButton( + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all( + Theme.of(context).brightness == Brightness.light + ? StreamChatTheme.of(context) + .colorTheme + .accentPrimary + : Colors.white), + elevation: MaterialStateProperty.all(0), + padding: MaterialStateProperty.all( + const EdgeInsets.symmetric(vertical: 16)), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(26), + ), + ), ), child: Text( 'Login', style: TextStyle( fontSize: 16, color: Theme.of(context).brightness != Brightness.light - ? StreamChatTheme.of(context).colorTheme.accentBlue + ? StreamChatTheme.of(context) + .colorTheme + .accentPrimary : Colors.white, ), ), @@ -243,7 +262,7 @@ class _AdvancedOptionsPageState extends State { if (loading) { return; } - if (_formKey.currentState.validate()) { + if (_formKey.currentState!.validate()) { final apiKey = _apiKeyController.text; final userId = _userIdController.text; final userToken = _userTokenController.text; @@ -261,7 +280,7 @@ class _AdvancedOptionsPageState extends State { borderRadius: BorderRadius.circular(16), color: StreamChatTheme.of(context) .colorTheme - .white, + .barsBg, ), height: 100, width: 100, @@ -298,7 +317,6 @@ class _AdvancedOptionsPageState extends State { key: kStreamToken, value: userToken, ); - await client.disconnect(); } catch (e) { var errorText = 'Error connecting, retry'; if (e is Map) { @@ -309,15 +327,14 @@ class _AdvancedOptionsPageState extends State { _apiKeyError = errorText.toUpperCase(); }); loading = false; - await client.disconnect(); return; } loading = false; await Navigator.pushNamedAndRemoveUntil( context, - Routes.APP, - ModalRoute.withName(Routes.APP), - arguments: client, + Routes.HOME, + ModalRoute.withName(Routes.HOME), + arguments: HomePageArgs(client), ); } }, diff --git a/packages/stream_chat_v1/lib/channel_file_display_screen.dart b/packages/stream_chat_v1/lib/channel_file_display_screen.dart new file mode 100644 index 0000000..2cffbc8 --- /dev/null +++ b/packages/stream_chat_v1/lib/channel_file_display_screen.dart @@ -0,0 +1,177 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +class ChannelFileDisplayScreen extends StatefulWidget { + /// The sorting used for the channels matching the filters. + /// 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. + /// Direction can be ascending or descending. + final List? sortOptions; + + /// Pagination parameters + /// limit: the number of users to return (max is 30) + /// offset: the offset (max is 1000) + /// message_limit: how many messages should be included to each channel + final PaginationParams? paginationParams; + + /// The builder used when the file list is empty. + final WidgetBuilder? emptyBuilder; + + const ChannelFileDisplayScreen({ + this.sortOptions, + this.paginationParams, + this.emptyBuilder, + }); + + @override + _ChannelFileDisplayScreenState createState() => + _ChannelFileDisplayScreenState(); +} + +class _ChannelFileDisplayScreenState extends State { + @override + void initState() { + super.initState(); + final messageSearchBloc = MessageSearchBloc.of(context); + messageSearchBloc.search( + filter: Filter.in_( + 'cid', + [StreamChannel.of(context).channel.cid!], + ), + messageFilter: Filter.in_( + 'attachments.type', + ['file'], + ), + sort: widget.sortOptions, + pagination: widget.paginationParams, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg, + appBar: AppBar( + brightness: Theme.of(context).brightness, + elevation: 1, + centerTitle: true, + title: Text( + 'Files', + style: TextStyle( + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis, + fontSize: 16.0), + ), + leading: Center( + child: InkWell( + onTap: () { + Navigator.of(context).pop(); + }, + child: Container( + width: 24.0, + height: 24.0, + child: StreamSvgIcon.left( + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis, + size: 24.0, + ), + ), + ), + ), + backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg, + ), + body: _buildMediaGrid(), + ); + } + + Widget _buildMediaGrid() { + final messageSearchBloc = MessageSearchBloc.of(context); + + return StreamBuilder>( + builder: (context, snapshot) { + if (snapshot.data == null) { + return Center( + child: const CircularProgressIndicator(), + ); + } + + if (snapshot.data!.isEmpty) { + if (widget.emptyBuilder != null) { + return widget.emptyBuilder!(context); + } + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + StreamSvgIcon.files( + size: 136.0, + color: StreamChatTheme.of(context).colorTheme.disabled, + ), + SizedBox(height: 16.0), + Text( + 'No Files', + style: TextStyle( + fontSize: 14.0, + color: + StreamChatTheme.of(context).colorTheme.textHighEmphasis, + ), + ), + SizedBox(height: 8.0), + Text( + 'Files sent in this chat will appear here', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14.0, + color: StreamChatTheme.of(context) + .colorTheme + .textHighEmphasis + .withOpacity(0.5), + ), + ), + ], + ), + ); + } + + final media = {}; + + for (var item in snapshot.data!) { + item.message.attachments.where((e) => e.type == 'file').forEach((e) { + media[e] = item.message; + }); + } + + return LazyLoadScrollView( + onEndOfPage: () => messageSearchBloc.search( + filter: Filter.in_( + 'cid', + [StreamChannel.of(context).channel.cid!], + ), + messageFilter: Filter.in_( + 'attachments.type', + ['file'], + ), + sort: widget.sortOptions, + pagination: widget.paginationParams!.copyWith( + offset: messageSearchBloc.messageResponses?.length ?? 0, + ), + ), + child: ListView.builder( + itemBuilder: (context, position) { + return Padding( + padding: const EdgeInsets.all(1.0), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: FileAttachment( + message: media.values.toList()[position], + attachment: media.keys.toList()[position], + ), + ), + ); + }, + itemCount: media.length, + ), + ); + }, + stream: messageSearchBloc.messagesStream, + ); + } +} diff --git a/packages/stream_chat_v1/lib/channel_list.dart b/packages/stream_chat_v1/lib/channel_list.dart new file mode 100644 index 0000000..c31fabb --- /dev/null +++ b/packages/stream_chat_v1/lib/channel_list.dart @@ -0,0 +1,199 @@ +import 'dart:async'; + +import 'package:example/routes/routes.dart'; +import 'package:example/search_text_field.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'channel_page.dart'; +import 'chat_info_screen.dart'; +import 'group_info_screen.dart'; + +class ChannelList extends StatefulWidget { + @override + _ChannelList createState() => _ChannelList(); +} + +class _ChannelList extends State { + TextEditingController? _controller; + + String _channelQuery = ''; + + bool _isSearchActive = false; + + Timer? _debounce; + + void _channelQueryListener() { + if (_debounce?.isActive ?? false) _debounce!.cancel(); + _debounce = Timer(const Duration(milliseconds: 350), () { + if (mounted) { + setState(() { + _channelQuery = _controller!.text; + _isSearchActive = _channelQuery.isNotEmpty; + }); + } + }); + } + + @override + void initState() { + super.initState(); + _controller = TextEditingController()..addListener(_channelQueryListener); + } + + @override + void dispose() { + _controller?.removeListener(_channelQueryListener); + _controller?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final user = StreamChat.of(context).user; + return WillPopScope( + onWillPop: () async { + if (_isSearchActive) { + _controller!.clear(); + setState(() => _isSearchActive = false); + return false; + } + return true; + }, + child: NestedScrollView( + floatHeaderSlivers: true, + headerSliverBuilder: (_, __) => [ + SliverToBoxAdapter( + child: SearchTextField( + controller: _controller, + showCloseButton: _isSearchActive, + ), + ), + ], + body: AnimatedSwitcher( + duration: const Duration(milliseconds: 350), + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onPanDown: (_) => FocusScope.of(context).unfocus(), + child: _isSearchActive + ? MessageSearchBloc( + child: MessageSearchListView( + showErrorTile: true, + messageQuery: _channelQuery, + filters: Filter.in_('members', [user!.id]), + sortOptions: [ + SortOption( + 'created_at', + direction: SortOption.ASC, + ), + ], + pullToRefresh: false, + paginationParams: PaginationParams(limit: 20), + emptyBuilder: (_) { + return LayoutBuilder( + builder: (context, viewportConstraints) { + return SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: viewportConstraints.maxHeight, + ), + child: Center( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(24), + child: StreamSvgIcon.search( + size: 96, + color: Colors.grey, + ), + ), + Text( + 'No results...', + ), + ], + ), + ), + ), + ); + }, + ); + }, + onItemTap: (messageResponse) async { + FocusScope.of(context).requestFocus(FocusNode()); + final client = StreamChat.of(context).client; + final message = messageResponse.message; + final channel = client.channel( + messageResponse.channel!.type, + id: messageResponse.channel!.id, + ); + if (channel.state == null) { + await channel.watch(); + } + Navigator.pushNamed( + context, + Routes.CHANNEL_PAGE, + arguments: ChannelPageArgs( + channel: channel, + initialMessage: message, + ), + ); + }, + ), + ) + : ChannelsBloc( + child: ChannelListView( + onStartChatPressed: () { + Navigator.pushNamed(context, Routes.NEW_CHAT); + }, + swipeToAction: true, + filter: Filter.in_('members', [user!.id]), + presence: true, + pagination: PaginationParams( + limit: 20, + ), + channelWidget: ChannelPage(), + onViewInfoTap: (channel) { + Navigator.pop(context); + if (channel.memberCount == 2 && channel.isDistinct) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: channel, + child: ChatInfoScreen( + messageTheme: StreamChatTheme.of(context) + .ownMessageTheme, + user: channel.state!.members + .where((m) => + m.userId != + channel.client.state.user!.id) + .first + .user, + ), + ), + ), + ); + } else { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: channel, + child: GroupInfoScreen( + messageTheme: StreamChatTheme.of(context) + .ownMessageTheme, + ), + ), + ), + ); + } + }, + ), + ), + ), + ), + ), + ); + } +} diff --git a/packages/stream_chat_v1/lib/channel_list_page.dart b/packages/stream_chat_v1/lib/channel_list_page.dart new file mode 100644 index 0000000..5379062 --- /dev/null +++ b/packages/stream_chat_v1/lib/channel_list_page.dart @@ -0,0 +1,284 @@ +import 'dart:async'; + +import 'package:example/routes/routes.dart'; +import 'package:example/user_mentions_page.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_app_badger/flutter_app_badger.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:streaming_shared_preferences/streaming_shared_preferences.dart'; + +import 'channel_list.dart'; + +class ChannelListPage extends StatefulWidget { + const ChannelListPage({ + Key? key, + }) : super(key: key); + + @override + _ChannelListPageState createState() => _ChannelListPageState(); +} + +class _ChannelListPageState extends State { + int _currentIndex = 0; + + bool _isSelected(int index) => _currentIndex == index; + + List get _navBarItems { + return [ + BottomNavigationBarItem( + icon: Stack( + clipBehavior: Clip.none, + children: [ + StreamSvgIcon.message( + color: _isSelected(0) + ? StreamChatTheme.of(context).colorTheme.textHighEmphasis + : Colors.grey, + ), + Positioned( + top: -3, + right: -16, + child: UnreadIndicator(), + ), + ], + ), + label: 'Chats', + ), + BottomNavigationBarItem( + icon: Stack( + clipBehavior: Clip.none, + children: [ + StreamSvgIcon.mentions( + color: _isSelected(1) + ? StreamChatTheme.of(context).colorTheme.textHighEmphasis + : Colors.grey, + ), + ], + ), + label: 'Mentions', + ), + ]; + } + + @override + Widget build(BuildContext context) { + final user = StreamChat.of(context).user; + if (user == null) { + return Offstage(); + } + return Scaffold( + backgroundColor: StreamChatTheme.of(context).colorTheme.appBg, + appBar: ChannelListHeader( + onNewChatButtonTap: () { + Navigator.pushNamed(context, Routes.NEW_CHAT); + }, + preNavigationCallback: () { + FocusScope.of(context).requestFocus(FocusNode()); + }, + ), + drawer: LeftDrawer( + user: user, + ), + drawerEdgeDragWidth: 50, + bottomNavigationBar: BottomNavigationBar( + backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg, + currentIndex: _currentIndex, + items: _navBarItems, + selectedLabelStyle: StreamChatTheme.of(context).textTheme.footnoteBold, + unselectedLabelStyle: + StreamChatTheme.of(context).textTheme.footnoteBold, + type: BottomNavigationBarType.fixed, + selectedItemColor: + StreamChatTheme.of(context).colorTheme.textHighEmphasis, + unselectedItemColor: Colors.grey, + onTap: (index) { + setState(() => _currentIndex = index); + }, + ), + body: IndexedStack( + index: _currentIndex, + children: [ + ChannelList(), + UserMentionsPage(), + ], + ), + ); + } + + StreamSubscription? badgeListener; + + @override + void initState() { + if (!kIsWeb) { + badgeListener = StreamChat.of(context) + .client + .state + .totalUnreadCountStream + .listen((count) { + if (count > 0) { + FlutterAppBadger.updateBadgeCount(count); + } else { + FlutterAppBadger.removeBadge(); + } + }); + } + super.initState(); + } + + @override + void dispose() { + badgeListener?.cancel(); + super.dispose(); + } +} + +class LeftDrawer extends StatelessWidget { + const LeftDrawer({ + Key? key, + required this.user, + }) : super(key: key); + + final User user; + + @override + Widget build(BuildContext context) { + return Drawer( + child: Container( + color: StreamChatTheme.of(context).colorTheme.barsBg, + child: SafeArea( + child: Padding( + padding: EdgeInsets.only( + top: MediaQuery.of(context).viewPadding.top + 8, + ), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only( + bottom: 20.0, + left: 8, + ), + child: Row( + children: [ + UserAvatar( + user: user, + showOnlineStatus: false, + constraints: BoxConstraints.tight(Size.fromRadius(20)), + ), + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Text( + user.name, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + ListTile( + leading: StreamSvgIcon.penWrite( + color: StreamChatTheme.of(context) + .colorTheme + .textHighEmphasis + .withOpacity(.5), + ), + onTap: () { + Navigator.popAndPushNamed( + context, + Routes.NEW_CHAT, + ); + }, + title: Text( + 'New direct message', + style: TextStyle( + fontSize: 14.5, + ), + ), + ), + ListTile( + leading: StreamSvgIcon.contacts( + color: StreamChatTheme.of(context) + .colorTheme + .textHighEmphasis + .withOpacity(.5), + ), + onTap: () { + Navigator.popAndPushNamed( + context, + Routes.NEW_GROUP_CHAT, + ); + }, + title: Text( + 'New group', + style: TextStyle( + fontSize: 14.5, + ), + ), + ), + Expanded( + child: Container( + alignment: Alignment.bottomCenter, + child: ListTile( + onTap: () async { + Navigator.pop(context); + + if (!kIsWeb) { + final secureStorage = FlutterSecureStorage(); + await secureStorage.deleteAll(); + } + + final client = StreamChat.of(context).client; + client.disconnectUser(); + await client.dispose(); + + await Navigator.of( + context, + rootNavigator: true, + ).pushNamedAndRemoveUntil( + Routes.CHOOSE_USER, + ModalRoute.withName(Routes.CHOOSE_USER), + ); + }, + leading: StreamSvgIcon.user( + color: StreamChatTheme.of(context) + .colorTheme + .textHighEmphasis + .withOpacity(.5), + ), + title: Text( + 'Sign out', + style: TextStyle( + fontSize: 14.5, + ), + ), + trailing: IconButton( + icon: StreamSvgIcon.iconMoon( + size: 24, + ), + color: StreamChatTheme.of(context) + .colorTheme + .textLowEmphasis, + onPressed: () async { + final sp = await StreamingSharedPreferences.instance; + sp.setInt( + 'theme', + Theme.of(context).brightness == Brightness.dark + ? 1 + : -1, + ); + }, + ), + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/packages/stream_chat_v1/lib/channel_media_display_screen.dart b/packages/stream_chat_v1/lib/channel_media_display_screen.dart new file mode 100644 index 0000000..f2ad261 --- /dev/null +++ b/packages/stream_chat_v1/lib/channel_media_display_screen.dart @@ -0,0 +1,235 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:video_player/video_player.dart'; + +class ChannelMediaDisplayScreen extends StatefulWidget { + /// The sorting used for the channels matching the filters. + /// 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. + /// Direction can be ascending or descending. + final List? sortOptions; + + /// Pagination parameters + /// limit: the number of users to return (max is 30) + /// offset: the offset (max is 1000) + /// message_limit: how many messages should be included to each channel + final PaginationParams? paginationParams; + + /// The builder used when the file list is empty. + final WidgetBuilder? emptyBuilder; + + final ShowMessageCallback? onShowMessage; + + final MessageTheme messageTheme; + + const ChannelMediaDisplayScreen({ + required this.messageTheme, + this.sortOptions, + this.paginationParams, + this.emptyBuilder, + this.onShowMessage, + }); + + @override + _ChannelMediaDisplayScreenState createState() => + _ChannelMediaDisplayScreenState(); +} + +class _ChannelMediaDisplayScreenState extends State { + Map controllerCache = {}; + + @override + void initState() { + super.initState(); + final messageSearchBloc = MessageSearchBloc.of(context); + messageSearchBloc.search( + filter: Filter.in_( + 'cid', + [StreamChannel.of(context).channel.cid!], + ), + messageFilter: Filter.in_( + 'attachments.type', + ['image', 'video'], + ), + sort: widget.sortOptions, + pagination: widget.paginationParams, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg, + appBar: AppBar( + brightness: Theme.of(context).brightness, + elevation: 1, + centerTitle: true, + title: Text( + 'Photos & Videos', + style: TextStyle( + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis, + fontSize: 16.0, + ), + ), + leading: StreamBackButton(), + backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg, + ), + body: _buildMediaGrid(), + ); + } + + Widget _buildMediaGrid() { + final messageSearchBloc = MessageSearchBloc.of(context); + + return StreamBuilder>( + builder: (context, snapshot) { + if (snapshot.data == null) { + return Center( + child: const CircularProgressIndicator(), + ); + } + + if (snapshot.data!.isEmpty) { + if (widget.emptyBuilder != null) { + return widget.emptyBuilder!(context); + } + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + StreamSvgIcon.pictures( + size: 136.0, + color: StreamChatTheme.of(context).colorTheme.disabled, + ), + SizedBox(height: 16.0), + Text( + 'No Media', + style: TextStyle( + fontSize: 14.0, + color: + StreamChatTheme.of(context).colorTheme.textHighEmphasis, + ), + ), + SizedBox(height: 8.0), + Text( + 'Photos or video sent in this chat will \nappear here', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14.0, + color: StreamChatTheme.of(context) + .colorTheme + .textHighEmphasis + .withOpacity(0.5), + ), + ), + ], + ), + ); + } + + final media = <_AssetPackage>[]; + + for (var item in snapshot.data!) { + item.message.attachments + .where((e) => + (e.type == 'image' || e.type == 'video') && + e.ogScrapeUrl == null) + .forEach((e) { + VideoPlayerController? controller; + if (e.type == 'video') { + var cachedController = controllerCache[e.assetUrl]; + + if (cachedController == null) { + controller = VideoPlayerController.network(e.assetUrl!); + controller.initialize(); + controllerCache[e.assetUrl] = controller; + } else { + controller = cachedController; + } + } + media.add(_AssetPackage(e, item.message, controller)); + }); + } + + return LazyLoadScrollView( + onEndOfPage: () => messageSearchBloc.search( + filter: Filter.in_( + 'cid', + [StreamChannel.of(context).channel.cid!], + ), + messageFilter: Filter.in_( + 'attachments.type', + ['image', 'video'], + ), + sort: widget.sortOptions, + pagination: widget.paginationParams!.copyWith( + offset: messageSearchBloc.messageResponses?.length ?? 0, + ), + ), + child: GridView.builder( + gridDelegate: + SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3), + itemBuilder: (context, position) { + var channel = StreamChannel.of(context).channel; + return Padding( + padding: const EdgeInsets.all(1.0), + child: InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: channel, + child: FullScreenMedia( + mediaAttachments: + media.map((e) => e.attachment).toList(), + startIndex: position, + message: media[position].message, + userName: media[position].message.user!.name, + onShowMessage: widget.onShowMessage, + ), + ), + ), + ); + }, + child: media[position].attachment.type == 'image' + ? IgnorePointer( + child: ImageAttachment( + attachment: media[position].attachment, + message: media[position].message, + showTitle: false, + size: Size( + MediaQuery.of(context).size.width * 0.8, + MediaQuery.of(context).size.height * 0.3, + ), + messageTheme: widget.messageTheme, + ), + ) + : VideoPlayer(media[position].videoPlayer!), + ), + ); + }, + itemCount: media.length, + ), + ); + }, + stream: messageSearchBloc.messagesStream, + ); + } + + @override + void dispose() { + super.dispose(); + for (var c in controllerCache.values) { + c!.dispose(); + } + } +} + +class _AssetPackage { + Attachment attachment; + Message message; + VideoPlayerController? videoPlayer; + + _AssetPackage(this.attachment, this.message, this.videoPlayer); +} diff --git a/packages/stream_chat_v1/lib/channel_page.dart b/packages/stream_chat_v1/lib/channel_page.dart new file mode 100644 index 0000000..571a54d --- /dev/null +++ b/packages/stream_chat_v1/lib/channel_page.dart @@ -0,0 +1,188 @@ +import 'package:collection/collection.dart'; +import 'package:example/routes/routes.dart'; +import 'package:example/thread_page.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'chat_info_screen.dart'; +import 'group_info_screen.dart'; + +class ChannelPageArgs { + final Channel? channel; + final Message? initialMessage; + + const ChannelPageArgs({ + this.channel, + this.initialMessage, + }); +} + +class ChannelPage extends StatefulWidget { + final int? initialScrollIndex; + final double? initialAlignment; + final bool highlightInitialMessage; + + const ChannelPage({ + Key? key, + this.initialScrollIndex, + this.initialAlignment, + this.highlightInitialMessage = false, + }) : super(key: key); + + @override + _ChannelPageState createState() => _ChannelPageState(); +} + +class _ChannelPageState extends State { + Message? _quotedMessage; + FocusNode? _focusNode; + + @override + void initState() { + _focusNode = FocusNode(); + super.initState(); + } + + @override + void dispose() { + _focusNode!.dispose(); + super.dispose(); + } + + void _reply(Message message) { + setState(() => _quotedMessage = message); + WidgetsBinding.instance!.addPostFrameCallback((timeStamp) { + _focusNode!.requestFocus(); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: StreamChatTheme.of(context).colorTheme.appBg, + appBar: ChannelHeader( + showTypingIndicator: false, + onImageTap: () async { + var channel = StreamChannel.of(context).channel; + + if (channel.memberCount == 2 && channel.isDistinct) { + final currentUser = StreamChat.of(context).user; + final otherUser = channel.state!.members.firstWhereOrNull( + (element) => element.user!.id != currentUser!.id, + ); + if (otherUser != null) { + final pop = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StreamChannel( + child: ChatInfoScreen( + messageTheme: StreamChatTheme.of(context).ownMessageTheme, + user: otherUser.user, + ), + channel: channel, + ), + ), + ); + + if (pop == true) { + Navigator.pop(context); + } + } + } else { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StreamChannel( + child: GroupInfoScreen( + messageTheme: StreamChatTheme.of(context).ownMessageTheme, + ), + channel: channel, + ), + ), + ); + } + }, + ), + body: Column( + children: [ + Expanded( + child: Stack( + children: [ + MessageListView( + initialScrollIndex: widget.initialScrollIndex, + initialAlignment: widget.initialAlignment, + highlightInitialMessage: widget.highlightInitialMessage, + onMessageSwiped: _reply, + messageBuilder: (context, details, messages, defaultMessage) { + return defaultMessage.copyWith( + onReplyTap: _reply, + onShowMessage: (m, c) async { + final client = StreamChat.of(context).client; + final message = m; + final channel = client.channel( + c.type, + id: c.id, + ); + if (channel.state == null) { + await channel.watch(); + } + Navigator.pushReplacementNamed( + context, + Routes.CHANNEL_PAGE, + arguments: ChannelPageArgs( + channel: channel, + initialMessage: message, + ), + ); + }, + ); + }, + threadBuilder: (_, parentMessage) { + return ThreadPage( + parent: parentMessage, + ); + }, + pinPermissions: ['owner', 'admin', 'member'], + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + alignment: Alignment.centerLeft, + color: StreamChatTheme.of(context) + .colorTheme + .appBg + .withOpacity(.9), + child: TypingIndicator( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + style: StreamChatTheme.of(context) + .textTheme + .footnote + .copyWith( + color: StreamChatTheme.of(context) + .colorTheme + .textLowEmphasis), + ), + ), + ), + ], + ), + ), + MessageInput( + focusNode: _focusNode, + quotedMessage: _quotedMessage, + onQuotedMessageCleared: () { + setState(() => _quotedMessage = null); + _focusNode!.unfocus(); + }, + ), + ], + ), + ); + } +} diff --git a/packages/stream_chat_v1/lib/chat_info_screen.dart b/packages/stream_chat_v1/lib/chat_info_screen.dart index 5c8441e..6c6bb1e 100644 --- a/packages/stream_chat_v1/lib/chat_info_screen.dart +++ b/packages/stream_chat_v1/lib/chat_info_screen.dart @@ -1,24 +1,34 @@ +import 'package:collection/collection.dart' show IterableExtension; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:jiffy/jiffy.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'main.dart'; +import 'channel_file_display_screen.dart'; +import 'channel_media_display_screen.dart'; +import 'channel_page.dart'; +import 'pinned_messages_screen.dart'; import 'routes/routes.dart'; /// Detail screen for a 1:1 chat correspondence class ChatInfoScreen extends StatefulWidget { /// 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 _ChatInfoScreenState createState() => _ChatInfoScreenState(); } class _ChatInfoScreenState extends State { - ValueNotifier mutedBool = ValueNotifier(false); + ValueNotifier mutedBool = ValueNotifier(false); @override void initState() { @@ -30,25 +40,25 @@ class _ChatInfoScreenState extends State { Widget build(BuildContext context) { final channel = StreamChannel.of(context).channel; return Scaffold( - backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow, + backgroundColor: StreamChatTheme.of(context).colorTheme.appBg, body: ListView( children: [ _buildUserHeader(), Container( height: 8.0, - color: StreamChatTheme.of(context).colorTheme.greyGainsboro, + color: StreamChatTheme.of(context).colorTheme.disabled, ), _buildOptionListTiles(), Container( height: 8.0, - color: StreamChatTheme.of(context).colorTheme.greyGainsboro, + color: StreamChatTheme.of(context).colorTheme.disabled, ), if ([ 'admin', 'owner', - ].contains(channel.state.members - .firstWhere((m) => m.userId == channel.client.state.user.id, - orElse: () => null) + ].contains(channel.state!.members + .firstWhereOrNull( + (m) => m.userId == channel.client.state.user!.id) ?.role)) _buildDeleteListTile(), ], @@ -58,7 +68,7 @@ class _ChatInfoScreenState extends State { Widget _buildUserHeader() { return Material( - color: StreamChatTheme.of(context).colorTheme.whiteSnow, + color: StreamChatTheme.of(context).colorTheme.appBg, child: SafeArea( child: Stack( children: [ @@ -68,7 +78,7 @@ class _ChatInfoScreenState extends State { Padding( padding: const EdgeInsets.all(16.0), child: UserAvatar( - user: widget.user, + user: widget.user!, constraints: BoxConstraints( maxWidth: 72.0, maxHeight: 72.0, @@ -78,23 +88,23 @@ class _ChatInfoScreenState extends State { ), ), Text( - widget.user.name, + widget.user!.name, style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold), ), SizedBox(height: 7.0), _buildConnectedTitleState(), SizedBox(height: 15.0), OptionListTile( - title: '@${widget.user.id}', - tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow, + title: '@${widget.user!.id}', + tileColor: StreamChatTheme.of(context).colorTheme.appBg, trailing: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Text( - widget.user.name, + widget.user!.name, style: TextStyle( color: StreamChatTheme.of(context) .colorTheme - .black + .textHighEmphasis .withOpacity(0.5), fontSize: 16.0), ), @@ -124,7 +134,7 @@ class _ChatInfoScreenState extends State { // title: 'Notifications', // leading: StreamSvgIcon.Icon_notification( // size: 24.0, - // color: StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5), + // color: StreamChatTheme.of(context).colorTheme.textHighEmphasis.withOpacity(0.5), // ), // trailing: CupertinoSwitch( // value: true, @@ -138,7 +148,7 @@ class _ChatInfoScreenState extends State { mutedBool.value = snapshot.data; return OptionListTile( - tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow, + tileColor: StreamChatTheme.of(context).colorTheme.appBg, title: 'Mute user', titleTextStyle: StreamChatTheme.of(context).textTheme.body, leading: Padding( @@ -147,21 +157,21 @@ class _ChatInfoScreenState extends State { size: 24.0, color: StreamChatTheme.of(context) .colorTheme - .black + .textHighEmphasis .withOpacity(0.5), ), ), trailing: snapshot.data == null ? CircularProgressIndicator() - : ValueListenableBuilder( + : ValueListenableBuilder( valueListenable: mutedBool, builder: (context, value, _) { return CupertinoSwitch( - value: value, + value: value!, onChanged: (val) { mutedBool.value = val; - if (snapshot.data) { + if (snapshot.data!) { channel.channel.unmute(); } else { channel.channel.mute(); @@ -176,7 +186,7 @@ class _ChatInfoScreenState extends State { // title: 'Block User', // leading: StreamSvgIcon.Icon_user_delete( // size: 24.0, - // color: StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5), + // color: StreamChatTheme.of(context).colorTheme.textHighEmphasis.withOpacity(0.5), // ), // trailing: CupertinoSwitch( // value: widget.user.banned, @@ -190,20 +200,83 @@ class _ChatInfoScreenState extends State { // ), // onTap: () {}, // ), + OptionListTile( + title: 'Pinned Messages', + tileColor: StreamChatTheme.of(context).colorTheme.appBg, + titleTextStyle: StreamChatTheme.of(context).textTheme.body, + leading: Padding( + padding: const EdgeInsets.symmetric(horizontal: 22.0), + child: StreamSvgIcon.pin( + size: 24.0, + color: StreamChatTheme.of(context) + .colorTheme + .textHighEmphasis + .withOpacity(0.5), + ), + ), + trailing: StreamSvgIcon.right( + color: StreamChatTheme.of(context).colorTheme.textLowEmphasis, + ), + onTap: () { + final channel = StreamChannel.of(context).channel; + + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: channel, + child: MessageSearchBloc( + child: PinnedMessagesScreen( + messageTheme: widget.messageTheme, + sortOptions: [ + SortOption( + 'created_at', + direction: SortOption.ASC, + ), + ], + paginationParams: PaginationParams(limit: 20), + onShowMessage: (m, c) async { + final client = StreamChat.of(context).client; + final message = m; + final channel = client.channel( + c.type, + id: c.id, + ); + if (channel.state == null) { + await channel.watch(); + } + Navigator.pushNamed( + context, + Routes.CHANNEL_PAGE, + arguments: ChannelPageArgs( + channel: channel, + initialMessage: message, + ), + ); + }, + ), + ), + ), + ), + ); + }, + ), OptionListTile( title: 'Photos & Videos', - tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow, + tileColor: StreamChatTheme.of(context).colorTheme.appBg, titleTextStyle: StreamChatTheme.of(context).textTheme.body, leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: StreamSvgIcon.pictures( size: 36.0, - color: - StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5), + color: StreamChatTheme.of(context) + .colorTheme + .textHighEmphasis + .withOpacity(0.5), ), ), trailing: StreamSvgIcon.right( - color: StreamChatTheme.of(context).colorTheme.grey, + color: StreamChatTheme.of(context).colorTheme.textLowEmphasis, ), onTap: () { final channel = StreamChannel.of(context).channel; @@ -215,6 +288,7 @@ class _ChatInfoScreenState extends State { channel: channel, child: MessageSearchBloc( child: ChannelMediaDisplayScreen( + messageTheme: widget.messageTheme, sortOptions: [ SortOption( 'created_at', @@ -250,18 +324,20 @@ class _ChatInfoScreenState extends State { ), OptionListTile( title: 'Files', - tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow, + tileColor: StreamChatTheme.of(context).colorTheme.appBg, titleTextStyle: StreamChatTheme.of(context).textTheme.body, leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 18.0), child: StreamSvgIcon.files( size: 32.0, - color: - StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5), + color: StreamChatTheme.of(context) + .colorTheme + .textHighEmphasis + .withOpacity(0.5), ), ), trailing: StreamSvgIcon.right( - color: StreamChatTheme.of(context).colorTheme.grey, + color: StreamChatTheme.of(context).colorTheme.textLowEmphasis, ), onTap: () { final channel = StreamChannel.of(context).channel; @@ -289,18 +365,20 @@ class _ChatInfoScreenState extends State { ), OptionListTile( title: 'Shared groups', - tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow, + tileColor: StreamChatTheme.of(context).colorTheme.appBg, titleTextStyle: StreamChatTheme.of(context).textTheme.body, leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 22.0), child: StreamSvgIcon.iconGroup( size: 24.0, - color: - StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5), + color: StreamChatTheme.of(context) + .colorTheme + .textHighEmphasis + .withOpacity(0.5), ), ), trailing: StreamSvgIcon.right( - color: StreamChatTheme.of(context).colorTheme.grey, + color: StreamChatTheme.of(context).colorTheme.textLowEmphasis, ), onTap: () { Navigator.push( @@ -317,21 +395,21 @@ class _ChatInfoScreenState extends State { Widget _buildDeleteListTile() { return OptionListTile( title: 'Delete Conversation', - tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow, + tileColor: StreamChatTheme.of(context).colorTheme.appBg, titleTextStyle: StreamChatTheme.of(context).textTheme.body.copyWith( - color: StreamChatTheme.of(context).colorTheme.accentRed, + color: StreamChatTheme.of(context).colorTheme.accentError, ), leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 22.0), child: StreamSvgIcon.delete( - color: StreamChatTheme.of(context).colorTheme.accentRed, + color: StreamChatTheme.of(context).colorTheme.accentError, size: 24.0, ), ), onTap: () { _showDeleteDialog(); }, - titleColor: StreamChatTheme.of(context).colorTheme.accentRed, + titleColor: StreamChatTheme.of(context).colorTheme.accentError, ); } @@ -343,7 +421,7 @@ class _ChatInfoScreenState extends State { question: 'Are you sure you want to delete this conversation?', cancelText: 'CANCEL', icon: StreamSvgIcon.delete( - color: StreamChatTheme.of(context).colorTheme.accentRed, + color: StreamChatTheme.of(context).colorTheme.accentError, ), ); var channel = StreamChannel.of(context).channel; @@ -367,7 +445,7 @@ class _ChatInfoScreenState extends State { style: TextStyle( color: StreamChatTheme.of(context) .colorTheme - .black + .textHighEmphasis .withOpacity(0.5)), ); } else { @@ -376,7 +454,7 @@ class _ChatInfoScreenState extends State { style: TextStyle( color: StreamChatTheme.of(context) .colorTheme - .black + .textHighEmphasis .withOpacity(0.5)), ); } @@ -385,7 +463,7 @@ class _ChatInfoScreenState extends State { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - if (widget.user.online) + if (widget.user!.online) Material( type: MaterialType.circle, child: Container( @@ -396,13 +474,13 @@ class _ChatInfoScreenState extends State { ), child: Material( shape: CircleBorder(), - color: StreamChatTheme.of(context).colorTheme.accentGreen, + color: StreamChatTheme.of(context).colorTheme.accentInfo, ), ), - color: StreamChatTheme.of(context).colorTheme.white, + color: StreamChatTheme.of(context).colorTheme.barsBg, ), alternativeWidget, - if (widget.user.online) + if (widget.user!.online) SizedBox( width: 24.0, ), @@ -412,8 +490,8 @@ class _ChatInfoScreenState extends State { } class _SharedGroupsScreen extends StatefulWidget { - final User mainUser; - final User otherUser; + final User? mainUser; + final User? otherUser; _SharedGroupsScreen(this.mainUser, this.otherUser); @@ -427,7 +505,7 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> { var chat = StreamChat.of(context); return Scaffold( - backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow, + backgroundColor: StreamChatTheme.of(context).colorTheme.appBg, appBar: AppBar( brightness: Theme.of(context).brightness, elevation: 1, @@ -435,28 +513,18 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> { title: Text( 'Shared Groups', style: TextStyle( - color: StreamChatTheme.of(context).colorTheme.black, + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis, fontSize: 16.0), ), leading: StreamBackButton(), - backgroundColor: StreamChatTheme.of(context).colorTheme.white, + backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg, ), body: StreamBuilder>( stream: chat.client.queryChannels( - filter: { - r'$and': [ - { - 'members': { - r'$in': [widget.otherUser.id], - }, - }, - { - 'members': { - r'$in': [widget.mainUser.id], - }, - } - ], - }, + filter: Filter.and([ + Filter.in_('members', [widget.otherUser!.id]), + Filter.in_('members', [widget.mainUser!.id]), + ]), ), builder: (context, snapshot) { if (!snapshot.hasData) { @@ -465,21 +533,23 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> { ); } - if (snapshot.data.isEmpty) { + if (snapshot.data!.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ StreamSvgIcon.message( size: 136.0, - color: StreamChatTheme.of(context).colorTheme.greyGainsboro, + color: StreamChatTheme.of(context).colorTheme.disabled, ), SizedBox(height: 16.0), Text( 'No Shared Groups', style: TextStyle( fontSize: 14.0, - color: StreamChatTheme.of(context).colorTheme.black, + color: StreamChatTheme.of(context) + .colorTheme + .textHighEmphasis, ), ), SizedBox(height: 8.0), @@ -490,7 +560,7 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> { fontSize: 14.0, color: StreamChatTheme.of(context) .colorTheme - .black + .textHighEmphasis .withOpacity(0.5), ), ), @@ -499,11 +569,11 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> { ); } - final channels = snapshot.data + final channels = snapshot.data! .where((c) => - c.state.members.any((m) => - m.userId != widget.mainUser.id && - m.userId != widget.otherUser.id) || + c.state!.members.any((m) => + m.userId != widget.mainUser!.id && + m.userId != widget.otherUser!.id) || !c.isDistinct) .toList(); @@ -523,24 +593,24 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> { Widget _buildListTile(Channel channel) { var extraData = channel.extraData; - var members = channel.state.members; + var members = channel.state!.members; var textStyle = TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold); return Container( height: 64.0, child: LayoutBuilder(builder: (context, constraints) { - String title; + String? title; if (extraData['name'] == null) { final otherMembers = members.where( - (member) => member.userId != StreamChat.of(context).user.id); + (member) => member.userId != StreamChat.of(context).user!.id); if (otherMembers.isNotEmpty) { final maxWidth = constraints.maxWidth; - final maxChars = maxWidth / textStyle.fontSize; + final maxChars = maxWidth / textStyle.fontSize!; var currentChars = 0; final currentMembers = []; otherMembers.forEach((element) { - final newLength = currentChars + element.user.name.length; + final newLength = currentChars + element.user!.name.length; if (newLength < maxChars) { currentChars = newLength; currentMembers.add(element); @@ -550,12 +620,12 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> { final exceedingMembers = otherMembers.length - currentMembers.length; title = - '${currentMembers.map((e) => e.user.name).join(', ')} ${exceedingMembers > 0 ? '+ $exceedingMembers' : ''}'; + '${currentMembers.map((e) => e.user!.name).join(', ')} ${exceedingMembers > 0 ? '+ $exceedingMembers' : ''}'; } else { title = 'No title'; } } else { - title = extraData['name']; + title = extraData['name'] as String; } return Column( @@ -565,7 +635,7 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> { children: [ Padding( padding: const EdgeInsets.all(8.0), - child: ChannelImage( + child: ChannelAvatar( channel: channel, constraints: BoxConstraints(maxWidth: 40.0, maxHeight: 40.0), @@ -583,7 +653,7 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> { style: TextStyle( color: StreamChatTheme.of(context) .colorTheme - .black + .textHighEmphasis .withOpacity(0.5)), ), ) @@ -592,8 +662,10 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> { ), Container( height: 1.0, - color: - StreamChatTheme.of(context).colorTheme.black.withOpacity(.08), + color: StreamChatTheme.of(context) + .colorTheme + .textHighEmphasis + .withOpacity(.08), ), ], ); diff --git a/packages/stream_chat_v1/lib/chips_input_text_field.dart b/packages/stream_chat_v1/lib/chips_input_text_field.dart index 0fc0791..a67895e 100644 --- a/packages/stream_chat_v1/lib/chips_input_text_field.dart +++ b/packages/stream_chat_v1/lib/chips_input_text_field.dart @@ -6,18 +6,18 @@ typedef OnChipAdded = void Function(T chip); typedef OnChipRemoved = void Function(T chip); class ChipsInputTextField extends StatefulWidget { - final TextEditingController controller; - final FocusNode focusNode; - final ValueChanged onInputChanged; + final TextEditingController? controller; + final FocusNode? focusNode; + final ValueChanged? onInputChanged; final ChipBuilder chipBuilder; - final OnChipAdded onChipAdded; - final OnChipRemoved onChipRemoved; + final OnChipAdded? onChipAdded; + final OnChipRemoved? onChipRemoved; final String hint; const ChipsInputTextField({ - Key key, - @required this.chipBuilder, - @required this.controller, + Key? key, + required this.chipBuilder, + required this.controller, this.onInputChanged, this.focusNode, this.onChipAdded, @@ -35,7 +35,7 @@ class ChipInputTextFieldState extends State> { void addItem(T item) { setState(() => _chips.add(item)); - if (widget.onChipAdded != null) widget.onChipAdded(item); + if (widget.onChipAdded != null) widget.onChipAdded!(item); } void removeItem(T item) { @@ -43,7 +43,7 @@ class ChipInputTextFieldState extends State> { _chips.remove(item); if (_chips.isEmpty) resumeItemAddition(); }); - if (widget.onChipRemoved != null) widget.onChipRemoved(item); + if (widget.onChipRemoved != null) widget.onChipRemoved!(item); } void pauseItemAddition() { @@ -66,7 +66,7 @@ class ChipInputTextFieldState extends State> { onTap: _pauseItemAddition ? resumeItemAddition : null, child: Material( elevation: 1, - color: StreamChatTheme.of(context).colorTheme.white, + color: StreamChatTheme.of(context).colorTheme.barsBg, child: Container( child: Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 16), @@ -82,7 +82,7 @@ class ChipInputTextFieldState extends State> { .copyWith( color: StreamChatTheme.of(context) .colorTheme - .black + .textHighEmphasis .withOpacity(.5)), ), ), @@ -119,7 +119,7 @@ class ChipInputTextFieldState extends State> { .copyWith( color: StreamChatTheme.of(context) .colorTheme - .black + .textHighEmphasis .withOpacity(.5)), ), ), @@ -134,14 +134,14 @@ class ChipInputTextFieldState extends State> { ? StreamSvgIcon.user( color: StreamChatTheme.of(context) .colorTheme - .black + .textHighEmphasis .withOpacity(0.5), size: 24, ) : StreamSvgIcon.userAdd( color: StreamChatTheme.of(context) .colorTheme - .black + .textHighEmphasis .withOpacity(0.5), size: 24, ), diff --git a/packages/stream_chat_v1/lib/choose_user_page.dart b/packages/stream_chat_v1/lib/choose_user_page.dart index a6dd5e1..e487acd 100644 --- a/packages/stream_chat_v1/lib/choose_user_page.dart +++ b/packages/stream_chat_v1/lib/choose_user_page.dart @@ -1,4 +1,5 @@ import 'package:example/app_config.dart'; +import 'package:example/home_page.dart'; import 'package:example/stream_version.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -18,7 +19,7 @@ class ChooseUserPage extends StatelessWidget { final users = defaultUsers; return Scaffold( - backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow, + backgroundColor: StreamChatTheme.of(context).colorTheme.appBg, body: SafeArea( child: Column( crossAxisAlignment: CrossAxisAlignment.center, @@ -32,7 +33,7 @@ class ChooseUserPage extends StatelessWidget { child: SvgPicture.asset( 'assets/logo.svg', height: 40, - color: StreamChatTheme.of(context).colorTheme.accentBlue, + color: StreamChatTheme.of(context).colorTheme.accentPrimary, ), ), ), @@ -54,7 +55,7 @@ class ChooseUserPage extends StatelessWidget { separatorBuilder: (context, i) { return Container( height: 1, - color: StreamChatTheme.of(context).colorTheme.greyWhisper, + color: StreamChatTheme.of(context).colorTheme.borders, ); }, itemCount: users.length + 1, @@ -78,7 +79,7 @@ class ChooseUserPage extends StatelessWidget { borderRadius: BorderRadius.circular(16), color: StreamChatTheme.of(context) .colorTheme - .white, + .barsBg, ), height: 100, width: 100, @@ -89,8 +90,11 @@ class ChooseUserPage extends StatelessWidget { ), ); - final client = StreamChat.of(context).client; - client.apiKey = kDefaultStreamApiKey; + final client = StreamChatClient( + kDefaultStreamApiKey, + logLevel: Level.INFO, + ); + await client.connectUser( user, token, @@ -115,6 +119,7 @@ class ChooseUserPage extends StatelessWidget { context, Routes.HOME, ModalRoute.withName(Routes.HOME), + arguments: HomePageArgs(client), ); }, leading: UserAvatar( @@ -136,13 +141,13 @@ class ChooseUserPage extends StatelessWidget { .copyWith( color: StreamChatTheme.of(context) .colorTheme - .grey, + .textLowEmphasis, ), ), trailing: StreamSvgIcon.arrowRight( color: StreamChatTheme.of(context) .colorTheme - .accentBlue, + .accentPrimary, ), ); }), @@ -152,11 +157,12 @@ class ChooseUserPage extends StatelessWidget { }, leading: CircleAvatar( child: StreamSvgIcon.settings( - color: StreamChatTheme.of(context).colorTheme.black, + color: StreamChatTheme.of(context) + .colorTheme + .textHighEmphasis, ), - backgroundColor: StreamChatTheme.of(context) - .colorTheme - .greyWhisper, + backgroundColor: + StreamChatTheme.of(context).colorTheme.borders, ), title: Text( 'Advanced Options', @@ -168,8 +174,9 @@ class ChooseUserPage extends StatelessWidget { .textTheme .footnote .copyWith( - color: - StreamChatTheme.of(context).colorTheme.grey, + color: StreamChatTheme.of(context) + .colorTheme + .textLowEmphasis, ), ), trailing: SvgPicture.asset( diff --git a/packages/stream_chat_v1/lib/group_chat_details_screen.dart b/packages/stream_chat_v1/lib/group_chat_details_screen.dart index afc9d12..a2f51ed 100644 --- a/packages/stream_chat_v1/lib/group_chat_details_screen.dart +++ b/packages/stream_chat_v1/lib/group_chat_details_screen.dart @@ -2,15 +2,15 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:uuid/uuid.dart'; -import 'main.dart'; +import 'channel_page.dart'; import 'routes/routes.dart'; class GroupChatDetailsScreen extends StatefulWidget { - final List selectedUsers; + final List? selectedUsers; const GroupChatDetailsScreen({ - Key key, - @required this.selectedUsers, + Key? key, + required this.selectedUsers, }) : super(key: key); @override @@ -20,14 +20,14 @@ class GroupChatDetailsScreen extends StatefulWidget { class _GroupChatDetailsScreenState extends State { final _selectedUsers = []; - TextEditingController _groupNameController; + TextEditingController? _groupNameController; bool _isGroupNameEmpty = true; int get _totalUsers => _selectedUsers.length; void _groupNameListener() { - final name = _groupNameController.text; + final name = _groupNameController!.text; if (mounted) { setState(() { _isGroupNameEmpty = name.isEmpty; @@ -38,7 +38,7 @@ class _GroupChatDetailsScreenState extends State { @override void initState() { super.initState(); - _selectedUsers.addAll(widget.selectedUsers); + _selectedUsers.addAll(widget.selectedUsers!); _groupNameController = TextEditingController() ..addListener(_groupNameListener); } @@ -59,16 +59,16 @@ class _GroupChatDetailsScreenState extends State { return false; }, child: Scaffold( - backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow, + backgroundColor: StreamChatTheme.of(context).colorTheme.appBg, appBar: AppBar( brightness: Theme.of(context).brightness, elevation: 1, - backgroundColor: StreamChatTheme.of(context).colorTheme.white, + backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg, leading: const StreamBackButton(), title: Text( 'Name of Group Chat', style: TextStyle( - color: StreamChatTheme.of(context).colorTheme.black, + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis, fontSize: 16, ), ), @@ -83,7 +83,9 @@ class _GroupChatDetailsScreenState extends State { 'NAME', style: TextStyle( fontSize: 12, - color: StreamChatTheme.of(context).colorTheme.grey, + color: StreamChatTheme.of(context) + .colorTheme + .textLowEmphasis, ), ), SizedBox(width: 16), @@ -101,7 +103,9 @@ class _GroupChatDetailsScreenState extends State { hintText: 'Choose a group chat name', hintStyle: TextStyle( fontSize: 14, - color: StreamChatTheme.of(context).colorTheme.grey, + color: StreamChatTheme.of(context) + .colorTheme + .textLowEmphasis, ), ), ), @@ -117,20 +121,20 @@ class _GroupChatDetailsScreenState extends State { icon: StreamSvgIcon.check( size: 24, color: _isGroupNameEmpty - ? StreamChatTheme.of(context).colorTheme.grey - : StreamChatTheme.of(context).colorTheme.accentBlue, + ? StreamChatTheme.of(context).colorTheme.textLowEmphasis + : StreamChatTheme.of(context).colorTheme.accentPrimary, ), onPressed: _isGroupNameEmpty ? null : () async { try { - final groupName = _groupNameController.text; + final groupName = _groupNameController!.text; final client = StreamChat.of(context).client; final channel = client.channel('messaging', id: Uuid().v4(), extraData: { 'members': [ - client.state.user.id, + client.state.user!.id, ..._selectedUsers.map((e) => e.id), ], 'name': groupName, @@ -188,7 +192,9 @@ class _GroupChatDetailsScreenState extends State { child: Text( '$_totalUsers ${_totalUsers > 1 ? 'Members' : 'Member'}', style: TextStyle( - color: StreamChatTheme.of(context).colorTheme.grey, + color: StreamChatTheme.of(context) + .colorTheme + .textLowEmphasis, ), ), ), @@ -201,9 +207,7 @@ class _GroupChatDetailsScreenState extends State { itemCount: _selectedUsers.length + 1, separatorBuilder: (_, __) => Container( height: 1, - color: StreamChatTheme.of(context) - .colorTheme - .greyWhisper, + color: StreamChatTheme.of(context).colorTheme.borders, ), itemBuilder: (_, index) { if (index == _selectedUsers.length) { @@ -211,7 +215,7 @@ class _GroupChatDetailsScreenState extends State { height: 1, color: StreamChatTheme.of(context) .colorTheme - .greyWhisper, + .borders, ); } final user = _selectedUsers[index]; @@ -237,7 +241,7 @@ class _GroupChatDetailsScreenState extends State { Icons.clear_rounded, color: StreamChatTheme.of(context) .colorTheme - .black, + .textHighEmphasis, ), padding: const EdgeInsets.all(0), splashRadius: 24, @@ -266,7 +270,8 @@ class _GroupChatDetailsScreenState extends State { void _showErrorAlert() { showModalBottomSheet( - backgroundColor: StreamChatTheme.of(context).colorTheme.white, + useRootNavigator: false, + backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg, context: context, shape: RoundedRectangleBorder( borderRadius: BorderRadius.only( @@ -281,7 +286,7 @@ class _GroupChatDetailsScreenState extends State { height: 26.0, ), StreamSvgIcon.error( - color: StreamChatTheme.of(context).colorTheme.accentRed, + color: StreamChatTheme.of(context).colorTheme.accentError, size: 24.0, ), SizedBox( @@ -299,14 +304,16 @@ class _GroupChatDetailsScreenState extends State { height: 36.0, ), Container( - color: - StreamChatTheme.of(context).colorTheme.black.withOpacity(.08), + color: StreamChatTheme.of(context) + .colorTheme + .textHighEmphasis + .withOpacity(.08), height: 1.0, ), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - FlatButton( + TextButton( child: Text( 'OK', style: StreamChatTheme.of(context) @@ -315,7 +322,7 @@ class _GroupChatDetailsScreenState extends State { .copyWith( color: StreamChatTheme.of(context) .colorTheme - .accentBlue), + .accentPrimary), ), onPressed: () { Navigator.of(context).pop(); diff --git a/packages/stream_chat_v1/lib/group_info_screen.dart b/packages/stream_chat_v1/lib/group_info_screen.dart index b76cc9b..4193213 100644 --- a/packages/stream_chat_v1/lib/group_info_screen.dart +++ b/packages/stream_chat_v1/lib/group_info_screen.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:collection/collection.dart' show IterableExtension; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:jiffy/jiffy.dart'; @@ -7,39 +8,49 @@ import 'package:stream_chat_flutter/src/option_list_tile.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'channel_file_display_screen.dart'; +import 'channel_media_display_screen.dart'; +import 'channel_page.dart'; import 'chat_info_screen.dart'; -import 'main.dart'; +import 'pinned_messages_screen.dart'; import 'routes/routes.dart'; class GroupInfoScreen extends StatefulWidget { + final MessageTheme messageTheme; + + const GroupInfoScreen({ + Key? key, + required this.messageTheme, + }) : super(key: key); + @override _GroupInfoScreenState createState() => _GroupInfoScreenState(); } class _GroupInfoScreenState extends State { - TextEditingController _nameController; + TextEditingController? _nameController; - TextEditingController _searchController; + TextEditingController? _searchController; String _userNameQuery = ''; - Timer _debounce; - Function modalSetStateCallback; + Timer? _debounce; + Function? modalSetStateCallback; final FocusNode _focusNode = FocusNode(); bool listExpanded = false; - ValueNotifier mutedBool = ValueNotifier(false); + ValueNotifier mutedBool = ValueNotifier(false); void _userNameListener() { - if (_searchController.text == _userNameQuery) { + if (_searchController!.text == _userNameQuery) { return; } - if (_debounce?.isActive ?? false) _debounce.cancel(); + if (_debounce?.isActive ?? false) _debounce!.cancel(); _debounce = Timer(const Duration(milliseconds: 350), () { if (mounted && modalSetStateCallback != null) { - modalSetStateCallback(() { - _userNameQuery = _searchController.text; + modalSetStateCallback!(() { + _userNameQuery = _searchController!.text; }); } }); @@ -50,10 +61,12 @@ class _GroupInfoScreenState extends State { super.initState(); var channel = StreamChannel.of(context); _nameController = TextEditingController.fromValue( - TextEditingValue(text: channel.channel.extraData['name'] ?? '')); + TextEditingValue( + text: (channel.channel.extraData['name'] as String?) ?? ''), + ); _searchController = TextEditingController()..addListener(_userNameListener); - _nameController.addListener(() { + _nameController!.addListener(() { setState(() {}); }); mutedBool = ValueNotifier(StreamChannel.of(context).channel.isMuted); @@ -64,28 +77,27 @@ class _GroupInfoScreenState extends State { var channel = StreamChannel.of(context); return StreamBuilder>( - stream: channel.channel.state.membersStream, + stream: channel.channel.state!.membersStream, builder: (context, snapshot) { if (!snapshot.hasData) { return Container( - color: StreamChatTheme.of(context).colorTheme.greyGainsboro, + color: StreamChatTheme.of(context).colorTheme.disabled, child: Center(child: CircularProgressIndicator()), ); } - var userMember = snapshot.data.firstWhere( - (e) => e.user.id == StreamChat.of(context).user.id, - orElse: () => null, + var userMember = snapshot.data!.firstWhereOrNull( + (e) => e.user!.id == StreamChat.of(context).user!.id, ); var isOwner = userMember?.role == 'owner'; return Scaffold( - backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow, + backgroundColor: StreamChatTheme.of(context).colorTheme.appBg, appBar: AppBar( brightness: Theme.of(context).brightness, elevation: 1.0, toolbarHeight: 56.0, - backgroundColor: StreamChatTheme.of(context).colorTheme.white, + backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg, leading: StreamBackButton(), title: Column( children: [ @@ -96,8 +108,9 @@ class _GroupInfoScreenState extends State { return Text( 'Loading...', style: TextStyle( - color: - StreamChatTheme.of(context).colorTheme.black, + color: StreamChatTheme.of(context) + .colorTheme + .textHighEmphasis, fontSize: 16, ), maxLines: 1, @@ -109,11 +122,13 @@ class _GroupInfoScreenState extends State { _getChannelName( 2 * MediaQuery.of(context).size.width / 3, members: snapshot.data, - extraData: state.data.channel.extraData, + extraData: state.data!.channel!.extraData, maxFontSize: 16.0, - ), + )!, style: TextStyle( - color: StreamChatTheme.of(context).colorTheme.black, + color: StreamChatTheme.of(context) + .colorTheme + .textHighEmphasis, fontSize: 16, ), maxLines: 1, @@ -124,11 +139,11 @@ class _GroupInfoScreenState extends State { height: 3.0, ), 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( color: StreamChatTheme.of(context) .colorTheme - .black + .textHighEmphasis .withOpacity(0.5), fontSize: 12.0, ), @@ -148,7 +163,7 @@ class _GroupInfoScreenState extends State { child: StreamSvgIcon.userAdd( color: StreamChatTheme.of(context) .colorTheme - .accentBlue), + .accentPrimary), ), ), ), @@ -156,10 +171,10 @@ class _GroupInfoScreenState extends State { ), body: ListView( children: [ - _buildMembers(snapshot.data), + _buildMembers(snapshot.data!), Container( height: 8.0, - color: StreamChatTheme.of(context).colorTheme.greyGainsboro, + color: StreamChatTheme.of(context).colorTheme.disabled, ), if (isOwner) _buildNameTile(), _buildOptionListTiles(), @@ -195,9 +210,8 @@ class _GroupInfoScreenState extends State { return Material( child: InkWell( onTap: () { - final userMember = groupMembers.firstWhere( - (e) => e.user.id == StreamChat.of(context).user.id, - orElse: () => null, + final userMember = groupMembers.firstWhereOrNull( + (e) => e.user!.id == StreamChat.of(context).user!.id, ); _showUserInfoModal(member.user, userMember?.role == 'owner'); }, @@ -211,7 +225,7 @@ class _GroupInfoScreenState extends State { padding: const EdgeInsets.symmetric( horizontal: 8.0, vertical: 12.0), child: UserAvatar( - user: member.user, + user: member.user!, constraints: BoxConstraints( maxHeight: 40.0, maxWidth: 40.0), ), @@ -222,18 +236,18 @@ class _GroupInfoScreenState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - member.user.name, + member.user!.name, style: TextStyle(fontWeight: FontWeight.bold), ), SizedBox( height: 1.0, ), Text( - _getLastSeen(member.user), + _getLastSeen(member.user!), style: TextStyle( color: StreamChatTheme.of(context) .colorTheme - .black + .textHighEmphasis .withOpacity(0.5)), ), ], @@ -246,7 +260,7 @@ class _GroupInfoScreenState extends State { style: TextStyle( color: StreamChatTheme.of(context) .colorTheme - .black + .textHighEmphasis .withOpacity(0.5)), ), ), @@ -256,13 +270,13 @@ class _GroupInfoScreenState extends State { height: 1.0, color: StreamChatTheme.of(context) .colorTheme - .greyGainsboro, + .disabled, ), ], ), ), ), - color: StreamChatTheme.of(context).colorTheme.whiteSnow, + color: StreamChatTheme.of(context).colorTheme.appBg, ); }, ), @@ -274,7 +288,7 @@ class _GroupInfoScreenState extends State { }); }, child: Material( - color: StreamChatTheme.of(context).colorTheme.whiteSnow, + color: StreamChatTheme.of(context).colorTheme.appBg, child: Container( height: 65.0, child: Column( @@ -286,8 +300,9 @@ class _GroupInfoScreenState extends State { padding: const EdgeInsets.symmetric( horizontal: 21.0, vertical: 12.0), child: StreamSvgIcon.down( - color: - StreamChatTheme.of(context).colorTheme.grey, + color: StreamChatTheme.of(context) + .colorTheme + .textLowEmphasis, ), ), Expanded( @@ -300,7 +315,7 @@ class _GroupInfoScreenState extends State { style: TextStyle( color: StreamChatTheme.of(context) .colorTheme - .grey), + .textLowEmphasis), ), ], ), @@ -310,8 +325,7 @@ class _GroupInfoScreenState extends State { ), Container( height: 1.0, - color: - StreamChatTheme.of(context).colorTheme.greyGainsboro, + color: StreamChatTheme.of(context).colorTheme.disabled, ), ], ), @@ -324,10 +338,10 @@ class _GroupInfoScreenState extends State { Widget _buildNameTile() { var channel = StreamChannel.of(context).channel; - var channelName = channel.extraData['name'] ?? ''; + var channelName = (channel.extraData['name'] as String?) ?? ''; return Material( - color: StreamChatTheme.of(context).colorTheme.whiteSnow, + color: StreamChatTheme.of(context).colorTheme.appBg, child: Container( height: 56.0, alignment: Alignment.center, @@ -340,7 +354,7 @@ class _GroupInfoScreenState extends State { style: StreamChatTheme.of(context).textTheme.footnote.copyWith( color: StreamChatTheme.of(context) .colorTheme - .black + .textHighEmphasis .withOpacity(0.5)), ), ), @@ -351,7 +365,8 @@ class _GroupInfoScreenState extends State { child: TextField( focusNode: _focusNode, controller: _nameController, - cursorColor: StreamChatTheme.of(context).colorTheme.black, + cursorColor: + StreamChatTheme.of(context).colorTheme.textHighEmphasis, decoration: InputDecoration.collapsed( hintText: 'Add a group name', hintStyle: StreamChatTheme.of(context) @@ -360,7 +375,7 @@ class _GroupInfoScreenState extends State { .copyWith( color: StreamChatTheme.of(context) .colorTheme - .black + .textHighEmphasis .withOpacity(0.5))), style: TextStyle( fontWeight: FontWeight.bold, @@ -368,8 +383,7 @@ class _GroupInfoScreenState extends State { ), ), ), - if ((channelName == null) || - (channelName != _nameController.text.trim())) + if (channelName != _nameController!.text.trim()) Row( mainAxisSize: MainAxisSize.min, children: [ @@ -377,12 +391,12 @@ class _GroupInfoScreenState extends State { child: StreamSvgIcon.closeSmall(), onTap: () { setState(() { - _nameController.text = _getChannelName( + _nameController!.text = _getChannelName( 2 * MediaQuery.of(context).size.width / 3, - members: channel.state.members, + members: channel.state!.members, extraData: channel.extraData, maxFontSize: 16.0, - ); + )!; _focusNode.unfocus(); }); }, @@ -391,16 +405,17 @@ class _GroupInfoScreenState extends State { padding: const EdgeInsets.only(right: 16.0, left: 8.0), child: InkWell( child: StreamSvgIcon.check( - color: - StreamChatTheme.of(context).colorTheme.accentBlue, + color: StreamChatTheme.of(context) + .colorTheme + .accentPrimary, size: 24.0, ), onTap: () { StreamChannel.of(context).channel.update({ - 'name': _nameController.text.trim(), + 'name': _nameController!.text.trim(), }).catchError((err) { setState(() { - _nameController.text = channelName; + _nameController!.text = channelName; _focusNode.unfocus(); }); }); @@ -424,7 +439,7 @@ class _GroupInfoScreenState extends State { // title: 'Notifications', // leading: StreamSvgIcon.Icon_notification( // size: 24.0, - // color: StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5), + // color: StreamChatTheme.of(context).colorTheme.textHighEmphasis.withOpacity(0.5), // ), // trailing: CupertinoSwitch( // value: true, @@ -438,9 +453,8 @@ class _GroupInfoScreenState extends State { mutedBool.value = snapshot.data; return OptionListTile( - tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow, - separatorColor: - StreamChatTheme.of(context).colorTheme.greyGainsboro, + tileColor: StreamChatTheme.of(context).colorTheme.appBg, + separatorColor: StreamChatTheme.of(context).colorTheme.disabled, title: 'Mute group', titleTextStyle: StreamChatTheme.of(context).textTheme.body, leading: Padding( @@ -449,21 +463,21 @@ class _GroupInfoScreenState extends State { size: 24.0, color: StreamChatTheme.of(context) .colorTheme - .black + .textHighEmphasis .withOpacity(0.5), ), ), trailing: snapshot.data == null ? CircularProgressIndicator() - : ValueListenableBuilder( + : ValueListenableBuilder( valueListenable: mutedBool, builder: (context, value, _) { return CupertinoSwitch( - value: value, + value: value!, onChanged: (val) { mutedBool.value = val; - if (snapshot.data) { + if (snapshot.data!) { channel.channel.unmute(); } else { channel.channel.mute(); @@ -475,20 +489,83 @@ class _GroupInfoScreenState extends State { ); }), OptionListTile( - tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow, - separatorColor: StreamChatTheme.of(context).colorTheme.greyGainsboro, + title: 'Pinned Messages', + tileColor: StreamChatTheme.of(context).colorTheme.appBg, + titleTextStyle: StreamChatTheme.of(context).textTheme.body, + leading: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: StreamSvgIcon.pin( + size: 24.0, + color: StreamChatTheme.of(context) + .colorTheme + .textHighEmphasis + .withOpacity(0.5), + ), + ), + trailing: StreamSvgIcon.right( + color: StreamChatTheme.of(context).colorTheme.textLowEmphasis, + ), + onTap: () { + final channel = StreamChannel.of(context).channel; + + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: channel, + child: MessageSearchBloc( + child: PinnedMessagesScreen( + messageTheme: widget.messageTheme, + sortOptions: [ + SortOption( + 'created_at', + direction: SortOption.ASC, + ), + ], + paginationParams: PaginationParams(limit: 20), + onShowMessage: (m, c) async { + final client = StreamChat.of(context).client; + final message = m; + final channel = client.channel( + c.type, + id: c.id, + ); + if (channel.state == null) { + await channel.watch(); + } + Navigator.pushNamed( + context, + Routes.CHANNEL_PAGE, + arguments: ChannelPageArgs( + channel: channel, + initialMessage: message, + ), + ); + }, + ), + ), + ), + ), + ); + }, + ), + OptionListTile( + tileColor: StreamChatTheme.of(context).colorTheme.appBg, + separatorColor: StreamChatTheme.of(context).colorTheme.disabled, title: 'Photos & Videos', titleTextStyle: StreamChatTheme.of(context).textTheme.body, leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 12.0), child: StreamSvgIcon.pictures( size: 32.0, - color: - StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5), + color: StreamChatTheme.of(context) + .colorTheme + .textHighEmphasis + .withOpacity(0.5), ), ), trailing: StreamSvgIcon.right( - color: StreamChatTheme.of(context).colorTheme.grey, + color: StreamChatTheme.of(context).colorTheme.textLowEmphasis, ), onTap: () { var channel = StreamChannel.of(context).channel; @@ -500,6 +577,7 @@ class _GroupInfoScreenState extends State { channel: channel, child: MessageSearchBloc( child: ChannelMediaDisplayScreen( + messageTheme: widget.messageTheme, sortOptions: [ SortOption( 'created_at', @@ -534,20 +612,22 @@ class _GroupInfoScreenState extends State { }, ), OptionListTile( - tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow, - separatorColor: StreamChatTheme.of(context).colorTheme.greyGainsboro, + tileColor: StreamChatTheme.of(context).colorTheme.appBg, + separatorColor: StreamChatTheme.of(context).colorTheme.disabled, title: 'Files', titleTextStyle: StreamChatTheme.of(context).textTheme.body, leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 12.0), child: StreamSvgIcon.files( size: 32.0, - color: - StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5), + color: StreamChatTheme.of(context) + .colorTheme + .textHighEmphasis + .withOpacity(0.5), ), ), trailing: StreamSvgIcon.right( - color: StreamChatTheme.of(context).colorTheme.grey, + color: StreamChatTheme.of(context).colorTheme.textLowEmphasis, ), onTap: () { var channel = StreamChannel.of(context).channel; @@ -575,9 +655,8 @@ class _GroupInfoScreenState extends State { ), if (!channel.channel.isDistinct) OptionListTile( - tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow, - separatorColor: - StreamChatTheme.of(context).colorTheme.greyGainsboro, + tileColor: StreamChatTheme.of(context).colorTheme.appBg, + separatorColor: StreamChatTheme.of(context).colorTheme.disabled, title: 'Leave Group', titleTextStyle: StreamChatTheme.of(context).textTheme.body, leading: Padding( @@ -586,7 +665,7 @@ class _GroupInfoScreenState extends State { size: 24.0, color: StreamChatTheme.of(context) .colorTheme - .black + .textHighEmphasis .withOpacity(0.5), ), ), @@ -602,12 +681,12 @@ class _GroupInfoScreenState extends State { question: 'Are you sure you want to leave this conversation?', cancelText: 'CANCEL', icon: StreamSvgIcon.userRemove( - color: StreamChatTheme.of(context).colorTheme.accentRed, + color: StreamChatTheme.of(context).colorTheme.accentError, ), ); if (res == true) { 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); } }, @@ -620,6 +699,7 @@ class _GroupInfoScreenState extends State { var channel = StreamChannel.of(context).channel; showDialog( + useRootNavigator: false, context: context, barrierColor: StreamChatTheme.of(context).colorTheme.overlay, builder: (context) { @@ -645,7 +725,7 @@ class _GroupInfoScreenState extends State { child: UserListView( selectedUsers: {}, onUserTap: (user, _) async { - _searchController.clear(); + _searchController!.clear(); await channel.addMembers([user.id]); Navigator.pop(context); @@ -655,18 +735,18 @@ class _GroupInfoScreenState extends State { pagination: PaginationParams( limit: 25, ), - filter: { - if (_searchController.text.isNotEmpty) - 'name': { - r'$autocomplete': _userNameQuery, - }, - 'id': { - r'$nin': [ - StreamChat.of(context).user.id, - ...channel.state.members.map((e) => e.userId), - ], - }, - }, + filter: Filter.and( + [ + if (_searchController!.text.isNotEmpty) + Filter.autoComplete('name', _userNameQuery), + Filter.notIn('id', [ + StreamChat.of(context).user!.id, + ...channel.state!.members + .map(((e) => e.userId)) + .whereType(), + ]), + ], + ), sort: [ SortOption( 'name', @@ -691,7 +771,7 @@ class _GroupInfoScreenState extends State { size: 96, color: StreamChatTheme.of(context) .colorTheme - .grey, + .textLowEmphasis, ), ), Text( @@ -729,28 +809,28 @@ class _GroupInfoScreenState extends State { height: 36, child: TextField( controller: _searchController, - cursorColor: theme.colorTheme.black, + cursorColor: theme.colorTheme.textHighEmphasis, autofocus: true, decoration: InputDecoration( hintText: 'Search', hintStyle: theme.textTheme.body.copyWith( - color: theme.colorTheme.grey, + color: theme.colorTheme.textLowEmphasis, ), prefixIconConstraints: BoxConstraints.tight(Size(40, 24)), prefixIcon: StreamSvgIcon.search( - color: theme.colorTheme.black, + color: theme.colorTheme.textHighEmphasis, size: 24, ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(24.0), borderSide: BorderSide( - color: theme.colorTheme.greyWhisper, + color: theme.colorTheme.borders, ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(24.0), borderSide: BorderSide( - color: theme.colorTheme.greyWhisper, + color: theme.colorTheme.borders, )), contentPadding: const EdgeInsets.all(0), ), @@ -760,7 +840,7 @@ class _GroupInfoScreenState extends State { SizedBox(width: 16.0), IconButton( icon: StreamSvgIcon.closeSmall( - color: theme.colorTheme.grey, + color: theme.colorTheme.textLowEmphasis, ), constraints: BoxConstraints.tightFor( height: 24, @@ -776,149 +856,175 @@ class _GroupInfoScreenState extends State { ); } - void _showUserInfoModal(User user, bool isUserAdmin) { + void _showUserInfoModal(User? user, bool isUserAdmin) { var channel = StreamChannel.of(context).channel; + final color = StreamChatTheme.of(context).colorTheme.barsBg; showModalBottomSheet( + useRootNavigator: false, context: context, clipBehavior: Clip.antiAlias, isScrollControlled: true, + backgroundColor: color, builder: (context) { - return StreamChannel( - channel: channel, - child: Material( - color: StreamChatTheme.of(context).colorTheme.white, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - height: 24.0, - ), - Center( - child: Text( - user.name, - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, - ), + return SafeArea( + child: StreamChannel( + channel: channel, + child: Material( + color: color, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: 24.0, ), - ), - SizedBox( - height: 5.0, - ), - _buildConnectedTitleState(user), - Center( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: UserAvatar( - user: user, - constraints: BoxConstraints( - maxHeight: 64.0, - minHeight: 64.0, + Center( + child: Text( + user!.name, + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.bold, ), - borderRadius: BorderRadius.circular(32.0), ), ), - ), - if (StreamChat.of(context).user.id != user.id) - _buildModalListTile( - context, - StreamSvgIcon.user( - color: StreamChatTheme.of(context).colorTheme.grey, - size: 24.0, + SizedBox( + height: 5.0, + ), + _buildConnectedTitleState(user)!, + Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: UserAvatar( + user: user, + constraints: BoxConstraints( + maxHeight: 64.0, + minHeight: 64.0, + ), + borderRadius: BorderRadius.circular(32.0), + ), ), - 'View info', - () async { - var client = StreamChat.of(context).client; + ), + if (StreamChat.of(context).user!.id != user.id) + _buildModalListTile( + context, + StreamSvgIcon.user( + color: StreamChatTheme.of(context) + .colorTheme + .textLowEmphasis, + size: 24.0, + ), + 'View info', + () async { + var client = StreamChat.of(context).client; - var c = client.channel('messaging', extraData: { - 'members': [ - user.id, - StreamChat.of(context).user.id, - ], - }); + var c = client.channel('messaging', extraData: { + 'members': [ + user.id, + StreamChat.of(context).user!.id, + ], + }); - await c.watch(); + await c.watch(); - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => StreamChannel( - channel: c, - child: ChatInfoScreen( - user: user, + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: c, + child: ChatInfoScreen( + messageTheme: widget.messageTheme, + user: user, + ), ), ), - ), - ); - }, - ), - if (StreamChat.of(context).user.id != user.id) - _buildModalListTile( - context, - StreamSvgIcon.message( - color: StreamChatTheme.of(context).colorTheme.grey, - size: 24.0, + ); + }, ), - 'Message', - () async { - var client = StreamChat.of(context).client; + if (StreamChat.of(context).user!.id != user.id) + _buildModalListTile( + context, + StreamSvgIcon.message( + color: StreamChatTheme.of(context) + .colorTheme + .textLowEmphasis, + size: 24.0, + ), + 'Message', + () async { + var client = StreamChat.of(context).client; - var c = client.channel('messaging', extraData: { - 'members': [ - user.id, - StreamChat.of(context).user.id, - ], - }); + var c = client.channel('messaging', extraData: { + 'members': [ + user.id, + StreamChat.of(context).user!.id, + ], + }); - await c.watch(); + await c.watch(); - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => StreamChannel( - channel: c, - child: ChannelPage(), + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: c, + child: ChannelPage(), + ), ), - ), - ); - }, - ), - if (!channel.isDistinct && - StreamChat.of(context).user.id != user.id && - isUserAdmin) - _buildModalListTile( - context, - StreamSvgIcon.iconUserSettings( - color: StreamChatTheme.of(context).colorTheme.grey, - size: 24.0, - ), - 'Make Owner', () { - // TODO: Add make owner implementation (Remaining from backend) - }), - if (!channel.isDistinct && - StreamChat.of(context).user.id != user.id && - isUserAdmin) - _buildModalListTile( - context, - StreamSvgIcon.userRemove( - color: StreamChatTheme.of(context).colorTheme.accentRed, - size: 24.0, - ), - 'Remove From Group', () async { - await channel.removeMembers([user.id]); - Navigator.pop(context); - }, color: StreamChatTheme.of(context).colorTheme.accentRed), - _buildModalListTile( - context, - StreamSvgIcon.closeSmall( - color: StreamChatTheme.of(context).colorTheme.grey, - size: 24.0, + ); + }, ), - 'Cancel', () { - Navigator.pop(context); - }), - ], + // if (!channel.isDistinct && + // StreamChat.of(context).user!.id != user.id && + // isUserAdmin) + // _buildModalListTile( + // context, + // StreamSvgIcon.iconUserSettings( + // color: StreamChatTheme.of(context).colorTheme.textLowEmphasis, + // size: 24.0, + // ), + // 'Make Owner', () { + // // TODO: Add make owner implementation (Remaining from backend) + // }), + if (!channel.isDistinct && + StreamChat.of(context).user!.id != user.id && + isUserAdmin) + _buildModalListTile( + context, + StreamSvgIcon.userRemove( + color: StreamChatTheme.of(context) + .colorTheme.accentError, + size: 24.0, + ), + 'Remove From Group', () async { + final res = await showConfirmationDialog( + context, + title: 'Remove member', + okText: 'REMOVE', + question: + 'Are you sure you want to remove this member?', + cancelText: 'CANCEL', + ); + + if (res == true) { + await channel.removeMembers([user.id]); + } + Navigator.pop(context); + }, + color: StreamChatTheme.of(context) + .colorTheme.accentError), + _buildModalListTile( + context, + StreamSvgIcon.closeSmall( + color: StreamChatTheme.of(context) + .colorTheme + .textLowEmphasis, + size: 24.0, + ), + 'Cancel', () { + Navigator.pop(context); + }), + ], + ), ), ), ); @@ -932,7 +1038,7 @@ class _GroupInfoScreenState extends State { ); } - Widget _buildConnectedTitleState(User user) { + Widget? _buildConnectedTitleState(User? user) { var alternativeWidget; final otherMember = user; @@ -944,7 +1050,7 @@ class _GroupInfoScreenState extends State { style: TextStyle( color: StreamChatTheme.of(context) .colorTheme - .black + .textHighEmphasis .withOpacity(0.5)), ); } else { @@ -953,7 +1059,7 @@ class _GroupInfoScreenState extends State { style: TextStyle( color: StreamChatTheme.of(context) .colorTheme - .black + .textHighEmphasis .withOpacity(0.5)), ); } @@ -964,18 +1070,18 @@ class _GroupInfoScreenState extends State { Widget _buildModalListTile( BuildContext context, Widget leading, String title, VoidCallback onTap, - {Color color}) { - color ??= StreamChatTheme.of(context).colorTheme.black; + {Color? color}) { + color ??= StreamChatTheme.of(context).colorTheme.textHighEmphasis; return Material( - color: StreamChatTheme.of(context).colorTheme.white, + color: StreamChatTheme.of(context).colorTheme.barsBg, child: InkWell( onTap: onTap, child: Column( children: [ Container( height: 1.0, - color: StreamChatTheme.of(context).colorTheme.greyGainsboro, + color: StreamChatTheme.of(context).colorTheme.disabled, ), Container( height: 64.0, @@ -1001,24 +1107,24 @@ class _GroupInfoScreenState extends State { ); } - String _getChannelName( + String? _getChannelName( double width, { - List members, - Map extraData, - double maxFontSize, + List? members, + required Map extraData, + double? maxFontSize, }) { - String title; + String? title; var client = StreamChat.of(context); if (extraData['name'] == null) { final otherMembers = - members.where((member) => member.user.id != client.user.id); + members!.where((member) => member.user!.id != client.user!.id); if (otherMembers.isNotEmpty) { final maxWidth = width; - final maxChars = maxWidth / maxFontSize; + final maxChars = maxWidth / maxFontSize!; var currentChars = 0; final currentMembers = []; otherMembers.forEach((element) { - final newLength = currentChars + element.user.name.length; + final newLength = currentChars + element.user!.name.length; if (newLength < maxChars) { currentChars = newLength; currentMembers.add(element); @@ -1027,7 +1133,7 @@ class _GroupInfoScreenState extends State { final exceedingMembers = otherMembers.length - currentMembers.length; title = - '${currentMembers.map((e) => e.user.name).join(', ')} ${exceedingMembers > 0 ? '+ $exceedingMembers' : ''}'; + '${currentMembers.map((e) => e.user!.name).join(', ')} ${exceedingMembers > 0 ? '+ $exceedingMembers' : ''}'; } else { title = 'No title'; } diff --git a/packages/stream_chat_v1/lib/home_page.dart b/packages/stream_chat_v1/lib/home_page.dart new file mode 100644 index 0000000..828230f --- /dev/null +++ b/packages/stream_chat_v1/lib/home_page.dart @@ -0,0 +1,45 @@ +import 'package:example/routes/app_routes.dart'; +import 'package:example/routes/routes.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +class HomePageArgs { + final StreamChatClient chatClient; + + HomePageArgs(this.chatClient); +} + +class HomePage extends StatefulWidget { + HomePage({ + Key? key, + required this.chatClient, + }) : super(key: key); + + final StreamChatClient chatClient; + + @override + _HomePageState createState() => _HomePageState(); +} + +class _HomePageState extends State { + final GlobalKey _navigatorKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return StreamChat( + client: widget.chatClient, + child: WillPopScope( + onWillPop: () async { + final canPop = await _navigatorKey.currentState?.maybePop() ?? false; + return !canPop; + }, + child: Navigator( + key: _navigatorKey, + onGenerateRoute: AppRoutes.generateRoute, + initialRoute: Routes.CHANNEL_LIST_PAGE, + ), + ), + ); + } +} diff --git a/packages/stream_chat_v1/lib/main.dart b/packages/stream_chat_v1/lib/main.dart index 4e93955..e17ef4a 100644 --- a/packages/stream_chat_v1/lib/main.dart +++ b/packages/stream_chat_v1/lib/main.dart @@ -1,34 +1,27 @@ import 'dart:async'; -import 'package:example/chat_info_screen.dart'; import 'package:example/choose_user_page.dart'; -import 'package:example/group_info_screen.dart'; +import 'package:example/home_page.dart'; +import 'package:example/splash_screen.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_app_badger/flutter_app_badger.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:lottie/lottie.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_persistence/stream_chat_persistence.dart'; import 'package:streaming_shared_preferences/streaming_shared_preferences.dart'; import 'app_config.dart'; -import 'notifications_service.dart'; import 'routes/app_routes.dart'; import 'routes/routes.dart'; -import 'search_text_field.dart'; final chatPersistentClient = StreamChatPersistenceClient( logLevel: Level.SEVERE, - connectionMode: ConnectionMode.background, + connectionMode: ConnectionMode.regular, ); void main() async { - WidgetsFlutterBinding.ensureInitialized(); - runApp(MyApp()); } @@ -37,16 +30,12 @@ class MyApp extends StatefulWidget { _MyAppState createState() => _MyAppState(); } -class _MyAppState extends State with TickerProviderStateMixin { - InitData _initData; - bool _animCompleted = false; - Animation _animation, _scaleAnimation; - AnimationController _animationController, _scaleAnimationController; - Animation _colorAnimation; - int timeOfStartMs; +class _MyAppState extends State + with SplashScreenStateMixin, TickerProviderStateMixin { + InitData? _initData; Future _initConnection() async { - String apiKey, userId, token; + String? apiKey, userId, token; if (!kIsWeb) { final secureStorage = FlutterSecureStorage(); @@ -58,9 +47,9 @@ class _MyAppState extends State with TickerProviderStateMixin { final client = StreamChatClient( apiKey ?? kDefaultStreamApiKey, logLevel: Level.SEVERE, - )..chatPersistenceClient = chatPersistentClient; + ); - if (userId != null) { + if (userId != null && token != null) { await client.connectUser( User(id: userId), token, @@ -72,56 +61,9 @@ class _MyAppState extends State with TickerProviderStateMixin { return InitData(client, prefs); } - void _createAnimations() { - _scaleAnimationController = AnimationController( - vsync: this, - value: 0, - duration: Duration( - milliseconds: 500, - ), - ); - _scaleAnimation = Tween( - begin: 1.0, - end: 1.5, - ).animate(CurvedAnimation( - parent: _scaleAnimationController, - curve: Curves.easeInOutBack, - )); - - _animationController = AnimationController( - vsync: this, - duration: Duration( - milliseconds: 1000, - ), - ); - _animation = Tween( - begin: 0.0, - end: 1000.0, - ).animate(CurvedAnimation( - parent: _animationController, - curve: Curves.easeInOut, - )); - _colorAnimation = ColorTween( - begin: Color(0xff005FFF), - end: Color(0xff005FFF), - ).animate(CurvedAnimation( - parent: _animationController, - curve: Curves.easeInOut, - )); - _colorAnimation = ColorTween( - begin: Color(0xff005FFF), - end: Colors.transparent, - ).animate(CurvedAnimation( - parent: _animationController, - curve: Curves.easeInOut, - )); - } - @override void initState() { - timeOfStartMs = DateTime.now().millisecondsSinceEpoch; - - _createAnimations(); + final timeOfStartMs = DateTime.now().millisecondsSinceEpoch; _initConnection().then( (initData) { @@ -129,97 +71,23 @@ class _MyAppState extends State with TickerProviderStateMixin { _initData = initData; }); - var now = DateTime.now().millisecondsSinceEpoch; + final now = DateTime.now().millisecondsSinceEpoch; if (now - timeOfStartMs > 1500) { - SchedulerBinding.instance.addPostFrameCallback((timeStamp) { - _scaleAnimationController.forward().whenComplete(() { - _animationController.forward(); - }); + SchedulerBinding.instance!.addPostFrameCallback((timeStamp) { + forwardAnimations(); }); } else { Future.delayed(Duration(milliseconds: 1500)).then((value) { - _scaleAnimationController.forward().whenComplete(() { - _animationController.forward(); - }); - }); - } - - if (!kIsWeb) { - _initData.client.state?.totalUnreadCountStream?.listen((count) { - if (count > 0) { - FlutterAppBadger.updateBadgeCount(count); - } else { - FlutterAppBadger.removeBadge(); - } + forwardAnimations(); }); } }, ); - _animationController.addStatusListener((status) { - if (status == AnimationStatus.completed) { - setState(() { - _animCompleted = true; - }); - } - }); super.initState(); } - Widget _buildAnimation() { - return MaterialApp( - home: Stack( - clipBehavior: Clip.none, - alignment: Alignment.center, - children: [ - AnimatedBuilder( - animation: _scaleAnimation, - builder: (context, _) { - return Transform.scale( - scale: _scaleAnimation.value, - child: AnimatedBuilder( - animation: _colorAnimation, - builder: (context, snapshot) { - return Container( - alignment: Alignment.center, - constraints: BoxConstraints.expand(), - color: _colorAnimation == null - ? Color(0xff005FFF) - : _colorAnimation.value, - child: !_animationController.isAnimating - ? Lottie.asset( - 'assets/floating_boat.json', - alignment: Alignment.center, - ) - : SizedBox(), - ); - }), - ); - }, - ), - AnimatedBuilder( - animation: _animation, - builder: (context, snapshot) { - return Transform.scale( - scale: _animation.value, - child: Container( - width: 1.0, - height: 1.0, - decoration: BoxDecoration( - color: Colors.white - .withOpacity(1 - _animationController.value), - shape: BoxShape.circle, - ), - ), - ); - }, - ), - ], - ), - ); - } - @override Widget build(BuildContext context) { return Stack( @@ -227,31 +95,11 @@ class _MyAppState extends State with TickerProviderStateMixin { children: [ if (_initData != null) PreferenceBuilder( - preference: _initData.preferences.getInt( + preference: _initData!.preferences.getInt( 'theme', defaultValue: 0, ), builder: (context, snapshot) => MaterialApp( - builder: (context, child) { - return StreamChat( - client: _initData.client, - onBackgroundEventReceived: (e) => - showLocalNotification(e, _initData.client.state.user.id), - child: Builder( - builder: (context) => AnnotatedRegion( - child: child, - value: SystemUiOverlayStyle( - systemNavigationBarColor: - StreamChatTheme.of(context).colorTheme.white, - systemNavigationBarIconBrightness: - Theme.of(context).brightness == Brightness.dark - ? Brightness.light - : Brightness.dark, - ), - ), - ), - ); - }, theme: ThemeData.light(), darkTheme: ThemeData.dark(), themeMode: { @@ -259,809 +107,46 @@ class _MyAppState extends State with TickerProviderStateMixin { 0: ThemeMode.system, 1: ThemeMode.light, }[snapshot], + builder: (context, child) => StreamChatTheme( + data: StreamChatThemeData( + brightness: Theme.of(context).brightness, + ), + child: child!, + ), onGenerateRoute: AppRoutes.generateRoute, - initialRoute: _initData.client.state.user == null + onGenerateInitialRoutes: (initialRouteName) { + if (initialRouteName == Routes.HOME) { + return [ + AppRoutes.generateRoute( + RouteSettings( + name: Routes.HOME, + arguments: HomePageArgs(_initData!.client), + ), + )! + ]; + } + return [ + AppRoutes.generateRoute( + RouteSettings( + name: Routes.CHOOSE_USER, + ), + )! + ]; + }, + initialRoute: _initData!.client.state.user == null ? Routes.CHOOSE_USER : Routes.HOME, ), ), - if (!_animCompleted) _buildAnimation(), + if (!animationCompleted) buildAnimation(), ], ); } } -class HomePage extends StatefulWidget { - @override - _HomePageState createState() => _HomePageState(); -} - -class _HomePageState extends State { - int _currentIndex = 0; - - bool _isSelected(int index) => _currentIndex == index; - - List get _navBarItems { - return [ - BottomNavigationBarItem( - icon: Stack( - clipBehavior: Clip.none, - children: [ - StreamSvgIcon.message( - color: _isSelected(0) - ? StreamChatTheme.of(context).colorTheme.black - : Colors.grey, - ), - Positioned( - top: -3, - right: -16, - child: UnreadIndicator(), - ), - ], - ), - label: 'Chats', - ), - BottomNavigationBarItem( - icon: Stack( - clipBehavior: Clip.none, - children: [ - StreamSvgIcon.mentions( - color: _isSelected(1) - ? StreamChatTheme.of(context).colorTheme.black - : Colors.grey, - ), - ], - ), - label: 'Mentions', - ), - ]; - } - - @override - Widget build(BuildContext context) { - final user = StreamChat.of(context).user; - return Scaffold( - backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow, - appBar: ChannelListHeader( - onNewChatButtonTap: () { - Navigator.pushNamed(context, Routes.NEW_CHAT); - }, - preNavigationCallback: () { - FocusScope.of(context).requestFocus(FocusNode()); - }, - ), - drawer: _buildDrawer(context, user), - drawerEdgeDragWidth: 50, - bottomNavigationBar: BottomNavigationBar( - backgroundColor: StreamChatTheme.of(context).colorTheme.white, - currentIndex: _currentIndex, - items: _navBarItems, - selectedLabelStyle: StreamChatTheme.of(context).textTheme.footnoteBold, - unselectedLabelStyle: - StreamChatTheme.of(context).textTheme.footnoteBold, - type: BottomNavigationBarType.fixed, - selectedItemColor: StreamChatTheme.of(context).colorTheme.black, - unselectedItemColor: Colors.grey, - onTap: (index) { - setState(() => _currentIndex = index); - }, - ), - body: IndexedStack( - index: _currentIndex, - children: [ - ChannelListPage(), - UserMentionPage(), - ], - ), - ); - } - - Drawer _buildDrawer(BuildContext context, User user) { - return Drawer( - child: Container( - color: StreamChatTheme.of(context).colorTheme.white, - child: SafeArea( - child: Padding( - padding: EdgeInsets.only( - top: MediaQuery.of(context).viewPadding.top + 8, - ), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.only( - bottom: 20.0, - left: 8, - ), - child: Row( - children: [ - UserAvatar( - user: user, - showOnlineStatus: false, - constraints: BoxConstraints.tight(Size.fromRadius(20)), - ), - Padding( - padding: const EdgeInsets.only(left: 16.0), - child: Text( - user.name, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - ), - ListTile( - leading: StreamSvgIcon.penWrite( - color: StreamChatTheme.of(context) - .colorTheme - .black - .withOpacity(.5), - ), - onTap: () { - Navigator.popAndPushNamed( - context, - Routes.NEW_CHAT, - ); - }, - title: Text( - 'New direct message', - style: TextStyle( - fontSize: 14.5, - ), - ), - ), - ListTile( - leading: StreamSvgIcon.contacts( - color: StreamChatTheme.of(context) - .colorTheme - .black - .withOpacity(.5), - ), - onTap: () { - Navigator.popAndPushNamed( - context, - Routes.NEW_GROUP_CHAT, - ); - }, - title: Text( - 'New group', - style: TextStyle( - fontSize: 14.5, - ), - ), - ), - Expanded( - child: Container( - alignment: Alignment.bottomCenter, - child: ListTile( - onTap: () async { - Navigator.pop(context); - - if (!kIsWeb) { - final secureStorage = FlutterSecureStorage(); - await secureStorage.deleteAll(); - } - - StreamChat.of(context).client.disconnect( - clearUser: true, - ); - - await Navigator.pushReplacementNamed( - context, - Routes.CHOOSE_USER, - ); - }, - leading: StreamSvgIcon.user( - color: StreamChatTheme.of(context) - .colorTheme - .black - .withOpacity(.5), - ), - title: Text( - 'Sign out', - style: TextStyle( - fontSize: 14.5, - ), - ), - trailing: IconButton( - icon: StreamSvgIcon.iconMoon( - size: 24, - ), - color: StreamChatTheme.of(context).colorTheme.grey, - onPressed: () async { - final sp = await StreamingSharedPreferences.instance; - sp.setInt( - 'theme', - Theme.of(context).brightness == Brightness.dark - ? 1 - : -1, - ); - }, - ), - ), - ), - ), - ], - ), - ), - ), - ), - ); - } -} - -class UserMentionPage extends StatelessWidget { - @override - Widget build(BuildContext context) { - final user = StreamChat.of(context).user; - return MessageSearchBloc( - child: MessageSearchListView( - filters: { - 'members': { - r'$in': [user.id], - }, - }, - messageFilters: { - 'mentioned_users.id': { - r'$contains': user.id, - }, - }, - sortOptions: [ - SortOption( - 'created_at', - direction: SortOption.ASC, - ), - ], - paginationParams: PaginationParams(limit: 20), - showResultCount: false, - emptyBuilder: (_) { - return LayoutBuilder( - builder: (context, viewportConstraints) { - return SingleChildScrollView( - physics: AlwaysScrollableScrollPhysics(), - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: viewportConstraints.maxHeight, - ), - child: Center( - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(24), - child: StreamSvgIcon.mentions( - size: 96, - color: StreamChatTheme.of(context) - .colorTheme - .greyGainsboro, - ), - ), - Text( - 'No mentions exist yet...', - style: StreamChatTheme.of(context) - .textTheme - .body - .copyWith( - color: - StreamChatTheme.of(context).colorTheme.grey, - ), - ), - ], - ), - ), - ), - ); - }, - ); - }, - onItemTap: (messageResponse) async { - final client = StreamChat.of(context).client; - final message = messageResponse.message; - final channel = client.channel( - messageResponse.channel.type, - id: messageResponse.channel.id, - ); - if (channel.state == null) { - await channel.watch(); - } - Navigator.pushNamed( - context, - Routes.CHANNEL_PAGE, - arguments: ChannelPageArgs( - channel: channel, - initialMessage: message, - ), - ); - }, - ), - ); - } -} - -class ChannelListPage extends StatefulWidget { - @override - _ChannelListPageState createState() => _ChannelListPageState(); -} - -class _ChannelListPageState extends State { - TextEditingController _controller; - - String _channelQuery = ''; - - bool _isSearchActive = false; - - Timer _debounce; - - void _channelQueryListener() { - if (_debounce?.isActive ?? false) _debounce.cancel(); - _debounce = Timer(const Duration(milliseconds: 350), () { - if (mounted) { - setState(() { - _channelQuery = _controller.text; - _isSearchActive = _channelQuery.isNotEmpty; - }); - } - }); - } - - @override - void initState() { - super.initState(); - _controller = TextEditingController()..addListener(_channelQueryListener); - } - - @override - void dispose() { - _controller?.removeListener(_channelQueryListener); - _controller?.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final user = StreamChat.of(context).user; - return WillPopScope( - onWillPop: () async { - if (_isSearchActive) { - _controller.clear(); - setState(() => _isSearchActive = false); - return false; - } - return true; - }, - child: ChannelsBloc( - child: MessageSearchBloc( - child: NestedScrollView( - floatHeaderSlivers: true, - headerSliverBuilder: (_, __) => [ - SliverToBoxAdapter( - child: SearchTextField( - controller: _controller, - showCloseButton: _isSearchActive, - ), - ), - ], - body: AnimatedSwitcher( - duration: const Duration(milliseconds: 350), - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onPanDown: (_) => FocusScope.of(context).unfocus(), - child: _isSearchActive - ? MessageSearchListView( - showErrorTile: true, - messageQuery: _channelQuery, - filters: { - 'members': { - r'$in': [user.id] - }, - }, - sortOptions: [ - SortOption( - 'created_at', - direction: SortOption.ASC, - ), - ], - pullToRefresh: false, - paginationParams: PaginationParams(limit: 20), - emptyBuilder: (_) { - return LayoutBuilder( - builder: (context, viewportConstraints) { - return SingleChildScrollView( - physics: AlwaysScrollableScrollPhysics(), - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: viewportConstraints.maxHeight, - ), - child: Center( - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(24), - child: StreamSvgIcon.search( - size: 96, - color: Colors.grey, - ), - ), - Text( - 'No results...', - ), - ], - ), - ), - ), - ); - }, - ); - }, - onItemTap: (messageResponse) async { - FocusScope.of(context).requestFocus(FocusNode()); - final client = StreamChat.of(context).client; - final message = messageResponse.message; - final channel = client.channel( - messageResponse.channel.type, - id: messageResponse.channel.id, - ); - if (channel.state == null) { - await channel.watch(); - } - Navigator.pushNamed( - context, - Routes.CHANNEL_PAGE, - arguments: ChannelPageArgs( - channel: channel, - initialMessage: message, - ), - ); - }, - ) - : ChannelListView( - onStartChatPressed: () { - Navigator.pushNamed(context, Routes.NEW_CHAT); - }, - swipeToAction: true, - filter: { - 'members': { - r'$in': [user.id], - }, - }, - options: { - 'presence': true, - }, - pagination: PaginationParams( - limit: 20, - ), - channelWidget: ChannelPage(), - onViewInfoTap: (channel) { - Navigator.pop(context); - if (channel.memberCount == 2 && channel.isDistinct) { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => StreamChannel( - channel: channel, - child: ChatInfoScreen( - user: channel.state.members - .where((m) => - m.userId != - channel.client.state.user.id) - .first - .user, - ), - ), - ), - ); - } else { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => StreamChannel( - channel: channel, - child: GroupInfoScreen(), - ), - ), - ); - } - }, - ), - ), - ), - ), - ), - ), - ); - } -} - -class ChannelPageArgs { - final Channel channel; - final Message initialMessage; - - const ChannelPageArgs({ - this.channel, - this.initialMessage, - }); -} - -class ChannelPage extends StatefulWidget { - final int initialScrollIndex; - final double initialAlignment; - final bool highlightInitialMessage; - - const ChannelPage({ - Key key, - this.initialScrollIndex, - this.initialAlignment, - this.highlightInitialMessage = false, - }) : super(key: key); - - @override - _ChannelPageState createState() => _ChannelPageState(); -} - -class _ChannelPageState extends State { - Message _quotedMessage; - FocusNode _focusNode; - - @override - void initState() { - _focusNode = FocusNode(); - super.initState(); - } - - @override - void dispose() { - _focusNode.dispose(); - super.dispose(); - } - - void _reply(Message message) { - setState(() => _quotedMessage = message); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - _focusNode.requestFocus(); - }); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow, - appBar: ChannelHeader( - showTypingIndicator: false, - onImageTap: () async { - var channel = StreamChannel.of(context).channel; - - if (channel.memberCount == 2 && channel.isDistinct) { - final currentUser = StreamChat.of(context).user; - final otherUser = channel.state.members.firstWhere( - (element) => element.user.id != currentUser.id, - orElse: () => null, - ); - if (otherUser != null) { - final pop = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => StreamChannel( - child: ChatInfoScreen( - user: otherUser.user, - ), - channel: channel, - ), - ), - ); - - if (pop == true) { - Navigator.pop(context); - } - } - } else { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => StreamChannel( - child: GroupInfoScreen(), - channel: channel, - ), - ), - ); - } - }, - ), - body: Column( - children: [ - Expanded( - child: Stack( - children: [ - MessageListView( - initialScrollIndex: widget.initialScrollIndex, - initialAlignment: widget.initialAlignment, - highlightInitialMessage: widget.highlightInitialMessage, - onMessageSwiped: _reply, - onReplyTap: _reply, - threadBuilder: (_, parentMessage) { - return ThreadPage( - parent: parentMessage, - ); - }, - onShowMessage: (m, c) async { - final client = StreamChat.of(context).client; - final message = m; - final channel = client.channel( - c.type, - id: c.id, - ); - if (channel.state == null) { - await channel.watch(); - } - Navigator.pushReplacementNamed( - context, - Routes.CHANNEL_PAGE, - arguments: ChannelPageArgs( - channel: channel, - initialMessage: message, - ), - ); - }, - ), - Positioned( - bottom: 0, - left: 0, - right: 0, - child: Container( - alignment: Alignment.centerLeft, - color: StreamChatTheme.of(context) - .colorTheme - .whiteSnow - .withOpacity(.9), - child: TypingIndicator( - alignment: Alignment.centerLeft, - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), - style: StreamChatTheme.of(context) - .textTheme - .footnote - .copyWith( - color: - StreamChatTheme.of(context).colorTheme.grey), - ), - ), - ), - ], - ), - ), - MessageInput( - focusNode: _focusNode, - quotedMessage: _quotedMessage, - onQuotedMessageCleared: () { - setState(() => _quotedMessage = null); - _focusNode.unfocus(); - }, - ), - ], - ), - ); - } -} - -class ThreadPage extends StatefulWidget { - final Message parent; - final int initialScrollIndex; - final double initialAlignment; - - ThreadPage({ - Key key, - this.parent, - this.initialScrollIndex, - this.initialAlignment, - }) : super(key: key); - - @override - _ThreadPageState createState() => _ThreadPageState(); -} - -class _ThreadPageState extends State { - Message _quotedMessage; - FocusNode _focusNode = FocusNode(); - - @override - void dispose() { - _focusNode.dispose(); - super.dispose(); - } - - void _reply(Message message) { - setState(() => _quotedMessage = message); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - _focusNode.requestFocus(); - }); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow, - appBar: ThreadHeader( - parent: widget.parent, - ), - body: Column( - children: [ - Expanded( - child: MessageListView( - parentMessage: widget.parent, - initialScrollIndex: widget.initialScrollIndex, - initialAlignment: widget.initialAlignment, - onMessageSwiped: _reply, - onReplyTap: _reply, - ), - ), - if (widget.parent.type != 'deleted') - MessageInput( - parentMessage: widget.parent, - focusNode: _focusNode, - quotedMessage: _quotedMessage, - onQuotedMessageCleared: () { - setState(() => _quotedMessage = null); - _focusNode.unfocus(); - }, - ), - ], - ), - ); - } -} - class InitData { final StreamChatClient client; final StreamingSharedPreferences preferences; InitData(this.client, this.preferences); } - -class HolePainter extends CustomPainter { - HolePainter({ - @required this.color, - @required this.holeSize, - }); - - Color color; - double holeSize; - - @override - void paint(Canvas canvas, Size size) { - double radius = holeSize / 2; - Rect rect = Rect.fromLTWH(0, 0, size.width, size.height); - Rect outerCircleRect = Rect.fromCircle( - center: Offset(size.width / 2, size.height / 2), radius: radius); - Rect innerCircleRect = Rect.fromCircle( - center: Offset(size.width / 2, size.height / 2), radius: radius / 2); - - Path transparentHole = Path.combine( - PathOperation.difference, - Path()..addRect(rect), - Path() - ..addOval(outerCircleRect) - ..close(), - ); - - Path halfTransparentRing = Path.combine( - PathOperation.difference, - Path() - ..addOval(outerCircleRect) - ..close(), - Path() - ..addOval(innerCircleRect) - ..close(), - ); - - canvas.drawPath(transparentHole, Paint()..color = color); - canvas.drawPath( - halfTransparentRing, Paint()..color = color.withOpacity(0.5)); - } - - @override - bool shouldRepaint(CustomPainter oldDelegate) { - return true; - } -} diff --git a/packages/stream_chat_v1/lib/new_chat_screen.dart b/packages/stream_chat_v1/lib/new_chat_screen.dart index d4a6888..41a1ded 100644 --- a/packages/stream_chat_v1/lib/new_chat_screen.dart +++ b/packages/stream_chat_v1/lib/new_chat_screen.dart @@ -3,8 +3,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'channel_page.dart'; import 'chips_input_text_field.dart'; -import 'main.dart'; import 'routes/routes.dart'; class NewChatScreen extends StatefulWidget { @@ -16,9 +16,9 @@ class _NewChatScreenState extends State { final _chipInputTextFieldStateKey = GlobalKey>(); - TextEditingController _controller; + late TextEditingController _controller; - ChipInputTextFieldState get _chipInputTextFieldState => + ChipInputTextFieldState? get _chipInputTextFieldState => _chipInputTextFieldStateKey.currentState; String _userNameQuery = ''; @@ -30,14 +30,14 @@ class _NewChatScreenState extends State { bool _isSearchActive = false; - Channel channel; + Channel? channel; - Timer _debounce; + Timer? _debounce; bool _showUserList = true; void _userNameListener() { - if (_debounce?.isActive ?? false) _debounce.cancel(); + if (_debounce?.isActive ?? false) _debounce!.cancel(); _debounce = Timer(const Duration(milliseconds: 350), () { if (mounted) setState(() { @@ -66,17 +66,15 @@ class _NewChatScreenState extends State { final chatState = StreamChat.of(context); final res = await chatState.client.queryChannelsOnline( - options: { - 'state': false, - 'watch': false, - }, - filter: { + state: false, + watch: false, + filter: Filter.raw(value: { 'members': [ ..._selectedUsers.map((e) => e.id), - chatState.user.id, + chatState.user!.id, ], 'distinct': true, - }, + }), messageLimit: 0, paginationParams: PaginationParams( limit: 1, @@ -86,14 +84,14 @@ class _NewChatScreenState extends State { final _channelExisted = res.length == 1; if (_channelExisted) { channel = res.first; - await channel.watch(); + await channel!.watch(); } else { channel = chatState.client.channel( 'messaging', extraData: { 'members': [ ..._selectedUsers.map((e) => e.id), - chatState.user.id, + chatState.user!.id, ], }, ); @@ -110,27 +108,25 @@ class _NewChatScreenState extends State { void dispose() { _searchFocusNode.dispose(); _messageInputFocusNode.dispose(); - _controller?.clear(); - _controller?.removeListener(_userNameListener); - _controller?.dispose(); + _controller.clear(); + _controller.removeListener(_userNameListener); + _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow, + backgroundColor: StreamChatTheme.of(context).colorTheme.appBg, appBar: AppBar( brightness: Theme.of(context).brightness, elevation: 0, - backgroundColor: StreamChatTheme.of(context).colorTheme.white, + backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg, leading: const StreamBackButton(), title: Text( 'New Chat', - style: StreamChatTheme.of(context) - .textTheme - .headlineBold - .copyWith(color: StreamChatTheme.of(context).colorTheme.black), + style: StreamChatTheme.of(context).textTheme.headlineBold.copyWith( + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis), ), centerTitle: true, ), @@ -158,7 +154,7 @@ class _NewChatScreenState extends State { message: statusString, child: StreamChannel( showLoading: false, - channel: channel, + channel: channel!, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -169,7 +165,7 @@ class _NewChatScreenState extends State { chipBuilder: (context, user) { return GestureDetector( onTap: () { - _chipInputTextFieldState.removeItem(user); + _chipInputTextFieldState?.removeItem(user); _searchFocusNode.requestFocus(); }, child: Stack( @@ -179,7 +175,7 @@ class _NewChatScreenState extends State { decoration: BoxDecoration( color: StreamChatTheme.of(context) .colorTheme - .greyGainsboro, + .disabled, borderRadius: BorderRadius.circular(12), ), padding: const EdgeInsets.only(left: 24), @@ -191,7 +187,7 @@ class _NewChatScreenState extends State { style: TextStyle( color: StreamChatTheme.of(context) .colorTheme - .black, + .textHighEmphasis, ), ), ), @@ -242,7 +238,7 @@ class _NewChatScreenState extends State { child: StreamSvgIcon.contacts( color: StreamChatTheme.of(context) .colorTheme - .accentBlue, + .accentPrimary, size: 24, ), ), @@ -281,7 +277,7 @@ class _NewChatScreenState extends State { .copyWith( color: StreamChatTheme.of(context) .colorTheme - .black + .textHighEmphasis .withOpacity(.5))), ), ), @@ -299,24 +295,21 @@ class _NewChatScreenState extends State { _controller.clear(); if (!_selectedUsers.contains(user)) { _chipInputTextFieldState - ..addItem(user) + ?..addItem(user) ..pauseItemAddition(); } else { - _chipInputTextFieldState.removeItem(user); + _chipInputTextFieldState!.removeItem(user); } }, pagination: PaginationParams( limit: 25, ), - filter: { + filter: Filter.and([ if (_userNameQuery.isNotEmpty) - 'name': { - r'$autocomplete': _userNameQuery, - }, - 'id': { - r'$ne': StreamChat.of(context).user.id, - }, - }, + Filter.autoComplete('name', _userNameQuery), + Filter.notEqual( + 'id', StreamChat.of(context).user!.id), + ]), sort: [ SortOption( 'name', @@ -355,7 +348,7 @@ class _NewChatScreenState extends State { color: StreamChatTheme .of(context) .colorTheme - .black + .textHighEmphasis .withOpacity(.5)), ), ], @@ -370,7 +363,7 @@ class _NewChatScreenState extends State { ), ) : FutureBuilder( - future: channel.initialized, + future: channel!.initialized, builder: (context, snapshot) { if (snapshot.data == true) { return MessageListView(); @@ -383,7 +376,7 @@ class _NewChatScreenState extends State { fontSize: 12, color: StreamChatTheme.of(context) .colorTheme - .black + .textHighEmphasis .withOpacity(.5), ), ), @@ -394,14 +387,14 @@ class _NewChatScreenState extends State { MessageInput( focusNode: _messageInputFocusNode, preMessageSending: (message) async { - await channel.watch(); + await channel!.watch(); return message; }, onMessageSent: (m) { Navigator.pushNamedAndRemoveUntil( context, Routes.CHANNEL_PAGE, - ModalRoute.withName(Routes.HOME), + ModalRoute.withName(Routes.CHANNEL_LIST_PAGE), arguments: ChannelPageArgs(channel: channel), ); }, diff --git a/packages/stream_chat_v1/lib/new_group_chat_screen.dart b/packages/stream_chat_v1/lib/new_group_chat_screen.dart index 209d372..414606f 100644 --- a/packages/stream_chat_v1/lib/new_group_chat_screen.dart +++ b/packages/stream_chat_v1/lib/new_group_chat_screen.dart @@ -12,7 +12,7 @@ class NewGroupChatScreen extends StatefulWidget { } class _NewGroupChatScreenState extends State { - TextEditingController _controller; + TextEditingController? _controller; String _userNameQuery = ''; @@ -20,14 +20,14 @@ class _NewGroupChatScreenState extends State { bool _isSearchActive = false; - Timer _debounce; + Timer? _debounce; void _userNameListener() { - if (_debounce?.isActive ?? false) _debounce.cancel(); + if (_debounce?.isActive ?? false) _debounce!.cancel(); _debounce = Timer(const Duration(milliseconds: 350), () { if (mounted) { setState(() { - _userNameQuery = _controller.text; + _userNameQuery = _controller!.text; _isSearchActive = _userNameQuery.isNotEmpty; }); } @@ -51,15 +51,15 @@ class _NewGroupChatScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow, + backgroundColor: StreamChatTheme.of(context).colorTheme.appBg, appBar: AppBar( elevation: 1, - backgroundColor: StreamChatTheme.of(context).colorTheme.white, + backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg, leading: const StreamBackButton(), title: Text( 'Add Group Members', style: TextStyle( - color: StreamChatTheme.of(context).colorTheme.black, + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis, fontSize: 16, ), ), @@ -68,7 +68,7 @@ class _NewGroupChatScreenState extends State { if (_selectedUsers.isNotEmpty) IconButton( icon: StreamSvgIcon.arrowRight( - color: StreamChatTheme.of(context).colorTheme.accentBlue, + color: StreamChatTheme.of(context).colorTheme.accentPrimary, ), onPressed: () async { final updatedList = await Navigator.pushNamed( @@ -80,7 +80,7 @@ class _NewGroupChatScreenState extends State { setState(() { _selectedUsers ..clear() - ..addAll(updatedList); + ..addAll(updatedList as Iterable); }); } }, @@ -159,18 +159,18 @@ class _NewGroupChatScreenState extends State { decoration: BoxDecoration( color: StreamChatTheme.of(context) .colorTheme - .white, + .appBg, shape: BoxShape.circle, border: Border.all( color: StreamChatTheme.of(context) .colorTheme - .whiteSnow, + .appBg, ), ), child: StreamSvgIcon.close( color: StreamChatTheme.of(context) .colorTheme - .black, + .textHighEmphasis, size: 24, ), ), @@ -212,8 +212,9 @@ class _NewGroupChatScreenState extends State { ? 'Matches for \"$_userNameQuery\"' : 'On the platform', style: TextStyle( - color: - StreamChatTheme.of(context).colorTheme.grey, + color: StreamChatTheme.of(context) + .colorTheme + .textLowEmphasis, ), ), ), @@ -244,15 +245,11 @@ class _NewGroupChatScreenState extends State { pagination: PaginationParams( limit: 25, ), - filter: { + filter: Filter.and([ if (_userNameQuery.isNotEmpty) - 'name': { - r'$autocomplete': _userNameQuery, - }, - 'id': { - r'$ne': StreamChat.of(context).user.id, - } - }, + Filter.autoComplete('name', _userNameQuery), + Filter.notEqual('id', StreamChat.of(context).user!.id), + ]), sort: [ SortOption( 'name', @@ -277,7 +274,7 @@ class _NewGroupChatScreenState extends State { size: 96, color: StreamChatTheme.of(context) .colorTheme - .grey, + .textLowEmphasis, ), ), Text( @@ -288,7 +285,7 @@ class _NewGroupChatScreenState extends State { .copyWith( color: StreamChatTheme.of(context) .colorTheme - .grey, + .textLowEmphasis, ), ), ], @@ -315,15 +312,15 @@ class _HeaderDelegate extends SliverPersistentHeaderDelegate { final double height; const _HeaderDelegate({ - @required this.child, - @required this.height, + required this.child, + required this.height, }); @override Widget build( BuildContext context, double shrinkOffset, bool overlapsContent) { return Container( - color: StreamChatTheme.of(context).colorTheme.white, + color: StreamChatTheme.of(context).colorTheme.barsBg, child: child, ); } diff --git a/packages/stream_chat_v1/lib/notifications_service.dart b/packages/stream_chat_v1/lib/notifications_service.dart index c72ac19..56f6ad7 100644 --- a/packages/stream_chat_v1/lib/notifications_service.dart +++ b/packages/stream_chat_v1/lib/notifications_service.dart @@ -7,7 +7,7 @@ void showLocalNotification(Event event, String currentUserId) async { EventType.messageNew, EventType.notificationMessageNew, ].contains(event.type) || - event.user.id == currentUserId) { + event.user!.id == currentUserId) { return; } if (event.message == null) return; @@ -21,9 +21,9 @@ void showLocalNotification(Event event, String currentUserId) async { ); await flutterLocalNotificationsPlugin.initialize(initializationSettings); await flutterLocalNotificationsPlugin.show( - event.message.id.hashCode, - event.message.user.name, - event.message.text, + event.message!.id.hashCode, + event.message!.user!.name, + event.message!.text, NotificationDetails( android: AndroidNotificationDetails( 'message channel', diff --git a/packages/stream_chat_v1/lib/pinned_messages_screen.dart b/packages/stream_chat_v1/lib/pinned_messages_screen.dart new file mode 100644 index 0000000..11287bb --- /dev/null +++ b/packages/stream_chat_v1/lib/pinned_messages_screen.dart @@ -0,0 +1,213 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:video_player/video_player.dart'; + +class PinnedMessagesScreen extends StatefulWidget { + /// The sorting used for the channels matching the filters. + /// 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. + /// Direction can be ascending or descending. + final List? sortOptions; + + /// Pagination parameters + /// limit: the number of users to return (max is 30) + /// offset: the offset (max is 1000) + /// message_limit: how many messages should be included to each channel + final PaginationParams? paginationParams; + + /// The builder used when the file list is empty. + final WidgetBuilder? emptyBuilder; + + final ShowMessageCallback? onShowMessage; + + final MessageTheme messageTheme; + + const PinnedMessagesScreen({ + required this.messageTheme, + this.sortOptions, + this.paginationParams, + this.emptyBuilder, + this.onShowMessage, + }); + + @override + _PinnedMessagesScreenState createState() => _PinnedMessagesScreenState(); +} + +class _PinnedMessagesScreenState extends State { + Map controllerCache = {}; + + @override + void initState() { + super.initState(); + final messageSearchBloc = MessageSearchBloc.of(context); + messageSearchBloc.search( + filter: Filter.in_( + 'cid', + [StreamChannel.of(context).channel.cid!], + ), + messageFilter: Filter.equal( + 'pinned', + true, + ), + sort: widget.sortOptions, + pagination: widget.paginationParams, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg, + appBar: AppBar( + brightness: Theme.of(context).brightness, + elevation: 1, + centerTitle: true, + title: Text( + 'Pinned Messages', + style: TextStyle( + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis, + fontSize: 16.0, + ), + ), + leading: StreamBackButton(), + backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg, + ), + body: _buildMediaGrid(), + ); + } + + Widget _buildMediaGrid() { + final messageSearchBloc = MessageSearchBloc.of(context); + + return StreamBuilder>( + builder: (context, snapshot) { + if (snapshot.data == null) { + return Center( + child: const CircularProgressIndicator(), + ); + } + + if (snapshot.data!.isEmpty) { + if (widget.emptyBuilder != null) { + return widget.emptyBuilder!(context); + } + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + StreamSvgIcon.pin( + size: 136.0, + color: StreamChatTheme.of(context).colorTheme.disabled, + ), + SizedBox(height: 16.0), + Text( + 'No pinned items', + style: TextStyle( + fontSize: 17.0, + color: + StreamChatTheme.of(context).colorTheme.textHighEmphasis, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 8.0), + RichText( + textAlign: TextAlign.center, + text: TextSpan(children: [ + TextSpan( + text: 'Long-press an important message and\nchoose ', + style: TextStyle( + fontSize: 14.0, + color: StreamChatTheme.of(context) + .colorTheme + .textHighEmphasis + .withOpacity(0.5), + ), + ), + TextSpan( + text: 'Pin to conversation', + style: TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.bold, + color: StreamChatTheme.of(context) + .colorTheme + .textHighEmphasis + .withOpacity(0.5), + ), + ), + ]), + ), + ], + ), + ); + } + + var data = snapshot.data ?? []; + + return LazyLoadScrollView( + onEndOfPage: () => messageSearchBloc.search( + filter: Filter.in_( + 'cid', + [StreamChannel.of(context).channel.cid!], + ), + messageFilter: Filter.equal( + 'pinned', + true, + ), + sort: widget.sortOptions, + pagination: widget.paginationParams!.copyWith( + offset: messageSearchBloc.messageResponses?.length ?? 0, + ), + ), + child: ListView.builder( + itemBuilder: (context, position) { + var user = data[position].message.user!; + var attachments = data[position].message.attachments; + var text = data[position].message.text ?? ''; + + return ListTile( + leading: UserAvatar( + user: user, + constraints: BoxConstraints( + maxWidth: 40.0, + minHeight: 40.0, + ), + borderRadius: BorderRadius.circular(28), + ), + title: Text( + user.name, + style: TextStyle( + color: StreamChatTheme.of(context) + .colorTheme + .textHighEmphasis, + fontWeight: FontWeight.bold), + ), + subtitle: Text( + text != '' + ? text + : (attachments.isNotEmpty + ? '${attachments.length} attachment${attachments.length > 1 ? 's' : ''}' + : ''), + ), + onTap: () { + widget.onShowMessage?.call(data[position].message, + StreamChannel.of(context).channel); + }, + ); + }, + itemCount: snapshot.data!.length, + ), + ); + }, + stream: messageSearchBloc.messagesStream, + ); + } + + @override + void dispose() { + super.dispose(); + for (var c in controllerCache.values) { + c!.dispose(); + } + } +} diff --git a/packages/stream_chat_v1/lib/routes/app_routes.dart b/packages/stream_chat_v1/lib/routes/app_routes.dart index c641b04..145fa8e 100644 --- a/packages/stream_chat_v1/lib/routes/app_routes.dart +++ b/packages/stream_chat_v1/lib/routes/app_routes.dart @@ -1,18 +1,22 @@ -import 'routes.dart'; +import 'package:example/channel_list_page.dart'; import 'package:flutter/material.dart'; -import '../choose_user_page.dart'; -import '../advanced_options_page.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import '../main.dart'; -import '../group_chat_details_screen.dart'; -import '../new_group_chat_screen.dart'; -import '../new_chat_screen.dart'; + +import '../advanced_options_page.dart'; +import '../channel_page.dart'; import '../chat_info_screen.dart'; +import '../choose_user_page.dart'; +import '../group_chat_details_screen.dart'; import '../group_info_screen.dart'; +import '../home_page.dart'; +import '../main.dart'; +import '../new_chat_screen.dart'; +import '../new_group_chat_screen.dart'; +import 'routes.dart'; class AppRoutes { /// Add entry for new route here - static Route generateRoute(RouteSettings settings) { + static Route? generateRoute(RouteSettings settings) { final args = settings.arguments; switch (settings.name) { case Routes.APP: @@ -25,7 +29,10 @@ class AppRoutes { return MaterialPageRoute( settings: const RouteSettings(name: Routes.HOME), builder: (_) { - return HomePage(); + final homePageArgs = args as HomePageArgs; + return HomePage( + chatClient: homePageArgs.chatClient, + ); }); case Routes.CHOOSE_USER: return MaterialPageRoute( @@ -42,12 +49,13 @@ class AppRoutes { return MaterialPageRoute( settings: const RouteSettings(name: Routes.CHANNEL_PAGE), builder: (_) { - final arg = args as ChannelPageArgs; + final channelPageArgs = args as ChannelPageArgs; return StreamChannel( - channel: arg.channel, - initialMessageId: arg.initialMessage?.id, + channel: channelPageArgs.channel!, + initialMessageId: channelPageArgs.initialMessage?.id, child: ChannelPage( - highlightInitialMessage: arg.initialMessage != null, + highlightInitialMessage: + channelPageArgs.initialMessage != null, ), ); }); @@ -68,22 +76,31 @@ class AppRoutes { settings: const RouteSettings(name: Routes.NEW_GROUP_CHAT_DETAILS), builder: (_) { return GroupChatDetailsScreen( - selectedUsers: args, + selectedUsers: args as List?, ); }); case Routes.CHAT_INFO_SCREEN: return MaterialPageRoute( settings: const RouteSettings(name: Routes.CHAT_INFO_SCREEN), - builder: (_) { + builder: (context) { return ChatInfoScreen( - user: args, + user: args as User?, + messageTheme: StreamChatTheme.of(context).ownMessageTheme, ); }); case Routes.GROUP_INFO_SCREEN: return MaterialPageRoute( settings: const RouteSettings(name: Routes.GROUP_INFO_SCREEN), - builder: (_) { - return GroupInfoScreen(); + builder: (context) { + return GroupInfoScreen( + messageTheme: StreamChatTheme.of(context).ownMessageTheme, + ); + }); + case Routes.CHANNEL_LIST_PAGE: + return MaterialPageRoute( + settings: const RouteSettings(name: Routes.CHANNEL_LIST_PAGE), + builder: (context) { + return ChannelListPage(); }); // Default case, should not reach here. default: diff --git a/packages/stream_chat_v1/lib/routes/routes.dart b/packages/stream_chat_v1/lib/routes/routes.dart index 2280ca3..239bd28 100644 --- a/packages/stream_chat_v1/lib/routes/routes.dart +++ b/packages/stream_chat_v1/lib/routes/routes.dart @@ -10,4 +10,5 @@ class Routes { static const String NEW_GROUP_CHAT_DETAILS = '/new_group_chat_details'; static const String CHAT_INFO_SCREEN = '/chat_info_screen'; static const String GROUP_INFO_SCREEN = '/group_info_screen'; + static const String CHANNEL_LIST_PAGE = '/channel_list_page'; } diff --git a/packages/stream_chat_v1/lib/search_text_field.dart b/packages/stream_chat_v1/lib/search_text_field.dart index 9628ddb..7641a7b 100644 --- a/packages/stream_chat_v1/lib/search_text_field.dart +++ b/packages/stream_chat_v1/lib/search_text_field.dart @@ -2,15 +2,15 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; class SearchTextField extends StatelessWidget { - final TextEditingController controller; - final ValueChanged onChanged; + final TextEditingController? controller; + final ValueChanged? onChanged; final String hintText; - final VoidCallback onTap; + final VoidCallback? onTap; final bool showCloseButton; const SearchTextField({ - Key key, - @required this.controller, + Key? key, + required this.controller, this.onChanged, this.onTap, this.hintText = 'Search', @@ -22,9 +22,9 @@ class SearchTextField extends StatelessWidget { return Container( height: 36, decoration: BoxDecoration( - color: StreamChatTheme.of(context).colorTheme.white, + color: StreamChatTheme.of(context).colorTheme.barsBg, border: Border.all( - color: StreamChatTheme.of(context).colorTheme.greyWhisper, + color: StreamChatTheme.of(context).colorTheme.borders, ), borderRadius: BorderRadius.circular(24), ), @@ -48,7 +48,8 @@ class SearchTextField extends StatelessWidget { right: 8, ), child: StreamSvgIcon.search( - color: StreamChatTheme.of(context).colorTheme.black, + color: + StreamChatTheme.of(context).colorTheme.textHighEmphasis, size: 24, ), ), @@ -56,7 +57,7 @@ class SearchTextField extends StatelessWidget { hintStyle: StreamChatTheme.of(context).textTheme.body.copyWith( color: StreamChatTheme.of(context) .colorTheme - .black + .textHighEmphasis .withOpacity(.5)), contentPadding: const EdgeInsets.all(0), border: OutlineInputBorder( @@ -76,11 +77,11 @@ class SearchTextField extends StatelessWidget { ), splashRadius: 24, onPressed: () { - if (controller.text.isNotEmpty) { + if (controller!.text.isNotEmpty) { Future.microtask( () => [ - controller.clear(), - if (onChanged != null) onChanged(''), + controller!.clear(), + if (onChanged != null) onChanged!(''), ], ); } diff --git a/packages/stream_chat_v1/lib/splash_screen.dart b/packages/stream_chat_v1/lib/splash_screen.dart new file mode 100644 index 0000000..2c89228 --- /dev/null +++ b/packages/stream_chat_v1/lib/splash_screen.dart @@ -0,0 +1,122 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:lottie/lottie.dart'; + +mixin SplashScreenStateMixin on State + implements TickerProvider { + late Animation animation, scaleAnimation; + late AnimationController _animationController, _scaleAnimationController; + late Animation colorAnimation; + bool animationCompleted = false; + + void _createAnimations() { + _scaleAnimationController = AnimationController( + vsync: this, + value: 0, + duration: Duration( + milliseconds: 500, + ), + ); + scaleAnimation = Tween( + begin: 1.0, + end: 1.5, + ).animate(CurvedAnimation( + parent: _scaleAnimationController, + curve: Curves.easeInOutBack, + )); + + _animationController = AnimationController( + vsync: this, + duration: Duration( + milliseconds: 1000, + ), + ); + animation = Tween( + begin: 0.0, + end: 1000.0, + ).animate(CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOut, + )); + colorAnimation = ColorTween( + begin: Color(0xff005FFF), + end: Color(0xff005FFF), + ).animate(CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOut, + )); + colorAnimation = ColorTween( + begin: Color(0xff005FFF), + end: Colors.transparent, + ).animate(CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOut, + )); + } + + void forwardAnimations() { + _scaleAnimationController.forward().whenComplete(() { + _animationController.forward(); + }); + } + + Widget buildAnimation() => Stack( + clipBehavior: Clip.none, + alignment: Alignment.center, + children: [ + AnimatedBuilder( + animation: scaleAnimation, + builder: (context, _) { + return Transform.scale( + scale: scaleAnimation.value, + child: AnimatedBuilder( + animation: colorAnimation, + builder: (context, snapshot) { + return Container( + alignment: Alignment.center, + constraints: BoxConstraints.expand(), + color: colorAnimation.value, + child: !_animationController.isAnimating + ? Lottie.asset( + 'assets/floating_boat.json', + alignment: Alignment.center, + ) + : SizedBox(), + ); + }), + ); + }, + ), + AnimatedBuilder( + animation: animation, + builder: (context, snapshot) { + return Transform.scale( + scale: animation.value, + child: Container( + width: 1.0, + height: 1.0, + decoration: BoxDecoration( + color: Colors.white + .withOpacity(1 - _animationController.value), + shape: BoxShape.circle, + ), + ), + ); + }, + ), + ], + ); + + @override + void initState() { + _createAnimations(); + _animationController.addStatusListener((status) { + if (status == AnimationStatus.completed) { + setState(() { + animationCompleted = true; + }); + } + }); + super.initState(); + } +} diff --git a/packages/stream_chat_v1/lib/stream_version.dart b/packages/stream_chat_v1/lib/stream_version.dart index b3f926b..89004f7 100644 --- a/packages/stream_chat_v1/lib/stream_version.dart +++ b/packages/stream_chat_v1/lib/stream_version.dart @@ -5,7 +5,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; class StreamVersion extends StatelessWidget { const StreamVersion({ - Key key, + Key? key, }) : super(key: key); @override @@ -20,16 +20,16 @@ class StreamVersion extends StatelessWidget { return SizedBox(); } - final pubspec = snapshot.data; + final pubspec = snapshot.data!; final yaml = loadYaml(pubspec); final streamChatDep = yaml['packages']['stream_chat_flutter']['version']; return Text( - 'Stream SDK v ${streamChatDep}', + 'Stream SDK v $streamChatDep', style: TextStyle( fontSize: 14, - color: StreamChatTheme.of(context).colorTheme.greyGainsboro, + color: StreamChatTheme.of(context).colorTheme.disabled, ), ); }, diff --git a/packages/stream_chat_v1/lib/thread_page.dart b/packages/stream_chat_v1/lib/thread_page.dart new file mode 100644 index 0000000..5d032f8 --- /dev/null +++ b/packages/stream_chat_v1/lib/thread_page.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +class ThreadPage extends StatefulWidget { + final Message? parent; + final int? initialScrollIndex; + final double? initialAlignment; + + ThreadPage({ + Key? key, + this.parent, + this.initialScrollIndex, + this.initialAlignment, + }) : super(key: key); + + @override + _ThreadPageState createState() => _ThreadPageState(); +} + +class _ThreadPageState extends State { + Message? _quotedMessage; + FocusNode _focusNode = FocusNode(); + + @override + void dispose() { + _focusNode.dispose(); + super.dispose(); + } + + void _reply(Message message) { + setState(() => _quotedMessage = message); + WidgetsBinding.instance!.addPostFrameCallback((timeStamp) { + _focusNode.requestFocus(); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: StreamChatTheme.of(context).colorTheme.appBg, + appBar: ThreadHeader( + parent: widget.parent!, + ), + body: Column( + children: [ + Expanded( + child: MessageListView( + parentMessage: widget.parent, + initialScrollIndex: widget.initialScrollIndex, + initialAlignment: widget.initialAlignment, + onMessageSwiped: _reply, + messageBuilder: (context, details, messages, defaultMessage) { + return defaultMessage.copyWith( + onReplyTap: _reply, + ); + }, + pinPermissions: ['owner', 'admin', 'member'], + ), + ), + if (widget.parent!.type != 'deleted') + MessageInput( + parentMessage: widget.parent, + focusNode: _focusNode, + quotedMessage: _quotedMessage, + onQuotedMessageCleared: () { + setState(() => _quotedMessage = null); + _focusNode.unfocus(); + }, + ), + ], + ), + ); + } +} diff --git a/packages/stream_chat_v1/lib/user_mentions_page.dart b/packages/stream_chat_v1/lib/user_mentions_page.dart new file mode 100644 index 0000000..d02bd1c --- /dev/null +++ b/packages/stream_chat_v1/lib/user_mentions_page.dart @@ -0,0 +1,88 @@ +import 'package:example/routes/routes.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'channel_page.dart'; + +class UserMentionsPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + final user = StreamChat.of(context).user!; + return MessageSearchBloc( + child: MessageSearchListView( + filters: Filter.in_('members', [user.id]), + messageFilters: Filter.custom( + operator: r'$contains', + key: 'mentioned_users.id', + value: user.id, + ), + sortOptions: [ + SortOption( + 'created_at', + direction: SortOption.ASC, + ), + ], + paginationParams: PaginationParams(limit: 20), + showResultCount: false, + emptyBuilder: (_) { + return LayoutBuilder( + builder: (context, viewportConstraints) { + return SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: viewportConstraints.maxHeight, + ), + child: Center( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(24), + child: StreamSvgIcon.mentions( + size: 96, + color: + StreamChatTheme.of(context).colorTheme.disabled, + ), + ), + Text( + 'No mentions exist yet...', + style: StreamChatTheme.of(context) + .textTheme + .body + .copyWith( + color: StreamChatTheme.of(context) + .colorTheme + .textLowEmphasis, + ), + ), + ], + ), + ), + ), + ); + }, + ); + }, + onItemTap: (messageResponse) async { + final client = StreamChat.of(context).client; + final message = messageResponse.message; + final channel = client.channel( + messageResponse.channel!.type, + id: messageResponse.channel!.id, + ); + if (channel.state == null) { + await channel.watch(); + } + Navigator.pushNamed( + context, + Routes.CHANNEL_PAGE, + arguments: ChannelPageArgs( + channel: channel, + initialMessage: message, + ), + ); + }, + ), + ); + } +} diff --git a/packages/stream_chat_v1/pubspec.yaml b/packages/stream_chat_v1/pubspec.yaml index 69715f7..9dd7471 100644 --- a/packages/stream_chat_v1/pubspec.yaml +++ b/packages/stream_chat_v1/pubspec.yaml @@ -1,29 +1,37 @@ name: example description: A new Flutter project. publish_to: 'none' -version: 1.5.6 +version: 1.6.1 environment: - sdk: ">=2.2.2 <3.0.0" + sdk: '>=2.12.0 <3.0.0' dependencies: - flutter_app_badger: ^1.1.2 + flutter_app_badger: ^1.2.0 flutter: sdk: flutter - stream_chat_flutter: ^1.5.2 - stream_chat_persistence: ^1.5.1 - flutter_local_notifications: ^2.0.2 - flutter_svg: ^0.19.3 - flutter_secure_storage: ^3.3.5 - yaml: ^2.2.1 - uuid: ^2.2.2 - streaming_shared_preferences: ^1.0.2 - lottie: ^0.7.0+1 + stream_chat_flutter: + git: + url: https://github.com/GetStream/stream-chat-flutter.git + ref: develop + path: packages/stream_chat_flutter + stream_chat_persistence: + git: + url: https://github.com/GetStream/stream-chat-flutter.git + ref: develop + path: packages/stream_chat_persistence + flutter_local_notifications: ^5.0.0+4 + flutter_svg: ^0.22.0 + flutter_secure_storage: ^4.2.0 + yaml: ^3.1.0 + uuid: ^3.0.4 + streaming_shared_preferences: ^2.0.0 + lottie: ^1.0.1 + collection: ^1.15.0-nullsafety.4 dev_dependencies: - flutter_launcher_icons: ^0.8.1 + flutter_launcher_icons: ^0.9.0 test: any - flutter: assets: - assets/ diff --git a/packages/stream_chat_v1/test/widget_test.dart b/packages/stream_chat_v1/test/widget_test.dart deleted file mode 100644 index 2d257c0..0000000 --- a/packages/stream_chat_v1/test/widget_test.dart +++ /dev/null @@ -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); - }); -}