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'),