Merge branch 'feature/new-ui' into feature/file-picker

This commit is contained in:
Salvatore Giordano
2020-11-11 17:30:10 +01:00
29 changed files with 1178 additions and 224 deletions
+9
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
<svg width="80" height="40" viewBox="0 0 80 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M52.2984 12.8561L77.9046 11.0409C79.5948 10.9211 80.659 12.8259 79.6708 14.2023L61.7582 39.1527C61.3826 39.6759 60.7778 39.9863 60.1336 39.9863H19.9646C19.3214 39.9863 18.7174 39.6769 18.3414 39.1547L0.381197 14.2043C-0.609403 12.8281 0.454797 10.9207 2.1464 11.0409L27.6746 12.8563L38.578 0.666539C39.377 -0.226661 40.777 -0.221461 41.5694 0.677939L52.2984 12.8561ZM57.6608 36.0305L41.0426 29.8199V36.0305H57.6608ZM39.0426 36.0305V29.8199L22.4244 36.0305H39.0426ZM37.125 28.3363L20.146 34.6743L7.6548 17.3111L37.125 28.3363ZM42.9256 28.3363L59.9046 34.6743L72.3958 17.3111L42.9256 28.3363ZM39.0586 26.8871V5.98954L25.1758 21.6611L39.0586 26.8871ZM41.0426 26.8871V5.99214L54.9256 21.6611L41.0426 26.8871ZM21.1046 20.2165L24.2988 16.6273L8.6934 15.5089L21.1046 20.2165ZM58.8298 20.2165L55.6358 16.6273L71.2412 15.5089L58.8298 20.2165Z" fill="#006CFF"/>
</svg>

After

Width:  |  Height:  |  Size: 1004 B

+6
View File
@@ -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
@@ -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",
+286
View File
@@ -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<AdvancedOptionsPage> {
final _formKey = GlobalKey<FormState>();
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(),
],
),
),
);
},
),
);
}
}
+223
View File
@@ -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 = <String, User>{
'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(),
],
),
);
}
}
+101 -73
View File
@@ -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<String, dynamic> 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: {
+3 -3
View File
@@ -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(
+62
View File
@@ -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<String, dynamic> 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',
);
}
});
}
+39
View File
@@ -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<String>(
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),
),
);
},
),
);
}
}
+7 -2
View File
@@ -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:
+39 -26
View File
@@ -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(),
),
],
);
}
}
+4 -1
View File
@@ -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,
+5 -1
View File
@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
@@ -482,6 +483,8 @@ class _ChannelListViewState extends State<ChannelListView>
}
}
StreamSubscription _subscription;
@override
void initState() {
super.initState();
@@ -506,7 +509,7 @@ class _ChannelListViewState extends State<ChannelListView>
final client = StreamChat.of(context).client;
client
_subscription = client
.on(
EventType.connectionRecovered,
EventType.notificationAddedToChannel,
@@ -544,6 +547,7 @@ class _ChannelListViewState extends State<ChannelListView>
@override
void dispose() {
_subscription.cancel();
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
+16 -5
View File
@@ -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<List<Member>>(
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,
),
);
}
+48
View File
@@ -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<int>(
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,
),
),
),
),
);
},
);
}
}
+21 -10
View File
@@ -289,7 +289,9 @@ class _MessageListViewState extends State<MessageListView> {
return messageWidget;
},
),
if (widget.showScrollToBottom)
if (streamChannel.channel.state.members.contains((Member e) =>
e.userId == streamChannel.channel.client.state.user.id) &&
widget.showScrollToBottom)
StreamBuilder<int>(
stream: streamChannel.channel.state.unreadCountStream,
builder: (context, snapshot) {
@@ -374,8 +376,16 @@ class _MessageListViewState extends State<MessageListView> {
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<MessageListView> {
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;
+5 -1
View File
@@ -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,
+109 -29
View File
@@ -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<MessageWidget> {
);
}
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<MessageWidget> {
List<Widget> _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<MessageWidget> {
];
}
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<MessageWidget> {
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<MessageWidget> {
}
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<MessageWidget> {
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;
}
}
}
+1
View File
@@ -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) {
+4 -9
View File
@@ -199,15 +199,10 @@ class StreamChatState extends State<StreamChat> 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,
+6 -3
View File
@@ -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(
+30 -31
View File
@@ -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<int>(
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,
),
),
),
);
});
),
);
},
);
}
}
+112
View File
@@ -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,
),
),
),
),
],
);
}
}
+10 -22
View File
@@ -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: <Widget>[
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)
+5
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:url_launcher/url_launcher.dart';
Future<void> launchURL(BuildContext context, String url) async {
@@ -42,3 +43,7 @@ Future<bool> showConfirmationDialog(
},
);
}
/// Get random png with initials
String getRandomPicUrl(User user) =>
'https://getstream.io/random_png/?id=${user.id}&name=${user.name}';
+1
View File
@@ -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';
+7 -7
View File
@@ -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
+14 -1
View File
@@ -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'),