diff --git a/example/assets/logo.svg b/example/assets/logo.svg new file mode 100644 index 00000000..49eecf7c --- /dev/null +++ b/example/assets/logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index d20eaee6..f23764fa 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -38,7 +38,7 @@ PODS: - Firebase/Messaging (6.33.0): - Firebase/CoreOnly - FirebaseMessaging (~> 4.7.0) - - firebase_core (0.5.1): + - firebase_core (0.5.2): - Firebase/CoreOnly (~> 6.33.0) - Flutter - firebase_messaging (7.0.3): @@ -82,6 +82,8 @@ PODS: - Flutter - flutter_local_notifications (0.0.1): - Flutter + - flutter_secure_storage (3.3.1): + - Flutter - FMDB (2.7.5): - FMDB/standard (= 2.7.5) - FMDB/standard (2.7.5) @@ -161,6 +163,7 @@ DEPENDENCIES: - flutter_app_badger (from `.symlinks/plugins/flutter_app_badger/ios`) - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) + - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - image_picker (from `.symlinks/plugins/image_picker/ios`) - path_provider (from `.symlinks/plugins/path_provider/ios`) - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`) @@ -210,6 +213,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_keyboard_visibility/ios" flutter_local_notifications: :path: ".symlinks/plugins/flutter_local_notifications/ios" + flutter_secure_storage: + :path: ".symlinks/plugins/flutter_secure_storage/ios" image_picker: :path: ".symlinks/plugins/image_picker/ios" path_provider: @@ -232,7 +237,7 @@ SPEC CHECKSUMS: DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 file_picker: 3e6c3790de664ccf9b882732d9db5eaf6b8d4eb1 Firebase: 8db6f2d1b2c5e2984efba4949a145875a8f65fe5 - firebase_core: aa25a5dc6b492ecab37587c53d8420135f0cac90 + firebase_core: 350ba329d1641211bc6183a3236893cafdacfea7 firebase_messaging: 0aea2cd5885b65e19ede58ee3507f485c992cc75 FirebaseCore: d889d9e12535b7f36ac8bfbf1713a0836a3012cd FirebaseCoreDiagnostics: 770ac5958e1372ce67959ae4b4f31d8e127c3ac1 @@ -244,6 +249,7 @@ SPEC CHECKSUMS: flutter_app_badger: 65de4d6f0c34a891df49e6cfb8a1c0496426fa68 flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743 + flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a GoogleDataTransport: f56af7caa4ed338dc8e138a5d7c5973e66440833 GoogleUtilities: 7f2f5a07f888cdb145101d6042bc4422f57e70b3 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 6d377561..1baa240f 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -325,6 +325,7 @@ "${BUILT_PRODUCTS_DIR}/flutter_app_badger/flutter_app_badger.framework", "${BUILT_PRODUCTS_DIR}/flutter_keyboard_visibility/flutter_keyboard_visibility.framework", "${BUILT_PRODUCTS_DIR}/flutter_local_notifications/flutter_local_notifications.framework", + "${BUILT_PRODUCTS_DIR}/flutter_secure_storage/flutter_secure_storage.framework", "${BUILT_PRODUCTS_DIR}/image_picker/image_picker.framework", "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", "${BUILT_PRODUCTS_DIR}/path_provider/path_provider.framework", @@ -354,6 +355,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_app_badger.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_keyboard_visibility.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_local_notifications.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_secure_storage.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_picker.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider.framework", diff --git a/example/lib/advanced_options_page.dart b/example/lib/advanced_options_page.dart new file mode 100644 index 00000000..8a4c79c0 --- /dev/null +++ b/example/lib/advanced_options_page.dart @@ -0,0 +1,286 @@ +import 'dart:io'; + +import 'package:example/stream_version.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'main.dart'; +import 'notifications_service.dart'; + +class AdvancedOptionsPage extends StatefulWidget { + @override + _AdvancedOptionsPageState createState() => _AdvancedOptionsPageState(); +} + +class _AdvancedOptionsPageState extends State { + final _formKey = GlobalKey(); + + final TextEditingController _apiKeyController = TextEditingController(); + + final TextEditingController _userIdController = TextEditingController(); + + final TextEditingController _userTokenController = TextEditingController(); + + final TextEditingController _usernameController = TextEditingController(); + + bool loading = false; + + @override + Widget build(BuildContext context) { + return Scaffold( + resizeToAvoidBottomPadding: false, + appBar: AppBar( + backgroundColor: StreamChatTheme.of(context).primaryColor, + elevation: 1, + centerTitle: true, + title: Text( + 'Advanced Options', + style: Theme.of(context).textTheme.subtitle1.copyWith( + fontWeight: FontWeight.bold, + ), + ), + leading: IconButton( + icon: Icon( + StreamIcons.left, + color: Colors.black, + ), + onPressed: () { + Navigator.pop(context); + }, + ), + ), + body: Builder( + builder: (context) { + return Padding( + padding: const EdgeInsets.fromLTRB( + 16, + 16, + 16, + 0, + ), + child: Form( + key: _formKey, + child: Column( + children: [ + TextFormField( + controller: _apiKeyController, + validator: (value) { + if (value.isEmpty) { + return 'Please enter the Chat API Key'; + } + return null; + }, + decoration: InputDecoration( + labelStyle: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.black.withOpacity(.5), + ), + border: UnderlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + fillColor: Color(0xffF5F5F5), + filled: true, + labelText: 'Chat API Key', + ), + textInputAction: TextInputAction.next, + ), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: TextFormField( + controller: _userIdController, + validator: (value) { + if (value.isEmpty) { + return 'Please enter the User ID'; + } + return null; + }, + textInputAction: TextInputAction.next, + decoration: InputDecoration( + labelStyle: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + color: Colors.black.withOpacity(.5), + ), + border: UnderlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + fillColor: Color(0xffF5F5F5), + filled: true, + labelText: 'User ID', + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: TextFormField( + controller: _userTokenController, + validator: (value) { + if (value.isEmpty) { + return 'Please enter the user token'; + } + return null; + }, + textInputAction: TextInputAction.next, + decoration: InputDecoration( + labelStyle: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + color: Colors.black.withOpacity(.5), + ), + border: UnderlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + fillColor: Color(0xffF5F5F5), + filled: true, + labelText: 'User Token', + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: TextFormField( + controller: _usernameController, + textInputAction: TextInputAction.done, + decoration: InputDecoration( + labelStyle: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.black.withOpacity(.5), + ), + border: UnderlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + fillColor: Color(0xffF5F5F5), + filled: true, + labelText: 'Username (optional)', + ), + ), + ), + Expanded( + child: Align( + alignment: Alignment.bottomCenter, + child: FlatButton( + color: StreamChatTheme.of(context).accentColor, + minWidth: double.infinity, + height: 48, + child: Text( + 'Login', + style: TextStyle( + color: Colors.white, + fontSize: 16, + ), + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(26), + ), + onPressed: () async { + if (loading) { + return; + } + loading = true; + if (_formKey.currentState.validate()) { + final apiKey = _apiKeyController.text; + final userId = _userIdController.text; + final userToken = _userTokenController.text; + final username = _usernameController.text; + + showDialog( + barrierDismissible: false, + context: context, + builder: (context) => Center( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: Colors.white, + ), + height: 100, + width: 100, + child: Center( + child: CircularProgressIndicator(), + ), + ), + ), + ); + + print('CREATE CLIENT'); + final client = Client( + apiKey, + logLevel: Level.INFO, + showLocalNotification: + (!kIsWeb && Platform.isAndroid) + ? showLocalNotification + : null, + persistenceEnabled: true, + ); + + try { + await client.setUser( + User(id: userId, extraData: { + 'name': username, + }), + userToken, + ); + } catch (e) { + var errorText = 'Error connecting, retry'; + if (e is Map) { + errorText = e['message'] ?? errorText; + } + Navigator.pop(context); + Scaffold.of(context).showSnackBar( + SnackBar( + content: Text(errorText), + ), + ); + loading = false; + await client.disconnect(); + return; + } + + if (!kIsWeb) { + initNotifications(client); + } + + Navigator.pop(context); + Navigator.pop(context); + await Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) { + return MaterialApp( + theme: ThemeData.light(), + darkTheme: ThemeData.dark(), + //TODO change to system once dark theme is implemented + themeMode: ThemeMode.light, + builder: (context, widget) { + return StreamChat( + child: widget, + client: client, + ); + }, + home: ChannelListPage(), + ); + }, + ), + ); + loading = false; + } + }, + ), + ), + ), + StreamVersion(), + ], + ), + ), + ); + }, + ), + ); + } +} diff --git a/example/lib/choose_user_page.dart b/example/lib/choose_user_page.dart new file mode 100644 index 00000000..b5296f8a --- /dev/null +++ b/example/lib/choose_user_page.dart @@ -0,0 +1,223 @@ +import 'package:example/advanced_options_page.dart'; +import 'package:example/main.dart'; +import 'package:example/stream_version.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'notifications_service.dart'; + +const kStreamApiKey = 'STREAM_API_KEY'; +const kStreamUserId = 'STREAM_USER_ID'; +const kStreamToken = 'STREAM_TOKEN'; +const kDefaultStreamApiKey = 's2dxdhpxd94g'; + +class ChooseUserPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + final users = { + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidmlzaGFsIn0._JHWzo92fpTWZMZriJHXqOng6ShYVmWrdaIaPwEPKBg': + User( + id: 'vishal', + extraData: { + 'name': 'Vishal', + }, + ), + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A': + User( + id: 'super-band-9', + extraData: { + 'name': 'John Doe', + }, + ), + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoic2FsdmF0b3JlIn0.GMEKyEhONmFnqtkf1TR1A3oUOSIWhjfQv5RpI906dAM': + User( + id: 'salvatore', + extraData: { + 'name': 'Salvatore', + }, + ), + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidG9tbWFzbyJ9.GTalMeZHaBdpIM5w-KWrVIDSy-ODkHTRkf1GZbWAveM': + User( + id: 'tommaso', + extraData: { + 'name': 'Tommaso', + }, + ), + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiamFhcCJ9.xpGE2lu4OoYS7-2yy6PbF0gJnxOaxeGO4EU6xo4EdMU': + User( + id: 'jaap', + extraData: { + 'name': 'Jaap', + }, + ), + }; + + users.updateAll( + (_, value) => value..extraData['image'] = getRandomPicUrl(value)); + + return Scaffold( + body: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.only( + top: 34, + bottom: 20, + ), + child: Center( + child: SvgPicture.asset( + 'assets/logo.svg', + height: 40, + ), + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 13.0), + child: Text( + 'Welcome to Stream Chat', + style: TextStyle( + fontSize: 22, + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), + ), + Text( + 'Select a user to try the Flutter SDK:', + style: TextStyle( + fontSize: 14.5, + color: Colors.black, + ), + ), + Expanded( + child: ListView.separated( + separatorBuilder: (context, i) { + return Container( + width: double.infinity, + color: Colors.black12, + height: 1, + ); + }, + itemCount: users.length + 1, + itemBuilder: (context, i) { + return [ + ...users.entries.map((entry) { + final token = entry.key; + final user = entry.value; + return ListTile( + onTap: () async { + showDialog( + barrierDismissible: false, + context: context, + builder: (context) => Center( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: Colors.white, + ), + height: 100, + width: 100, + child: Center( + child: CircularProgressIndicator(), + ), + ), + ), + ); + + final secureStorage = FlutterSecureStorage(); + final client = StreamChat.of(context).client; + + await client.setUser( + user, + token, + ); + + await Future.wait([ + secureStorage.write( + key: kStreamApiKey, + value: kDefaultStreamApiKey, + ), + secureStorage.write( + key: kStreamUserId, + value: user.id, + ), + secureStorage.write( + key: kStreamToken, + value: token, + ), + ]); + + if (!kIsWeb) { + initNotifications(client); + } + + Navigator.pop(context); + await Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) { + return StreamChat( + client: client, + child: ChannelListPage(), + ); + }, + ), + ); + }, + leading: UserAvatar( + user: user, + constraints: BoxConstraints.tight( + Size.fromRadius(20), + ), + ), + title: Text( + user.name, + style: TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: Text('Stream test account'), + trailing: Icon( + StreamIcons.arrow_right, + color: StreamChatTheme.of(context).accentColor, + ), + ); + }), + ListTile( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => AdvancedOptionsPage(), + ), + ); + }, + leading: CircleAvatar( + child: Icon( + StreamIcons.settings, + color: Colors.black, + ), + backgroundColor: + StreamChatTheme.of(context).secondaryColor, + ), + title: Text( + 'Advanced Options', + style: TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: Text('Custom settings'), + trailing: Icon( + StreamIcons.arrow_right, + color: StreamChatTheme.of(context).accentColor, + ), + ), + ][i]; + }, + ), + ), + StreamVersion(), + ], + ), + ); + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index ca727cf7..1da02072 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,88 +1,35 @@ import 'dart:io'; +import 'package:example/choose_user_page.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_apns/apns.dart'; -import 'package:flutter_local_notifications/flutter_local_notifications.dart' - hide Message; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -void showLocalNotification(Message message, ChannelModel channel) async { - FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = - FlutterLocalNotificationsPlugin(); - final initializationSettingsAndroid = - AndroidInitializationSettings('launch_background'); - final initializationSettingsIOS = IOSInitializationSettings(); - final initializationSettings = InitializationSettings( - android: initializationSettingsAndroid, - iOS: initializationSettingsIOS, - ); - await flutterLocalNotificationsPlugin.initialize(initializationSettings); - await flutterLocalNotificationsPlugin.show( - message.id.hashCode, - '${message.user.name} @ ${channel.name}', - message.text, - NotificationDetails( - android: AndroidNotificationDetails( - 'message channel', - 'Message channel', - 'Channel used for showing messages', - priority: Priority.high, - importance: Importance.high, - ), - iOS: IOSNotificationDetails(), - ), - ); -} - -Future backgroundHandler(Map notification) async { - final messageId = notification['data']['message_id']; - - final notificationData = - await NotificationService.getAndStoreMessage(messageId); - - showLocalNotification( - notificationData.message, - notificationData.channel, - ); -} - -void _initNotifications(Client client) { - final connector = createPushConnector(); - connector.configure( - onBackgroundMessage: backgroundHandler, - ); - - connector.requestNotificationPermissions(); - connector.token.addListener(() { - if (connector.token.value != null) { - client.addDevice( - connector.token.value, - Platform.isAndroid ? 'firebase' : 'apn', - ); - } - }); -} +import 'notifications_service.dart'; void main() async { + WidgetsFlutterBinding.ensureInitialized(); + final secureStorage = FlutterSecureStorage(); + + final apiKey = await secureStorage.read(key: kStreamApiKey); + final userId = await secureStorage.read(key: kStreamUserId); + final client = Client( - 's2dxdhpxd94g', + apiKey ?? kDefaultStreamApiKey, logLevel: Level.INFO, showLocalNotification: (!kIsWeb && Platform.isAndroid) ? showLocalNotification : null, persistenceEnabled: true, ); - await client.setUser( - User(id: 'super-band-9', extraData: { - 'name': 'Jonathan Doe', - }), - 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A', - ); - - if (!kIsWeb) { - _initNotifications(client); + if (userId != null) { + final token = await secureStorage.read(key: kStreamToken); + await client.setUser( + User(id: userId), + token, + ); } runApp(MyApp(client)); @@ -98,16 +45,15 @@ class MyApp extends StatelessWidget { return MaterialApp( theme: ThemeData.light(), darkTheme: ThemeData.dark(), + //TODO change to system once dark theme is implemented themeMode: ThemeMode.light, - - ///TODO change to system once dark theme is implemented builder: (context, widget) { return StreamChat( child: widget, client: client, ); }, - home: ChannelListPage(), + home: client.state.user == null ? ChooseUserPage() : ChannelListPage(), ); } } @@ -115,7 +61,89 @@ class MyApp extends StatelessWidget { class ChannelListPage extends StatelessWidget { @override Widget build(BuildContext context) { + final user = StreamChat.of(context).user; return Scaffold( + drawer: Drawer( + child: Padding( + padding: EdgeInsets.only( + top: MediaQuery.of(context).viewPadding.top + 8, + ), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only( + bottom: 20.0, + left: 8, + ), + child: Row( + children: [ + UserAvatar( + user: user, + showOnlineStatus: false, + constraints: BoxConstraints.tight(Size.fromRadius(20)), + ), + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Text( + user.name, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + ListTile( + leading: Icon(StreamIcons.edit), + title: Text( + 'New direct message', + style: TextStyle( + fontSize: 14.5, + ), + ), + ), + ListTile( + leading: Icon(StreamIcons.group), + title: Text( + 'New group', + style: TextStyle( + fontSize: 14.5, + ), + ), + ), + Expanded( + child: Container( + alignment: Alignment.bottomCenter, + child: ListTile( + onTap: () async { + await StreamChat.of(context).client.disconnect(); + + final secureStorage = FlutterSecureStorage(); + await secureStorage.deleteAll(); + Navigator.pop(context); + await Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => ChooseUserPage(), + ), + ); + }, + leading: Icon(StreamIcons.user), + title: Text( + 'Sign out', + style: TextStyle( + fontSize: 14.5, + ), + ), + ), + ), + ), + ], + ), + ), + ), floatingActionButton: FloatingActionButton( child: Icon(Icons.add), onPressed: () { @@ -129,7 +157,7 @@ class ChannelListPage extends StatelessWidget { swipeToAction: true, filter: { 'members': { - '\$in': [StreamChat.of(context).user.id], + '\$in': [user.id], } }, options: { diff --git a/example/lib/notifications_service.dart b/example/lib/notifications_service.dart new file mode 100644 index 00000000..c5b77c0e --- /dev/null +++ b/example/lib/notifications_service.dart @@ -0,0 +1,62 @@ +import 'dart:io'; + +import 'package:flutter_apns/flutter_apns.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart' + hide Message; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +void showLocalNotification(Message message, ChannelModel channel) async { + final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); + final initializationSettingsAndroid = + AndroidInitializationSettings('launch_background'); + final initializationSettingsIOS = IOSInitializationSettings(); + final initializationSettings = InitializationSettings( + android: initializationSettingsAndroid, + iOS: initializationSettingsIOS, + ); + await flutterLocalNotificationsPlugin.initialize(initializationSettings); + await flutterLocalNotificationsPlugin.show( + message.id.hashCode, + '${message.user.name} @ ${channel.name}', + message.text, + NotificationDetails( + android: AndroidNotificationDetails( + 'message channel', + 'Message channel', + 'Channel used for showing messages', + priority: Priority.high, + importance: Importance.high, + ), + iOS: IOSNotificationDetails(), + ), + ); +} + +Future backgroundHandler(Map notification) async { + final messageId = notification['data']['message_id']; + + final notificationData = + await NotificationService.getAndStoreMessage(messageId); + + showLocalNotification( + notificationData.message, + notificationData.channel, + ); +} + +void initNotifications(Client client) { + final connector = createPushConnector(); + connector.configure( + onBackgroundMessage: backgroundHandler, + ); + + connector.requestNotificationPermissions(); + connector.token.addListener(() { + if (connector.token.value != null) { + client.addDevice( + connector.token.value, + Platform.isAndroid ? 'firebase' : 'apn', + ); + } + }); +} diff --git a/example/lib/stream_version.dart b/example/lib/stream_version.dart new file mode 100644 index 00000000..ee340f89 --- /dev/null +++ b/example/lib/stream_version.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:yaml/yaml.dart'; + +class StreamVersion extends StatelessWidget { + const StreamVersion({ + Key key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 16), + alignment: Alignment.bottomCenter, + child: FutureBuilder( + future: rootBundle.loadString('pubspec.lock'), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return SizedBox(); + } + + final pubspec = snapshot.data; + final yaml = loadYaml(pubspec); + final streamChatDep = + yaml['packages']['stream_chat_flutter']['version']; + + print('streamChatDep: ${streamChatDep}'); + return Text( + 'Stream SDK v ${streamChatDep}', + style: TextStyle( + fontSize: 14.5, + color: Colors.black.withOpacity(.13), + ), + ); + }, + ), + ); + } +} diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 3a6a4cb3..c6743cd4 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.44+46 +version: 1.0.53+55 environment: sdk: ">=2.2.2 <3.0.0" @@ -12,6 +12,9 @@ dependencies: path: ../ flutter_apns: ^1.3.1 flutter_local_notifications: ^2.0.0 + flutter_svg: ^0.19.1 + flutter_secure_storage: ^3.3.5 + yaml: ^2.2.1 dev_dependencies: flutter_test: @@ -22,6 +25,9 @@ dev_dependencies: test: any flutter: + assets: + - assets/ + - pubspec.lock uses-material-design: true flutter_icons: diff --git a/lib/src/channel_list_view.dart b/lib/src/channel_list_view.dart index 3b698782..9f09235b 100644 --- a/lib/src/channel_list_view.dart +++ b/lib/src/channel_list_view.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'package:flutter/foundation.dart'; @@ -482,6 +483,8 @@ class _ChannelListViewState extends State } } + StreamSubscription _subscription; + @override void initState() { super.initState(); @@ -506,7 +509,7 @@ class _ChannelListViewState extends State final client = StreamChat.of(context).client; - client + _subscription = client .on( EventType.connectionRecovered, EventType.notificationAddedToChannel, @@ -544,6 +547,7 @@ class _ChannelListViewState extends State @override void dispose() { + _subscription.cancel(); WidgetsBinding.instance.removeObserver(this); super.dispose(); } diff --git a/lib/src/channel_preview.dart b/lib/src/channel_preview.dart index cd1a0910..c23d7732 100644 --- a/lib/src/channel_preview.dart +++ b/lib/src/channel_preview.dart @@ -70,11 +70,20 @@ class ChannelPreview extends StatelessWidget { StreamChatTheme.of(context).channelPreviewTheme.title, ), ), - if (channel.state.members.contains( - (Member e) => e.userId == channel.client.state.user.id)) - UnreadIndicator( - channel: channel, - ), + StreamBuilder>( + stream: channel.state.membersStream, + initialData: channel.state.members, + builder: (context, snapshot) { + if (!snapshot.hasData || + snapshot.data.isEmpty || + !snapshot.data.any((Member e) => + e.user.id == channel.client.state.user.id)) { + return SizedBox(); + } + return UnreadIndicator( + channel: channel, + ); + }), ], ), subtitle: Row( diff --git a/lib/src/sending_indicator.dart b/lib/src/sending_indicator.dart index e93984e2..ce8915c3 100644 --- a/lib/src/sending_indicator.dart +++ b/lib/src/sending_indicator.dart @@ -18,6 +18,7 @@ class SendingIndicator extends StatelessWidget { return Icon( Icons.done_all, size: 8, + color: StreamChatTheme.of(context).accentColor, ); } if (message.status == MessageSendingStatus.SENT || message.status == null) { diff --git a/lib/src/stream_chat_theme.dart b/lib/src/stream_chat_theme.dart index cc959a35..3b3725ed 100644 --- a/lib/src/stream_chat_theme.dart +++ b/lib/src/stream_chat_theme.dart @@ -1,3 +1,4 @@ +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/src/channel_header.dart'; @@ -5,6 +6,7 @@ import 'package:stream_chat_flutter/src/channel_preview.dart'; import 'package:stream_chat_flutter/src/message_input.dart'; import 'package:stream_chat_flutter/src/reaction_icon.dart'; import 'package:stream_chat_flutter/src/stream_icons.dart'; +import 'package:stream_chat_flutter/src/utils.dart'; /// Inherited widget providing the [StreamChatThemeData] to the widget tree class StreamChatTheme extends InheritedWidget { @@ -218,6 +220,7 @@ class StreamChatThemeData { final accentColor = Color(0xff006cff); final isDark = theme.brightness == Brightness.dark; return StreamChatThemeData( + secondaryColor: Color(0xffEAEAEA), accentColor: accentColor, primaryColor: isDark ? Colors.black : Colors.white, primaryIconTheme: IconThemeData( @@ -225,9 +228,9 @@ class StreamChatThemeData { defaultChannelImage: (context, channel) => SizedBox(), backgroundColor: isDark ? Colors.black : Colors.white, defaultUserImage: (context, user) => Center( - child: Text( - user.name?.substring(0, 1) ?? '', - style: TextStyle(color: Colors.white), + child: CachedNetworkImage( + imageUrl: getRandomPicUrl(user), + fit: BoxFit.cover, ), ), channelPreviewTheme: ChannelPreviewTheme( diff --git a/lib/src/user_avatar.dart b/lib/src/user_avatar.dart index 814b21be..86bf7abd 100644 --- a/lib/src/user_avatar.dart +++ b/lib/src/user_avatar.dart @@ -27,6 +27,7 @@ class UserAvatar extends StatelessWidget { final hasImage = user.extraData?.containsKey('image') == true && user.extraData['image'] != null && user.extraData['image'] != ''; + final streamChatTheme = StreamChatTheme.of(context); return GestureDetector( onTap: onTap != null ? () { @@ -39,38 +40,22 @@ class UserAvatar extends StatelessWidget { children: [ ClipRRect( borderRadius: borderRadius ?? - StreamChatTheme.of(context) - .ownMessageTheme - .avatarTheme - .borderRadius, + streamChatTheme.ownMessageTheme.avatarTheme.borderRadius, child: Container( constraints: constraints ?? - StreamChatTheme.of(context) - .ownMessageTheme - .avatarTheme - .constraints, + streamChatTheme.ownMessageTheme.avatarTheme.constraints, decoration: BoxDecoration( - color: StreamChatTheme.of(context).accentColor, + color: streamChatTheme.accentColor, ), child: hasImage ? CachedNetworkImage( imageUrl: user.extraData['image'], errorWidget: (_, __, ___) { - return Center( - child: Text( - user.extraData?.containsKey('name') ?? false - ? user.extraData['name'][0] - : '', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - ); + return streamChatTheme.defaultUserImage(context, user); }, fit: BoxFit.cover, ) - : StreamChatTheme.of(context).defaultUserImage(context, user), + : streamChatTheme.defaultUserImage(context, user), ), ), if (showOnlineStatus && user.online == true) diff --git a/lib/src/utils.dart b/lib/src/utils.dart index b20e5f59..20e3ce1f 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:stream_chat/stream_chat.dart'; import 'package:url_launcher/url_launcher.dart'; Future launchURL(BuildContext context, String url) async { @@ -42,3 +43,7 @@ Future showConfirmationDialog( }, ); } + +/// Get random png with initials +String getRandomPicUrl(User user) => + 'https://getstream.io/random_png/?id=${user.id}&name=${user.name}'; diff --git a/lib/stream_chat_flutter.dart b/lib/stream_chat_flutter.dart index 4c5760bf..e433e58e 100644 --- a/lib/stream_chat_flutter.dart +++ b/lib/stream_chat_flutter.dart @@ -28,4 +28,5 @@ export 'src/system_message.dart'; export 'src/thread_header.dart'; export 'src/typing_indicator.dart'; export 'src/user_avatar.dart'; +export 'src/utils.dart'; export 'src/video_attachment.dart'; diff --git a/test/src/channel_preview_test.dart b/test/src/channel_preview_test.dart index 94c45ac2..578d4b28 100644 --- a/test/src/channel_preview_test.dart +++ b/test/src/channel_preview_test.dart @@ -19,6 +19,7 @@ void main() { when(clientState.user).thenReturn(OwnUser(id: 'user-id')); when(channel.lastMessageAt).thenReturn(lastMessageAt); when(channel.state).thenReturn(channelState); + when(channel.client).thenReturn(client); when(channel.isMuted).thenReturn(false); when(channel.isMutedStream).thenAnswer((i) => Stream.value(false)); when(channel.extraDataStream).thenAnswer((i) => Stream.value({ @@ -28,7 +29,19 @@ void main() { 'name': 'test name', }); when(channelState.unreadCount).thenReturn(1); - when(channelState.members).thenReturn([]); + when(channelState.unreadCountStream).thenAnswer((i) => Stream.value(1)); + when(channelState.membersStream).thenAnswer((i) => Stream.value([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ) + ])); + when(channelState.members).thenReturn([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ), + ]); when(channelState.lastMessage).thenReturn(Message( text: 'hello', user: User(id: 'other-user'),