diff --git a/CHANGELOG.md b/CHANGELOG.md index e51cb8bc..03db8a6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## 0.2.12 + +- Upgrade dependencies +- Check if user.extraData['image'] is not null before using it + +## 0.2.11+1 + +- Fix error with channel query while handling background notifications + ## 0.2.11 - Update llc dependency 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 5eecf8a6..595611e8 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -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) @@ -163,6 +165,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`) - photo_gallery (from `.symlinks/plugins/photo_gallery/ios`) @@ -213,6 +216,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: @@ -249,6 +254,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 de8fcb9a..ea52b6eb 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", @@ -355,6 +356,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/multiple_conversation.dart b/example/lib/multiple_conversation.dart index 647ae7dd..b5fdd5af 100644 --- a/example/lib/multiple_conversation.dart +++ b/example/lib/multiple_conversation.dart @@ -56,9 +56,9 @@ class ChannelListPage extends StatelessWidget { body: ChannelsBloc( child: ChannelListView( filter: { - 'members': { - '\$in': [StreamChat.of(context).user.id], - } + // 'members': { + // '\$in': [StreamChat.of(context).user.id], + // } }, sort: [SortOption('last_message_at')], pagination: PaginationParams( 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 79b9638e..ce1ab622 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,7 +1,6 @@ name: example description: A new Flutter project. - -version: 1.0.44+46 +version: 1.0.54+56 environment: sdk: ">=2.2.2 <3.0.0" @@ -13,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: @@ -23,6 +25,9 @@ dev_dependencies: test: any flutter: + assets: + - assets/ + - pubspec.lock uses-material-design: true flutter_icons: diff --git a/lib/src/back_button.dart b/lib/src/back_button.dart index 657a44ef..78b86ae5 100644 --- a/lib/src/back_button.dart +++ b/lib/src/back_button.dart @@ -1,44 +1,57 @@ import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/stream_icons.dart'; +import 'package:stream_chat_flutter/src/unread_indicator.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; class StreamBackButton extends StatelessWidget { const StreamBackButton({ Key key, this.onPressed, this.icon = Icons.arrow_back_ios_outlined, + this.showUnreads = false, }) : super(key: key); final VoidCallback onPressed; final IconData icon; + final bool showUnreads; @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(14.0), - child: RawMaterialButton( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), - elevation: 0, - highlightElevation: 0, - focusElevation: 0, - disabledElevation: 0, - hoverElevation: 0, - onPressed: () { - if (onPressed != null) { - onPressed(); - } else { - Navigator.of(context).pop(); - } - }, - fillColor: Theme.of(context).brightness == Brightness.dark - ? Colors.white.withOpacity(.1) - : Colors.black.withOpacity(.1), - child: Icon( - icon ?? Icons.arrow_back_ios_outlined, - size: 15, - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white - : Colors.black, + return Stack( + children: [ + Padding( + padding: const EdgeInsets.all(14.0), + child: RawMaterialButton( + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), + elevation: 0, + highlightElevation: 0, + focusElevation: 0, + disabledElevation: 0, + hoverElevation: 0, + onPressed: () { + if (onPressed != null) { + onPressed(); + } else { + Navigator.of(context).pop(); + } + }, + child: Icon( + icon ?? StreamIcons.left, + size: 24, + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white + : Colors.black, + ), + ), ), - ), + if (showUnreads) + Positioned( + top: 7, + right: 7, + child: UnreadIndicator(), + ), + ], ); } } diff --git a/lib/src/channel_header.dart b/lib/src/channel_header.dart index 81f95a87..ee0fd179 100644 --- a/lib/src/channel_header.dart +++ b/lib/src/channel_header.dart @@ -81,7 +81,10 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { brightness: Theme.of(context).brightness, elevation: 1, leading: showBackButton - ? StreamBackButton(onPressed: onBackPressed) + ? StreamBackButton( + onPressed: onBackPressed, + showUnreads: true, + ) : SizedBox(), backgroundColor: StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color, 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 8ed15657..ba3090c2 100644 --- a/lib/src/channel_preview.dart +++ b/lib/src/channel_preview.dart @@ -2,10 +2,10 @@ 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/src/unread_indicator.dart'; import '../stream_chat_flutter.dart'; import 'channel_name.dart'; +import 'channel_unread_indicator.dart'; /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_preview.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_preview_paint.png) @@ -70,9 +70,20 @@ class ChannelPreview extends StatelessWidget { StreamChatTheme.of(context).channelPreviewTheme.title, ), ), - 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 ChannelUnreadIndicator( + channel: channel, + ); + }), ], ), subtitle: Row( @@ -92,7 +103,7 @@ class ChannelPreview extends StatelessWidget { .isAfter(channel .state.lastMessage.createdAt)) .length == - channel.memberCount - 1, + (channel.memberCount ?? 0) - 1, ), ); } diff --git a/lib/src/channel_unread_indicator.dart b/lib/src/channel_unread_indicator.dart new file mode 100644 index 00000000..8080b1e9 --- /dev/null +++ b/lib/src/channel_unread_indicator.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; + +class ChannelUnreadIndicator extends StatelessWidget { + const ChannelUnreadIndicator({ + Key key, + @required this.channel, + }) : super(key: key); + + final Channel channel; + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: channel.state.unreadCountStream, + initialData: channel.state.unreadCount, + builder: (context, snapshot) { + if (!snapshot.hasData || snapshot.data == 0) { + return SizedBox(); + } + return Material( + borderRadius: BorderRadius.circular(8), + color: StreamChatTheme.of(context) + .channelPreviewTheme + .unreadCounterColor, + child: Padding( + padding: const EdgeInsets.only( + left: 5.0, + right: 5.0, + top: 2, + bottom: 1, + ), + child: Center( + child: Text( + '${snapshot.data}', + style: TextStyle( + fontSize: 11, + color: Colors.white, + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index 946ad66f..2a60b75b 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -289,7 +289,9 @@ class _MessageListViewState extends State { return messageWidget; }, ), - if (widget.showScrollToBottom) + if (streamChannel.channel.state.members.contains((Member e) => + e.userId == streamChannel.channel.client.state.user.id) && + widget.showScrollToBottom) StreamBuilder( stream: streamChannel.channel.state.unreadCountStream, builder: (context, snapshot) { @@ -374,8 +376,16 @@ class _MessageListViewState extends State { left: 10, top: -10, child: CircleAvatar( - radius: 20, - child: Text(unreadCount.toString()), + child: Padding( + padding: const EdgeInsets.all(3.0), + child: Text( + unreadCount.toString(), + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.bold, + ), + ), + ), ), ), ], @@ -555,13 +565,14 @@ class _MessageListViewState extends State { final channel = StreamChannel.of(context).channel; final readList = channel.state?.read - ?.where((element) => element.user.id != userId) - ?.where((read) => - (read.lastRead.isAfter(message.createdAt) || - read.lastRead.isAtSameMomentAs(message.createdAt)) && - (index == 0 || - read.lastRead.isBefore(messages[index - 1].createdAt))) - ?.toList(); + ?.where((element) => element.user.id != userId) + ?.where((read) => + (read.lastRead.isAfter(message.createdAt) || + read.lastRead.isAtSameMomentAs(message.createdAt)) && + (index == 0 || + read.lastRead.isBefore(messages[index - 1].createdAt))) + ?.toList() ?? + []; final allRead = readList.length >= (channel.memberCount ?? 0) - 1; diff --git a/lib/src/message_text.dart b/lib/src/message_text.dart index ed636a57..a043c53a 100644 --- a/lib/src/message_text.dart +++ b/lib/src/message_text.dart @@ -24,7 +24,11 @@ class MessageText extends StatelessWidget { final text = _replaceMentions(message.text); return MarkdownBody( data: text, - onTapLink: (text, link, title) { + onTapLink: ( + String link, + String href, + String title, + ) { if (link.startsWith('@')) { final mentionedUser = message.mentionedUsers.firstWhere( (u) => '@${u.name.replaceAll(' ', '')}' == link, diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart index 476217f3..a41bc97f 100644 --- a/lib/src/message_widget.dart +++ b/lib/src/message_widget.dart @@ -1,6 +1,7 @@ import 'dart:math'; import 'dart:ui'; +import 'package:cached_network_image/cached_network_image.dart'; import 'package:emojis/emoji.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; @@ -11,10 +12,12 @@ import 'package:jiffy/jiffy.dart'; import 'package:stream_chat_flutter/src/message_actions_modal.dart'; import 'package:stream_chat_flutter/src/message_reactions_modal.dart'; import 'package:stream_chat_flutter/src/reaction_bubble.dart'; +import 'package:stream_chat_flutter/src/url_attachment.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'image_group.dart'; import 'message_text.dart'; +import 'utils.dart'; typedef AttachmentBuilder = Widget Function(BuildContext, Message, Attachment); @@ -343,6 +346,24 @@ class _MessageWidgetState extends State { ); } + Widget _buildUrlAttachment() { + var urlAttachment = widget.message.attachments + .firstWhere((element) => element.ogScrapeUrl != null); + + var host = Uri.parse(urlAttachment.ogScrapeUrl).host; + var splitList = host.split('.'); + var hostName = splitList.length == 3 ? splitList[1] : splitList[0]; + var hostDisplayName = urlAttachment.authorName?.capitalize() ?? + _getWebsiteName(hostName.toLowerCase()) ?? + hostName.capitalize(); + + return UrlAttachment( + urlAttachment: urlAttachment, + hostDisplayName: hostDisplayName, + textPadding: widget.textPadding, + ); + } + Padding _buildBottomRow(double leftPadding) { return Padding( padding: EdgeInsets.only( @@ -525,7 +546,8 @@ class _MessageWidgetState extends State { List _parseAttachments(BuildContext context) { final images = widget.message.attachments - ?.where((element) => element.type == 'image') + ?.where((element) => + element.type == 'image' && element.ogScrapeUrl == null) ?.toList() ?? []; @@ -548,7 +570,9 @@ class _MessageWidgetState extends State { ]; } - return widget.message.attachments?.map((attachment) { + return widget.message.attachments + ?.where((element) => element.ogScrapeUrl == null) + ?.map((attachment) { final attachmentBuilder = widget.attachmentBuilders[attachment.type]; if (attachmentBuilder == null) { @@ -757,28 +781,36 @@ class _MessageWidgetState extends State { Widget child = Transform( transform: Matrix4.rotationY(widget.reverse ? pi : 0), alignment: Alignment.center, - child: Padding( - padding: widget.textPadding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - getFailedMessageWidget(context), - widget.textBuilder != null - ? widget.textBuilder(context, widget.message) - : MessageText( - onLinkTap: widget.onLinkTap, - message: widget.message, - onMentionTap: widget.onMentionTap, - messageTheme: isOnlyEmoji - ? widget.messageTheme.copyWith( - messageText: - widget.messageTheme.messageText.copyWith( - fontSize: 40, - )) - : widget.messageTheme, - ), - ], - ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: widget.textPadding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + getFailedMessageWidget(context), + widget.textBuilder != null + ? widget.textBuilder(context, widget.message) + : MessageText( + onLinkTap: widget.onLinkTap, + message: widget.message, + onMentionTap: widget.onMentionTap, + messageTheme: isOnlyEmoji + ? widget.messageTheme.copyWith( + messageText: + widget.messageTheme.messageText.copyWith( + fontSize: 40, + )) + : widget.messageTheme, + ), + ], + ), + ), + if (widget.message.attachments + .any((element) => element.ogScrapeUrl != null)) + _buildUrlAttachment(), + ], ), ); @@ -796,11 +828,18 @@ class _MessageWidgetState extends State { } Color _getBackgroundColor() { - return (widget.message.status == MessageSendingStatus.FAILED || - widget.message.status == MessageSendingStatus.FAILED_UPDATE || - widget.message.status == MessageSendingStatus.FAILED_DELETE) - ? Color(0xffd0021B).withOpacity(.1) - : widget.messageTheme.messageBackgroundColor; + if ((widget.message.status == MessageSendingStatus.FAILED || + widget.message.status == MessageSendingStatus.FAILED_UPDATE || + widget.message.status == MessageSendingStatus.FAILED_DELETE)) { + return Color(0xffd0021B).withOpacity(.1); + } + + if (widget.message.attachments + .any((element) => element.ogScrapeUrl != null)) { + return Color(0xFFE9F2FF); + } + + return widget.messageTheme.messageBackgroundColor; } void retryMessage(BuildContext context) { @@ -825,4 +864,45 @@ class _MessageWidgetState extends State { return; } } + + String _getWebsiteName(String hostName) { + switch (hostName) { + case 'reddit': + return 'Reddit'; + case 'youtube': + return 'Youtube'; + case 'wikipedia': + return 'Wikipedia'; + case 'twitter': + return 'Twitter'; + case 'facebook': + return 'Facebook'; + case 'amazon': + return 'Amazon'; + case 'yelp': + return 'Yelp'; + case 'imdb': + return 'IMDB'; + case 'pinterest': + return 'Pinterest'; + case 'tripadvisor': + return 'TripAdvisor'; + case 'instagram': + return 'Instagram'; + case 'walmart': + return 'Walmart'; + case 'craigslist': + return 'Craigslist'; + case 'ebay': + return 'eBay'; + case 'linkedin': + return 'LinkedIn'; + case 'google': + return 'Google'; + case 'apple': + return 'Apple'; + default: + return null; + } + } } 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.dart b/lib/src/stream_chat.dart index e32c7fa1..2763db37 100644 --- a/lib/src/stream_chat.dart +++ b/lib/src/stream_chat.dart @@ -199,15 +199,10 @@ class StreamChatState extends State with WidgetsBindingObserver { .where((e) => e.user?.id != user.id) .where((e) => e.message.silent != true) .listen((event) async { - var channel = client.state.channels[event.cid]; - - if (channel == null) { - channel = client.channel( - event.type, - id: event.cid.split(':')[1], - ); - await channel.query(); - } + final channel = client.channel( + event.channelType, + id: event.channelId, + ); client.showLocalNotification( event.message, 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/unread_indicator.dart b/lib/src/unread_indicator.dart index 7e49e202..c9f42fad 100644 --- a/lib/src/unread_indicator.dart +++ b/lib/src/unread_indicator.dart @@ -1,47 +1,46 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; class UnreadIndicator extends StatelessWidget { const UnreadIndicator({ Key key, - @required this.channel, }) : super(key: key); - final Channel channel; - @override Widget build(BuildContext context) { + final client = StreamChat.of(context).client; return StreamBuilder( - stream: channel.state.unreadCountStream, - initialData: channel.state.unreadCount, - builder: (context, snapshot) { - if (!snapshot.hasData || snapshot.data == 0) { - return SizedBox(); - } - return Material( - borderRadius: BorderRadius.circular(8), - color: StreamChatTheme.of(context) - .channelPreviewTheme - .unreadCounterColor, - child: Padding( - padding: const EdgeInsets.only( - left: 5.0, - right: 5.0, - top: 2, - bottom: 1, - ), - child: Center( - child: Text( - '${snapshot.data}', - style: TextStyle( - fontSize: 11, - color: Colors.white, - ), + stream: client.state.totalUnreadCountStream, + initialData: client.state.totalUnreadCount, + builder: (context, snapshot) { + if (!snapshot.hasData || snapshot.data == 0) { + return SizedBox(); + } + return Material( + borderRadius: BorderRadius.circular(8), + color: StreamChatTheme.of(context) + .channelPreviewTheme + .unreadCounterColor, + child: Padding( + padding: const EdgeInsets.only( + left: 5.0, + right: 5.0, + top: 2, + bottom: 1, + ), + child: Center( + child: Text( + '${snapshot.data}', + style: TextStyle( + fontSize: 11, + color: Colors.white, ), ), ), - ); - }); + ), + ); + }, + ); } } diff --git a/lib/src/url_attachment.dart b/lib/src/url_attachment.dart new file mode 100644 index 00000000..c6b0b3f7 --- /dev/null +++ b/lib/src/url_attachment.dart @@ -0,0 +1,112 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/utils.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +class UrlAttachment extends StatelessWidget { + Attachment urlAttachment; + String hostDisplayName; + EdgeInsets textPadding; + + UrlAttachment({ + @required this.urlAttachment, + @required this.hostDisplayName, + @required this.textPadding, + }); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (urlAttachment.imageUrl != null) + SizedBox( + height: 16.0, + ), + if (urlAttachment.imageUrl != null) + Container( + clipBehavior: Clip.antiAliasWithSaveLayer, + margin: EdgeInsets.symmetric(horizontal: 8.0), + child: Stack( + children: [ + Center( + child: CachedNetworkImage( + imageUrl: urlAttachment.imageUrl, + ), + ), + Positioned( + left: 0.0, + bottom: -1, + child: Container( + child: Padding( + padding: const EdgeInsets.only( + top: 8.0, + left: 8.0, + right: 8.0, + ), + child: Text( + hostDisplayName, + style: TextStyle( + fontWeight: FontWeight.w700, + color: Color(0xFF006CFF), + ), + ), + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topRight: Radius.circular(16.0), + ), + color: Color(0xFFE9F2FF), + ), + ), + ), + ], + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.0), + ), + ), + Padding( + padding: textPadding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (urlAttachment.title != null) + Text( + urlAttachment.title, + maxLines: 1, + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 12.0, + ), + ), + if (urlAttachment.text != null) + Text( + urlAttachment.text, + style: TextStyle( + fontWeight: FontWeight.w400, + fontSize: 12.0, + ), + ), + ], + ), + ), + ], + ), + Positioned.fill( + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () => launchURL( + context, + urlAttachment.ogScrapeUrl, + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/src/user_avatar.dart b/lib/src/user_avatar.dart index a86206eb..86bf7abd 100644 --- a/lib/src/user_avatar.dart +++ b/lib/src/user_avatar.dart @@ -24,6 +24,10 @@ class UserAvatar extends StatelessWidget { @override Widget build(BuildContext context) { + final hasImage = user.extraData?.containsKey('image') == true && + user.extraData['image'] != null && + user.extraData['image'] != ''; + final streamChatTheme = StreamChatTheme.of(context); return GestureDetector( onTap: onTap != null ? () { @@ -36,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: user.extraData?.containsKey('image') ?? false + 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/pubspec.yaml b/pubspec.yaml index ce0b3633..a1642842 100644 --- a/pubspec.yaml +++ b/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.11 +version: 0.2.12 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -21,13 +21,13 @@ dependencies: cached_network_image: ^2.2.0+1 flutter_markdown: ^0.5.0 url_launcher: ^5.4.11 - video_player: ^0.10.12+5 - chewie: ^0.9.10 - file_picker: ^2.0.8+1 - image_picker: ^0.6.7+2 - flutter_keyboard_visibility: ^3.2.1 - stream_chat: ^0.2.10+1 emojis: ^0.9.3 + video_player: ^1.0.0 + chewie: ^0.10.4 + file_picker: ^2.0.12 + image_picker: ^0.6.7+2 + flutter_keyboard_visibility: ^3.3.0 + stream_chat: ^0.2.11 mime: ^0.9.6+3 visibility_detector: ^0.1.5 http_parser: ^3.1.4 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'),