diff --git a/.gitignore b/.gitignore index 6d180702..f7a23bbb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .DS_Store .atom/ .idea/ +.vscode/ .packages .pub/ diff --git a/packages/dart_client/example/android/gradle.properties b/packages/dart_client/example/android/gradle.properties index 94adc3a3..a6738207 100644 --- a/packages/dart_client/example/android/gradle.properties +++ b/packages/dart_client/example/android/gradle.properties @@ -1,3 +1,4 @@ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true +android.enableR8=true diff --git a/packages/dart_client/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/dart_client/example/android/gradle/wrapper/gradle-wrapper.properties index 296b146b..de2ccd60 100644 --- a/packages/dart_client/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/dart_client/example/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-5.6.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip diff --git a/packages/dart_client/example/lib/main.dart b/packages/dart_client/example/lib/main.dart index 8f68c100..ed21df76 100644 --- a/packages/dart_client/example/lib/main.dart +++ b/packages/dart_client/example/lib/main.dart @@ -156,7 +156,7 @@ class _MessageViewState extends State { /// Convenience method for scrolling the list view when a new message is sent. void _updateList() { _scrollController.animateTo( - _scrollController.position.maxScrollExtent, + 0, duration: const Duration(milliseconds: 200), curve: Curves.easeOut, ); diff --git a/packages/flutter_widgets/CHANGELOG.md b/packages/flutter_widgets/CHANGELOG.md index 8ca3aa04..8a80dbc5 100644 --- a/packages/flutter_widgets/CHANGELOG.md +++ b/packages/flutter_widgets/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.0.0-rc + +- Integrated new-ui, split package functionality, + ## 0.2.20+2 - Added `shouldAddChannel` to ChannelsBloc in order to check if a channel has to be added to the list when a new message arrives diff --git a/packages/flutter_widgets/README.md b/packages/flutter_widgets/README.md index c5e4c9e7..8104e83a 100644 --- a/packages/flutter_widgets/README.md +++ b/packages/flutter_widgets/README.md @@ -60,13 +60,9 @@ If it seems related to the [flutter file picker plugin](https://github.com/migue ## Docs -### Business logic components - -We provide 3 Widgets dedicated to business logic and state management: - -- [StreamChat](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/StreamChat-class.html) -- [StreamChannel](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/StreamChannel-class.html) -- [ChannelsBloc](https://pub.dev/documentation/stream_chat_flutter/0.2.0-alpha+2/stream_chat_flutter/ChannelsBloc-class.html) +This package provides UI components required for integrating Stream Chat into your application. +Alternatively, you may use the core package (stream_chat_flutter_core) which allows more customisation and provides business logic but no UI components. +If you require the maximum amount of control over the API, please use the low level client package: stream_chat. ### UI Components diff --git a/packages/flutter_widgets/example/lib/choose_user_page.dart b/packages/flutter_widgets/example/lib/choose_user_page.dart index 6da88a29..8e36e748 100644 --- a/packages/flutter_widgets/example/lib/choose_user_page.dart +++ b/packages/flutter_widgets/example/lib/choose_user_page.dart @@ -11,52 +11,91 @@ import 'routes/routes.dart'; const kStreamApiKey = 'STREAM_API_KEY'; const kStreamUserId = 'STREAM_USER_ID'; const kStreamToken = 'STREAM_TOKEN'; -const kDefaultStreamApiKey = 'uj7qrdbfrzvg'; +const kDefaultStreamApiKey = 'kv7mcsxr24p8'; class ChooseUserPage extends StatelessWidget { @override Widget build(BuildContext context) { final users = { - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidmlzaGFsIn0.lCz-idDgaZ-xszjnuB_hTfeIOhTFmJtTB2fEjhwrcCI': - User( - id: 'vishal', - extraData: { - 'name': 'Vishal', - }, - ), - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.1AvvK2TvEsfcxAlfOK2iUMFACucnk6N-Q8f5bbfnjCM': - User( - id: 'super-band-9', - extraData: { - 'name': 'John Doe', - }, - ), - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoic2FsdmF0b3JlIn0.uQ-pz8e1_C-Df2rIFpkkvWf7g4M0aW2Zkr7pHVhmU1Y': + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoic2FsdmF0b3JlIn0.pgiJz7sIc7iP29BHKFwe3nLm5-OaR_1l2P-SlgiC9a8': User( id: 'salvatore', extraData: { - 'name': 'Salvatore', + 'name': 'Salvatore Giordano', + 'image': + 'https://ca.slack-edge.com/T02RM6X6B-USKK9FFRT-30c415e207a9-512', }, ), - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidG9tbWFzbyJ9.feLvMytd5_3-eETKrRdwtO3jcA0HxD8woFUbFU7gQCg': + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoic2FoaWwifQ.WnIUoB5gR2kcAsFhiDvkiD6zdHXZ-VSU2aQWWkhsvfo': + User( + id: 'sahil', + extraData: { + 'name': 'Sahil Kumar', + 'image': + 'https://ca.slack-edge.com/T02RM6X6B-U01EYU51M89-bbc152b40321-512', + }, + ), + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiYmVuIn0.nAz2sNFGQwY7rl2Og2z3TGHUsdpnN53tOsUglJFvLmg': + User( + id: 'ben', + extraData: { + 'name': 'Ben Golden', + 'image': + 'https://ca.slack-edge.com/T02RM6X6B-U01AXAF23MG-f57403a3cb0d-512', + }, + ), + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidGhpZXJyeSJ9.lEq6TrZtHzjoNtf7HHRufUPyGo_pa8vg4_XhEBp4ckY': + User( + id: 'thierry', + extraData: { + 'name': 'Thierry Schellenbach', + 'image': + 'https://ca.slack-edge.com/T02RM6X6B-U02RM6X6D-g28a1278a98e-512', + }, + ), + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidG9tbWFzbyJ9.GLSI0ESshERMo2WjUpysD709NEtn1zmGimUN2an7g9o': User( id: 'tommaso', extraData: { - 'name': 'Tommaso', + 'name': 'Tommaso Barbugli', + 'image': + 'https://ca.slack-edge.com/T02RM6X6B-U02U7SJP4-0f65a5997877-512', }, ), - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiamFhcCJ9.VLkLs6KH-jRRkPvhYMa4_lqN0R6WiK7JhJVmYvxcKz0': + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiZGV2ZW4ifQ.z3zI4PqJnNhc-1o-VKcmb6BnnQ0oxFNCRHwEulHqcWc': User( - id: 'jaap', + id: 'deven', extraData: { - 'name': 'Jaap', + 'name': 'Deven Joshi', + 'image': + 'https://ca.slack-edge.com/T02RM6X6B-U01AM7ELPTL-8a60da32704c-512', + }, + ), + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoibmVldmFzaCJ9.3EdHegTxibrz3A9cTiKmpEyawwcCVB8FXnoFzr4eKvw': + User( + id: 'neevash', + extraData: { + 'name': 'Neevash Ramdial', + 'image': + 'https://ca.slack-edge.com/T02RM6X6B-U01DZ046DS8-b00d321d2880-512', + }, + ), + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoicWF0ZXN0MSJ9.fnelU7HcP7QoEEsCGteNlF1fppofzNlrnpDQuIgeKCU': + User( + id: 'qatest1', + extraData: { + 'name': 'QA test 1', + }, + ), + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoicWF0ZXN0MiJ9.vSCqAEbs2WVmMWsOsa7065Fsjq-rsTih6qsHPynl7XM': + User( + id: 'qatest2', + extraData: { + 'name': 'QA test 2', }, ), }; - users.updateAll( - (_, value) => value..extraData['image'] = getRandomPicUrl(value)); - return Scaffold( backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow, body: SafeArea( diff --git a/packages/flutter_widgets/example/lib/new_chat_screen.dart b/packages/flutter_widgets/example/lib/new_chat_screen.dart index 5772f5e2..baddd570 100644 --- a/packages/flutter_widgets/example/lib/new_chat_screen.dart +++ b/packages/flutter_widgets/example/lib/new_chat_screen.dart @@ -135,287 +135,284 @@ class _NewChatScreenState extends State { centerTitle: true, ), body: ValueListenableBuilder( - valueListenable: StreamChat.of(context).client.wsConnectionStatus, - builder: (context, status, _) { - String statusString = ''; - bool showStatus = true; + valueListenable: StreamChat.of(context).client.wsConnectionStatus, + builder: (context, status, _) { + String statusString = ''; + bool showStatus = true; - switch (status) { - case ConnectionStatus.connected: - statusString = 'Connected'; - showStatus = false; - break; - case ConnectionStatus.connecting: - statusString = 'Reconnecting...'; - break; - case ConnectionStatus.disconnected: - statusString = 'Disconnected'; - break; - } - return InfoTile( - showMessage: showStatus, - tileAnchor: Alignment.topCenter, - childAnchor: Alignment.topCenter, - message: statusString, - child: StreamChannel( - showLoading: false, - channel: channel, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ChipsInputTextField( - key: _chipInputTextFieldStateKey, - controller: _controller, - focusNode: _searchFocusNode, - chipBuilder: (context, user) { - return GestureDetector( - onTap: () { - _chipInputTextFieldState.removeItem(user); - _searchFocusNode.requestFocus(); - }, - child: Stack( - alignment: AlignmentDirectional.centerStart, + switch (status) { + case ConnectionStatus.connected: + statusString = 'Connected'; + showStatus = false; + break; + case ConnectionStatus.connecting: + statusString = 'Reconnecting...'; + break; + case ConnectionStatus.disconnected: + statusString = 'Disconnected'; + break; + } + return InfoTile( + showMessage: showStatus, + tileAnchor: Alignment.topCenter, + childAnchor: Alignment.topCenter, + message: statusString, + child: StreamChannel( + showLoading: false, + channel: channel, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ChipsInputTextField( + key: _chipInputTextFieldStateKey, + controller: _controller, + focusNode: _searchFocusNode, + chipBuilder: (context, user) { + return GestureDetector( + onTap: () { + _chipInputTextFieldState.removeItem(user); + _searchFocusNode.requestFocus(); + }, + child: Stack( + alignment: AlignmentDirectional.centerStart, + children: [ + Container( + decoration: BoxDecoration( + color: StreamChatTheme.of(context) + .colorTheme + .greyGainsboro, + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.only(left: 24), + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 4, 12, 4), + child: Text( + user.name, + maxLines: 1, + style: TextStyle( + color: StreamChatTheme.of(context) + .colorTheme + .black, + ), + ), + ), + ), + Container( + foregroundDecoration: BoxDecoration( + color: StreamChatTheme.of(context) + .colorTheme + .overlay, + shape: BoxShape.circle, + ), + child: UserAvatar( + showOnlineStatus: false, + user: user, + constraints: BoxConstraints.tightFor( + height: 24, + width: 24, + ), + ), + ), + StreamSvgIcon.close(), + ], + ), + ); + }, + onChipAdded: (user) { + setState(() => _selectedUsers.add(user)); + }, + onChipRemoved: (user) { + setState(() => _selectedUsers.remove(user)); + }, + ), + if (!_isSearchActive && !_selectedUsers.isNotEmpty) + Container( + child: InkWell( + onTap: () { + Navigator.pushNamed( + context, + Routes.NEW_GROUP_CHAT, + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( children: [ - Container( - decoration: BoxDecoration( - color: StreamChatTheme.of(context) - .colorTheme - .greyGainsboro, - borderRadius: BorderRadius.circular(12), - ), - padding: const EdgeInsets.only(left: 24), - child: Padding( - padding: - const EdgeInsets.fromLTRB(8, 4, 12, 4), - child: Text( - user.name, - maxLines: 1, - style: TextStyle( - color: StreamChatTheme.of(context) - .colorTheme - .black, - ), + StreamNeumorphicButton( + child: Center( + child: StreamSvgIcon.contacts( + color: StreamChatTheme.of(context) + .colorTheme + .accentBlue, + size: 24, ), ), ), - Container( - foregroundDecoration: BoxDecoration( - color: StreamChatTheme.of(context) - .colorTheme - .overlay, - shape: BoxShape.circle, - ), - child: UserAvatar( - showOnlineStatus: false, - user: user, - constraints: BoxConstraints.tightFor( - height: 24, - width: 24, - ), - ), + SizedBox(width: 8), + Text( + 'Create a Group', + style: StreamChatTheme.of(context) + .textTheme + .bodyBold, ), - StreamSvgIcon.close(), ], ), - ); - }, - onChipAdded: (user) { - setState(() => _selectedUsers.add(user)); - }, - onChipRemoved: (user) { - setState(() => _selectedUsers.remove(user)); - }, + ), + ), ), - if (!_isSearchActive && !_selectedUsers.isNotEmpty) - Container( - child: InkWell( - onTap: () { - Navigator.pushNamed( - context, - Routes.NEW_GROUP_CHAT, - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Row( - children: [ - StreamNeumorphicButton( - child: Center( - child: StreamSvgIcon.contacts( - color: StreamChatTheme.of(context) - .colorTheme - .accentBlue, - size: 24, - ), - ), - ), - SizedBox(width: 8), - Text( - 'Create a Group', - style: StreamChatTheme.of(context) - .textTheme - .bodyBold, - ), - ], - ), - ), - ), + if (_showUserList) + Container( + width: double.maxFinite, + decoration: BoxDecoration( + gradient: + StreamChatTheme.of(context).colorTheme.bgGradient, ), - if (_showUserList) - Container( - width: double.maxFinite, - decoration: BoxDecoration( - gradient: - StreamChatTheme.of(context).colorTheme.bgGradient, - ), - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 8, - ), - child: Text( - _isSearchActive - ? "Matches for \"$_userNameQuery\"" - : 'On the platform', - style: StreamChatTheme.of(context) - .textTheme - .footnote - .copyWith( - color: StreamChatTheme.of(context) - .colorTheme - .black - .withOpacity(.5))), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 8, ), + child: Text( + _isSearchActive + ? "Matches for \"$_userNameQuery\"" + : 'On the platform', + style: StreamChatTheme.of(context) + .textTheme + .footnote + .copyWith( + color: StreamChatTheme.of(context) + .colorTheme + .black + .withOpacity(.5))), ), - Expanded( - child: _showUserList - ? GestureDetector( - behavior: HitTestBehavior.opaque, - onPanDown: (_) => - FocusScope.of(context).unfocus(), - child: UsersBloc( - child: UserListView( - selectedUsers: _selectedUsers, - groupAlphabetically: - _isSearchActive ? false : true, - onUserTap: (user, _) { - _controller.clear(); - if (!_selectedUsers.contains(user)) { - _chipInputTextFieldState - ..addItem(user) - ..pauseItemAddition(); - } else { - _chipInputTextFieldState.removeItem(user); - } - }, - pagination: PaginationParams( - limit: 25, - ), - filter: { - if (_userNameQuery.isNotEmpty) - 'name': { - r'$autocomplete': _userNameQuery, - }, - 'id': { - r'$ne': StreamChat.of(context).user.id, + ), + Expanded( + child: _showUserList + ? GestureDetector( + behavior: HitTestBehavior.opaque, + onPanDown: (_) => FocusScope.of(context).unfocus(), + child: UsersBloc( + child: UserListView( + selectedUsers: _selectedUsers, + groupAlphabetically: + _isSearchActive ? false : true, + onUserTap: (user, _) { + _controller.clear(); + if (!_selectedUsers.contains(user)) { + _chipInputTextFieldState + ..addItem(user) + ..pauseItemAddition(); + } else { + _chipInputTextFieldState.removeItem(user); + } + }, + pagination: PaginationParams( + limit: 25, + ), + filter: { + if (_userNameQuery.isNotEmpty) + 'name': { + r'$autocomplete': _userNameQuery, }, + 'id': { + r'$ne': StreamChat.of(context).user.id, }, - sort: [ - SortOption( - 'name', - direction: 1, - ), - ], - 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, - ), + }, + sort: [ + SortOption( + 'name', + direction: 1, + ), + ], + 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 user matches these keywords...', - style: StreamChatTheme.of( - context) - .textTheme - .footnote - .copyWith( - color: StreamChatTheme - .of(context) - .colorTheme - .black - .withOpacity( - .5)), - ), - ], - ), + ), + Text( + 'No user matches these keywords...', + style: StreamChatTheme.of( + context) + .textTheme + .footnote + .copyWith( + color: StreamChatTheme + .of(context) + .colorTheme + .black + .withOpacity(.5)), + ), + ], ), ), - ); - }, - ); - }, - ), + ), + ); + }, + ); + }, ), - ) - : FutureBuilder( - future: channel.initialized, - builder: (context, snapshot) { - if (snapshot.data == true) { - return MessageListView(); - } - - return Center( - child: Text( - 'No chats here yet...', - style: TextStyle( - fontSize: 12, - color: StreamChatTheme.of(context) - .colorTheme - .black - .withOpacity(.5), - ), - ), - ); - }, ), - ), - MessageInput( - focusNode: _messageInputFocusNode, - preMessageSending: (message) async { - await channel.watch(); - return message; - }, - onMessageSent: (m) { - Navigator.pushNamedAndRemoveUntil( - context, - Routes.CHANNEL_PAGE, - ModalRoute.withName(Routes.HOME), - arguments: ChannelPageArgs(channel: channel), - ); - }, - ), - ], - ), + ) + : FutureBuilder( + future: channel.initialized, + builder: (context, snapshot) { + if (snapshot.data == true) { + return MessageListView(); + } + + return Center( + child: Text( + 'No chats here yet...', + style: TextStyle( + fontSize: 12, + color: StreamChatTheme.of(context) + .colorTheme + .black + .withOpacity(.5), + ), + ), + ); + }, + ), + ), + MessageInput( + focusNode: _messageInputFocusNode, + preMessageSending: (message) async { + await channel.watch(); + return message; + }, + onMessageSent: (m) { + Navigator.pushNamedAndRemoveUntil( + context, + Routes.CHANNEL_PAGE, + ModalRoute.withName(Routes.HOME), + arguments: ChannelPageArgs(channel: channel), + ); + }, + ), + ], ), - ); - }), + ), + ); + }, + ), ); } } diff --git a/packages/flutter_widgets/example/pubspec.yaml b/packages/flutter_widgets/example/pubspec.yaml index 1a3f93df..73d1c2f1 100644 --- a/packages/flutter_widgets/example/pubspec.yaml +++ b/packages/flutter_widgets/example/pubspec.yaml @@ -1,7 +1,7 @@ name: example description: A new Flutter project. publish_to: 'none' -version: 1.1.3+6 +version: 1.2.0 environment: sdk: ">=2.2.2 <3.0.0" diff --git a/packages/flutter_widgets/lib/src/attachment_error.dart b/packages/flutter_widgets/lib/src/attachment_error.dart index d24423c4..46b7b814 100644 --- a/packages/flutter_widgets/lib/src/attachment_error.dart +++ b/packages/flutter_widgets/lib/src/attachment_error.dart @@ -1,8 +1,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:stream_chat/stream_chat.dart'; - +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import '../stream_chat_flutter.dart'; class AttachmentError extends StatelessWidget { diff --git a/packages/flutter_widgets/lib/src/attachment_title.dart b/packages/flutter_widgets/lib/src/attachment_title.dart index 7ed42a59..af5a1115 100644 --- a/packages/flutter_widgets/lib/src/attachment_title.dart +++ b/packages/flutter_widgets/lib/src/attachment_title.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'stream_chat_theme.dart'; import 'utils.dart'; diff --git a/packages/flutter_widgets/lib/src/channel_file_display_screen.dart b/packages/flutter_widgets/lib/src/channel_file_display_screen.dart index d5aef38c..244729a4 100644 --- a/packages/flutter_widgets/lib/src/channel_file_display_screen.dart +++ b/packages/flutter_widgets/lib/src/channel_file_display_screen.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/lazy_load_scroll_view.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; class ChannelFileDisplayScreen extends StatefulWidget { diff --git a/packages/flutter_widgets/lib/src/channel_header.dart b/packages/flutter_widgets/lib/src/channel_header.dart index 8b920f23..fb681d59 100644 --- a/packages/flutter_widgets/lib/src/channel_header.dart +++ b/packages/flutter_widgets/lib/src/channel_header.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter/src/back_button.dart'; import 'package:stream_chat_flutter/src/channel_info.dart'; import 'package:stream_chat_flutter/src/channel_name.dart'; @@ -9,7 +9,6 @@ import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import '../stream_chat_flutter.dart'; import './channel_name.dart'; import 'channel_image.dart'; -import 'stream_channel.dart'; /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_header.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_header_paint.png) diff --git a/packages/flutter_widgets/lib/src/channel_image.dart b/packages/flutter_widgets/lib/src/channel_image.dart index 2f5862f3..18d67dfb 100644 --- a/packages/flutter_widgets/lib/src/channel_image.dart +++ b/packages/flutter_widgets/lib/src/channel_image.dart @@ -1,6 +1,6 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; -import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter/src/group_image.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; diff --git a/packages/flutter_widgets/lib/src/channel_list_header.dart b/packages/flutter_widgets/lib/src/channel_list_header.dart index e8583d11..8ced0422 100644 --- a/packages/flutter_widgets/lib/src/channel_list_header.dart +++ b/packages/flutter_widgets/lib/src/channel_list_header.dart @@ -2,7 +2,7 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter/src/stream_neumorphic_button.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; diff --git a/packages/flutter_widgets/lib/src/channel_list_view.dart b/packages/flutter_widgets/lib/src/channel_list_view.dart index 0cc7634b..84eb1298 100644 --- a/packages/flutter_widgets/lib/src/channel_list_view.dart +++ b/packages/flutter_widgets/lib/src/channel_list_view.dart @@ -5,16 +5,13 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:shimmer/shimmer.dart'; -import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_flutter/src/channels_bloc.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/utils.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import '../stream_chat_flutter.dart'; import 'channel_bottom_sheet.dart'; import 'channel_preview.dart'; -import 'stream_channel.dart'; -import 'stream_chat.dart'; /// Callback called when tapping on a channel typedef ChannelTapCallback = void Function(Channel, Widget); @@ -158,168 +155,163 @@ class _ChannelListViewState extends State with WidgetsBindingObserver { final ScrollController _scrollController = ScrollController(); final SlidableController _slideController = SlidableController(); + final ChannelListController _channelListController = ChannelListController(); @override Widget build(BuildContext context) { - final channelsBloc = ChannelsBloc.of(context); + var child = ChannelListCore( + channelListController: _channelListController, + listBuilder: (context, list) { + return _buildListView(list); + }, + emptyBuilder: (BuildContext context) { + return _buildEmptyWidget(); + }, + errorBuilder: (Error error) { + return _buildErrorWidget(context); + }, + loadingBuilder: (BuildContext context) { + return _buildLoadingWidget(); + }, + pagination: widget.pagination, + options: widget.options, + sort: widget.sort, + filter: widget.filter, + ); if (!widget.pullToRefresh) { - return _buildListView(channelsBloc); + return child; + } else { + return RefreshIndicator( + onRefresh: () async { + _channelListController.loadData(); + }, + child: child, + ); + } + } + + Widget _buildListView( + List channels, + ) { + var child; + + if (channels.isNotEmpty) { + if (widget.crossAxisCount > 1) { + child = GridView.builder( + padding: widget.padding, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: widget.crossAxisCount), + itemCount: channels.length, + physics: AlwaysScrollableScrollPhysics(), + controller: _scrollController, + itemBuilder: (context, index) { + return _gridItemBuilder(context, index, channels); + }, + ); + } else { + child = ListView.separated( + padding: widget.padding, + physics: AlwaysScrollableScrollPhysics(), + itemCount: + channels.isNotEmpty ? channels.length + 1 : channels.length, + separatorBuilder: (_, index) { + if (widget.separatorBuilder != null) { + return widget.separatorBuilder(context, index); + } + return _separatorBuilder(context, index); + }, + itemBuilder: (context, index) { + return _listItemBuilder(context, index, channels); + }, + ); + } } - return RefreshIndicator( - onRefresh: () async { - return channelsBloc.queryChannels( - filter: widget.filter, - sortOptions: widget.sort, - paginationParams: widget.pagination, - options: widget.options, - ); - }, - child: _buildListView(channelsBloc), + return AnimatedSwitcher( + child: child, + duration: Duration(milliseconds: 500), ); } - StreamBuilder> _buildListView( - ChannelsBlocState channelsBlocState, - ) { - return StreamBuilder>( - stream: channelsBlocState.channelsStream, - builder: (context, snapshot) { - var child; - if (snapshot.hasError) { - child = _buildErrorWidget( - snapshot, - context, - channelsBlocState, - ); - } else if (!snapshot.hasData) { - child = _buildLoadingWidget(); - } else { - final channels = snapshot.data; + Widget _buildEmptyWidget() { + if (widget.emptyBuilder != null) { + return widget.emptyBuilder(context); + } - if (channels.isEmpty && widget.emptyBuilder != null) { - child = widget.emptyBuilder(context); - } - - if (channels.isEmpty && widget.emptyBuilder == null) { - child = LayoutBuilder( - builder: (context, viewportConstraints) { - return SingleChildScrollView( - physics: AlwaysScrollableScrollPhysics(), - child: Stack( - children: [ - ConstrainedBox( - constraints: BoxConstraints( - minHeight: viewportConstraints.maxHeight, - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: StreamSvgIcon.message( - size: 136, - color: StreamChatTheme.of(context) - .colorTheme - .greyGainsboro, - ), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - 'Let’s start chatting!', - style: StreamChatTheme.of(context) - .textTheme - .headline, - ), - ), - Padding( - padding: const EdgeInsets.symmetric( - vertical: 8.0, - horizontal: 52, - ), - child: Text( - 'How about sending your first message to a friend?', - textAlign: TextAlign.center, - style: StreamChatTheme.of(context) - .textTheme - .body - .copyWith( - color: StreamChatTheme.of(context) - .colorTheme - .grey, - ), - ), - ), - ], - ), + return LayoutBuilder( + builder: (context, viewportConstraints) { + return SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: Stack( + children: [ + ConstrainedBox( + constraints: BoxConstraints( + minHeight: viewportConstraints.maxHeight, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: StreamSvgIcon.message( + size: 136, + color: StreamChatTheme.of(context) + .colorTheme + .greyGainsboro, ), - if (widget.onStartChatPressed != null) - Positioned( - right: 0, - left: 0, - bottom: 32, - child: Center( - child: FlatButton( - onPressed: widget.onStartChatPressed, - child: Text( - 'Start a chat', - style: StreamChatTheme.of(context) - .textTheme - .bodyBold - .copyWith( - color: StreamChatTheme.of(context) - .colorTheme - .accentBlue, - ), - ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + 'Let’s start chatting!', + style: StreamChatTheme.of(context).textTheme.headline, + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 52, + ), + child: Text( + 'How about sending your first message to a friend?', + textAlign: TextAlign.center, + style: StreamChatTheme.of(context) + .textTheme + .body + .copyWith( + color: + StreamChatTheme.of(context).colorTheme.grey, ), - ), - ), - ], + ), + ), + ], + ), + ), + if (widget.onStartChatPressed != null) + Positioned( + right: 0, + left: 0, + bottom: 32, + child: Center( + child: FlatButton( + onPressed: widget.onStartChatPressed, + child: Text( + 'Start a chat', + style: StreamChatTheme.of(context) + .textTheme + .bodyBold + .copyWith( + color: StreamChatTheme.of(context) + .colorTheme + .accentBlue, + ), + ), + ), ), - ); - }, - ); - } - - if (channels.isNotEmpty) { - if (widget.crossAxisCount > 1) { - child = GridView.builder( - padding: widget.padding, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: widget.crossAxisCount), - itemCount: channels.length, - physics: AlwaysScrollableScrollPhysics(), - controller: _scrollController, - itemBuilder: (context, index) { - return _gridItemBuilder(context, index, channels); - }, - ); - } else { - child = ListView.separated( - padding: widget.padding, - physics: AlwaysScrollableScrollPhysics(), - itemCount: - channels.isNotEmpty ? channels.length + 1 : channels.length, - separatorBuilder: (_, index) { - if (widget.separatorBuilder != null) { - return widget.separatorBuilder(context, index); - } - return _separatorBuilder(context, index); - }, - itemBuilder: (context, index) { - return _listItemBuilder(context, index, channels); - }, - ); - } - } - } - - return AnimatedSwitcher( - child: child, - duration: Duration(milliseconds: 500), + ), + ], + ), ); }, ); @@ -443,27 +435,8 @@ class _ChannelListViewState extends State } Widget _buildErrorWidget( - AsyncSnapshot> snapshot, BuildContext context, - ChannelsBlocState channelsBlocState, ) { - if (snapshot.error is Error) { - print((snapshot.error as Error).stackTrace); - } - - if (widget.errorBuilder != null) { - return widget.errorBuilder(snapshot.error); - } - - var message = snapshot.error.toString(); - if (snapshot.error is DioError) { - final dioError = snapshot.error as DioError; - if (dioError.type == DioErrorType.RESPONSE) { - message = dioError.message; - } else { - message = 'Check your connection and retry'; - } - } return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -484,20 +457,9 @@ class _ChannelListViewState extends State ), style: Theme.of(context).textTheme.headline6, ), - Padding( - padding: const EdgeInsets.only( - top: 16.0, - ), - child: Text(message), - ), FlatButton( onPressed: () { - channelsBlocState.queryChannels( - filter: widget.filter, - sortOptions: widget.sort, - paginationParams: widget.pagination, - options: widget.options, - ); + _channelListController.loadData(); }, child: Text('Retry'), ), @@ -714,14 +676,7 @@ class _ChannelListViewState extends State if (_scrollController.position.maxScrollExtent == _scrollController.offset && _scrollController.offset != 0) { - channelsProvider.queryChannels( - filter: widget.filter, - sortOptions: widget.sort, - paginationParams: widget.pagination.copyWith( - offset: channelsProvider.channels?.length ?? 0, - ), - options: widget.options, - ); + _channelListController.paginateData(); } } @@ -759,12 +714,7 @@ class _ChannelListViewState extends State EventType.channelVisible, ) .listen((event) { - channelsBloc.queryChannels( - filter: widget.filter, - sortOptions: widget.sort, - paginationParams: widget.pagination, - options: widget.options, - ); + _channelListController.loadData(); }); } @@ -777,13 +727,7 @@ class _ChannelListViewState extends State widget.pagination?.toJson()?.toString() != oldWidget.pagination?.toJson()?.toString() || widget.options?.toString() != oldWidget.options?.toString()) { - final channelsBloc = ChannelsBloc.of(context); - channelsBloc.queryChannels( - filter: widget.filter, - sortOptions: widget.sort, - paginationParams: widget.pagination, - options: widget.options, - ); + _channelListController.loadData(); } } diff --git a/packages/flutter_widgets/lib/src/channel_media_display_screen.dart b/packages/flutter_widgets/lib/src/channel_media_display_screen.dart index 8b69fac8..85b571bc 100644 --- a/packages/flutter_widgets/lib/src/channel_media_display_screen.dart +++ b/packages/flutter_widgets/lib/src/channel_media_display_screen.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/lazy_load_scroll_view.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:video_player/video_player.dart'; diff --git a/packages/flutter_widgets/lib/src/channel_name.dart b/packages/flutter_widgets/lib/src/channel_name.dart index bf3eff22..7907de9a 100644 --- a/packages/flutter_widgets/lib/src/channel_name.dart +++ b/packages/flutter_widgets/lib/src/channel_name.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; -import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import '../stream_chat_flutter.dart'; -import 'stream_channel.dart'; /// It shows the current [Channel] name using a [Text] widget. /// diff --git a/packages/flutter_widgets/lib/src/channel_preview.dart b/packages/flutter_widgets/lib/src/channel_preview.dart index 3564be7a..eddd0ea6 100644 --- a/packages/flutter_widgets/lib/src/channel_preview.dart +++ b/packages/flutter_widgets/lib/src/channel_preview.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:jiffy/jiffy.dart'; -import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import '../stream_chat_flutter.dart'; diff --git a/packages/flutter_widgets/lib/src/channel_unread_indicator.dart b/packages/flutter_widgets/lib/src/channel_unread_indicator.dart index c15db13a..cb9dc362 100644 --- a/packages/flutter_widgets/lib/src/channel_unread_indicator.dart +++ b/packages/flutter_widgets/lib/src/channel_unread_indicator.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; class ChannelUnreadIndicator extends StatelessWidget { diff --git a/packages/flutter_widgets/lib/src/file_attachment.dart b/packages/flutter_widgets/lib/src/file_attachment.dart index dc7fae4b..0fff2445 100644 --- a/packages/flutter_widgets/lib/src/file_attachment.dart +++ b/packages/flutter_widgets/lib/src/file_attachment.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; -import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/utils.dart'; diff --git a/packages/flutter_widgets/lib/src/full_screen_media.dart b/packages/flutter_widgets/lib/src/full_screen_media.dart index 473c9a05..98ed3384 100644 --- a/packages/flutter_widgets/lib/src/full_screen_media.dart +++ b/packages/flutter_widgets/lib/src/full_screen_media.dart @@ -6,7 +6,6 @@ import 'package:photo_view/photo_view.dart'; import 'package:stream_chat_flutter/src/image_footer.dart'; import 'package:stream_chat_flutter/src/image_header.dart'; import 'package:video_player/video_player.dart'; -import 'stream_channel.dart'; import '../stream_chat_flutter.dart'; diff --git a/packages/flutter_widgets/lib/src/image_footer.dart b/packages/flutter_widgets/lib/src/image_footer.dart index 0131561e..4e87423e 100644 --- a/packages/flutter_widgets/lib/src/image_footer.dart +++ b/packages/flutter_widgets/lib/src/image_footer.dart @@ -7,7 +7,7 @@ import 'package:dio/dio.dart'; import 'package:esys_flutter_share/esys_flutter_share.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; diff --git a/packages/flutter_widgets/lib/src/image_group.dart b/packages/flutter_widgets/lib/src/image_group.dart index bb577a4b..3a290c60 100644 --- a/packages/flutter_widgets/lib/src/image_group.dart +++ b/packages/flutter_widgets/lib/src/image_group.dart @@ -1,6 +1,6 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; -import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter/src/full_screen_media.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; diff --git a/packages/flutter_widgets/lib/src/image_header.dart b/packages/flutter_widgets/lib/src/image_header.dart index 1694f0b7..a818ee13 100644 --- a/packages/flutter_widgets/lib/src/image_header.dart +++ b/packages/flutter_widgets/lib/src/image_header.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'image_actions_modal.dart'; -import 'stream_channel.dart'; class ImageHeader extends StatelessWidget implements PreferredSizeWidget { /// True if this header shows the leading back button diff --git a/packages/flutter_widgets/lib/src/media_list_view.dart b/packages/flutter_widgets/lib/src/media_list_view.dart index 249348c4..d84a2759 100644 --- a/packages/flutter_widgets/lib/src/media_list_view.dart +++ b/packages/flutter_widgets/lib/src/media_list_view.dart @@ -4,7 +4,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:photo_manager/photo_manager.dart'; -import 'package:stream_chat_flutter/src/lazy_load_scroll_view.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import '../stream_chat_flutter.dart'; diff --git a/packages/flutter_widgets/lib/src/message_actions_modal.dart b/packages/flutter_widgets/lib/src/message_actions_modal.dart index 4aa2cde4..6ca7c688 100644 --- a/packages/flutter_widgets/lib/src/message_actions_modal.dart +++ b/packages/flutter_widgets/lib/src/message_actions_modal.dart @@ -4,9 +4,8 @@ import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter/src/reaction_picker.dart'; -import 'package:stream_chat_flutter/src/stream_channel.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/utils.dart'; diff --git a/packages/flutter_widgets/lib/src/message_input.dart b/packages/flutter_widgets/lib/src/message_input.dart index 70b8b46f..0966e75a 100644 --- a/packages/flutter_widgets/lib/src/message_input.dart +++ b/packages/flutter_widgets/lib/src/message_input.dart @@ -13,7 +13,7 @@ import 'package:http_parser/http_parser.dart' as httpParser; import 'package:image_picker/image_picker.dart'; import 'package:mime/mime.dart'; import 'package:photo_manager/photo_manager.dart'; -import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter/src/compress_video_service.dart'; import 'package:stream_chat_flutter/src/media_list_view.dart'; import 'package:stream_chat_flutter/src/message_list_view.dart'; @@ -26,7 +26,6 @@ import 'package:video_compress/video_compress.dart'; import '../stream_chat_flutter.dart'; import 'extension.dart'; import 'quoted_message_widget.dart'; -import 'stream_channel.dart'; typedef FileUploader = Future Function(PlatformFile, Channel); typedef AttachmentThumbnailBuilder = Widget Function( diff --git a/packages/flutter_widgets/lib/src/message_list_view.dart b/packages/flutter_widgets/lib/src/message_list_view.dart index 9a0fe955..9ce29117 100644 --- a/packages/flutter_widgets/lib/src/message_list_view.dart +++ b/packages/flutter_widgets/lib/src/message_list_view.dart @@ -6,9 +6,8 @@ import 'package:flutter/material.dart'; import 'package:jiffy/jiffy.dart'; import 'package:rxdart/rxdart.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; -import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter/src/info_tile.dart'; -import 'package:stream_chat_flutter/src/lazy_load_scroll_view.dart'; import 'package:stream_chat_flutter/src/message_widget.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/system_message.dart'; @@ -16,7 +15,6 @@ import 'package:visibility_detector/visibility_detector.dart'; import '../stream_chat_flutter.dart'; import 'date_divider.dart'; -import 'stream_channel.dart'; import 'swipeable.dart'; import 'extension.dart'; @@ -238,309 +236,300 @@ class _MessageListViewState extends State { bool _inBetweenList = false; + MessageListController _messageListController = MessageListController(); + @override Widget build(BuildContext context) { - final messagesStream = _isThreadConversation - ? streamChannel.channel.state.threadsStream - .where((threads) => threads.containsKey(widget.parentMessage.id)) - .map((threads) => threads[widget.parentMessage.id]) - : streamChannel.channel.state?.messagesStream; + return MessageListCore( + loadingBuilder: (context) { + return Center( + child: const CircularProgressIndicator(), + ); + }, + emptyBuilder: (context) { + return Center( + child: Text( + 'No chats here yet...', + style: StreamChatTheme.of(context).textTheme.footnote.copyWith( + color: StreamChatTheme.of(context) + .colorTheme + .black + .withOpacity(.5)), + ), + ); + }, + messageListBuilder: (context, list) { + return _buildListView(list); + }, + messageListController: _messageListController, + parentMessage: widget.parentMessage, + showScrollToBottom: widget.showScrollToBottom, + errorWidgetBuilder: (BuildContext context, Object error) { + return Center( + child: Text( + 'Something went wrong', + style: StreamChatTheme.of(context).textTheme.footnote.copyWith( + color: StreamChatTheme.of(context) + .colorTheme + .black + .withOpacity(.5)), + ), + ); + }, + ); + } - return StreamBuilder>( - stream: messagesStream?.map((messages) => messages - ?.where((e) => - (!e.isDeleted && e.shadowed != true) || - (e.isDeleted && - e.user.id == streamChannel.channel.client.state.user.id)) - ?.toList()), - builder: (context, snapshot) { - if (!snapshot.hasData) { - return Center( - child: const CircularProgressIndicator(), - ); + Widget _buildListView(List data) { + messages = data; + final newMessagesListLength = messages.length; + + if (_messageListLength != null) { + if (_bottomPaginationActive || (_inBetweenList && _upToDate)) { + if (_itemPositionListener.itemPositions.value?.isNotEmpty == true) { + final first = _itemPositionListener.itemPositions.value.first; + final diff = newMessagesListLength - _messageListLength; + if (diff > 0) { + initialIndex = first.index + diff; + initialAlignment = first.itemLeadingEdge; } + } + } else if (!_topPaginationActive && _upToDate) { + // Reset the index in-case we send any new message + initialIndex = 0; + initialAlignment = 0; + } + } - final messageList = snapshot.data?.reversed?.toList() ?? []; - if (messageList.isEmpty) { - if (_upToDate) { - return Center( - child: Text( - 'No chats here yet...', - style: StreamChatTheme.of(context) - .textTheme - .footnote - .copyWith( - color: StreamChatTheme.of(context) - .colorTheme - .black - .withOpacity(.5)), + _messageListLength = newMessagesListLength; + final _client = StreamChat.of(context).client; + + return Stack( + alignment: Alignment.center, + children: [ + ValueListenableBuilder( + valueListenable: _client.wsConnectionStatus, + builder: (context, status, _) { + String statusString = ''; + bool showStatus = true; + + switch (status) { + case ConnectionStatus.connected: + statusString = 'Connected'; + showStatus = false; + break; + case ConnectionStatus.connecting: + statusString = 'Reconnecting...'; + break; + case ConnectionStatus.disconnected: + statusString = 'Disconnected'; + break; + } + + return InfoTile( + showMessage: + widget.showConnectionStateTile ? showStatus : false, + tileAnchor: Alignment.topCenter, + childAnchor: Alignment.topCenter, + message: statusString, + child: LazyLoadScrollView( + onStartOfPage: () async { + _inBetweenList = false; + if (!_upToDate) { + _topPaginationActive = false; + _bottomPaginationActive = true; + return _paginateData( + streamChannel, + QueryDirection.bottom, + ); + } + }, + onEndOfPage: () async { + _inBetweenList = false; + _topPaginationActive = true; + _bottomPaginationActive = false; + return _paginateData( + streamChannel, + QueryDirection.top, + ); + }, + onInBetweenOfPage: () { + _inBetweenList = true; + }, + child: ScrollablePositionedList.separated( + key: ValueKey(initialIndex + initialAlignment), + itemPositionsListener: _itemPositionListener, + addAutomaticKeepAlives: true, + initialScrollIndex: initialIndex ?? 0, + initialAlignment: initialAlignment ?? 0, + physics: widget.scrollPhysics, + itemScrollController: _scrollController, + reverse: true, + itemCount: + messages.length + 2 + (_isThreadConversation ? 1 : 0), + separatorBuilder: (context, i) { + if (i == messages.length) return Offstage(); + if (i == 0) return SizedBox(height: 30); + if (i == messages.length + 1) { + final replyCount = widget.parentMessage.replyCount; + return Container( + decoration: BoxDecoration( + gradient: StreamChatTheme.of(context) + .colorTheme + .bgGradient, + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + '$replyCount ${replyCount == 1 ? 'Reply' : 'Replies'}', + textAlign: TextAlign.center, + style: StreamChatTheme.of(context) + .channelTheme + .channelHeaderTheme + .lastMessageAt, + ), + ), + ); + } + + final message = messages[i]; + final nextMessage = messages[i - 1]; + if (!Jiffy(message.createdAt.toLocal()).isSame( + nextMessage.createdAt.toLocal(), + Units.DAY, + )) { + final divider = widget.dateDividerBuilder != null + ? widget.dateDividerBuilder( + nextMessage.createdAt.toLocal(), + ) + : DateDivider( + dateTime: nextMessage.createdAt.toLocal(), + ); + return Padding( + padding: const EdgeInsets.symmetric(vertical: 12.0), + child: divider, + ); + } + final timeDiff = + Jiffy(nextMessage.createdAt.toLocal()).diff( + message.createdAt.toLocal(), + Units.MINUTE, + ); + + final isNextUserSame = + message.user.id == nextMessage.user?.id; + final isThread = message.replyCount > 0; + final isDeleted = message.isDeleted; + if (timeDiff >= 1 || + !isNextUserSame || + isThread || + isDeleted) { + return SizedBox(height: 8); + } + return SizedBox(height: 2); + }, + itemBuilder: (context, i) { + if (i == messages.length + 2) { + if (widget.parentMessageBuilder != null) { + return widget.parentMessageBuilder( + context, + widget.parentMessage, + ); + } else { + return buildParentMessage(widget.parentMessage); + } + } + if (i == messages.length + 1) { + return _buildLoadingIndicator( + streamChannel, + QueryDirection.top, + ); + } + if (i == 0) { + return _buildLoadingIndicator( + streamChannel, + QueryDirection.bottom, + ); + } + final message = messages[i - 1]; + + Widget messageWidget; + + if (i == 1) { + messageWidget = _buildBottomMessage( + context, + message, + messages, + streamChannel, + ); + } else if (i == messages.length - 1) { + messageWidget = _buildTopMessage( + context, + message, + messages, + streamChannel, + ); + } else { + if (widget.messageBuilder != null) { + messageWidget = Builder( + key: ValueKey('MESSAGE-${message.id}'), + builder: (context) => widget.messageBuilder( + context, + MessageDetails( + context, + message, + messages, + i, + ), + messages), + ); + } else { + messageWidget = buildMessage(message, messages, i); + } + } + return messageWidget; + }, + ), ), ); - } - } else { - messages = messageList; - } - - final newMessagesListLength = messages.length; - - if (_messageListLength != null) { - if (_bottomPaginationActive || (_inBetweenList && _upToDate)) { - if (_itemPositionListener.itemPositions.value?.isNotEmpty == - true) { - final first = _itemPositionListener.itemPositions.value.first; - final diff = newMessagesListLength - _messageListLength; - if (diff > 0) { - initialIndex = first.index + diff; - initialAlignment = first.itemLeadingEdge; - } + }), + if (widget.showScrollToBottom) _buildScrollToBottom(), + Positioned( + top: 20.0, + child: ValueListenableBuilder>( + valueListenable: _itemPositionListener.itemPositions, + builder: (context, values, _) { + final items = _itemPositionListener.itemPositions?.value; + if (items.isEmpty || messages.isEmpty) { + return SizedBox(); } - } else if (!_topPaginationActive && _upToDate) { - // Reset the index in-case we send any new message - initialIndex = 0; - initialAlignment = 0; - } - } - _messageListLength = newMessagesListLength; - final _client = StreamChat.of(context).client; + var index = _getTopElement(values).index; - return Stack( - alignment: Alignment.center, - children: [ - ValueListenableBuilder( - valueListenable: _client.wsConnectionStatus, - builder: (context, status, _) { - String statusString = ''; - bool showStatus = true; + if (index > messages.length) { + return SizedBox(); + } - switch (status) { - case ConnectionStatus.connected: - statusString = 'Connected'; - showStatus = false; - break; - case ConnectionStatus.connecting: - statusString = 'Reconnecting...'; - break; - case ConnectionStatus.disconnected: - statusString = 'Disconnected'; - break; - } + if (index == messages.length) { + index = max(index - 1, 0); + } - return InfoTile( - showMessage: - widget.showConnectionStateTile ? showStatus : false, - tileAnchor: Alignment.topCenter, - childAnchor: Alignment.topCenter, - message: statusString, - child: LazyLoadScrollView( - onStartOfPage: () async { - _inBetweenList = false; - if (!_upToDate) { - _topPaginationActive = false; - _bottomPaginationActive = true; - return _paginateData( - streamChannel, - QueryDirection.bottom, - ); - } - }, - onEndOfPage: () async { - _inBetweenList = false; - _topPaginationActive = true; - _bottomPaginationActive = false; - return _paginateData( - streamChannel, - QueryDirection.top, - ); - }, - onInBetweenOfPage: () { - _inBetweenList = true; - }, - child: ScrollablePositionedList.separated( - key: ValueKey(initialIndex + initialAlignment), - itemPositionsListener: _itemPositionListener, - addAutomaticKeepAlives: true, - initialScrollIndex: initialIndex ?? 0, - initialAlignment: initialAlignment ?? 0, - physics: widget.scrollPhysics, - itemScrollController: _scrollController, - reverse: true, - itemCount: messages.length + - 2 + - (_isThreadConversation ? 1 : 0), - separatorBuilder: (context, i) { - if (i == messages.length) return Offstage(); - if (i == 0) return SizedBox(height: 30); - if (i == messages.length + 1) { - final replyCount = - widget.parentMessage.replyCount; - return Container( - decoration: BoxDecoration( - gradient: StreamChatTheme.of(context) - .colorTheme - .bgGradient, - ), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - '$replyCount ${replyCount == 1 ? 'Reply' : 'Replies'}', - textAlign: TextAlign.center, - style: StreamChatTheme.of(context) - .channelTheme - .channelHeaderTheme - .lastMessageAt, - ), - ), - ); - } - - final message = messages[i]; - final nextMessage = messages[i - 1]; - if (!Jiffy(message.createdAt.toLocal()).isSame( - nextMessage.createdAt.toLocal(), - Units.DAY, - )) { - final divider = widget.dateDividerBuilder != null - ? widget.dateDividerBuilder( - nextMessage.createdAt.toLocal(), - ) - : DateDivider( - dateTime: nextMessage.createdAt.toLocal(), - ); - return Padding( - padding: - const EdgeInsets.symmetric(vertical: 12.0), - child: divider, - ); - } - final timeDiff = - Jiffy(nextMessage.createdAt.toLocal()).diff( - message.createdAt.toLocal(), - Units.MINUTE, - ); - - final isNextUserSame = - message.user.id == nextMessage.user?.id; - final isThread = message.replyCount > 0; - final isDeleted = message.isDeleted; - if (timeDiff >= 1 || - !isNextUserSame || - isThread || - isDeleted) { - return SizedBox(height: 8); - } - return SizedBox(height: 2); - }, - itemBuilder: (context, i) { - if (i == messages.length + 2) { - if (widget.parentMessageBuilder != null) { - return widget.parentMessageBuilder( - context, - widget.parentMessage, - ); - } else { - return buildParentMessage(widget.parentMessage); - } - } - if (i == messages.length + 1) { - return _buildLoadingIndicator( - streamChannel, - QueryDirection.top, - ); - } - if (i == 0) { - return _buildLoadingIndicator( - streamChannel, - QueryDirection.bottom, - ); - } - final message = messages[i - 1]; - - Widget messageWidget; - - if (i == 1) { - messageWidget = _buildBottomMessage( - context, - message, - messages, - streamChannel, - ); - } else if (i == messages.length - 1) { - messageWidget = _buildTopMessage( - context, - message, - messages, - streamChannel, - ); - } else { - if (widget.messageBuilder != null) { - messageWidget = Builder( - key: - ValueKey('MESSAGE-${message.id}'), - builder: (context) => widget.messageBuilder( - context, - MessageDetails( - context, - message, - messages, - i, - ), - messages), - ); - } else { - messageWidget = - buildMessage(message, messages, i); - } - } - return messageWidget; - }, - ), - ), + return widget.dateDividerBuilder != null + ? widget.dateDividerBuilder( + messages[index].createdAt.toLocal(), + ) + : DateDivider( + dateTime: messages[index].createdAt.toLocal(), ); - }), - if (widget.showScrollToBottom) _buildScrollToBottom(), - Positioned( - top: 20.0, - child: ValueListenableBuilder>( - valueListenable: _itemPositionListener.itemPositions, - builder: (context, values, _) { - final items = _itemPositionListener.itemPositions?.value; - if (items.isEmpty || messages.isEmpty) { - return SizedBox(); - } - - var index = _getTopElement(values).index; - - if (index > messages.length) { - return SizedBox(); - } - - if (index == messages.length) { - index = max(index - 1, 0); - } - - return widget.dateDividerBuilder != null - ? widget.dateDividerBuilder( - messages[index].createdAt.toLocal(), - ) - : DateDivider( - dateTime: messages[index].createdAt.toLocal(), - ); - }, - ), - ), - ], - ); - }); + }, + ), + ), + ], + ); } Future _paginateData( StreamChannelState channel, QueryDirection direction) { - if (!_isThreadConversation) { - return channel.queryMessages(direction: direction); - } else { - return channel.getReplies(widget.parentMessage.id); - } + return _messageListController.paginateData(direction: direction); } ItemPosition _getTopElement(Iterable values) { diff --git a/packages/flutter_widgets/lib/src/message_reactions_modal.dart b/packages/flutter_widgets/lib/src/message_reactions_modal.dart index 2f117aca..e3d4f201 100644 --- a/packages/flutter_widgets/lib/src/message_reactions_modal.dart +++ b/packages/flutter_widgets/lib/src/message_reactions_modal.dart @@ -1,7 +1,7 @@ import 'dart:ui'; import 'package:flutter/material.dart'; -import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter/src/reaction_bubble.dart'; import 'package:stream_chat_flutter/src/reaction_picker.dart'; import 'package:stream_chat_flutter/src/stream_chat.dart'; diff --git a/packages/flutter_widgets/lib/src/message_search_item.dart b/packages/flutter_widgets/lib/src/message_search_item.dart index 9c951a56..4d8d9148 100644 --- a/packages/flutter_widgets/lib/src/message_search_item.dart +++ b/packages/flutter_widgets/lib/src/message_search_item.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:jiffy/jiffy.dart'; -import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// It shows the current [Message] preview. diff --git a/packages/flutter_widgets/lib/src/message_search_list_view.dart b/packages/flutter_widgets/lib/src/message_search_list_view.dart index b176e148..2cbea145 100644 --- a/packages/flutter_widgets/lib/src/message_search_list_view.dart +++ b/packages/flutter_widgets/lib/src/message_search_list_view.dart @@ -1,13 +1,9 @@ -import 'dart:convert'; - import 'package:flutter/material.dart'; -import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter/src/info_tile.dart'; import 'package:stream_chat_flutter/src/message_search_item.dart'; import '../stream_chat_flutter.dart'; -import 'lazy_load_scroll_view.dart'; -import 'message_search_bloc.dart'; /// Callback called when tapping on a user typedef MessageSearchItemTapCallback = void Function(GetMessageResponse); @@ -120,23 +116,115 @@ class MessageSearchListView extends StatefulWidget { } class _MessageSearchListViewState extends State { - @override - void initState() { - super.initState(); - final messageSearchBloc = MessageSearchBloc.of(context); - messageSearchBloc.search( - filter: widget.filters, - sort: widget.sortOptions, - query: widget.messageQuery, - pagination: widget.paginationParams, - messageFilter: widget.messageFilters, - ); - } + MessageSearchListController _messageSearchListController = + MessageSearchListController(); @override Widget build(BuildContext context) { - final messageSearchBloc = MessageSearchBloc.of(context); - return _buildListView(messageSearchBloc); + return MessageSearchListCore( + filters: widget.filters, + sortOptions: widget.sortOptions, + messageQuery: widget.messageQuery, + paginationParams: widget.paginationParams, + messageFilters: widget.messageFilters, + messageSearchListController: _messageSearchListController, + emptyBuilder: (context) { + if (widget.emptyBuilder != null) { + return widget.emptyBuilder(context, widget.messageQuery); + } + return LayoutBuilder( + builder: (context, viewportConstraints) { + return SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: viewportConstraints.maxHeight, + ), + child: Center( + child: Text('There are no messages currently'), + ), + ), + ); + }, + ); + }, + errorBuilder: (error) { + if (error is Error) { + print((error).stackTrace); + } + + if (widget.errorBuilder != null) { + return widget.errorBuilder(error); + } + + var message = error.toString(); + if (error is DioError) { + final dioError = error as DioError; + if (dioError.type == DioErrorType.RESPONSE) { + message = dioError.message; + } else { + message = 'Check your connection and retry'; + } + } + return InfoTile( + showMessage: widget.showErrorTile, + tileAnchor: Alignment.topCenter, + childAnchor: Alignment.topCenter, + message: 'An error occurred.', + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text.rich( + TextSpan( + children: [ + WidgetSpan( + child: Padding( + padding: const EdgeInsets.only(right: 2.0), + child: Icon(Icons.error_outline), + ), + ), + TextSpan(text: 'Error loading messages'), + ], + ), + style: Theme.of(context).textTheme.headline6, + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: Text(message), + ), + RaisedButton( + onPressed: () { + _messageSearchListController.loadData(); + }, + child: Text('Retry'), + ), + ], + ), + ), + ); + }, + loadingBuilder: (context) { + return LayoutBuilder( + builder: (context, viewportConstraints) { + return SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: viewportConstraints.maxHeight, + ), + child: Center( + child: CircularProgressIndicator(), + ), + ), + ); + }, + ); + }, + childBuilder: (list) { + return _buildListView(list); + }, + ); } Widget _separatorBuilder(BuildContext context, int index) { @@ -157,8 +245,9 @@ class _MessageSearchListViewState extends State { ); } - Widget _buildQueryProgressIndicator( - context, MessageSearchBlocState messageSearchBloc) { + Widget _buildQueryProgressIndicator(context) { + MessageSearchBlocState messageSearchBloc = MessageSearchBloc.of(context); + return StreamBuilder( stream: messageSearchBloc.queryMessagesLoading, initialData: false, @@ -187,204 +276,66 @@ class _MessageSearchListViewState extends State { }); } - Widget _buildListView(MessageSearchBlocState messageSearchBloc) { - return StreamBuilder>( - stream: messageSearchBloc.messagesStream, - builder: (context, snapshot) { - if (snapshot.hasError) { - if (snapshot.error is Error) { - print((snapshot.error as Error).stackTrace); - } + Widget _buildListView(List data) { + final items = data; - if (widget.errorBuilder != null) { - return widget.errorBuilder(snapshot.error); - } - - var message = snapshot.error.toString(); - if (snapshot.error is DioError) { - final dioError = snapshot.error as DioError; - if (dioError.type == DioErrorType.RESPONSE) { - message = dioError.message; - } else { - message = 'Check your connection and retry'; - } - } - return InfoTile( - showMessage: widget.showErrorTile, - tileAnchor: Alignment.topCenter, - childAnchor: Alignment.topCenter, - message: 'An error occurred.', - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text.rich( - TextSpan( - children: [ - WidgetSpan( - child: Padding( - padding: const EdgeInsets.only(right: 2.0), - child: Icon(Icons.error_outline), - ), - ), - TextSpan(text: 'Error loading messages'), - ], - ), - style: Theme.of(context).textTheme.headline6, - ), - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: Text(message), - ), - RaisedButton( - onPressed: () { - messageSearchBloc.search( - filter: widget.filters, - sort: widget.sortOptions, - query: widget.messageQuery, - pagination: widget.paginationParams, - messageFilter: widget.messageFilters, - ); - }, - child: Text('Retry'), - ), - ], - ), - ), - ); + Widget child = ListView.separated( + physics: AlwaysScrollableScrollPhysics(), + itemCount: items.isNotEmpty ? items.length + 1 : items.length, + separatorBuilder: (_, index) { + if (widget.separatorBuilder != null) { + return widget.separatorBuilder(context, index); } - - if (!snapshot.hasData) { - return LayoutBuilder( - builder: (context, viewportConstraints) { - return SingleChildScrollView( - physics: AlwaysScrollableScrollPhysics(), - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: viewportConstraints.maxHeight, - ), - child: Center( - child: CircularProgressIndicator(), - ), - ), - ); - }, - ); + return _separatorBuilder(context, index); + }, + itemBuilder: (context, index) { + if (index < items.length) { + return _listItemBuilder(context, items[index]); } - - final items = snapshot.data; - - if (items.isEmpty) { - if (widget.emptyBuilder != null) { - return widget.emptyBuilder(context, widget.messageQuery); - } - return LayoutBuilder( - builder: (context, viewportConstraints) { - return SingleChildScrollView( - physics: AlwaysScrollableScrollPhysics(), - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: viewportConstraints.maxHeight, - ), - child: Center( - child: Text('There are no messages currently'), - ), - ), - ); - }, - ); - } - - Widget child = ListView.separated( - physics: AlwaysScrollableScrollPhysics(), - itemCount: items.isNotEmpty ? items.length + 1 : items.length, - separatorBuilder: (_, index) { - if (widget.separatorBuilder != null) { - return widget.separatorBuilder(context, index); - } - return _separatorBuilder(context, index); - }, - itemBuilder: (context, index) { - if (index < items.length) { - return _listItemBuilder(context, items[index]); - } - return _buildQueryProgressIndicator(context, messageSearchBloc); - }, - ); - if (widget.pullToRefresh) { - child = RefreshIndicator( - onRefresh: () async => messageSearchBloc.search( - filter: widget.filters, - sort: widget.sortOptions, - query: widget.messageQuery, - pagination: widget.paginationParams, - messageFilter: widget.messageFilters, - ), - child: child, - ); - } - - child = LazyLoadScrollView( - onEndOfPage: () => messageSearchBloc.loadMore( - filter: widget.filters, - sort: widget.sortOptions, - pagination: widget.paginationParams.copyWith( - offset: messageSearchBloc.messageResponses?.length ?? 0, - ), - query: widget.messageQuery, - messageFilter: widget.messageFilters, - ), - child: child, - ); - - if (widget.showResultCount) { - child = Column( - children: [ - Container( - width: double.maxFinite, - decoration: BoxDecoration( - gradient: StreamChatTheme.of(context).colorTheme.bgGradient, - ), - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 8, - ), - child: Text( - '${items.length} results', - style: TextStyle( - color: StreamChatTheme.of(context).colorTheme.grey, - ), - ), - ), - ), - Expanded(child: child), - ], - ); - } - return child; + return _buildQueryProgressIndicator(context); }, ); - } - - @override - void didUpdateWidget(MessageSearchListView oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.filters?.toString() != oldWidget.filters?.toString() || - jsonEncode(widget.sortOptions) != jsonEncode(oldWidget.sortOptions) || - widget.paginationParams?.toJson()?.toString() != - oldWidget.paginationParams?.toJson()?.toString() || - widget.messageQuery?.toString() != oldWidget.messageQuery?.toString() || - widget.messageFilters?.toString() != - oldWidget.messageFilters?.toString()) { - final messageSearchBloc = MessageSearchBloc.of(context); - messageSearchBloc.search( - filter: widget.filters, - sort: widget.sortOptions, - query: widget.messageQuery, - pagination: widget.paginationParams, - messageFilter: widget.messageFilters, + if (widget.pullToRefresh) { + child = RefreshIndicator( + onRefresh: () async { + _messageSearchListController.loadData(); + }, + child: child, ); } + + child = LazyLoadScrollView( + onEndOfPage: () async { + return _messageSearchListController.paginateData(); + }, + child: child, + ); + + if (widget.showResultCount) { + child = Column( + children: [ + Container( + width: double.maxFinite, + decoration: BoxDecoration( + gradient: StreamChatTheme.of(context).colorTheme.bgGradient, + ), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 8, + ), + child: Text( + '${items.length} results', + style: TextStyle( + color: StreamChatTheme.of(context).colorTheme.grey, + ), + ), + ), + ), + Expanded(child: child), + ], + ); + } + return child; } } diff --git a/packages/flutter_widgets/lib/src/message_text.dart b/packages/flutter_widgets/lib/src/message_text.dart index c64d9520..373acbfc 100644 --- a/packages/flutter_widgets/lib/src/message_text.dart +++ b/packages/flutter_widgets/lib/src/message_text.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; -import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'stream_chat_theme.dart'; import 'utils.dart'; diff --git a/packages/flutter_widgets/lib/src/quoted_message_widget.dart b/packages/flutter_widgets/lib/src/quoted_message_widget.dart index e4e10e17..8b0912eb 100644 --- a/packages/flutter_widgets/lib/src/quoted_message_widget.dart +++ b/packages/flutter_widgets/lib/src/quoted_message_widget.dart @@ -2,7 +2,7 @@ import 'dart:math'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; -import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:video_player/video_player.dart'; import 'attachment_error.dart'; diff --git a/packages/flutter_widgets/lib/src/stream_chat.dart b/packages/flutter_widgets/lib/src/stream_chat.dart index 5d1b7484..37a163ec 100644 --- a/packages/flutter_widgets/lib/src/stream_chat.dart +++ b/packages/flutter_widgets/lib/src/stream_chat.dart @@ -4,8 +4,9 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_app_badger/flutter_app_badger.dart'; import 'package:flutter_portal/flutter_portal.dart'; -import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// Widget used to provide information about the chat to the widget tree /// @@ -61,9 +62,8 @@ class StreamChat extends StatefulWidget { } /// The current state of the StreamChat widget -class StreamChatState extends State with WidgetsBindingObserver { +class StreamChatState extends State { Client get client => widget.client; - Timer _disconnectTimer; @override Widget build(BuildContext context) { @@ -81,7 +81,10 @@ class StreamChatState extends State with WidgetsBindingObserver { accentColor: streamTheme.colorTheme.accentBlue, scaffoldBackgroundColor: streamTheme.colorTheme.white, ), - child: widget.child, + child: StreamChatCore( + child: widget.child, + client: client, + ), ); }, ), @@ -106,7 +109,6 @@ class StreamChatState extends State with WidgetsBindingObserver { @override void initState() { super.initState(); - WidgetsBinding.instance.addObserver(this); client.state?.totalUnreadCountStream?.listen((count) { if (count > 0) { FlutterAppBadger.updateBadgeCount(count); @@ -115,67 +117,4 @@ class StreamChatState extends State with WidgetsBindingObserver { } }); } - - StreamSubscription _newMessageSubscription; - - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - if (client.state?.user != null) { - if (state == AppLifecycleState.paused) { - if (client.showLocalNotification != null) { - _newMessageSubscription = client - .on(EventType.messageNew) - .where((e) => e.user?.id != user.id) - .where((e) => e.message.silent != true) - .where((e) => e.message.shadowed != true) - .listen((event) async { - final channel = client.channel( - event.channelType, - id: event.channelId, - ); - - client.showLocalNotification( - event.message, - ChannelModel( - id: channel.id, - createdAt: channel.createdAt, - extraData: channel.extraData, - type: channel.type, - memberCount: channel.memberCount, - frozen: channel.frozen, - cid: channel.cid, - deletedAt: channel.deletedAt, - config: channel.config, - createdBy: channel.createdBy, - updatedAt: channel.updatedAt, - lastMessageAt: channel.lastMessageAt, - ), - ); - }); - _disconnectTimer = Timer(client.backgroundKeepAlive, () { - client.disconnect(); - }); - } else { - client.disconnect(); - } - } else if (state == AppLifecycleState.resumed) { - _newMessageSubscription?.cancel(); - if (_disconnectTimer?.isActive == true) { - _disconnectTimer.cancel(); - } else { - if (client.wsConnectionStatus.value == - ConnectionStatus.disconnected) { - NotificationService.handleIosMessageQueue(client); - client.connect(); - } - } - } - } - } - - @override - void dispose() { - WidgetsBinding.instance.removeObserver(this); - super.dispose(); - } } diff --git a/packages/flutter_widgets/lib/src/stream_chat_theme.dart b/packages/flutter_widgets/lib/src/stream_chat_theme.dart index 3945a033..c65f36cc 100644 --- a/packages/flutter_widgets/lib/src/stream_chat_theme.dart +++ b/packages/flutter_widgets/lib/src/stream_chat_theme.dart @@ -1,6 +1,6 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; -import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter/src/channel_header.dart'; import 'package:stream_chat_flutter/src/channel_preview.dart'; import 'package:stream_chat_flutter/src/message_input.dart'; diff --git a/packages/flutter_widgets/lib/src/thread_header.dart b/packages/flutter_widgets/lib/src/thread_header.dart index 35d554a6..b33b8bc1 100644 --- a/packages/flutter_widgets/lib/src/thread_header.dart +++ b/packages/flutter_widgets/lib/src/thread_header.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; diff --git a/packages/flutter_widgets/lib/src/typing_indicator.dart b/packages/flutter_widgets/lib/src/typing_indicator.dart index 177e9e21..8a53c3e8 100644 --- a/packages/flutter_widgets/lib/src/typing_indicator.dart +++ b/packages/flutter_widgets/lib/src/typing_indicator.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lottie/lottie.dart'; -import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_flutter/src/stream_channel.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// Widget to show the current list of typing users class TypingIndicator extends StatelessWidget { diff --git a/packages/flutter_widgets/lib/src/user_avatar.dart b/packages/flutter_widgets/lib/src/user_avatar.dart index 706c4d7e..f38a9a68 100644 --- a/packages/flutter_widgets/lib/src/user_avatar.dart +++ b/packages/flutter_widgets/lib/src/user_avatar.dart @@ -1,6 +1,6 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; -import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import '../stream_chat_flutter.dart'; diff --git a/packages/flutter_widgets/lib/src/user_item.dart b/packages/flutter_widgets/lib/src/user_item.dart index 58a354fc..cb82e311 100644 --- a/packages/flutter_widgets/lib/src/user_item.dart +++ b/packages/flutter_widgets/lib/src/user_item.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:jiffy/jiffy.dart'; -import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/user_list_view.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; diff --git a/packages/flutter_widgets/lib/src/user_list_view.dart b/packages/flutter_widgets/lib/src/user_list_view.dart index c5bac576..a53ba97a 100644 --- a/packages/flutter_widgets/lib/src/user_list_view.dart +++ b/packages/flutter_widgets/lib/src/user_list_view.dart @@ -1,9 +1,5 @@ -import 'dart:convert'; - import 'package:flutter/material.dart'; -import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_flutter/src/lazy_load_scroll_view.dart'; -import 'package:stream_chat_flutter/src/users_bloc.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'user_item.dart'; @@ -142,209 +138,168 @@ class _UserListViewState extends State with WidgetsBindingObserver { bool get _isListView => widget.crossAxisCount == 1; - @override - void initState() { - super.initState(); - final usersBloc = UsersBloc.of(context); - usersBloc.queryUsers( - filter: widget.filter, - sort: widget.sort, - pagination: widget.pagination, - options: widget.options, - ); - } + UserListController _userListController = UserListController(); @override Widget build(BuildContext context) { - final usersBloc = UsersBloc.of(context); - - if (!widget.pullToRefresh) { - return _buildListView(usersBloc); - } - - return RefreshIndicator( - onRefresh: () async { - return usersBloc.queryUsers( - filter: widget.filter, - sort: widget.sort, - options: widget.options, - pagination: widget.pagination, + var child = UserListCore( + errorBuilder: (err) { + return _buildError(err); + }, + emptyBuilder: (context) { + return _buildEmpty(); + }, + loadingBuilder: (context) { + return LayoutBuilder( + builder: (context, viewportConstraints) { + return SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: viewportConstraints.maxHeight, + ), + child: Center( + child: CircularProgressIndicator(), + ), + ), + ); + }, ); }, - child: _buildListView(usersBloc), + listBuilder: (context, list) { + return _buildListView(list); + }, + pagination: widget.pagination, + options: widget.options, + sort: widget.sort, + filter: widget.filter, + groupAlphabetically: widget.groupAlphabetically, + userListController: _userListController, ); + + if (!widget.pullToRefresh) { + return child; + } else { + return RefreshIndicator( + onRefresh: () async { + _userListController.loadData(); + }, + child: child, + ); + } } bool get isListAlreadySorted => widget.sort?.any((e) => e.field == 'name' && e.direction == 1) ?? false; - Stream> _buildUserStream( - UsersBlocState usersBlocState, - ) { - return usersBlocState.usersStream.map( - (users) { - if (widget.groupAlphabetically) { - var temp = users; - if (!isListAlreadySorted) { - temp = users..sort((curr, next) => curr.name.compareTo(next.name)); - } - final groupedUsers = >{}; - for (var e in temp) { - final alphabet = e.name[0]?.toUpperCase(); - groupedUsers[alphabet] = [...groupedUsers[alphabet] ?? [], e]; - } - final items = []; - for (var key in groupedUsers.keys) { - items.add(ListHeaderItem(key)); - items.addAll(groupedUsers[key].map((e) => ListUserItem(e))); - } - return items; - } - return users.map((e) => ListUserItem(e)).toList(); + Widget _buildError(Error error) { + print((error).stackTrace); + + if (widget.errorBuilder != null) { + return widget.errorBuilder(error); + } + + var message = error.toString(); + if (error is DioError) { + final dioError = error as DioError; + if (dioError.type == DioErrorType.RESPONSE) { + message = dioError.message; + } else { + message = 'Check your connection and retry'; + } + } + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text.rich( + TextSpan( + children: [ + WidgetSpan( + child: Padding( + padding: const EdgeInsets.only( + right: 2.0, + ), + child: Icon(Icons.error_outline), + ), + ), + TextSpan(text: 'Error loading channels'), + ], + ), + style: Theme.of(context).textTheme.headline6, + ), + Padding( + padding: const EdgeInsets.only( + top: 16.0, + ), + child: Text(message), + ), + FlatButton( + onPressed: () { + _userListController.loadData(); + }, + child: Text('Retry'), + ), + ], + ), + ); + } + + Widget _buildEmpty() { + if (widget.emptyBuilder != null) { + return widget.emptyBuilder(context); + } + + return LayoutBuilder( + builder: (context, viewportConstraints) { + return SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: viewportConstraints.maxHeight, + ), + child: Center( + child: Text('There are no users currently'), + ), + ), + ); }, ); } - StreamBuilder> _buildListView( - UsersBlocState usersBlocState, + Widget _buildListView( + List items, ) { - return StreamBuilder( - stream: _buildUserStream(usersBlocState), - builder: (context, snapshot) { - if (snapshot.hasError) { - if (snapshot.error is Error) { - print((snapshot.error as Error).stackTrace); - } - - if (widget.errorBuilder != null) { - return widget.errorBuilder(snapshot.error); - } - - var message = snapshot.error.toString(); - if (snapshot.error is DioError) { - final dioError = snapshot.error as DioError; - if (dioError.type == DioErrorType.RESPONSE) { - message = dioError.message; - } else { - message = 'Check your connection and retry'; - } - } - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text.rich( - TextSpan( - children: [ - WidgetSpan( - child: Padding( - padding: const EdgeInsets.only( - right: 2.0, - ), - child: Icon(Icons.error_outline), - ), - ), - TextSpan(text: 'Error loading channels'), - ], - ), - style: Theme.of(context).textTheme.headline6, - ), - Padding( - padding: const EdgeInsets.only( - top: 16.0, - ), - child: Text(message), - ), - FlatButton( - onPressed: () { - usersBlocState.queryUsers( - filter: widget.filter, - sort: widget.sort, - pagination: widget.pagination, - options: widget.options, - ); - }, - child: Text('Retry'), - ), - ], + final child = _isListView + ? ListView.separated( + physics: AlwaysScrollableScrollPhysics(), + itemCount: items.isNotEmpty ? items.length + 1 : items.length, + separatorBuilder: (_, index) { + if (widget.separatorBuilder != null) { + return widget.separatorBuilder(context, index); + } + return _separatorBuilder(context, index); + }, + itemBuilder: (context, index) { + return _listItemBuilder(context, index, items); + }, + ) + : GridView.builder( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: widget.crossAxisCount, ), - ); - } - - if (!snapshot.hasData) { - return LayoutBuilder( - builder: (context, viewportConstraints) { - return SingleChildScrollView( - physics: AlwaysScrollableScrollPhysics(), - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: viewportConstraints.maxHeight, - ), - child: Center( - child: CircularProgressIndicator(), - ), - ), - ); + itemCount: items.isNotEmpty ? items.length + 1 : items.length, + physics: AlwaysScrollableScrollPhysics(), + itemBuilder: (context, index) { + return _gridItemBuilder(context, index, items); }, ); - } - final items = snapshot.data; - - if (items.isEmpty && widget.emptyBuilder != null) { - return widget.emptyBuilder(context); - } - - if (items.isEmpty && widget.emptyBuilder == null) { - return LayoutBuilder( - builder: (context, viewportConstraints) { - return SingleChildScrollView( - physics: AlwaysScrollableScrollPhysics(), - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: viewportConstraints.maxHeight, - ), - child: Center( - child: Text('There are no users currently'), - ), - ), - ); - }, - ); - } - - final child = _isListView - ? ListView.separated( - physics: AlwaysScrollableScrollPhysics(), - itemCount: items.isNotEmpty ? items.length + 1 : items.length, - separatorBuilder: (_, index) { - if (widget.separatorBuilder != null) { - return widget.separatorBuilder(context, index); - } - return _separatorBuilder(context, index); - }, - itemBuilder: (context, index) { - return _listItemBuilder(context, index, items); - }, - ) - : GridView.builder( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: widget.crossAxisCount, - ), - itemCount: items.isNotEmpty ? items.length + 1 : items.length, - physics: AlwaysScrollableScrollPhysics(), - itemBuilder: (context, index) { - return _gridItemBuilder(context, index, items); - }, - ); - - return LazyLoadScrollView( - onEndOfPage: () async { - return _listenUserPagination(usersBlocState); - }, - child: child, - ); + return LazyLoadScrollView( + onEndOfPage: () async { + return _userListController.paginateData(); }, + child: child, ); } @@ -483,72 +438,4 @@ class _UserListViewState extends State color: StreamChatTheme.of(context).colorTheme.greyWhisper, ); } - - void _listenUserPagination(UsersBlocState usersProvider) { - usersProvider.queryUsers( - filter: widget.filter, - sort: widget.sort, - pagination: widget.pagination.copyWith( - offset: usersProvider.users?.length ?? 0, - ), - options: widget.options, - ); - } - - @override - void didUpdateWidget(UserListView oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.filter?.toString() != oldWidget.filter?.toString() || - jsonEncode(widget.sort) != jsonEncode(oldWidget.sort) || - widget.pagination?.toJson()?.toString() != - oldWidget.pagination?.toJson()?.toString() || - widget.options?.toString() != oldWidget.options?.toString()) { - final usersBloc = UsersBloc.of(context); - usersBloc.queryUsers( - filter: widget.filter, - sort: widget.sort, - pagination: widget.pagination, - options: widget.options, - ); - } - } -} - -abstract class ListItem { - String get key { - if (this is ListHeaderItem) { - final header = (this as ListHeaderItem).heading; - return 'HEADER-$header'; - } - if (this is ListUserItem) { - final user = (this as ListUserItem).user; - return 'USER-${user.id}'; - } - return null; - } - - Widget when({ - @required Widget Function(String heading) headerItem, - @required Widget Function(User user) userItem, - }) { - if (this is ListHeaderItem) { - return headerItem((this as ListHeaderItem).heading); - } - if (this is ListUserItem) { - return userItem((this as ListUserItem).user); - } - return SizedBox(); - } -} - -class ListHeaderItem extends ListItem { - final String heading; - - ListHeaderItem(this.heading); -} - -class ListUserItem extends ListItem { - final User user; - - ListUserItem(this.user); } diff --git a/packages/flutter_widgets/lib/src/user_reaction_display.dart b/packages/flutter_widgets/lib/src/user_reaction_display.dart index 92f66aa2..af2797ca 100644 --- a/packages/flutter_widgets/lib/src/user_reaction_display.dart +++ b/packages/flutter_widgets/lib/src/user_reaction_display.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter/src/user_avatar.dart'; class UserReactionDisplay extends StatelessWidget { diff --git a/packages/flutter_widgets/lib/src/utils.dart b/packages/flutter_widgets/lib/src/utils.dart index 060d2416..93d87ab2 100644 --- a/packages/flutter_widgets/lib/src/utils.dart +++ b/packages/flutter_widgets/lib/src/utils.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:url_launcher/url_launcher.dart'; import '../stream_chat_flutter.dart'; diff --git a/packages/flutter_widgets/lib/stream_chat_flutter.dart b/packages/flutter_widgets/lib/stream_chat_flutter.dart index df113234..3f52c8c6 100644 --- a/packages/flutter_widgets/lib/stream_chat_flutter.dart +++ b/packages/flutter_widgets/lib/stream_chat_flutter.dart @@ -1,5 +1,3 @@ -export 'package:stream_chat/stream_chat.dart'; - export 'src/back_button.dart'; export 'src/channel_header.dart'; export 'src/channel_image.dart'; @@ -7,7 +5,6 @@ export 'src/channel_list_header.dart'; export 'src/channel_list_view.dart'; export 'src/channel_name.dart'; export 'src/channel_preview.dart'; -export 'src/channels_bloc.dart'; export 'src/date_divider.dart'; export 'src/deleted_message.dart'; export 'src/file_attachment.dart'; @@ -22,8 +19,6 @@ export 'src/message_text.dart'; export 'src/message_widget.dart'; export 'src/reaction_picker.dart'; export 'src/sending_indicator.dart'; -export 'src/stream_channel.dart'; -export 'src/stream_chat.dart'; export 'src/stream_chat_theme.dart'; export 'src/stream_neumorphic_button.dart'; export 'src/stream_svg_icon.dart'; @@ -35,11 +30,8 @@ export 'src/user_item.dart'; export 'src/user_item.dart'; export 'src/user_list_view.dart'; export 'src/user_list_view.dart'; -export 'src/users_bloc.dart'; -export 'src/users_bloc.dart'; export 'src/utils.dart'; export 'src/video_attachment.dart'; -export 'src/message_search_bloc.dart'; export 'src/message_search_item.dart'; export 'src/message_search_list_view.dart'; export 'src/unread_indicator.dart'; @@ -47,3 +39,5 @@ export 'src/option_list_tile.dart'; export 'src/channel_file_display_screen.dart'; export 'src/channel_media_display_screen.dart'; export 'src/info_tile.dart'; +export 'src/stream_chat.dart'; +export 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; diff --git a/packages/flutter_widgets/pubspec.yaml b/packages/flutter_widgets/pubspec.yaml index 5d7ff855..93e84f07 100644 --- a/packages/flutter_widgets/pubspec.yaml +++ b/packages/flutter_widgets/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_flutter homepage: https://github.com/GetStream/stream-chat-flutter description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter. -version: 0.2.13+1 +version: 1.0.0-rc repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -11,6 +11,9 @@ environment: dependencies: flutter: sdk: flutter + # TODO: Change over to main dependency when deployed + stream_chat_flutter_core: + path: ../stream_chat_flutter_core flutter_app_badger: ^1.1.2 photo_view: ^0.10.3 rxdart: ^0.24.1 diff --git a/packages/flutter_widgets/test/src/mocks.dart b/packages/flutter_widgets/test/src/mocks.dart index 37fa5530..25b438d6 100644 --- a/packages/flutter_widgets/test/src/mocks.dart +++ b/packages/flutter_widgets/test/src/mocks.dart @@ -1,5 +1,5 @@ import 'package:mockito/mockito.dart'; -import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; class MockClient extends Mock implements Client {} diff --git a/packages/stream_chat_flutter_core/.gitignore b/packages/stream_chat_flutter_core/.gitignore new file mode 100644 index 00000000..1985397a --- /dev/null +++ b/packages/stream_chat_flutter_core/.gitignore @@ -0,0 +1,74 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +build/ + +# Android related +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Flutter.podspec +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/Flutter/flutter_export_environment.sh +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 diff --git a/packages/stream_chat_flutter_core/.metadata b/packages/stream_chat_flutter_core/.metadata new file mode 100644 index 00000000..5eb50347 --- /dev/null +++ b/packages/stream_chat_flutter_core/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 78910062997c3a836feee883712c241a5fd22983 + channel: stable + +project_type: package diff --git a/packages/stream_chat_flutter_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md new file mode 100644 index 00000000..ec6ae55e --- /dev/null +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0-rc + +* First release diff --git a/packages/stream_chat_flutter_core/LICENSE b/packages/stream_chat_flutter_core/LICENSE new file mode 100644 index 00000000..49088d47 --- /dev/null +++ b/packages/stream_chat_flutter_core/LICENSE @@ -0,0 +1,219 @@ +SOURCE CODE LICENSE AGREEMENT + +IMPORTANT - READ THIS CAREFULLY BEFORE DOWNLOADING, INSTALLING, USING OR +ELECTRONICALLY ACCESSING THIS PROPRIETARY PRODUCT. + +THIS IS A LEGAL AGREEMENT BETWEEN STREAM.IO, INC. (“STREAM.IO”) AND THE +BUSINESS ENTITY OR PERSON FOR WHOM YOU (“YOU”) ARE ACTING (“CUSTOMER”) AS THE +LICENSEE OF THE PROPRIETARY SOFTWARE INTO WHICH THIS AGREEMENT HAS BEEN +INCLUDED (THE “AGREEMENT”). YOU AGREE THAT YOU ARE THE CUSTOMER, OR YOU ARE AN +EMPLOYEE OR AGENT OF CUSTOMER AND ARE ENTERING INTO THIS AGREEMENT FOR LICENSE +OF THE SOFTWARE BY CUSTOMER FOR CUSTOMER’S BUSINESS PURPOSES AS DESCRIBED IN +AND IN ACCORDANCE WITH THIS AGREEMENT. YOU HEREBY AGREE THAT YOU ENTER INTO +THIS AGREEMENT ON BEHALF OF CUSTOMER AND THAT YOU HAVE THE AUTHORITY TO BIND +CUSTOMER TO THIS AGREEMENT. + +STREAM.IO IS WILLING TO LICENSE THE SOFTWARE TO CUSTOMER ONLY ON THE FOLLOWING +CONDITIONS: (1) YOU ARE A CURRENT CUSTOMER OF STREAM.IO; (2) YOU ARE NOT A +COMPETITOR OF STREAM.IO; AND (3) THAT YOU ACCEPT ALL THE TERMS IN THIS +AGREEMENT. BY DOWNLOADING, INSTALLING, CONFIGURING, ACCESSING OR OTHERWISE +USING THE SOFTWARE, INCLUDING ANY UPDATES, UPGRADES, OR NEWER VERSIONS, YOU +REPRESENT, WARRANT AND ACKNOWLEDGE THAT (A) CUSTOMER IS A CURRENT CUSTOMER OF +STREAM.IO; (B) CUSTOMER IS NOT A COMPETITOR OF STREAM.IO; AND THAT (C) YOU HAVE +READ THIS AGREEMENT, UNDERSTAND THIS AGREEMENT, AND THAT CUSTOMER AGREES TO BE +BOUND BY ALL THE TERMS OF THIS AGREEMENT. + +IF YOU DO NOT AGREE TO ALL THE TERMS AND CONDITIONS OF THIS AGREEMENT, +STREAM.IO IS UNWILLING TO LICENSE THE SOFTWARE TO CUSTOMER, AND THEREFORE, DO +NOT COMPLETE THE DOWNLOAD PROCESS, ACCESS OR OTHERWISE USE THE SOFTWARE, AND +CUSTOMER SHOULD IMMEDIATELY RETURN THE SOFTWARE AND CEASE ANY USE OF THE +SOFTWARE. + +1. SOFTWARE. The Stream.io software accompanying this Agreement, may include +Source Code, Executable Object Code, associated media, printed materials and +documentation (collectively, the “Software”). The Software also includes any +updates or upgrades to or new versions of the original Software, if and when +made available to you by Stream.io. “Source Code” means computer programming +code in human readable form that is not suitable for machine execution without +the intervening steps of interpretation or compilation. “Executable Object +Code" means the computer programming code in any other form than Source Code +that is not readily perceivable by humans and suitable for machine execution +without the intervening steps of interpretation or compilation. “Site” means a +Customer location controlled by Customer. “Authorized User” means any employee +or contractor of Customer working at the Site, who has signed a written +confidentiality agreement with Customer or is otherwise bound in writing by +confidentiality and use obligations at least as restrictive as those imposed +under this Agreement. + +2. LICENSE GRANT. Subject to the terms and conditions of this Agreement, in +consideration for the representations, warranties, and covenants made by +Customer in this Agreement, Stream.io grants to Customer, during the term of +this Agreement, a personal, non-exclusive, non-transferable, non-sublicensable +license to: + +a. install and use Software Source Code on password protected computers at a Site, +restricted to Authorized Users; + +b. create derivative works, improvements (whether or not patentable), extensions +and other modifications to the Software Source Code (“Modifications”) to build +unique scalable newsfeeds, activity streams, and in-app messaging via Stream’s +application program interface (“API”); + +c. compile the Software Source Code to create Executable Object Code versions of +the Software Source Code and Modifications to build such newsfeeds, activity +streams, and in-app messaging via the API; + +d. install, execute and use such Executable Object Code versions solely for +Customer’s internal business use (including development of websites through +which data generated by Stream services will be streamed (“Apps”)); + +e. use and distribute such Executable Object Code as part of Customer’s Apps; and + +f. make electronic copies of the Software and Modifications as required for backup +or archival purposes. + +3. RESTRICTIONS. Customer is responsible for all activities that occur in +connection with the Software. Customer will not, and will not attempt to: (a) +sublicense or transfer the Software or any Source Code related to the Software +or any of Customer’s rights under this Agreement, except as otherwise provided +in this Agreement, (b) use the Software Source Code for the benefit of a third +party or to operate a service; (c) allow any third party to access or use the +Software Source Code; (d) sublicense or distribute the Software Source Code or +any Modifications in Source Code or other derivative works based on any part of +the Software Source Code; (e) use the Software in any manner that competes with +Stream.io or its business; or (e) otherwise use the Software in any manner that +exceeds the scope of use permitted in this Agreement. Customer shall use the +Software in compliance with any accompanying documentation any laws applicable +to Customer. + +4. OPEN SOURCE. Customer and its Authorized Users shall not use any software or +software components that are open source in conjunction with the Software +Source Code or any Modifications in Source Code or in any way that could +subject the Software to any open source licenses. + +5. CONTRACTORS. Under the rights granted to Customer under this Agreement, +Customer may permit its employees, contractors, and agencies of Customer to +become Authorized Users to exercise the rights to the Software granted to +Customer in accordance with this Agreement solely on behalf of Customer to +provide services to Customer; provided that Customer shall be liable for the +acts and omissions of all Authorized Users to the extent any of such acts or +omissions, if performed by Customer, would constitute a breach of, or otherwise +give rise to liability to Customer under, this Agreement. Customer shall not +and shall not permit any Authorized User to use the Software except as +expressly permitted in this Agreement. + +6. COMPETITIVE PRODUCT DEVELOPMENT. Customer shall not use the Software in any way +to engage in the development of products or services which could be reasonably +construed to provide a complete or partial functional or commercial alternative +to Stream.io’s products or services (a “Competitive Product”). Customer shall +ensure that there is no direct or indirect use of, or sharing of, Software +source code, or other information based upon or derived from the Software to +develop such products or services. Without derogating from the generality of +the foregoing, development of Competitive Products shall include having direct +or indirect access to, supervising, consulting or assisting in the development +of, or producing any specifications, documentation, object code or source code +for, all or part of a Competitive Product. + +7. LIMITATION ON MODIFICATIONS. Notwithstanding any provision in this Agreement, +Modifications may only be created and used by Customer as permitted by this +Agreement and Modification Source Code may not be distributed to third parties. +Customer will not assert against Stream.io, its affiliates, or their customers, +direct or indirect, agents and contractors, in any way, any patent rights that +Customer may obtain relating to any Modifications for Stream.io, its +affiliates’, or their customers’, direct or indirect, agents’ and contractors’ +manufacture, use, import, offer for sale or sale of any Stream.io products or +services. + +8. DELIVERY AND ACCEPTANCE. The Software will be delivered electronically pursuant +to Stream.io standard download procedures. The Software is deemed accepted upon +delivery. + +9. IMPLEMENTATION AND SUPPORT. Stream.io has no obligation under this Agreement to +provide any support or consultation concerning the Software. + +10. TERM AND TERMINATION. The term of this Agreement begins when the Software is +downloaded or accessed and shall continue until terminated. Either party may +terminate this Agreement upon written notice. This Agreement shall +automatically terminate if Customer is or becomes a competitor of Stream.io or +makes or sells any Competitive Products. Upon termination of this Agreement for +any reason, (a) all rights granted to Customer in this Agreement immediately +cease to exist, (b) Customer must promptly discontinue all use of the Software +and return to Stream.io or destroy all copies of the Software in Customer’s +possession or control. Any continued use of the Software by Customer or attempt +by Customer to exercise any rights under this Agreement after this Agreement +has terminated shall be considered copyright infringement and subject Customer +to applicable remedies for copyright infringement. Sections 2, 5, 6, 8 and 9 +shall survive expiration or termination of this Agreement for any reason. + +11. OWNERSHIP. As between the parties, the Software and all worldwide intellectual +property rights and proprietary rights relating thereto or embodied therein, +are the exclusive property of Stream.io and its suppliers. Stream.io and its +suppliers reserve all rights in and to the Software not expressly granted to +Customer in this Agreement, and no other licenses or rights are granted by +implication, estoppel or otherwise. + +12. WARRANTY DISCLAIMER. USE OF THIS SOFTWARE IS ENTIRELY AT YOURS AND CUSTOMER’S +OWN RISK. THE SOFTWARE IS PROVIDED “AS IS” WITHOUT ANY WARRANTY OF ANY KIND +WHATSOEVER. STREAM.IO DOES NOT MAKE, AND HEREBY DISCLAIMS, ANY WARRANTY OF ANY +KIND, WHETHER EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING WITHOUT +LIMITATION, THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE, TITLE, NON-INFRINGEMENT OF THIRD-PARTY RIGHTS, RESULTS, EFFORTS, +QUALITY OR QUIET ENJOYMENT. STREAM.IO DOES NOT WARRANT THAT THE SOFTWARE IS +ERROR-FREE, WILL FUNCTION WITHOUT INTERRUPTION, WILL MEET ANY SPECIFIC NEED +THAT CUSTOMER HAS, THAT ALL DEFECTS WILL BE CORRECTED OR THAT IT IS +SUFFICIENTLY DOCUMENTED TO BE USABLE BY CUSTOMER. TO THE EXTENT THAT STREAM.IO +MAY NOT DISCLAIM ANY WARRANTY AS A MATTER OF APPLICABLE LAW, THE SCOPE AND +DURATION OF SUCH WARRANTY WILL BE THE MINIMUM PERMITTED UNDER SUCH LAW. +CUSTOMER ACKNOWLEDGES THAT IT HAS RELIED ON NO WARRANTIES OTHER THAN THE +EXPRESS WARRANTIES IN THIS AGREEMENT. + +13. LIMITATION OF LIABILITY. TO THE FULLEST EXTENT PERMISSIBLE BY LAW, STREAM.IO’S +TOTAL LIABILITY FOR ALL DAMAGES ARISING OUT OF OR RELATED TO THE SOFTWARE OR +THIS AGREEMENT, WHETHER IN CONTRACT, TORT (INCLUDING NEGLIGENCE) OR OTHERWISE, +SHALL NOT EXCEED $100. IN NO EVENT WILL STREAM.IO BE LIABLE FOR ANY INDIRECT, +CONSEQUENTIAL, EXEMPLARY, PUNITIVE, SPECIAL OR INCIDENTAL DAMAGES OF ANY KIND +WHATSOEVER, INCLUDING ANY LOST DATA AND LOST PROFITS, ARISING FROM OR RELATING +TO THE SOFTWARE EVEN IF STREAM.IO HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. CUSTOMER ACKNOWLEDGES THAT THIS PROVISION REFLECTS THE AGREED UPON +ALLOCATION OF RISK FOR THIS AGREEMENT AND THAT STREAM.IO WOULD NOT ENTER INTO +THIS AGREEMENT WITHOUT THESE LIMITATIONS ON ITS LIABILITY. + +14. General. Customer may not assign or transfer this Agreement, by operation of +law or otherwise, or any of its rights under this Agreement (including the +license rights granted to Customer) to any third party without Stream.io’s +prior written consent, which consent will not be unreasonably withheld or +delayed. Stream.io may assign this Agreement, without consent, including, but +limited to, affiliate or any successor to all or substantially all its business +or assets to which this Agreement relates, whether by merger, sale of assets, +sale of stock, reorganization or otherwise. Any attempted assignment or +transfer in violation of the foregoing will be null and void. Stream.io shall +not be liable hereunder by reason of any failure or delay in the performance of +its obligations hereunder for any cause which is beyond the reasonable control. +All notices, consents, and approvals under this Agreement must be delivered in +writing by courier, by electronic mail, or by certified or registered mail, +(postage prepaid and return receipt requested) to the other party at the +address set forth in the customer agreement between Stream.io and Customer and +will be effective upon receipt or when delivery is refused. This Agreement will +be governed by and interpreted in accordance with the laws of the State of +Colorado, without reference to its choice of laws rules. The United Nations +Convention on Contracts for the International Sale of Goods does not apply to +this Agreement. Any action or proceeding arising from or relating to this +Agreement shall be brought in a federal or state court in Denver, Colorado, and +each party irrevocably submits to the jurisdiction and venue of any such court +in any such action or proceeding. All waivers must be in writing. Any waiver or +failure to enforce any provision of this Agreement on one occasion will not be +deemed a waiver of any other provision or of such provision on any other +occasion. If any provision of this Agreement is unenforceable, such provision +will be changed and interpreted to accomplish the objectives of such provision +to the greatest extent possible under applicable law and the remaining +provisions will continue in full force and effect. Customer shall not violate +any applicable law, rule or regulation, including those regarding the export of +technical data. The headings of Sections of this Agreement are for convenience +and are not to be used in interpreting this Agreement. As used in this +Agreement, the word “including” means “including but not limited to.” This +Agreement (including all exhibits and attachments) constitutes the entire +agreement between the parties regarding the subject hereof and supersedes all +prior or contemporaneous agreements, understandings and communication, whether +written or oral. This Agreement may be amended only by a written document +signed by both parties. The terms of any purchase order or similar document +submitted by Customer to Stream.io will have no effect. diff --git a/packages/stream_chat_flutter_core/README.md b/packages/stream_chat_flutter_core/README.md new file mode 100644 index 00000000..a62da863 --- /dev/null +++ b/packages/stream_chat_flutter_core/README.md @@ -0,0 +1,90 @@ +# Official Flutter SDK Core for [Stream Chat](https://getstream.io/chat/) + +

+ Flutter Chat +

+ +> The official Flutter core components for Stream Chat, a service for +> building chat applications. + +[![Pub](https://img.shields.io/pub/v/stream_chat_flutter.svg)](https://pub.dartlang.org/packages/stream_chat_flutter) +![](https://img.shields.io/badge/platform-flutter%20%7C%20flutter%20web-ff69b4.svg?style=flat-square) +[![Gitter](https://badges.gitter.im/GetStream/stream-chat-flutter.svg)](https://gitter.im/GetStream/stream-chat-flutter?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) +![CI](https://github.com/GetStream/stream-chat-flutter/workflows/CI/badge.svg?branch=master) + + +**Quick Links** + +- [Register](https://getstream.io/chat/trial/) to get an API key for Stream Chat +- [Flutter Chat Tutorial](https://getstream.io/chat/flutter/tutorial/) +- [Chat UI Kit](https://getstream.io/chat/ui-kit/) + +## Flutter Chat Tutorial + +The best place to start is the [Flutter Chat Tutorial](https://getstream.io/chat/flutter/tutorial/). +It teaches you how to use this SDK and also shows how to make frequently required changes. + +## Example App + +This repo includes a fully functional example app with setup instructions. +The example is available under the [example](https://github.com/GetStream/stream-chat-flutter-core/tree/master/example) folder. + +## Add dependency +Add this to your package's pubspec.yaml file, use the latest version [![Pub](https://img.shields.io/pub/v/stream_chat_flutter.svg)](https://pub.dartlang.org/packages/stream_chat_flutter) +```yaml +dependencies: + stream_chat_flutter_core: ^latest_version +``` + +You should then run `flutter packages get` + +This package requires no custom setup on any platform since it does not depend on any platform-specific dependency + +## Docs + +This package provides business logic to fetch common things required for integrating Stream Chat into your application. +The core package allows more customisation and hence provides business logic but no UI components. +Please use the stream_chat_flutter package for the full fledged suite of UI components or stream_chat for the low-level client. + +The package primarily contains three types of classes: + +1) Business Logic Components +2) Core Components +3) Core Controllers + +### Business Logic Components + +These components allow you to have the maximum and lower-level control of the queries being executed. +The BLoCs we provide are: + +1) ChannelsBloc +2) MessageSearchBloc +3) UsersBloc + +### Core Components + +Core components usually are an easy way to fetch data associated with Stream Chat which are decoupled from UI and often expose UI builders. +Data fetching can be controlled with the controllers of the respective core components. + +1) ChannelListCore (Fetch a list of channels) +2) MessageListCore (Fetch a list of messages from a channel) +3) MessageSearchListCore (Fetch a list of search messages) +4) UserListCore (Fetch a list of users) +5) StreamChatCore (This is different from the other core components - it is a version of StreamChat decoupled from theme and initialisations.) + +### Core Controllers + +Core Controllers are supplied to respective CoreList widgets which allows reloading and pagination of data whenever needed. + +1) ChannelListController +2) MessageListController +3) MessageSearchListController +4) ChannelListController + +## Contributing + +We welcome code changes that improve this library or fix a problem, +please make sure to follow all best practices and add tests if applicable before submitting a Pull Request on Github. +We are pleased to merge your code into the official repository. +Make sure to sign our [Contributor License Agreement (CLA)](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) first. +See our license file for more details. diff --git a/packages/stream_chat_flutter_core/example/.gitignore b/packages/stream_chat_flutter_core/example/.gitignore new file mode 100644 index 00000000..9d532b18 --- /dev/null +++ b/packages/stream_chat_flutter_core/example/.gitignore @@ -0,0 +1,41 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json diff --git a/packages/stream_chat_flutter_core/example/.metadata b/packages/stream_chat_flutter_core/example/.metadata new file mode 100644 index 00000000..182cccaf --- /dev/null +++ b/packages/stream_chat_flutter_core/example/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 78910062997c3a836feee883712c241a5fd22983 + channel: stable + +project_type: app diff --git a/packages/stream_chat_flutter_core/example/README.md b/packages/stream_chat_flutter_core/example/README.md new file mode 100644 index 00000000..fef640e1 --- /dev/null +++ b/packages/stream_chat_flutter_core/example/README.md @@ -0,0 +1,16 @@ +# example + +Example app for testing stream_chat_flutter_core + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/packages/stream_chat_flutter_core/example/android/.gitignore b/packages/stream_chat_flutter_core/example/android/.gitignore new file mode 100644 index 00000000..0a741cb4 --- /dev/null +++ b/packages/stream_chat_flutter_core/example/android/.gitignore @@ -0,0 +1,11 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties diff --git a/packages/stream_chat_flutter_core/example/android/app/build.gradle b/packages/stream_chat_flutter_core/example/android/app/build.gradle new file mode 100644 index 00000000..edcec7b9 --- /dev/null +++ b/packages/stream_chat_flutter_core/example/android/app/build.gradle @@ -0,0 +1,54 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 29 + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "example.example" + minSdkVersion 16 + targetSdkVersion 29 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} diff --git a/packages/stream_chat_flutter_core/example/android/app/src/debug/AndroidManifest.xml b/packages/stream_chat_flutter_core/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 00000000..397a9492 --- /dev/null +++ b/packages/stream_chat_flutter_core/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/stream_chat_flutter_core/example/android/app/src/main/AndroidManifest.xml b/packages/stream_chat_flutter_core/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..d2eb734a --- /dev/null +++ b/packages/stream_chat_flutter_core/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + diff --git a/packages/stream_chat_flutter_core/example/android/app/src/main/java/example/example/MainActivity.java b/packages/stream_chat_flutter_core/example/android/app/src/main/java/example/example/MainActivity.java new file mode 100644 index 00000000..334c0856 --- /dev/null +++ b/packages/stream_chat_flutter_core/example/android/app/src/main/java/example/example/MainActivity.java @@ -0,0 +1,5 @@ +package example.example; + +import io.flutter.embedding.android.FlutterActivity; + +public class MainActivity extends FlutterActivity {} diff --git a/packages/stream_chat_flutter_core/example/android/app/src/main/res/drawable/launch_background.xml b/packages/stream_chat_flutter_core/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 00000000..304732f8 --- /dev/null +++ b/packages/stream_chat_flutter_core/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/stream_chat_flutter_core/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/stream_chat_flutter_core/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..db77bb4b Binary files /dev/null and b/packages/stream_chat_flutter_core/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/packages/stream_chat_flutter_core/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/stream_chat_flutter_core/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..17987b79 Binary files /dev/null and b/packages/stream_chat_flutter_core/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/packages/stream_chat_flutter_core/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/stream_chat_flutter_core/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..09d43914 Binary files /dev/null and b/packages/stream_chat_flutter_core/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/packages/stream_chat_flutter_core/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/stream_chat_flutter_core/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..d5f1c8d3 Binary files /dev/null and b/packages/stream_chat_flutter_core/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/packages/stream_chat_flutter_core/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/stream_chat_flutter_core/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..4d6372ee Binary files /dev/null and b/packages/stream_chat_flutter_core/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/packages/stream_chat_flutter_core/example/android/app/src/main/res/values/styles.xml b/packages/stream_chat_flutter_core/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..1f83a33f --- /dev/null +++ b/packages/stream_chat_flutter_core/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/stream_chat_flutter_core/example/android/app/src/profile/AndroidManifest.xml b/packages/stream_chat_flutter_core/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 00000000..397a9492 --- /dev/null +++ b/packages/stream_chat_flutter_core/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/stream_chat_flutter_core/example/android/build.gradle b/packages/stream_chat_flutter_core/example/android/build.gradle new file mode 100644 index 00000000..e0d7ae2c --- /dev/null +++ b/packages/stream_chat_flutter_core/example/android/build.gradle @@ -0,0 +1,29 @@ +buildscript { + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.5.0' + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/packages/stream_chat_flutter_core/example/android/gradle.properties b/packages/stream_chat_flutter_core/example/android/gradle.properties new file mode 100644 index 00000000..a6738207 --- /dev/null +++ b/packages/stream_chat_flutter_core/example/android/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true +android.enableR8=true diff --git a/packages/stream_chat_flutter_core/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/stream_chat_flutter_core/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..de2ccd60 --- /dev/null +++ b/packages/stream_chat_flutter_core/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +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 diff --git a/packages/stream_chat_flutter_core/example/android/settings.gradle b/packages/stream_chat_flutter_core/example/android/settings.gradle new file mode 100644 index 00000000..44e62bcf --- /dev/null +++ b/packages/stream_chat_flutter_core/example/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/packages/stream_chat_flutter_core/example/ios/.gitignore b/packages/stream_chat_flutter_core/example/ios/.gitignore new file mode 100644 index 00000000..e96ef602 --- /dev/null +++ b/packages/stream_chat_flutter_core/example/ios/.gitignore @@ -0,0 +1,32 @@ +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/packages/stream_chat_flutter_core/example/ios/Flutter/AppFrameworkInfo.plist b/packages/stream_chat_flutter_core/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 00000000..6b4c0f78 --- /dev/null +++ b/packages/stream_chat_flutter_core/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 8.0 + + diff --git a/packages/stream_chat_flutter_core/example/ios/Flutter/Debug.xcconfig b/packages/stream_chat_flutter_core/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 00000000..e8efba11 --- /dev/null +++ b/packages/stream_chat_flutter_core/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/stream_chat_flutter_core/example/ios/Flutter/Release.xcconfig b/packages/stream_chat_flutter_core/example/ios/Flutter/Release.xcconfig new file mode 100644 index 00000000..399e9340 --- /dev/null +++ b/packages/stream_chat_flutter_core/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/stream_chat_flutter_core/example/ios/Runner.xcodeproj/project.pbxproj b/packages/stream_chat_flutter_core/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..9943511d --- /dev/null +++ b/packages/stream_chat_flutter_core/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,495 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1020; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = example.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = example.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = example.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/stream_chat_flutter_core/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/stream_chat_flutter_core/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..1d526a16 --- /dev/null +++ b/packages/stream_chat_flutter_core/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/stream_chat_flutter_core/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/stream_chat_flutter_core/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/packages/stream_chat_flutter_core/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/stream_chat_flutter_core/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/stream_chat_flutter_core/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..f9b0d7c5 --- /dev/null +++ b/packages/stream_chat_flutter_core/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/stream_chat_flutter_core/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/stream_chat_flutter_core/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..a28140cf --- /dev/null +++ b/packages/stream_chat_flutter_core/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/stream_chat_flutter_core/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/stream_chat_flutter_core/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..1d526a16 --- /dev/null +++ b/packages/stream_chat_flutter_core/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/stream_chat_flutter_core/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/stream_chat_flutter_core/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/packages/stream_chat_flutter_core/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/stream_chat_flutter_core/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/stream_chat_flutter_core/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..f9b0d7c5 --- /dev/null +++ b/packages/stream_chat_flutter_core/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/stream_chat_flutter_core/example/ios/Runner/AppDelegate.swift b/packages/stream_chat_flutter_core/example/ios/Runner/AppDelegate.swift new file mode 100644 index 00000000..70693e4a --- /dev/null +++ b/packages/stream_chat_flutter_core/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..d36b1fab --- /dev/null +++ b/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 00000000..dc9ada47 Binary files /dev/null and b/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 00000000..28c6bf03 Binary files /dev/null and b/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 00000000..2ccbfd96 Binary files /dev/null and b/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 00000000..f091b6b0 Binary files /dev/null and b/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 00000000..4cde1211 Binary files /dev/null and b/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 00000000..d0ef06e7 Binary files /dev/null and b/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 00000000..dcdc2306 Binary files /dev/null and b/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 00000000..2ccbfd96 Binary files /dev/null and b/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 00000000..c8f9ed8f Binary files /dev/null and b/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 00000000..a6d6b860 Binary files /dev/null and b/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 00000000..a6d6b860 Binary files /dev/null and b/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 00000000..75b2d164 Binary files /dev/null and b/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 00000000..c4df70d3 Binary files /dev/null and b/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 00000000..6a84f41e Binary files /dev/null and b/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 00000000..d0e1f585 Binary files /dev/null and b/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 00000000..0bedcf2f --- /dev/null +++ b/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 00000000..89c2725b --- /dev/null +++ b/packages/stream_chat_flutter_core/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/packages/stream_chat_flutter_core/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/stream_chat_flutter_core/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..f2e259c7 --- /dev/null +++ b/packages/stream_chat_flutter_core/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/stream_chat_flutter_core/example/ios/Runner/Base.lproj/Main.storyboard b/packages/stream_chat_flutter_core/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 00000000..f3c28516 --- /dev/null +++ b/packages/stream_chat_flutter_core/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/stream_chat_flutter_core/example/ios/Runner/Info.plist b/packages/stream_chat_flutter_core/example/ios/Runner/Info.plist new file mode 100644 index 00000000..a060db61 --- /dev/null +++ b/packages/stream_chat_flutter_core/example/ios/Runner/Info.plist @@ -0,0 +1,45 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/packages/stream_chat_flutter_core/example/ios/Runner/Runner-Bridging-Header.h b/packages/stream_chat_flutter_core/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 00000000..308a2a56 --- /dev/null +++ b/packages/stream_chat_flutter_core/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/packages/stream_chat_flutter_core/example/lib/main.dart b/packages/stream_chat_flutter_core/example/lib/main.dart new file mode 100644 index 00000000..3a86850f --- /dev/null +++ b/packages/stream_chat_flutter_core/example/lib/main.dart @@ -0,0 +1,306 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; + +Future main() async { + /// Create a new instance of [Client] passing the apikey obtained from your + /// project dashboard. + final client = Client('b67pax5b2wdq'); + + /// Set the current user. In a production scenario, this should be done using + /// a backend to generate a user token using our server SDK. + /// Please see the following for more information: + /// https://getstream.io/chat/docs/ios_user_setup_and_tokens/ + await client.setUser( + User( + id: 'cool-shadow-7', + extraData: { + 'image': + 'https://getstream.io/random_png/?id=cool-shadow-7&name=Cool+shadow', + }, + ), + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiY29vbC1zaGFkb3ctNyJ9.gkOlCRb1qgy4joHPaxFwPOdXcGvSPvp6QY0S4mpRkVo', + ); + + runApp( + StreamExample( + client: client, + ), + ); +} + +/// Example application using Stream Chat core widgets. +/// Stream Chat Core is a set of Flutter wrappers which provide basic functionality +/// for building Flutter applications using Stream. +/// If you'd prefer using pre-made UI widgets for your app, please see our other +/// package, `stream_chat_flutter`. +class StreamExample extends StatelessWidget { + /// Minimal example using Stream's core Flutter package. + /// If you'd prefer using pre-made UI widgets for your app, please see our other + /// package, `stream_chat_flutter`. + const StreamExample({ + Key key, + @required this.client, + }) : super(key: key); + + /// Instance of Stream Client. + /// Stream's [Client] can be used to connect to our servers and set the default + /// user for the application. Performing these actions trigger a websocket connection + /// allowing for real-time updates. + final Client client; + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Stream Chat Core Example', + home: HomeScreen(), + builder: (context, child) => StreamChatCore( + client: client, + child: child, + ), + ); + } +} + +/// Basic layout displaying a list of [Channel]s the user is a part of. +/// This is implemented using [ChannelListCore]. +/// +/// [ChannelListCore] is a `builder` with callbacks for constructing UIs based +/// on different scenarios. +class HomeScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Channels'), + ), + body: ChannelsBloc( + child: ChannelListCore( + 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: CircularProgressIndicator(), + ), + ); + }, + errorBuilder: (Error error) { + return Center( + child: Text( + 'Oh no, something went wrong. Please check your config.'), + ); + }, + listBuilder: ( + BuildContext context, + List channels, + ) => + ListView.builder( + itemCount: channels.length, + itemBuilder: (BuildContext context, int index) { + final _item = channels[index]; + return ListTile( + title: Text(_item.name), + subtitle: Text(_item.state.lastMessage.text), + onTap: () { + /// Display a list of messages when the user taps on an item. + /// We can use [StreamChannel] to wrap our [MessageScreen] screen + /// with the selected channel. + /// + /// This allows us to use a built-in inherited widget for accessing + /// our `channel` later on. + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: _item, + child: MessageScreen(), + ), + ), + ); + }, + ); + }, + ), + ), + ), + ); + } +} + +/// A list of messages sent in the current channel. +/// When a user taps on a channel in [HomeScreen], a navigator push [MessageScreen] +/// to display the list of messages in the selected channel. +/// +/// This is implemented using [MessageListCore], a convenience builder with +/// callbacks for building UIs based on different api results. +class MessageScreen extends StatefulWidget { + @override + _MessageScreenState createState() => _MessageScreenState(); +} + +class _MessageScreenState extends State { + TextEditingController _controller; + ScrollController _scrollController; + + @override + void initState() { + super.initState(); + _controller = TextEditingController(); + _scrollController = ScrollController(); + } + + @override + void dispose() { + _controller.dispose(); + _scrollController.dispose(); + super.dispose(); + } + + void _updateList() { + _scrollController.animateTo( + 0, + duration: const Duration(milliseconds: 200), + curve: Curves.easeOut, + ); + } + + @override + Widget build(BuildContext context) { + /// To access the current channel, we can use the `.of()` method on [StreamChannel] + /// to fetch the closest instance. + final channel = StreamChannel.of(context).channel; + return Scaffold( + appBar: AppBar( + title: Text(channel.name), + ), + body: SafeArea( + child: Column( + children: [ + Expanded( + child: MessageListCore( + emptyBuilder: (BuildContext context) { + return Center( + child: Text('Nothing here yet'), + ); + }, + loadingBuilder: (BuildContext context) { + return Center( + child: SizedBox( + height: 100.0, + width: 100.0, + child: CircularProgressIndicator(), + ), + ); + }, + messageListBuilder: ( + BuildContext context, + List messages, + ) { + return ListView.builder( + controller: _scrollController, + itemCount: messages.length, + reverse: true, + itemBuilder: (BuildContext context, int index) { + final item = messages[index]; + final client = StreamChatCore.of(context).client; + if (item.user.id == client.uid) { + return Align( + alignment: Alignment.centerRight, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text(item.text), + ), + ); + } else { + return Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text(item.text), + ), + ); + } + }, + ); + }, + errorWidgetBuilder: (BuildContext context, error) { + print(error?.toString()); + return Center( + child: SizedBox( + height: 100.0, + width: 100.0, + child: Text('Oh no, an error occured. Please see logs.'), + ), + ); + }, + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _controller, + decoration: const InputDecoration( + hintText: 'Enter your message', + ), + ), + ), + Material( + type: MaterialType.circle, + color: Colors.blue, + clipBehavior: Clip.hardEdge, + child: InkWell( + onTap: () async { + if (_controller.value.text.isNotEmpty) { + await channel.sendMessage( + Message(text: _controller.value.text), + ); + _controller.clear(); + _updateList(); + } + }, + child: const Padding( + padding: EdgeInsets.all(8.0), + child: Center( + child: Icon( + Icons.send, + color: Colors.white, + ), + ), + ), + ), + ) + ], + ), + ) + ], + ), + ), + ); + } +} + +/// Extensions can be used to add functionality to the SDK. In the examples +/// below, we add two simple extensions to the [Client] and [Channel]. +extension on Client { + /// Fetches the current user id. + String get uid => state.user.id; +} + +extension on Channel { + /// Fetches the name of the channel by accessing [extraData] or [cid]. + String get name { + final _channelName = extraData['name']; + if (_channelName != null) { + return _channelName; + } else { + return cid; + } + } +} diff --git a/packages/stream_chat_flutter_core/example/pubspec.yaml b/packages/stream_chat_flutter_core/example/pubspec.yaml new file mode 100644 index 00000000..b0071176 --- /dev/null +++ b/packages/stream_chat_flutter_core/example/pubspec.yaml @@ -0,0 +1,77 @@ +name: example +description: Example app for testing stream_chat_flutter_core + +# The following line prevents the package from being accidentally published to +# pub.dev using `pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +version: 1.0.0+1 + +environment: + sdk: ">=2.7.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + stream_chat_flutter_core: + path: ../ + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/packages/stream_chat_flutter_core/lib/src/channel_list_core.dart b/packages/stream_chat_flutter_core/lib/src/channel_list_core.dart new file mode 100644 index 00000000..d2fd53c7 --- /dev/null +++ b/packages/stream_chat_flutter_core/lib/src/channel_list_core.dart @@ -0,0 +1,266 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter_core/src/channels_bloc.dart'; + +import 'stream_chat_core.dart'; + +/// [ChannelListCore] is a simplified class that allows fetching a list of channels while exposing UI builders. +/// A [ChannelListController] is used to reload and paginate data. +/// +/// ```dart +/// class ChannelListPage extends StatelessWidget { +/// @override +/// Widget build(BuildContext context) { +/// return Scaffold( +/// body: ChannelListCore( +/// filter: { +/// 'members': { +/// '\$in': [StreamChat.of(context).user.id], +/// } +/// }, +/// sort: [SortOption('last_message_at')], +/// pagination: PaginationParams( +/// limit: 20, +/// ), +/// errorBuilder: (err) { +/// return Center( +/// child: Text('An error has occured'), +/// ); +/// }, +/// emptyBuilder: (context) { +/// return Center( +/// child: Text('Nothing here...'), +/// ); +/// }, +/// emptyBuilder: (context) { +/// return Center( +/// child: CircularProgressIndicator(), +/// ); +/// }, +/// listBuilder: (context, list) { +/// return ChannelPage(list); +/// } +/// ), +/// ); +/// } +/// } +/// ``` +/// +/// +/// Make sure to have a [StreamChatCore] ancestor in order to provide the information about the channels. +class ChannelListCore extends StatefulWidget { + /// Instantiate a new ChannelListView + ChannelListCore({ + Key key, + this.filter, + this.options, + this.sort, + this.pagination, + @required this.errorBuilder, + @required this.emptyBuilder, + @required this.loadingBuilder, + @required this.listBuilder, + this.channelListController, + }) : super(key: key); + + /// A [ChannelListController] allows reloading and pagination. + /// Use [ChannelListController.loadData] and [ChannelListController.paginateData] respectively for reloading and pagination. + final ChannelListController channelListController; + + /// The builder that will be used in case of error + final Widget Function(Error error) errorBuilder; + + /// The builder that will be used in case of loading + final WidgetBuilder loadingBuilder; + + /// The builder which is used when list of channels loads + final Function(BuildContext, List) listBuilder; + + /// The builder used when the channel list is empty. + final WidgetBuilder emptyBuilder; + + /// The query filters to use. + /// You can query on any of the custom fields you've defined on the [Channel]. + /// You can also filter other built-in channel fields. + final Map filter; + + /// Query channels options. + /// + /// state: if true returns the Channel state + /// watch: if true listen to changes to this Channel in real time. + final Map options; + + /// 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 sort; + + /// Pagination parameters + /// limit: the number of channels to return (max is 30) + /// offset: the offset (max is 1000) + /// message_limit: how many messages should be included to each channel + final PaginationParams pagination; + + @override + _ChannelListCoreState createState() => _ChannelListCoreState(); +} + +class _ChannelListCoreState extends State + with WidgetsBindingObserver { + @override + Widget build(BuildContext context) { + final channelsBloc = ChannelsBloc.of(context); + + return _buildListView(channelsBloc); + } + + StreamBuilder> _buildListView( + ChannelsBlocState channelsBlocState, + ) { + return StreamBuilder>( + stream: channelsBlocState.channelsStream, + builder: (context, snapshot) { + var child; + if (snapshot.hasError) { + child = _buildErrorWidget( + snapshot, + context, + channelsBlocState, + ); + } else if (!snapshot.hasData) { + child = _buildLoadingWidget(); + } else { + final channels = snapshot.data; + + child = widget.emptyBuilder(context); + + if (channels.isNotEmpty) { + return widget.listBuilder(context, channels); + } + } + + return child; + }, + ); + } + + Widget _buildLoadingWidget() { + return widget.loadingBuilder(context); + } + + Widget _buildErrorWidget( + AsyncSnapshot> snapshot, + BuildContext context, + ChannelsBlocState channelsBlocState, + ) { + if (snapshot.error is Error) { + print((snapshot.error as Error).stackTrace); + } + + return widget.errorBuilder(snapshot.error); + } + + void loadData() { + final channelsBloc = ChannelsBloc.of(context); + + channelsBloc.queryChannels( + filter: widget.filter, + sortOptions: widget.sort, + paginationParams: widget.pagination, + options: widget.options, + ); + } + + void paginateData() { + final channelsBloc = ChannelsBloc.of(context); + + channelsBloc.queryChannels( + filter: widget.filter, + sortOptions: widget.sort, + paginationParams: widget.pagination.copyWith( + offset: channelsBloc.channels?.length ?? 0, + ), + options: widget.options, + ); + } + + StreamSubscription _subscription; + + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addObserver(this); + + final channelsBloc = ChannelsBloc.of(context); + channelsBloc.queryChannels( + filter: widget.filter, + sortOptions: widget.sort, + paginationParams: widget.pagination, + options: widget.options, + ); + + final client = StreamChatCore.of(context).client; + + _subscription = client + .on( + EventType.connectionRecovered, + EventType.notificationAddedToChannel, + EventType.notificationMessageNew, + EventType.channelVisible, + ) + .listen((event) { + channelsBloc.queryChannels( + filter: widget.filter, + sortOptions: widget.sort, + paginationParams: widget.pagination, + options: widget.options, + ); + }); + + if (widget.channelListController != null) { + widget.channelListController.loadData = loadData; + widget.channelListController.paginateData = paginateData; + } + } + + @override + void didUpdateWidget(ChannelListCore oldWidget) { + super.didUpdateWidget(oldWidget); + + if (widget.filter?.toString() != oldWidget.filter?.toString() || + jsonEncode(widget.sort) != jsonEncode(oldWidget.sort) || + widget.pagination?.toJson()?.toString() != + oldWidget.pagination?.toJson()?.toString() || + widget.options?.toString() != oldWidget.options?.toString()) { + final channelsBloc = ChannelsBloc.of(context); + channelsBloc.queryChannels( + filter: widget.filter, + sortOptions: widget.sort, + paginationParams: widget.pagination, + options: widget.options, + ); + } + } + + @override + void dispose() { + _subscription.cancel(); + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } +} + +/// Controller used for paginating data in [ChannelListCore] +class ChannelListController { + /// Call this function to reload data + VoidCallback loadData; + + /// Call this function to load further data + VoidCallback paginateData; +} diff --git a/packages/flutter_widgets/lib/src/channels_bloc.dart b/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart similarity index 96% rename from packages/flutter_widgets/lib/src/channels_bloc.dart rename to packages/stream_chat_flutter_core/lib/src/channels_bloc.dart index c3c5c6f1..2eef0f8a 100644 --- a/packages/flutter_widgets/lib/src/channels_bloc.dart +++ b/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart @@ -3,8 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:rxdart/rxdart.dart'; import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_flutter/src/stream_chat.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_chat_flutter_core/src/stream_chat_core.dart'; /// Widget dedicated to the management of a channel list with pagination class ChannelsBloc extends StatefulWidget { @@ -80,7 +79,7 @@ class ChannelsBlocState extends State Map options, bool onlyOffline = false, }) async { - final client = StreamChat.of(context).client; + final client = StreamChatCore.of(context).client; if (client.state?.user == null || _queryChannelsLoadingController.value == true) { @@ -121,7 +120,7 @@ class ChannelsBlocState extends State void initState() { super.initState(); - final client = StreamChat.of(context).client; + final client = StreamChatCore.of(context).client; if (!widget.lockChannelsOrder) { _subscriptions.add(client.on(EventType.messageNew).listen((e) { diff --git a/packages/flutter_widgets/lib/src/lazy_load_scroll_view.dart b/packages/stream_chat_flutter_core/lib/src/lazy_load_scroll_view.dart similarity index 100% rename from packages/flutter_widgets/lib/src/lazy_load_scroll_view.dart rename to packages/stream_chat_flutter_core/lib/src/lazy_load_scroll_view.dart diff --git a/packages/stream_chat_flutter_core/lib/src/message_list_core.dart b/packages/stream_chat_flutter_core/lib/src/message_list_core.dart new file mode 100644 index 00000000..697e7e39 --- /dev/null +++ b/packages/stream_chat_flutter_core/lib/src/message_list_core.dart @@ -0,0 +1,190 @@ +import 'dart:async'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'stream_channel.dart'; + +/// A signature for a callback which exposes an error and returns a function. +/// This Callback can be used in cases where an API failure occurs and the widget +/// is unable to render data. +typedef StreamErrorBuilder = Widget Function( + BuildContext context, Object error); + +/// [MessageListCore] is a simplified class that allows fetching a list of messages while exposing UI builders. +/// A [MessageListController] is used to paginate data. +/// +/// ```dart +/// class ChannelPage extends StatelessWidget { +/// const ChannelPage({ +/// Key key, +/// }) : super(key: key); +/// +/// @override +/// Widget build(BuildContext context) { +/// return Scaffold( +/// body: Column( +/// children: [ +/// Expanded( +/// child: MessageListCore( +/// emptyBuilder: (context) { +/// return Center( +/// child: Text('Nothing here...'), +/// ); +/// }, +/// loadingBuilder: (context) { +/// return Center( +/// child: CircularProgressIndicator(), +/// ); +/// }, +/// messageListBuilder: (context, list) { +/// return MessagesPage(list); +/// }, +/// errorWidgetBuilder: (context, err) { +/// return Center( +/// child: Text('Error'), +/// ); +/// }, +/// ), +/// ), +/// ], +/// ), +/// ); +/// } +/// } +/// ``` +/// +/// +/// Make sure to have a [StreamChannel] ancestor in order to provide the information about the channels. +/// The widget uses a [ListView.custom] to render the list of channels. +/// +/// The widget components render the ui based on the first ancestor of type [StreamChatTheme]. +/// Modify it to change the widget appearance. +class MessageListCore extends StatefulWidget { + /// Instantiate a new MessageListView + MessageListCore({ + Key key, + this.showScrollToBottom = true, + this.parentMessage, + @required this.loadingBuilder, + @required this.emptyBuilder, + @required this.messageListBuilder, + @required this.errorWidgetBuilder, + this.messageListController, + }) : super(key: key); + + /// A [MessageListController] allows pagination. + /// Use [ChannelListController.paginateData] pagination. + final MessageListController messageListController; + + /// Function called when messages are fetched + final Widget Function(BuildContext, List) messageListBuilder; + + /// Function used to build a loading widget + final WidgetBuilder loadingBuilder; + + /// Function used to build an empty widget + final WidgetBuilder emptyBuilder; + + /// Callback triggered when an error occurs while performing the given request. + /// This parameter can be used to display an error message to users in the event + /// of a connection failure. + final StreamErrorBuilder errorWidgetBuilder; + + /// If true will show a scroll to bottom message when there are new messages and the scroll offset is not zero + final bool showScrollToBottom; + + /// Parent message in case of a thread + final Message parentMessage; + + @override + _MessageListCoreState createState() => _MessageListCoreState(); +} + +class _MessageListCoreState extends State { + StreamChannelState streamChannel; + + bool get _upToDate => streamChannel.channel.state.isUpToDate; + + bool get _isThreadConversation => widget.parentMessage != null; + + int initialIndex; + double initialAlignment; + + List messages = []; + + bool initialMessageHighlightComplete = false; + + @override + Widget build(BuildContext context) { + final messagesStream = _isThreadConversation + ? streamChannel.channel.state.threadsStream + .where((threads) => threads.containsKey(widget.parentMessage.id)) + .map((threads) => threads[widget.parentMessage.id]) + : streamChannel.channel.state?.messagesStream; + + return StreamBuilder>( + stream: messagesStream?.map((messages) => messages + ?.where((e) => + (!e.isDeleted && e.shadowed != true) || + (e.isDeleted && + e.user.id == streamChannel.channel.client.state.user.id)) + ?.toList()), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return widget.loadingBuilder(context); + } else if (snapshot.hasError) { + return widget.errorWidgetBuilder(context, snapshot.error); + } else { + final messageList = snapshot.data?.reversed?.toList() ?? []; + if (messageList.isEmpty) { + if (_upToDate) { + return widget.emptyBuilder(context); + } + } else { + messages = messageList; + } + return widget.messageListBuilder(context, messages); + } + }, + ); + } + + Future paginateData( + {QueryDirection direction = QueryDirection.bottom}) { + if (!_isThreadConversation) { + return streamChannel.queryMessages(direction: direction); + } else { + return streamChannel.getReplies(widget.parentMessage.id); + } + } + + @override + void initState() { + streamChannel = StreamChannel.of(context); + + if (_isThreadConversation) { + streamChannel.getReplies(widget.parentMessage.id); + } + + if (widget.messageListController != null) { + widget.messageListController.paginateData = paginateData; + } + + super.initState(); + } + + @override + void dispose() { + if (!_upToDate) { + streamChannel.reloadChannel(); + } + super.dispose(); + } +} + +/// Controller used for paginating data in [ChannelListView] +class MessageListController { + /// Call this function to load further data + Function({QueryDirection direction}) paginateData; +} diff --git a/packages/flutter_widgets/lib/src/message_search_bloc.dart b/packages/stream_chat_flutter_core/lib/src/message_search_bloc.dart similarity index 97% rename from packages/flutter_widgets/lib/src/message_search_bloc.dart rename to packages/stream_chat_flutter_core/lib/src/message_search_bloc.dart index 67b6455d..ca6a983d 100644 --- a/packages/flutter_widgets/lib/src/message_search_bloc.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_search_bloc.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:rxdart/rxdart.dart'; import 'package:stream_chat/stream_chat.dart'; -import 'stream_chat.dart'; +import 'stream_chat_core.dart'; /// Widget dedicated to the management of a message list with pagination class MessageSearchBloc extends StatefulWidget { @@ -122,7 +122,7 @@ class MessageSearchBlocState extends State String query, PaginationParams pagination, }) { - final client = StreamChat.of(context).client; + final client = StreamChatCore.of(context).client; return client.search( filter, sort, diff --git a/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart b/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart new file mode 100644 index 00000000..e5ff9527 --- /dev/null +++ b/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart @@ -0,0 +1,204 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'message_search_bloc.dart'; + +/// +/// [MessageSearchListCore] is a simplified class that allows searching for messages across channels while exposing UI builders. +/// A [MessageSearchListController] is used to load and paginate data. +/// +/// ```dart +/// class MessageSearchPage extends StatelessWidget { +/// @override +/// Widget build(BuildContext context) { +/// return Scaffold( +/// body: MessageSearchListCore( +/// messageQuery: _channelQuery, +/// filters: { +/// 'members': { +/// r'$in': [user.id] +/// } +/// }, +/// paginationParams: PaginationParams(limit: 20), +/// ), +/// ); +/// } +/// } +/// ``` +/// +/// +/// Make sure to have a [MessageSearchBloc] ancestor in order to provide the information about the messages. +/// The widget uses a [ListView.separated] to render the list of messages. +/// +/// The widget components render the ui based on the first ancestor of type [StreamChatTheme]. +/// Modify it to change the widget appearance. +class MessageSearchListCore extends StatefulWidget { + /// Instantiate a new MessageSearchListView + const MessageSearchListCore({ + Key key, + this.messageQuery, + this.filters, + this.sortOptions, + this.paginationParams, + this.messageFilters, + @required this.emptyBuilder, + @required this.errorBuilder, + @required this.loadingBuilder, + @required this.childBuilder, + this.messageSearchListController, + }) : super(key: key); + + /// A [MessageSearchListController] allows reloading and pagination. + /// Use [MessageSearchListController.loadData] and [MessageSearchListController.paginateData] respectively for reloading and pagination. + final MessageSearchListController messageSearchListController; + + /// Message String to search on + final String messageQuery; + + /// The query filters to use. + /// You can query on any of the custom fields you've defined on the [Channel]. + /// You can also filter other built-in channel fields. + final Map filters; + + /// 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 message query filters to use. + /// You can query on any of the custom fields you've defined on the [Channel]. + /// You can also filter other built-in channel fields. + final Map messageFilters; + + /// The builder that is used when the search messages are fetched + final Widget Function(List) childBuilder; + + /// The builder used when the channel list is empty. + final WidgetBuilder emptyBuilder; + + /// The builder that will be used in case of error + final Widget Function(Error error) errorBuilder; + + /// The builder that will be used in case of loading + final WidgetBuilder loadingBuilder; + + @override + _MessageSearchListCoreState createState() => _MessageSearchListCoreState(); +} + +class _MessageSearchListCoreState extends State { + @override + void initState() { + super.initState(); + final messageSearchBloc = MessageSearchBloc.of(context); + messageSearchBloc.search( + filter: widget.filters, + sort: widget.sortOptions, + query: widget.messageQuery, + pagination: widget.paginationParams, + messageFilter: widget.messageFilters, + ); + + if (widget.messageSearchListController != null) { + widget.messageSearchListController.loadData = loadData; + widget.messageSearchListController.paginateData = paginateData; + } + } + + @override + Widget build(BuildContext context) { + final messageSearchBloc = MessageSearchBloc.of(context); + return _buildListView(messageSearchBloc); + } + + Widget _buildListView(MessageSearchBlocState messageSearchBloc) { + return StreamBuilder>( + stream: messageSearchBloc.messagesStream, + builder: (context, snapshot) { + if (snapshot.hasError) { + if (snapshot.error is Error) { + print((snapshot.error as Error).stackTrace); + } + + return widget.errorBuilder(snapshot.error); + } + + if (!snapshot.hasData) { + return widget.loadingBuilder(context); + } + + final items = snapshot.data; + + if (items.isEmpty) { + return widget.emptyBuilder(context); + } + + return widget.childBuilder(snapshot.data); + }, + ); + } + + void loadData() { + final messageSearchBloc = MessageSearchBloc.of(context); + + messageSearchBloc.search( + filter: widget.filters, + sort: widget.sortOptions, + query: widget.messageQuery, + pagination: widget.paginationParams, + messageFilter: widget.messageFilters, + ); + } + + void paginateData() { + final messageSearchBloc = MessageSearchBloc.of(context); + + messageSearchBloc.loadMore( + filter: widget.filters, + sort: widget.sortOptions, + pagination: widget.paginationParams.copyWith( + offset: messageSearchBloc.messageResponses?.length ?? 0, + ), + query: widget.messageQuery, + messageFilter: widget.messageFilters, + ); + } + + @override + void didUpdateWidget(MessageSearchListCore oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.filters?.toString() != oldWidget.filters?.toString() || + jsonEncode(widget.sortOptions) != jsonEncode(oldWidget.sortOptions) || + widget.paginationParams?.toJson()?.toString() != + oldWidget.paginationParams?.toJson()?.toString() || + widget.messageQuery?.toString() != oldWidget.messageQuery?.toString() || + widget.messageFilters?.toString() != + oldWidget.messageFilters?.toString()) { + final messageSearchBloc = MessageSearchBloc.of(context); + messageSearchBloc.search( + filter: widget.filters, + sort: widget.sortOptions, + query: widget.messageQuery, + pagination: widget.paginationParams, + messageFilter: widget.messageFilters, + ); + } + } +} + +/// Controller used for paginating data in [ChannelListView] +class MessageSearchListController { + /// Call this function to reload data + VoidCallback loadData; + + /// Call this function to load further data + VoidCallback paginateData; +} diff --git a/packages/flutter_widgets/lib/src/stream_channel.dart b/packages/stream_chat_flutter_core/lib/src/stream_channel.dart similarity index 96% rename from packages/flutter_widgets/lib/src/stream_channel.dart rename to packages/stream_chat_flutter_core/lib/src/stream_channel.dart index 8b4b97ef..85f7ecca 100644 --- a/packages/flutter_widgets/lib/src/stream_channel.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_channel.dart @@ -4,12 +4,20 @@ import 'package:flutter/material.dart'; import 'package:rxdart/rxdart.dart'; import 'package:stream_chat/stream_chat.dart'; -enum QueryDirection { top, bottom } +/// Specifies query direction for pagination +enum QueryDirection { + /// Query earlier messages + top, + + /// Query later messages + bottom, +} /// Widget used to provide information about the channel to the widget tree /// /// Use [StreamChannel.of] to get the current [StreamChannelState] instance. class StreamChannel extends StatefulWidget { + // ignore: public_member_api_docs const StreamChannel({ Key key, @required this.child, @@ -20,8 +28,13 @@ class StreamChannel extends StatefulWidget { assert(channel != null), super(key: key); + // ignore: public_member_api_docs final Widget child; + + /// [channel] specifies the channel with which child should be wrapped final Channel channel; + + /// Shows a loading indicator final bool showLoading; /// If passed the channel will load from this particular message. @@ -45,6 +58,7 @@ class StreamChannel extends StatefulWidget { StreamChannelState createState() => StreamChannelState(); } +// ignore: public_member_api_docs class StreamChannelState extends State { /// Current channel Channel get channel => widget.channel; diff --git a/packages/stream_chat_flutter_core/lib/src/stream_chat_core.dart b/packages/stream_chat_flutter_core/lib/src/stream_chat_core.dart new file mode 100644 index 00000000..73f5eb61 --- /dev/null +++ b/packages/stream_chat_flutter_core/lib/src/stream_chat_core.dart @@ -0,0 +1,148 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat/stream_chat.dart'; + +/// Widget used to provide information about the chat to the widget tree +/// +/// class MyApp extends StatelessWidget { +/// final Client client; +/// +/// MyApp(this.client); +/// +/// @override +/// Widget build(BuildContext context) { +/// return MaterialApp( +/// home: Container( +/// child: StreamChat( +/// client: client, +/// child: ChannelListPage(), +/// ), +/// ), +/// ); +/// } +/// } +/// +/// Use [StreamChatCore.of] to get the current [StreamChatCoreState] instance. +class StreamChatCore extends StatefulWidget { + // ignore: public_member_api_docs + final Client client; + // ignore: public_member_api_docs + final Widget child; + + // ignore: public_member_api_docs + StreamChatCore({ + Key key, + @required this.client, + @required this.child, + }) : super( + key: key, + ); + + @override + StreamChatCoreState createState() => StreamChatCoreState(); + + /// Use this method to get the current [StreamChatCoreState] instance + static StreamChatCoreState of(BuildContext context) { + StreamChatCoreState streamChatState; + + streamChatState = context.findAncestorStateOfType(); + + if (streamChatState == null) { + throw Exception( + 'You must have a StreamChat widget at the top of your widget tree'); + } + + return streamChatState; + } +} + +/// The current state of the StreamChat widget +class StreamChatCoreState extends State + with WidgetsBindingObserver { + // ignore: public_member_api_docs + Client get client => widget.client; + Timer _disconnectTimer; + + @override + Widget build(BuildContext context) { + return widget.child; + } + + /// The current user + User get user => widget.client.state.user; + + /// The current user as a stream + Stream get userStream => widget.client.state.userStream; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + } + + StreamSubscription _newMessageSubscription; + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (client.state?.user != null) { + if (state == AppLifecycleState.paused) { + if (client.showLocalNotification != null) { + _newMessageSubscription = client + .on(EventType.messageNew) + .where((e) => e.user?.id != user.id) + .where((e) => e.message.silent != true) + .where((e) => e.message.shadowed != true) + .listen((event) async { + final channel = client.channel( + event.channelType, + id: event.channelId, + ); + + client.showLocalNotification( + event.message, + ChannelModel( + id: channel.id, + createdAt: channel.createdAt, + extraData: channel.extraData, + type: channel.type, + memberCount: channel.memberCount, + frozen: channel.frozen, + cid: channel.cid, + deletedAt: channel.deletedAt, + config: channel.config, + createdBy: channel.createdBy, + updatedAt: channel.updatedAt, + lastMessageAt: channel.lastMessageAt, + ), + ); + }); + _disconnectTimer = Timer(client.backgroundKeepAlive, () { + client.disconnect(); + }); + } else { + client.disconnect(); + } + } else if (state == AppLifecycleState.resumed) { + _newMessageSubscription?.cancel(); + if (_disconnectTimer?.isActive == true) { + _disconnectTimer.cancel(); + } else { + if (client.wsConnectionStatus.value == + ConnectionStatus.disconnected) { + NotificationService.handleIosMessageQueue(client); + client.connect(); + } + } + } + } + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + _disconnectTimer?.cancel(); + super.dispose(); + } +} diff --git a/packages/stream_chat_flutter_core/lib/src/user_list_core.dart b/packages/stream_chat_flutter_core/lib/src/user_list_core.dart new file mode 100644 index 00000000..e323591f --- /dev/null +++ b/packages/stream_chat_flutter_core/lib/src/user_list_core.dart @@ -0,0 +1,307 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter_core/src/users_bloc.dart'; + +/// +/// [UserListCore] is a simplified class that allows fetching users while exposing UI builders. +/// A [UserListController] is used to load and paginate data. +/// +/// ```dart +/// class UsersListPage extends StatelessWidget { +/// @override +/// Widget build(BuildContext context) { +/// return Scaffold( +/// body: UsersListCore( +/// filter: { +/// 'members': { +/// '\$in': [StreamChat.of(context).user.id], +/// } +/// }, +/// sort: [SortOption('last_message_at')], +/// pagination: PaginationParams( +/// limit: 20, +/// ), +/// errorBuilder: (err) { +/// return Center( +/// child: Text('An error has occured'), +/// ); +/// }, +/// emptyBuilder: (context) { +/// return Center( +/// child: Text('Nothing here...'), +/// ); +/// }, +/// emptyBuilder: (context) { +/// return Center( +/// child: CircularProgressIndicator(), +/// ); +/// }, +/// listBuilder: (context, list) { +/// return UsersPage(list); +/// } +/// ), +/// ); +/// } +/// } +/// ``` +/// +/// +/// Make sure to have a [UsersBloc] ancestor in order to provide the information about the users. +/// The widget uses a [ListView.separated], [GridView.builder] to render the list, grid of channels. +/// +/// The widget components render the ui based on the first ancestor of type [StreamChatTheme]. +/// Modify it to change the widget appearance. +class UserListCore extends StatefulWidget { + /// Instantiate a new UserListView + const UserListCore({ + Key key, + this.filter, + this.options, + this.sort, + this.pagination, + this.groupAlphabetically = false, + @required this.errorBuilder, + @required this.emptyBuilder, + @required this.loadingBuilder, + @required this.listBuilder, + this.userListController, + }) : super(key: key); + + /// A [UserListController] allows reloading and pagination. + /// Use [UserListController.loadData] and [UserListController.paginateData] respectively for reloading and pagination. + final UserListController userListController; + + /// The builder that will be used in case of error + final Widget Function(Error error) errorBuilder; + + /// The builder that will be used to build the list + final Widget Function(BuildContext context, List users) listBuilder; + + /// The builder that will be used for loading + final WidgetBuilder loadingBuilder; + + /// The builder used when the channel list is empty. + final WidgetBuilder emptyBuilder; + + /// The query filters to use. + /// You can query on any of the custom fields you've defined on the [Channel]. + /// You can also filter other built-in channel fields. + final Map filter; + + /// Query channels options. + /// + /// state: if true returns the Channel state + /// watch: if true listen to changes to this Channel in real time. + final Map options; + + /// 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 sort; + + /// 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 pagination; + + /// Set it to true to group users by their first character + /// + /// defaults to false + final bool groupAlphabetically; + + @override + _UserListCoreState createState() => _UserListCoreState(); +} + +class _UserListCoreState extends State + with WidgetsBindingObserver { + @override + void initState() { + super.initState(); + final usersBloc = UsersBloc.of(context); + usersBloc.queryUsers( + filter: widget.filter, + sort: widget.sort, + pagination: widget.pagination, + options: widget.options, + ); + + if (widget.userListController != null) { + widget.userListController.loadData = loadData; + widget.userListController.paginateData = paginateData; + } + } + + @override + Widget build(BuildContext context) { + final usersBloc = UsersBloc.of(context); + + return _buildListView(usersBloc); + } + + bool get isListAlreadySorted => + widget.sort?.any((e) => e.field == 'name' && e.direction == 1) ?? false; + + Stream> _buildUserStream( + UsersBlocState usersBlocState, + ) { + return usersBlocState.usersStream.map( + (users) { + if (widget.groupAlphabetically) { + var temp = users; + if (!isListAlreadySorted) { + temp = users..sort((curr, next) => curr.name.compareTo(next.name)); + } + final groupedUsers = >{}; + for (final e in temp) { + final alphabet = e.name[0]?.toUpperCase(); + groupedUsers[alphabet] = [...groupedUsers[alphabet] ?? [], e]; + } + final items = []; + for (final key in groupedUsers.keys) { + items.add(ListHeaderItem(key)); + items.addAll(groupedUsers[key].map((e) => ListUserItem(e))); + } + return items; + } + return users.map((e) => ListUserItem(e)).toList(); + }, + ); + } + + StreamBuilder> _buildListView( + UsersBlocState usersBlocState, + ) { + return StreamBuilder( + stream: _buildUserStream(usersBlocState), + builder: (context, snapshot) { + if (snapshot.hasError) { + if (snapshot.error is Error) { + print((snapshot.error as Error).stackTrace); + } + + return widget.errorBuilder(snapshot.error); + } + + if (!snapshot.hasData) { + return widget.loadingBuilder(context); + } + + final items = snapshot.data; + + if (items.isEmpty) { + return widget.emptyBuilder(context); + } + + if (items.isEmpty) { + return widget.emptyBuilder(context); + } + + return widget.listBuilder(context, items); + }, + ); + } + + void loadData() { + final usersBloc = UsersBloc.of(context); + + usersBloc.queryUsers( + filter: widget.filter, + sort: widget.sort, + pagination: widget.pagination, + options: widget.options, + ); + } + + void paginateData() { + final usersBloc = UsersBloc.of(context); + + usersBloc.queryUsers( + filter: widget.filter, + sort: widget.sort, + pagination: widget.pagination.copyWith( + offset: usersBloc.users?.length ?? 0, + ), + options: widget.options, + ); + } + + @override + void didUpdateWidget(UserListCore oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.filter?.toString() != oldWidget.filter?.toString() || + jsonEncode(widget.sort) != jsonEncode(oldWidget.sort) || + widget.pagination?.toJson()?.toString() != + oldWidget.pagination?.toJson()?.toString() || + widget.options?.toString() != oldWidget.options?.toString()) { + final usersBloc = UsersBloc.of(context); + usersBloc.queryUsers( + filter: widget.filter, + sort: widget.sort, + pagination: widget.pagination, + options: widget.options, + ); + } + } +} + +// ignore: public_member_api_docs +abstract class ListItem { + // ignore: public_member_api_docs + String get key { + if (this is ListHeaderItem) { + final header = (this as ListHeaderItem).heading; + return 'HEADER-$header'; + } + if (this is ListUserItem) { + final user = (this as ListUserItem).user; + return 'USER-${user.id}'; + } + return null; + } + + // ignore: public_member_api_docs + Widget when({ + @required Widget Function(String heading) headerItem, + @required Widget Function(User user) userItem, + }) { + if (this is ListHeaderItem) { + return headerItem((this as ListHeaderItem).heading); + } + if (this is ListUserItem) { + return userItem((this as ListUserItem).user); + } + return SizedBox(); + } +} + +// ignore: public_member_api_docs +class ListHeaderItem extends ListItem { + // ignore: public_member_api_docs + final String heading; + + // ignore: public_member_api_docs + ListHeaderItem(this.heading); +} + +// ignore: public_member_api_docs +class ListUserItem extends ListItem { + // ignore: public_member_api_docs + final User user; + + // ignore: public_member_api_docs + ListUserItem(this.user); +} + +/// Controller used for paginating data in [ChannelListView] +class UserListController { + /// Call this function to reload data + VoidCallback loadData; + + /// Call this function to load further data + VoidCallback paginateData; +} diff --git a/packages/flutter_widgets/lib/src/users_bloc.dart b/packages/stream_chat_flutter_core/lib/src/users_bloc.dart similarity index 97% rename from packages/flutter_widgets/lib/src/users_bloc.dart rename to packages/stream_chat_flutter_core/lib/src/users_bloc.dart index 91cbaf80..489461ef 100644 --- a/packages/flutter_widgets/lib/src/users_bloc.dart +++ b/packages/stream_chat_flutter_core/lib/src/users_bloc.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:rxdart/rxdart.dart'; import 'package:stream_chat/stream_chat.dart'; -import 'stream_chat.dart'; +import 'stream_chat_core.dart'; /// Widget dedicated to the management of a users list with pagination class UsersBloc extends StatefulWidget { @@ -56,7 +56,7 @@ class UsersBlocState extends State Map options, PaginationParams pagination, }) async { - final client = StreamChat.of(context).client; + final client = StreamChatCore.of(context).client; if (client.state?.user == null || _queryUsersLoadingController.value == true) { diff --git a/packages/stream_chat_flutter_core/lib/stream_chat_flutter_core.dart b/packages/stream_chat_flutter_core/lib/stream_chat_flutter_core.dart new file mode 100644 index 00000000..623c4424 --- /dev/null +++ b/packages/stream_chat_flutter_core/lib/stream_chat_flutter_core.dart @@ -0,0 +1,13 @@ +library stream_chat_flutter_core; + +export 'src/channel_list_core.dart'; +export 'src/channels_bloc.dart'; +export 'src/lazy_load_scroll_view.dart'; +export 'src/message_list_core.dart'; +export 'src/message_search_bloc.dart'; +export 'src/message_search_list_core.dart'; +export 'src/stream_channel.dart'; +export 'src/stream_chat_core.dart'; +export 'src/user_list_core.dart'; +export 'src/users_bloc.dart'; +export 'package:stream_chat/stream_chat.dart'; diff --git a/packages/stream_chat_flutter_core/pubspec.yaml b/packages/stream_chat_flutter_core/pubspec.yaml new file mode 100644 index 00000000..19dd8eb0 --- /dev/null +++ b/packages/stream_chat_flutter_core/pubspec.yaml @@ -0,0 +1,23 @@ +name: stream_chat_flutter_core +homepage: https://github.com/GetStream/stream-chat-flutter +description: Stream Chat official Flutter SDK Core. Build your own chat experience using Dart and Flutter. +version: 1.0.0-rc +repository: https://github.com/GetStream/stream-chat-flutter +issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues + +environment: + sdk: ">=2.7.0 <3.0.0" + flutter: ">=1.17.0" + +dependencies: + stream_chat: ^0.2.23+3 + rxdart: ^0.24.1 + flutter: + sdk: flutter + +dev_dependencies: + mockito: ^4.1.4 + flutter_test: + sdk: flutter + fake_async: ^1.1.0 + \ No newline at end of file diff --git a/packages/stream_chat_flutter_core/test/mocks.dart b/packages/stream_chat_flutter_core/test/mocks.dart new file mode 100644 index 00000000..37fa5530 --- /dev/null +++ b/packages/stream_chat_flutter_core/test/mocks.dart @@ -0,0 +1,10 @@ +import 'package:mockito/mockito.dart'; +import 'package:stream_chat/stream_chat.dart'; + +class MockClient extends Mock implements Client {} + +class MockClientState extends Mock implements ClientState {} + +class MockChannel extends Mock implements Channel {} + +class MockChannelState extends Mock implements ChannelClientState {} diff --git a/packages/stream_chat_flutter_core/test/stream_chat_core_test.dart b/packages/stream_chat_flutter_core/test/stream_chat_core_test.dart new file mode 100644 index 00000000..714375de --- /dev/null +++ b/packages/stream_chat_flutter_core/test/stream_chat_core_test.dart @@ -0,0 +1,189 @@ +import 'dart:async'; + +import 'package:fake_async/fake_async.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:mockito/mockito.dart'; + +import 'mocks.dart'; + +class MockShowLocalNotifications extends Mock { + void call(Message m, ChannelModel cm); +} + +void main() { + testWidgets( + 'StreamChatCore.of(context) should throw an exception', + (WidgetTester tester) async { + await tester.pumpWidget( + Builder( + builder: (context) { + expect(() => StreamChatCore.of(context), throwsException); + return Container(); + }, + ), + ); + }, + ); + + testWidgets( + 'StreamChatCore.of(context) should return the StreamChatCore ancestor', + (WidgetTester tester) async { + final client = MockClient(); + await tester.pumpWidget( + StreamChatCore( + client: client, + child: Builder( + builder: (context) { + final sc = StreamChatCore.of(context); + expect(sc, isNotNull); + return Container(); + }, + ), + ), + ); + }, + ); + + testWidgets( + 'StreamChatCore.of(context).client should return the client', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn( + OwnUser( + id: 'test', + ), + ); + final userStream = Stream.value( + OwnUser( + id: 'test', + ), + ); + when(clientState.userStream).thenAnswer( + (_) => userStream, + ); + + await tester.pumpWidget( + StreamChatCore( + client: client, + child: Builder( + builder: (context) { + final sc = StreamChatCore.of(context); + expect(sc.client, client); + expect(sc.user, client.state.user); + expect(sc.userStream, userStream); + return Container(); + }, + ), + ), + ); + }, + ); + + testWidgets( + 'StreamChatCore should disconnect on background', + (WidgetTester tester) async { + await fakeAsync((_async) { + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn( + OwnUser( + id: 'test', + ), + ); + final showLocalNotificationMock = MockShowLocalNotifications().call; + when(client.showLocalNotification) + .thenReturn(showLocalNotificationMock); + when(client.backgroundKeepAlive).thenReturn(Duration( + seconds: 4, + )); + final eventStreamController = StreamController(); + when(client.on(EventType.messageNew)) + .thenAnswer((_) => eventStreamController.stream); + + when(client.channel('test', id: 'testid')).thenReturn(channel); + + final scKey = GlobalKey(); + tester.pumpWidget( + StreamChatCore( + key: scKey, + client: client, + child: Builder( + builder: (context) { + return Container(); + }, + ), + ), + ); + + final sc = scKey.currentState; + sc.didChangeAppLifecycleState(AppLifecycleState.paused); + + _async.elapse(Duration(seconds: 5)); + + verify(client.disconnect()).called(1); + }); + }, + ); + + testWidgets( + 'StreamChatCore should handle notifications when on background and connected', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn( + OwnUser( + id: 'test', + ), + ); + final showLocalNotificationMock = MockShowLocalNotifications().call; + when(client.showLocalNotification).thenReturn(showLocalNotificationMock); + when(client.backgroundKeepAlive).thenReturn(Duration( + seconds: 4, + )); + final eventStreamController = StreamController(); + when(client.on(EventType.messageNew)) + .thenAnswer((_) => eventStreamController.stream); + + when(client.channel('test', id: 'testid')).thenReturn(channel); + + final scKey = GlobalKey(); + await tester.pumpWidget( + StreamChatCore( + key: scKey, + client: client, + child: Builder( + builder: (context) { + return Container(); + }, + ), + ), + ); + + final sc = scKey.currentState; + sc.didChangeAppLifecycleState(AppLifecycleState.paused); + final event = Event( + type: EventType.messageNew, + message: Message(text: 'hey'), + channelType: 'test', + channelId: 'testid', + user: User(id: 'other user'), + ); + eventStreamController.add(event); + + await untilCalled(showLocalNotificationMock(any, any)); + + verify(showLocalNotificationMock( + event.message, + any, + )).called(1); + }, + ); +}