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';
/// 
/// 
@@ -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'),