diff --git a/analysis_options.yaml b/analysis_options.yaml deleted file mode 100644 index 3ba5d599..00000000 --- a/analysis_options.yaml +++ /dev/null @@ -1,13 +0,0 @@ -include: package:pedantic/analysis_options.1.9.0.yaml - -analyzer: - exclude: - # Ignore generated files - - '**/*.g.dart' - - 'lib/src/generated/*.dart' - -linter: - rules: - public_member_api_docs: true - prefer_final_in_for_each: true - prefer_final_locals: true \ No newline at end of file diff --git a/example/ios/Flutter/.last_build_id b/example/ios/Flutter/.last_build_id deleted file mode 100644 index 1e10204b..00000000 --- a/example/ios/Flutter/.last_build_id +++ /dev/null @@ -1 +0,0 @@ -c1153e968f5d6bcc19c41ac2592825f6 \ No newline at end of file diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift deleted file mode 100644 index 12b4b519..00000000 --- a/example/ios/Runner/AppDelegate.swift +++ /dev/null @@ -1,45 +0,0 @@ -import UIKit -import Flutter - -@UIApplicationMain -@objc class AppDelegate: FlutterAppDelegate { - let sharedDefaults = UserDefaults(suiteName: "group.io.stream.flutter") - - override func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - - if #available(iOS 10.0, *) { - UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate - } - if let messageQueue = sharedDefaults?.stringArray(forKey: "messageQueue") { - UserDefaults.standard.setValue(messageQueue, forKey: "flutter.messageQueue") - sharedDefaults?.removeObject(forKey: "messageQueue") - } - - GeneratedPluginRegistrant.register(with: self) - return super.application(application, didFinishLaunchingWithOptions: launchOptions) - } - - override func applicationDidEnterBackground(_ application: UIApplication) { - if let apiKey = UserDefaults.standard.string(forKey: "flutter.KEY_API_KEY") { - sharedDefaults?.setValue(apiKey, forKey: "KEY_API_KEY") - } - - if let token = UserDefaults.standard.string(forKey: "flutter.KEY_TOKEN") { - sharedDefaults?.setValue(token, forKey: "KEY_TOKEN") - } - - if let userId = UserDefaults.standard.string(forKey: "flutter.KEY_USER_ID") { - sharedDefaults?.setValue(userId, forKey: "KEY_USER_ID") - } - } - - override func applicationWillEnterForeground(_ application: UIApplication) { - if let messageQueue = sharedDefaults?.stringArray(forKey: "messageQueue") { - UserDefaults.standard.setValue(messageQueue, forKey: "flutter.messageQueue") - sharedDefaults?.removeObject(forKey: "messageQueue") - } - } -} diff --git a/example/lib/main.dart b/example/lib/main.dart deleted file mode 100644 index 4b8418e9..00000000 --- a/example/lib/main.dart +++ /dev/null @@ -1,398 +0,0 @@ -import 'dart:io'; - -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: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 ? PushProvider.firebase : PushProvider.apn, - ); - } - }); -} - -void main() async { - final client = Client( - 's2dxdhpxd94g', - logLevel: Level.INFO, - showLocalNotification: - (!kIsWeb && Platform.isAndroid) ? showLocalNotification : null, - persistenceEnabled: true, - ); - - await client.setUser( - User(id: 'super-band-9'), - 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A', - ); - - if (!kIsWeb) { - _initNotifications(client); - } - - runApp(MyApp(client)); -} - -class MyApp extends StatelessWidget { - final Client client; - - MyApp(this.client); - - @override - Widget build(BuildContext context) { - return MaterialApp( - theme: ThemeData.light(), - darkTheme: ThemeData.dark(), - themeMode: ThemeMode.system, - builder: (context, widget) { - return StreamChat( - child: widget, - client: client, - ); - }, - home: ChannelListPage(), - ); - } -} - -class ChannelListPage extends StatelessWidget { - @override - Widget build(BuildContext context) { - return Scaffold( - floatingActionButton: FloatingActionButton( - child: Icon(Icons.add), - onPressed: () { - Navigator.of(context).push(MaterialPageRoute(builder: (context) { - return CreateChannelPage(); - })); - }, - ), - body: ChannelsBloc( - child: ChannelListView( - filter: { - 'members': { - '\$in': [StreamChat.of(context).user.id], - } - }, - sort: [SortOption('last_message_at')], - pagination: PaginationParams( - limit: 20, - ), - channelWidget: ChannelPage(), - ), - ), - ); - } -} - -class ChannelPage extends StatelessWidget { - const ChannelPage({ - Key key, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: ChannelHeader(), - body: Column( - children: [ - Expanded( - child: Stack( - children: [ - MessageListView( - threadBuilder: (_, parentMessage) { - return ThreadPage( - parent: parentMessage, - ); - }, - ), - Positioned.fill( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8.0, - vertical: 4, - ), - child: TypingIndicator( - alignment: Alignment.bottomRight, - ), - ), - ), - ], - ), - ), - MessageInput(), - ], - ), - ); - } -} - -class ThreadPage extends StatelessWidget { - final Message parent; - - ThreadPage({ - Key key, - this.parent, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: ThreadHeader( - parent: parent, - ), - body: Column( - children: [ - Expanded( - child: MessageListView( - parentMessage: parent, - ), - ), - if (parent.type != 'deleted') - MessageInput( - parentMessage: parent, - ), - ], - ), - ); - } -} - -class CreateChannelPage extends StatefulWidget { - @override - _CreateChannelPageState createState() => _CreateChannelPageState(); -} - -class _CreateChannelPageState extends State { - final ScrollController _scrollController = ScrollController(); - Client client; - List users = []; - List selectedUsers = []; - int offset = 0; - bool loading = false; - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - elevation: 0, - backgroundColor: Colors.transparent, - title: Text( - 'Create a channel', - style: Theme.of(context).textTheme.headline6, - ), - ), - floatingActionButton: - selectedUsers.isNotEmpty ? _buildFAB(context) : SizedBox(), - body: _buildListView(), - ); - } - - ListView _buildListView() { - return ListView.builder( - controller: _scrollController, - itemBuilder: _itemBuilder, - itemCount: users.length, - ); - } - - Widget _itemBuilder(context, i) { - final user = users[i]; - return ListTile( - onLongPress: () { - _selectUser(user); - }, - selected: selectedUsers.contains(user), - onTap: () { - if (selectedUsers.isNotEmpty) { - return _selectUser(user); - } - _createChannel(context, [user]); - }, - leading: UserAvatar( - user: user, - ), - title: Text(user.name), - ); - } - - Widget _buildFAB(BuildContext context) { - return FloatingActionButton( - child: Icon(Icons.done), - onPressed: () async { - String name; - if (selectedUsers.length > 1) { - name = await _showEnterNameDialog(context); - if (name?.isNotEmpty != true) { - return; - } - } - - _createChannel(context, selectedUsers, name); - }, - ); - } - - Future _showEnterNameDialog(BuildContext context) { - final controller = TextEditingController(); - return showDialog( - context: context, - builder: (context) => SimpleDialog( - contentPadding: const EdgeInsets.all(16), - title: Text('Enter a name for the channel'), - children: [ - TextField( - controller: controller, - decoration: InputDecoration( - border: OutlineInputBorder(), - ), - ), - ButtonBar( - children: [ - FlatButton( - onPressed: () => Navigator.pop(context), - child: Text('Cancel'), - ), - FlatButton( - onPressed: () => Navigator.pop(context, controller.text), - child: Text('Ok'), - ), - ], - ), - ], - ), - ); - } - - Future _createChannel( - BuildContext context, - List users, [ - String name, - ]) async { - final channel = client.channel('messaging', extraData: { - 'members': [ - client.state.user.id, - ...users.map((e) => e.id), - ], - if (name != null) 'name': name, - }); - await channel.watch(); - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) { - return StreamChannel( - child: ChannelPage(), - channel: channel, - ); - }, - ), - ); - } - - void _selectUser(User user) { - if (!selectedUsers.contains(user)) { - setState(() { - selectedUsers.add(user); - }); - } else { - setState(() { - selectedUsers.remove(user); - }); - } - } - - @override - void initState() { - super.initState(); - - client = StreamChat.of(context).client; - - _scrollController.addListener(() async { - if (!loading && - _scrollController.offset >= - _scrollController.position.maxScrollExtent - 100) { - offset += 25; - await _queryUsers(); - } - }); - - _queryUsers(); - } - - Future _queryUsers() { - loading = true; - return client.queryUsers( - pagination: PaginationParams( - limit: 25, - offset: offset, - ), - sort: [ - SortOption( - 'name', - direction: SortOption.ASC, - ), - ], - ).then((value) { - setState(() { - users = [ - ...users, - ...value.users, - ]; - }); - }).whenComplete(() => loading = false); - } -} diff --git a/example/pubspec.yaml b/example/pubspec.yaml deleted file mode 100644 index ca750779..00000000 --- a/example/pubspec.yaml +++ /dev/null @@ -1,24 +0,0 @@ -name: example -description: A new Flutter project. -version: 1.0.27+28 - -environment: - sdk: ">=2.2.2 <3.0.0" - -dependencies: - flutter: - sdk: flutter - stream_chat_flutter: - path: ../ - flutter_apns: ^1.3.1 - flutter_local_notifications: ^2.0.0 - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_driver: - sdk: flutter - test: any - -flutter: - uses-material-design: true diff --git a/lib/src/channel_list_view.dart b/lib/src/channel_list_view.dart deleted file mode 100644 index 51c02611..00000000 --- a/lib/src/channel_list_view.dart +++ /dev/null @@ -1,472 +0,0 @@ -import 'dart:convert'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_flutter/src/channels_bloc.dart'; - -import '../stream_chat_flutter.dart'; -import 'channel_preview.dart'; -import 'stream_channel.dart'; -import 'stream_chat.dart'; - -/// Callback called when tapping on a channel -typedef ChannelTapCallback = void Function(Channel, Widget); - -/// Builder used to create a custom [ChannelPreview] from a [Channel] -typedef ChannelPreviewBuilder = Widget Function(BuildContext, Channel); - -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_list_view.png) -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_list_view_paint.png) -/// -/// It shows the list of current channels. -/// -/// ```dart -/// class ChannelListPage extends StatelessWidget { -/// @override -/// Widget build(BuildContext context) { -/// return Scaffold( -/// body: ChannelListView( -/// filter: { -/// 'members': { -/// '\$in': [StreamChat.of(context).user.id], -/// } -/// }, -/// sort: [SortOption('last_message_at')], -/// pagination: PaginationParams( -/// limit: 20, -/// ), -/// channelWidget: ChannelPage(), -/// ), -/// ); -/// } -/// } -/// ``` -/// -/// -/// Make sure to have a [StreamChat] ancestor in order to provide the information about the channels. -/// The widget uses a [ListView.custom] to render the list of channels. -/// -/// The widget components render the ui based on the first ancestor of type [StreamChatTheme]. -/// Modify it to change the widget appearance. -class ChannelListView extends StatefulWidget { - /// Instantiate a new ChannelListView - ChannelListView({ - Key key, - this.filter, - this.options, - this.sort, - this.pagination, - this.onChannelTap, - this.onChannelLongPress, - this.channelWidget, - this.channelPreviewBuilder, - this.separatorBuilder, - this.errorBuilder, - this.emptyBuilder, - this.onImageTap, - this.loadingBuilder, - this.pullToRefresh = true, - }) : super(key: key); - - /// The builder that will be used in case of error - final Widget Function(Error error) errorBuilder; - - /// The builder used when the channel list is empty. - final WidgetBuilder emptyBuilder; - - /// The query filters to use. - /// You can query on any of the custom fields you've defined on the [Channel]. - /// You can also filter other built-in channel fields. - final Map filter; - - /// The builder used while loading. - final WidgetBuilder loadingBuilder; - - /// Query channels options. - /// - /// state: if true returns the Channel state - /// watch: if true listen to changes to this Channel in real time. - final Map options; - - /// The sorting used for the channels matching the filters. - /// Sorting is based on field and direction, multiple sorting options can be provided. - /// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count. - /// Direction can be ascending or descending. - final List sort; - - /// Pagination parameters - /// limit: the number of channels to return (max is 30) - /// offset: the offset (max is 1000) - /// message_limit: how many messages should be included to each channel - final PaginationParams pagination; - - /// Function called when tapping on a channel - /// By default it calls [Navigator.push] building a [MaterialPageRoute] - /// with the widget [channelWidget] as child. - final ChannelTapCallback onChannelTap; - - /// Function called when long pressing on a channel - final Function(Channel) onChannelLongPress; - - /// Widget used when opening a channel - final Widget channelWidget; - - /// Builder used to create a custom channel preview - final ChannelPreviewBuilder channelPreviewBuilder; - - /// Builder used to create a custom item separator - final Function(BuildContext, int) separatorBuilder; - - /// The function called when the image is tapped - final Function(Channel) onImageTap; - - /// Set it to false to disable the pull-to-refresh widget - final bool pullToRefresh; - - @override - _ChannelListViewState createState() => _ChannelListViewState(); -} - -class _ChannelListViewState extends State - with WidgetsBindingObserver { - final ScrollController _scrollController = ScrollController(); - - @override - Widget build(BuildContext context) { - final channelsBloc = ChannelsBloc.of(context); - - if (!widget.pullToRefresh) { - return _buildListView(channelsBloc); - } - - return RefreshIndicator( - onRefresh: () async { - return channelsBloc.queryChannels( - filter: widget.filter, - sortOptions: widget.sort, - paginationParams: widget.pagination, - options: widget.options, - ); - }, - child: _buildListView(channelsBloc), - ); - } - - StreamBuilder> _buildListView( - ChannelsBlocState channelsBlocState, - ) { - return StreamBuilder>( - stream: channelsBlocState.channelsStream, - builder: (context, snapshot) { - if (snapshot.hasError) { - if (snapshot.error is Error) { - print((snapshot.error as Error).stackTrace); - } - - if (widget.errorBuilder != null) { - return widget.errorBuilder(snapshot.error); - } - - var message = snapshot.error.toString(); - if (snapshot.error is DioError) { - final dioError = snapshot.error as DioError; - if (dioError.type == DioErrorType.RESPONSE) { - message = dioError.message; - } else { - message = 'Check your connection and retry'; - } - } - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text.rich( - TextSpan( - children: [ - WidgetSpan( - child: Padding( - padding: const EdgeInsets.only( - right: 2.0, - ), - child: Icon(Icons.error_outline), - ), - ), - TextSpan(text: 'Error loading channels'), - ], - ), - style: Theme.of(context).textTheme.headline6, - ), - Padding( - padding: const EdgeInsets.only( - top: 16.0, - ), - child: Text(message), - ), - FlatButton( - onPressed: () { - channelsBlocState.queryChannels( - filter: widget.filter, - sortOptions: widget.sort, - paginationParams: widget.pagination, - options: widget.options, - ); - }, - child: Text('Retry'), - ), - ], - ), - ); - } - - if (!snapshot.hasData) { - return LayoutBuilder( - builder: (context, viewportConstraints) { - return SingleChildScrollView( - physics: AlwaysScrollableScrollPhysics(), - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: viewportConstraints.maxHeight, - ), - child: widget.loadingBuilder != null - ? widget.loadingBuilder(context) - : Center( - child: CircularProgressIndicator(), - ), - ), - ); - }, - ); - } - - final channels = snapshot.data; - - if (channels.isEmpty && widget.emptyBuilder != null) { - return widget.emptyBuilder(context); - } - - if (channels.isEmpty && widget.emptyBuilder == null) { - return LayoutBuilder( - builder: (context, viewportConstraints) { - return SingleChildScrollView( - physics: AlwaysScrollableScrollPhysics(), - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: viewportConstraints.maxHeight, - ), - child: Center( - child: Text('You have no channels currently'), - ), - ), - ); - }, - ); - } - - return ListView.custom( - physics: AlwaysScrollableScrollPhysics(), - controller: _scrollController, - childrenDelegate: SliverChildBuilderDelegate( - (context, i) { - return _itemBuilder(context, i, channels); - }, - childCount: (channels.length * 2) + 1, - findChildIndexCallback: (key) { - final ValueKey valueKey = key; - final index = channels.indexWhere( - (channel) => 'CHANNEL-${channel.id}' == valueKey.value); - return index != -1 ? (index * 2) : null; - }, - ), - ); - }); - } - - Widget _itemBuilder(context, int i, List channels) { - if (i % 2 != 0) { - if (widget.separatorBuilder != null) { - return widget.separatorBuilder(context, i); - } - return _separatorBuilder(context, i); - } - - i = i ~/ 2; - - final channelsProvider = ChannelsBloc.of(context); - if (i < channels.length) { - final channel = channels[i]; - - ChannelTapCallback onTap; - if (widget.onChannelTap != null) { - onTap = widget.onChannelTap; - } else { - onTap = (client, _) { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return StreamChannel( - child: widget.channelWidget, - channel: client, - ); - }, - ), - ); - }; - } - - return StreamChannel( - key: ValueKey('CHANNEL-${channel.id}'), - channel: channel, - child: Builder( - builder: (context) { - Widget child; - if (widget.channelPreviewBuilder != null) { - child = widget.channelPreviewBuilder( - context, - channel, - ); - } else { - child = ChannelPreview( - onLongPress: widget.onChannelLongPress, - channel: channel, - onImageTap: widget.onImageTap != null - ? () { - widget.onImageTap(channel); - } - : null, - onTap: (channel) { - onTap(channel, widget.channelWidget); - }, - ); - } - return child; - }, - ), - ); - } else { - if (widget.loadingBuilder != null) { - return widget.loadingBuilder(context); - } - return _buildQueryProgressIndicator(context, channelsProvider); - } - } - - Widget _buildQueryProgressIndicator( - context, ChannelsBlocState channelsProvider) { - return StreamBuilder( - stream: channelsProvider.queryChannelsLoading, - initialData: false, - builder: (context, snapshot) { - if (snapshot.hasError) { - return Container( - color: Color(0xffd0021B).withAlpha(26), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: Center( - child: Text('Error loading channels'), - ), - ), - ); - } - return Container( - height: 100, - padding: EdgeInsets.all(32), - child: Center( - child: snapshot.data ? CircularProgressIndicator() : Container(), - ), - ); - }); - } - - Widget _separatorBuilder(context, i) { - return Container( - height: 1, - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white.withOpacity(0.1) - : Colors.black.withOpacity(0.1), - margin: EdgeInsets.symmetric(horizontal: 16), - ); - } - - void _listenChannelPagination(ChannelsBlocState channelsProvider) { - if (_scrollController.position.maxScrollExtent == - _scrollController.offset && - _scrollController.offset != 0) { - channelsProvider.queryChannels( - filter: widget.filter, - sortOptions: widget.sort, - paginationParams: widget.pagination.copyWith( - offset: channelsProvider.channels?.length ?? 0, - ), - options: widget.options, - ); - } - } - - @override - void initState() { - super.initState(); - - WidgetsBinding.instance.addObserver(this); - - final channelsBloc = ChannelsBloc.of(context); - channelsBloc.queryChannels( - filter: widget.filter, - sortOptions: widget.sort, - paginationParams: widget.pagination, - options: widget.options, - ); - - _scrollController.addListener(() { - channelsBloc.queryChannelsLoading.first.then((loading) { - if (!loading) { - _listenChannelPagination(channelsBloc); - } - }); - }); - - final client = StreamChat.of(context).client; - - client - .on( - EventType.connectionRecovered, - EventType.notificationAddedToChannel, - EventType.notificationMessageNew, - EventType.channelVisible, - ) - .listen((event) { - channelsBloc.queryChannels( - filter: widget.filter, - sortOptions: widget.sort, - paginationParams: widget.pagination, - options: widget.options, - ); - }); - } - - @override - void didUpdateWidget(ChannelListView oldWidget) { - super.didUpdateWidget(oldWidget); - - if (widget.filter?.toString() != oldWidget.filter?.toString() || - jsonEncode(widget.sort) != jsonEncode(oldWidget.sort) || - widget.pagination?.toJson()?.toString() != - oldWidget.pagination?.toJson()?.toString() || - widget.options?.toString() != oldWidget.options?.toString()) { - final channelsBloc = ChannelsBloc.of(context); - channelsBloc.queryChannels( - filter: widget.filter, - sortOptions: widget.sort, - paginationParams: widget.pagination, - options: widget.options, - ); - } - } - - @override - void dispose() { - WidgetsBinding.instance.removeObserver(this); - super.dispose(); - } -} diff --git a/lib/src/channel_preview.dart b/lib/src/channel_preview.dart deleted file mode 100644 index 61603055..00000000 --- a/lib/src/channel_preview.dart +++ /dev/null @@ -1,176 +0,0 @@ -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'; - -/// ![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) -/// -/// It shows the current [Channel] preview. -/// -/// The widget uses a [StreamBuilder] to render the channel information image as soon as it updates. -/// -/// Usually you don't use this widget as it's the default channel preview used by [ChannelListView]. -/// -/// The widget renders the ui based on the first ancestor of type [StreamChatTheme]. -/// Modify it to change the widget appearance. -class ChannelPreview extends StatelessWidget { - /// Function called when tapping this widget - final void Function(Channel) onTap; - - /// Function called when long pressing this widget - final void Function(Channel) onLongPress; - - /// Channel displayed - final Channel channel; - - /// The function called when the image is tapped - final VoidCallback onImageTap; - - ChannelPreview({ - @required this.channel, - Key key, - this.onTap, - this.onLongPress, - this.onImageTap, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return ListTile( - onTap: () { - if (onTap != null) { - onTap(channel); - } - }, - onLongPress: () { - if (onLongPress != null) { - onLongPress(channel); - } - }, - leading: ChannelImage( - onTap: onImageTap, - ), - title: ChannelName( - textStyle: StreamChatTheme.of(context).channelPreviewTheme.title, - ), - subtitle: _buildSubtitle(context), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - _buildDate(context), - if (channel.state.unreadCount > 0) - UnreadIndicator( - channel: channel, - ), - ], - ), - ); - } - - Widget _buildDate(BuildContext context) { - return StreamBuilder( - stream: channel.lastMessageAtStream, - initialData: channel.lastMessageAt, - builder: (context, snapshot) { - if (!snapshot.hasData) { - return SizedBox(); - } - final lastMessageAt = snapshot.data.toLocal(); - - String stringDate; - final now = DateTime.now(); - - if (now.year != lastMessageAt.year || - now.month != lastMessageAt.month || - now.day != lastMessageAt.day) { - stringDate = Jiffy(lastMessageAt.toLocal()).format('dd/MM/yyyy'); - } else { - stringDate = Jiffy(lastMessageAt.toLocal()).format('HH:mm'); - } - - return Text( - stringDate, - style: StreamChatTheme.of(context).channelPreviewTheme.lastMessageAt, - ); - }, - ); - } - - Widget _buildSubtitle(BuildContext context) { - return StreamBuilder( - initialData: channel.state.unreadCount, - stream: channel.state.unreadCountStream, - builder: (context, snapshot) { - final opacity = (snapshot.data ?? 0) > 0 ? 1.0 : 0.5; - return TypingIndicator( - channel: channel, - alternativeWidget: _buildLastMessage(context, opacity), - style: - StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith( - color: StreamChatTheme.of(context) - .channelPreviewTheme - .subtitle - .color - .withOpacity(opacity), - ), - ); - }, - ); - } - - Widget _buildLastMessage(BuildContext context, double opacity) { - return StreamBuilder>( - stream: channel.state.messagesStream, - initialData: channel.state.messages, - builder: (context, snapshot) { - final lastMessage = snapshot.data?.lastWhere( - (m) => m.shadowed != true, - orElse: () => null, - ); - if (lastMessage == null) { - return SizedBox(); - } - - var text = lastMessage.text; - if (lastMessage.isDeleted) { - text = 'This message was deleted.'; - } else if (lastMessage.attachments != null) { - final prefix = lastMessage.attachments - .map((e) { - if (e.type == 'image') { - return '๐Ÿ“ท'; - } else if (e.type == 'video') { - return '๐ŸŽฌ'; - } else if (e.type == 'giphy') { - return 'GIF'; - } - return null; - }) - .where((e) => e != null) - .join(' '); - - text = '$prefix ${lastMessage.text ?? ''}'; - } - - return Text( - text, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: - StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith( - color: StreamChatTheme.of(context) - .channelPreviewTheme - .subtitle - .color - .withOpacity(opacity), - ), - ); - }, - ); - } -} diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart deleted file mode 100644 index c8d089f1..00000000 --- a/lib/src/message_input.dart +++ /dev/null @@ -1,1084 +0,0 @@ -import 'dart:async'; - -import 'package:file_picker/file_picker.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; -import 'package:http_parser/http_parser.dart'; -import 'package:image_picker/image_picker.dart'; -import 'package:mime/mime.dart'; -import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_flutter/src/message_list_view.dart'; -import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; -import 'package:stream_chat_flutter/src/user_avatar.dart'; - -import '../stream_chat_flutter.dart'; -import 'stream_channel.dart'; - -typedef FileUploader = Future Function(PlatformFile, Channel); -typedef AttachmentThumbnailBuilder = Widget Function( - BuildContext, - _SendingAttachment, -); - -enum ActionsLocation { - left, - right, -} - -enum SendButtonLocation { - inside, - outside, -} - -enum DefaultAttachmentTypes { - image, - video, - file, -} - -/// Inactive state -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_input.png) -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_input_paint.png) -/// Focused state -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_input2.png) -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_input2_paint.png) -/// -/// Widget used to enter the message and add attachments -/// -/// ```dart -/// class ChannelPage extends StatelessWidget { -/// const ChannelPage({ -/// Key key, -/// }) : super(key: key); -/// -/// @override -/// Widget build(BuildContext context) { -/// return Scaffold( -/// appBar: ChannelHeader(), -/// body: Column( -/// children: [ -/// Expanded( -/// child: MessageListView( -/// threadBuilder: (_, parentMessage) { -/// return ThreadPage( -/// parent: parentMessage, -/// ); -/// }, -/// ), -/// ), -/// MessageInput(), -/// ], -/// ), -/// ); -/// } -/// } -/// ``` -/// -/// You usually put this widget in the same page of a [MessageListView] as the bottom widget. -/// -/// The widget renders the ui based on the first ancestor of type [StreamChatTheme]. -/// Modify it to change the widget appearance. -class MessageInput extends StatefulWidget { - /// Instantiate a new MessageInput - MessageInput({ - Key key, - this.onMessageSent, - this.preMessageSending, - this.parentMessage, - this.editMessage, - this.maxHeight = 150, - this.keyboardType = TextInputType.multiline, - this.disableAttachments = false, - this.doImageUploadRequest, - this.doFileUploadRequest, - this.initialMessage, - this.textEditingController, - this.actions, - this.actionsLocation = ActionsLocation.left, - this.attachmentThumbnailBuilders, - this.inputTextStyle, - this.attachmentIconColor, - this.autofocus = false, - this.sendButtonLocation = SendButtonLocation.inside, - this.animationDuration = const Duration(milliseconds: 300), - }) : super(key: key); - - /// Message to edit - final Message editMessage; - - /// Message to start with - final Message initialMessage; - - /// If set true TextField will be active by default. Default is false. - final bool autofocus; - - /// Function called after sending the message - final void Function(Message) onMessageSent; - - /// Function called right before sending the message - /// Use this to transform the message - final FutureOr Function(Message) preMessageSending; - - /// Parent message in case of a thread - final Message parentMessage; - - /// Maximum Height for the TextField to grow before it starts scrolling - final double maxHeight; - - /// The keyboard type assigned to the TextField - final TextInputType keyboardType; - - /// If true the attachments button will not be displayed - final bool disableAttachments; - - /// Override image upload request - final FileUploader doImageUploadRequest; - - /// Override file upload request - final FileUploader doFileUploadRequest; - - /// The text controller of the TextField - final TextEditingController textEditingController; - - /// List of action widgets - final List actions; - - /// The location of the custom actions - final ActionsLocation actionsLocation; - - /// The location of the send button - final SendButtonLocation sendButtonLocation; - - /// Map that defines a thumbnail builder for an attachment type - final Map attachmentThumbnailBuilders; - - /// Text style used in message text field. If null, [MessageInput] uses - /// `Theme.of(context).textTheme.bodyText2`. - final TextStyle inputTextStyle; - - /// The duration of the send button animation - final Duration animationDuration; - - /// Color used for attachment icon. - final Color attachmentIconColor; - - @override - MessageInputState createState() => MessageInputState(); - - /// Use this method to get the current [StreamChatState] instance - static MessageInputState of(BuildContext context) { - MessageInputState messageInputState; - - messageInputState = context.findAncestorStateOfType(); - - if (messageInputState == null) { - throw Exception( - 'You must have a MessageInput widget as anchestor of your widget tree'); - } - - return messageInputState; - } -} - -class MessageInputState extends State { - final List<_SendingAttachment> _attachments = []; - final _focusNode = FocusNode(); - final List _mentionedUsers = []; - - final _imagePicker = ImagePicker(); - bool _inputEnabled = true; - bool _messageIsPresent = false; - bool _typingStarted = false; - OverlayEntry _commandsOverlay, _mentionsOverlay; - - /// The editing controller passed to the input TextField - TextEditingController textEditingController; - - @override - Widget build(BuildContext context) { - return SafeArea( - child: GestureDetector( - onPanUpdate: (details) { - if (details.delta.dy > 0) { - _focusNode.unfocus(); - } - }, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Expanded( - child: Stack( - clipBehavior: Clip.none, - children: [ - _buildBorder(context), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildAttachments(), - _buildTextField(context), - ], - ), - ], - ), - ), - if (widget.sendButtonLocation == SendButtonLocation.outside) - _animateSendButton(context), - ], - ), - ), - ), - ); - } - - Flex _buildTextField(BuildContext context) { - return Flex( - direction: Axis.horizontal, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - if (!widget.disableAttachments) _buildAttachmentButton(), - if (widget.actionsLocation == ActionsLocation.left) - ...widget.actions ?? [], - _buildTextInput(context), - if (widget.actionsLocation == ActionsLocation.right) - ...widget.actions ?? [], - if (widget.sendButtonLocation == SendButtonLocation.inside) - _animateSendButton(context), - ], - ); - } - - Widget _animateSendButton(BuildContext context) { - if (widget.animationDuration == Duration.zero) { - return _buildSendButton(context); - } - return AnimatedCrossFade( - crossFadeState: ((_messageIsPresent || _attachments.isNotEmpty) && - _attachments.every((a) => a.uploaded == true)) - ? CrossFadeState.showFirst - : CrossFadeState.showSecond, - firstChild: _buildSendButton(context), - secondChild: SizedBox(), - duration: widget.animationDuration, - alignment: Alignment.center, - ); - } - - Expanded _buildTextInput(BuildContext context) { - return Expanded( - child: LimitedBox( - maxHeight: widget.maxHeight, - child: TextField( - key: Key('messageInputText'), - enabled: _inputEnabled, - minLines: null, - maxLines: null, - onSubmitted: (_) { - sendMessage(); - }, - keyboardType: widget.keyboardType, - controller: textEditingController, - focusNode: _focusNode, - onTap: () { - setState(() { - _typingStarted = true; - }); - }, - style: widget.inputTextStyle ?? Theme.of(context).textTheme.bodyText2, - autofocus: widget.autofocus, - decoration: - StreamChatTheme.of(context).channelTheme.messageInputDecoration ?? - InputDecoration( - hintText: 'Write a message', - hintStyle: widget.inputTextStyle ?? - Theme.of(context).textTheme.bodyText2, - prefixText: ' ', - border: InputBorder.none, - ), - textCapitalization: TextCapitalization.sentences, - ), - ), - ); - } - - Positioned _buildBorder(BuildContext context) { - return Positioned.fill( - child: Container( - width: MediaQuery.of(context).size.width, - padding: EdgeInsets.all(2), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10.0), - gradient: _getGradient(context), - ), - child: Container( - decoration: BoxDecoration( - color: StreamChatTheme.of(context) - .channelTheme - .inputBackground - .withAlpha(255), - borderRadius: BorderRadius.circular(10.0), - ), - child: Container( - decoration: BoxDecoration( - color: StreamChatTheme.of(context).channelTheme.inputBackground, - borderRadius: BorderRadius.circular(10.0), - border: Border.all( - color: _typingStarted - ? Colors.transparent - : Theme.of(context).brightness == Brightness.dark - ? Colors.white.withOpacity(.2) - : Colors.black.withOpacity(.2)), - ), - ), - ), - ), - ); - } - - OverlayEntry _buildCommandsOverlayEntry() { - final text = textEditingController.text; - final commands = StreamChannel.of(context) - .channel - .config - .commands - .where((c) => c.name.contains(text.replaceFirst('/', ''))) - .toList(); - - RenderBox renderBox = context.findRenderObject(); - final size = renderBox.size; - - return OverlayEntry(builder: (context) { - return Positioned( - bottom: size.height + MediaQuery.of(context).viewInsets.bottom, - left: 0, - right: 0, - child: Material( - color: StreamChatTheme.of(context).primaryColor, - child: Container( - constraints: BoxConstraints.loose(Size.fromHeight(400)), - decoration: BoxDecoration( - boxShadow: [ - BoxShadow( - spreadRadius: -8, - blurRadius: 5.0, - offset: Offset(0, -4), - ), - ], - color: StreamChatTheme.of(context).primaryColor, - ), - child: ListView( - padding: const EdgeInsets.all(0), - shrinkWrap: true, - children: commands - .map( - (c) => ListTile( - title: Text.rich( - TextSpan( - text: '${c.name}', - style: TextStyle(fontWeight: FontWeight.bold), - children: [ - TextSpan( - text: ' ${c.args}', - style: TextStyle( - fontWeight: FontWeight.w300, - ), - ), - ], - ), - ), - subtitle: Text(c.description), - onTap: () { - _setCommand(c); - }, - ), - ) - .toList(), - ), - ), - ), - ); - }); - } - - OverlayEntry _buildMentionsOverlayEntry() { - final splits = textEditingController.text - .substring(0, textEditingController.value.selection.baseOffset) - .split('@'); - final query = splits.last.toLowerCase(); - - Future> queryMembers; - - if (query.isNotEmpty) { - queryMembers = StreamChannel.of(context).channel.queryMembers( - filter: { - 'name': { - '\$autocomplete': query, - }, - }, - sort: [ - SortOption( - 'name', - direction: SortOption.ASC, - ), - ], - ).then((res) => res.members); - } - - final members = StreamChannel.of(context).channel.state.members?.where((m) { - return m.user.name.toLowerCase().contains(query); - })?.toList() ?? - []; - - RenderBox renderBox = context.findRenderObject(); - final size = renderBox.size; - - return OverlayEntry(builder: (context) { - return Positioned( - bottom: size.height + MediaQuery.of(context).viewInsets.bottom, - left: 0, - right: 0, - child: Material( - color: StreamChatTheme.of(context).primaryColor, - child: Container( - constraints: BoxConstraints.loose(Size.fromHeight(400)), - decoration: BoxDecoration( - boxShadow: [ - BoxShadow( - spreadRadius: -8, - blurRadius: 5.0, - offset: Offset(0, -4), - ), - ], - color: StreamChatTheme.of(context).primaryColor, - ), - child: FutureBuilder>( - future: queryMembers ?? Future.value(members), - initialData: members, - builder: (context, snapshot) { - return ListView( - padding: const EdgeInsets.all(0), - shrinkWrap: true, - children: (snapshot.data ?? members) - .map((m) => ListTile( - leading: UserAvatar( - user: m.user, - ), - title: Text('${m.user.name}'), - onTap: () { - _mentionedUsers.add(m.user); - - splits[splits.length - 1] = m.user.name; - final rejoin = splits.join('@'); - - textEditingController.value = TextEditingValue( - text: rejoin + - textEditingController.text.substring( - textEditingController - .selection.baseOffset), - selection: TextSelection.collapsed( - offset: rejoin.length, - ), - ); - - _mentionsOverlay?.remove(); - _mentionsOverlay = null; - }, - )) - .toList(), - ); - }), - ), - ), - ); - }); - } - - void _setCommand(Command c) { - textEditingController.value = TextEditingValue( - text: '/${c.name} ', - selection: TextSelection.collapsed( - offset: c.name.length + 2, - ), - ); - _commandsOverlay?.remove(); - _commandsOverlay = null; - } - - Gradient _getGradient(BuildContext context) { - if (_typingStarted) { - if (widget.editMessage == null) { - return StreamChatTheme.of(context).channelTheme.inputGradient; - } - return LinearGradient( - colors: [Colors.lightGreen, Colors.green], - ); - } else { - return null; - } - } - - Widget _buildAttachments() { - return Wrap( - direction: Axis.horizontal, - children: _attachments - .map( - (attachment) => Padding( - padding: const EdgeInsets.all(8.0), - child: ClipRRect( - borderRadius: BorderRadius.circular(10), - child: Stack( - children: [ - Container( - height: 50, - width: 50, - child: _buildAttachment(attachment), - ), - _buildRemoveButton(attachment), - attachment.uploaded - ? SizedBox() - : Positioned.fill( - child: Center( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: CircularProgressIndicator(), - ), - ), - ), - ], - ), - ), - ), - ) - .toList(), - ); - } - - Positioned _buildRemoveButton(_SendingAttachment attachment) { - return Positioned( - height: 16, - width: 16, - top: 4, - right: 4, - child: RawMaterialButton( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - elevation: 0, - highlightElevation: 0, - focusElevation: 0, - disabledElevation: 0, - hoverElevation: 0, - onPressed: () { - setState(() { - _attachments.remove(attachment); - }); - }, - fillColor: Colors.white.withOpacity(.5), - child: Center( - child: Icon( - Icons.close, - size: 15, - ), - ), - ), - ); - } - - Widget _buildAttachment(_SendingAttachment attachment) { - if (widget.attachmentThumbnailBuilders - ?.containsKey(attachment.attachment.type) == - true) { - return widget.attachmentThumbnailBuilders[attachment.attachment.type]( - context, - attachment, - ); - } - - switch (attachment.attachment.type) { - case 'image': - case 'giphy': - return attachment.file != null - ? Image.memory( - attachment.file.bytes, - fit: BoxFit.cover, - ) - : Image.network( - attachment.attachment.imageUrl ?? - attachment.attachment.thumbUrl, - fit: BoxFit.cover, - ); - break; - case 'video': - return Container( - child: Icon(Icons.videocam), - color: Colors.black26, - ); - break; - default: - return Container( - child: Icon(Icons.insert_drive_file), - color: Colors.black26, - ); - } - } - - Material _buildAttachmentButton() { - return Material( - clipBehavior: Clip.hardEdge, - type: MaterialType.transparency, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(32), - ), - child: IconButton( - onPressed: () { - showAttachmentModal(); - }, - icon: Icon( - Icons.add_circle_outline, - color: widget.attachmentIconColor, - ), - ), - ); - } - - /// Show the attachment modal, making the user choose where to pick a media from - void showAttachmentModal() { - if (_focusNode.hasFocus) { - _focusNode.unfocus(); - } - - showModalBottomSheet( - clipBehavior: Clip.hardEdge, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(32), - topRight: Radius.circular(32), - ), - ), - context: context, - isScrollControlled: true, - builder: (_) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - title: Text( - 'Add a file', - style: TextStyle( - fontWeight: FontWeight.bold, - ), - ), - ), - ListTile( - leading: Icon(Icons.image), - title: Text('Upload a photo'), - onTap: () { - pickFile(DefaultAttachmentTypes.image, false); - Navigator.pop(context); - }, - ), - ListTile( - leading: Icon(Icons.video_library), - title: Text('Upload a video'), - onTap: () { - pickFile(DefaultAttachmentTypes.video, false); - Navigator.pop(context); - }, - ), - if (!kIsWeb) - ListTile( - leading: Icon(Icons.camera_alt), - title: Text('Photo from camera'), - onTap: () { - pickFile(DefaultAttachmentTypes.image, true); - Navigator.pop(context); - }, - ), - if (!kIsWeb) - ListTile( - leading: Icon(Icons.videocam), - title: Text('Video from camera'), - onTap: () { - pickFile(DefaultAttachmentTypes.video, true); - Navigator.pop(context); - }, - ), - ListTile( - leading: Icon(Icons.insert_drive_file), - title: Text('Upload a file'), - onTap: () { - pickFile(DefaultAttachmentTypes.file, false); - Navigator.pop(context); - }, - ), - ], - ); - }); - } - - /// Add an attachment to the sending message - /// Use this to add custom type attachments - void addAttachment(Attachment attachment) { - setState(() { - _attachments.add(_SendingAttachment( - attachment: attachment, - uploaded: true, - )); - }); - } - - /// Pick a file from the device - /// If [camera] is true then the camera will open - void pickFile(DefaultAttachmentTypes fileType, [bool camera = false]) async { - setState(() { - _inputEnabled = false; - }); - - PlatformFile file; - String attachmentType; - - if (fileType == DefaultAttachmentTypes.image) { - attachmentType = 'image'; - } else if (fileType == DefaultAttachmentTypes.video) { - attachmentType = 'video'; - } else if (fileType == DefaultAttachmentTypes.file) { - attachmentType = 'file'; - } - - if (camera) { - PickedFile pickedFile; - if (fileType == DefaultAttachmentTypes.image) { - pickedFile = await _imagePicker.getImage(source: ImageSource.camera); - } else if (fileType == DefaultAttachmentTypes.video) { - pickedFile = await _imagePicker.getVideo(source: ImageSource.camera); - } - final bytes = await pickedFile.readAsBytes(); - file = PlatformFile( - path: pickedFile.path, - bytes: bytes, - ); - } else { - FileType type; - if (fileType == DefaultAttachmentTypes.image) { - type = FileType.image; - } else if (fileType == DefaultAttachmentTypes.video) { - type = FileType.video; - } else if (fileType == DefaultAttachmentTypes.file) { - type = FileType.any; - } - final res = await FilePicker.platform.pickFiles( - type: type, - withData: true, - ); - if (res?.files?.isNotEmpty == true) { - file = res.files.single; - print('file.bytes?.length: ${file.bytes?.length}'); - } - } - - setState(() { - _inputEnabled = true; - }); - - if (file == null) { - return; - } - - final channel = StreamChannel.of(context).channel; - final attachment = _SendingAttachment( - file: file, - attachment: Attachment( - localUri: file.path != null ? Uri.parse(file.path) : null, - type: attachmentType, - ), - ); - - setState(() { - _attachments.add(attachment); - }); - - final url = await _uploadAttachment(file, fileType, channel); - - if (fileType == DefaultAttachmentTypes.image) { - attachment.attachment = attachment.attachment.copyWith( - imageUrl: url, - ); - } else { - attachment.attachment = attachment.attachment.copyWith( - assetUrl: url, - ); - } - - setState(() { - attachment.uploaded = true; - }); - } - - Future _uploadAttachment( - PlatformFile file, - DefaultAttachmentTypes type, - Channel channel, - ) async { - String url; - if (type == DefaultAttachmentTypes.image) { - if (widget.doImageUploadRequest != null) { - url = await widget.doImageUploadRequest(file, channel); - } else { - url = await _uploadImage(file, channel); - } - } else { - if (widget.doFileUploadRequest != null) { - url = await widget.doFileUploadRequest(file, channel); - } else { - url = await _uploadFile(file, channel); - } - } - return url; - } - - Future _uploadImage(PlatformFile file, Channel channel) async { - final filename = file.name ?? file.path?.split('/')?.last; - final bytes = file.bytes; - final res = await channel.sendImage( - MultipartFile.fromBytes( - bytes, - filename: filename, - contentType: MediaType.parse(lookupMimeType(filename)), - ), - ); - return res.file; - } - - Future _uploadFile(PlatformFile file, Channel channel) async { - final filename = file.name ?? file.path?.split('/')?.last; - final bytes = file.bytes; - final res = await channel.sendFile( - MultipartFile.fromBytes( - bytes, - filename: filename, - contentType: MediaType.parse(lookupMimeType(filename)), - ), - ); - return res.file; - } - - Widget _buildSendButton(BuildContext context) { - return IconTheme( - data: - StreamChatTheme.of(context).channelTheme.messageInputButtonIconTheme, - child: Material( - clipBehavior: Clip.hardEdge, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(32), - ), - color: Colors.transparent, - child: IconButton( - key: Key('sendButton'), - onPressed: (textEditingController.text.trim().isEmpty && - _attachments.isEmpty) - ? null - : () { - sendMessage(); - }, - icon: Icon( - Icons.send, - ), - ), - ), - ); - } - - /// Sends the current message - void sendMessage() async { - final text = textEditingController.text.trim(); - if (text.isEmpty && _attachments.isEmpty) { - return; - } - - final attachments = List<_SendingAttachment>.from(_attachments); - - textEditingController.clear(); - _attachments.clear(); - - setState(() { - _messageIsPresent = false; - _typingStarted = false; - }); - - _commandsOverlay?.remove(); - _commandsOverlay = null; - _mentionsOverlay?.remove(); - _mentionsOverlay = null; - - final channel = StreamChannel.of(context).channel; - - Future sendingFuture; - Message message; - if (widget.editMessage != null) { - message = widget.editMessage.copyWith( - text: text, - attachments: _getAttachments(attachments).toList(), - mentionedUsers: - _mentionedUsers.where((u) => text.contains('@${u.name}')).toList(), - ); - } else { - message = (widget.initialMessage ?? Message()).copyWith( - parentId: widget.parentMessage?.id, - text: text, - attachments: _getAttachments(attachments).toList(), - mentionedUsers: - _mentionedUsers.where((u) => text.contains('@${u.name}')).toList(), - ); - } - - if (widget.preMessageSending != null) { - message = await widget.preMessageSending(message); - } - - if (widget.editMessage == null || - widget.editMessage.status == MessageSendingStatus.FAILED) { - sendingFuture = channel.sendMessage(message); - } else { - sendingFuture = StreamChat.of(context).client.updateMessage( - message, - channel.cid, - ); - } - - return sendingFuture.then((resp) { - if (widget.onMessageSent != null) { - widget.onMessageSent(resp.message); - } else { - if (widget.editMessage != null) { - Navigator.pop(context); - } - } - }); - } - - Iterable _getAttachments(List<_SendingAttachment> attachments) { - return attachments.map((attachment) { - return attachment.attachment; - }); - } - - StreamSubscription _keyboardListener; - - @override - void initState() { - super.initState(); - - if (!kIsWeb) { - _keyboardListener = - KeyboardVisibilityController().onChange.listen((visible) { - if (visible) { - _onChange(); - } else { - _commandsOverlay?.remove(); - _commandsOverlay = null; - _mentionsOverlay?.remove(); - _mentionsOverlay = null; - } - }); - } - - textEditingController = - widget.textEditingController ?? TextEditingController(); - - textEditingController.addListener(_onChange); - - if (widget.editMessage != null || widget.initialMessage != null) { - _parseExistingMessage(widget.editMessage ?? widget.initialMessage); - } - } - - Timer _debounce; - void _onChange() { - if (_debounce?.isActive == true) _debounce.cancel(); - _debounce = Timer( - const Duration(milliseconds: 350), - () { - if (!mounted) { - return; - } - final s = textEditingController.text; - StreamChannel.of(context).channel.keyStroke( - widget.parentMessage?.id, - ); - - setState(() { - _messageIsPresent = s.trim().isNotEmpty; - }); - - _commandsOverlay?.remove(); - _commandsOverlay = null; - _mentionsOverlay?.remove(); - _mentionsOverlay = null; - - if (s.trim().startsWith('/')) { - _commandsOverlay = _buildCommandsOverlayEntry(); - Overlay.of(context).insert(_commandsOverlay); - } - - if (_messageIsPresent && - textEditingController.selection.isCollapsed && - textEditingController.selection.baseOffset > 0 && - textEditingController.text - .substring(0, textEditingController.selection.baseOffset) - .split(' ') - .last - .contains('@')) { - _mentionsOverlay = _buildMentionsOverlayEntry(); - Overlay.of(context).insert(_mentionsOverlay); - } - }, - ); - } - - void _parseExistingMessage(Message message) { - textEditingController.text = message.text; - - _typingStarted = true; - _messageIsPresent = true; - - message.attachments?.forEach((attachment) { - _attachments.add(_SendingAttachment( - attachment: attachment, - uploaded: true, - )); - }); - } - - @override - void dispose() { - _commandsOverlay?.remove(); - _mentionsOverlay?.remove(); - _keyboardListener?.cancel(); - super.dispose(); - } - - bool _initialized = false; - @override - void didChangeDependencies() { - if (widget.editMessage != null && !_initialized) { - FocusScope.of(context).requestFocus(_focusNode); - _initialized = true; - } - super.didChangeDependencies(); - } -} - -class _SendingAttachment { - PlatformFile file; - Attachment attachment; - bool uploaded; - - _SendingAttachment({ - this.file, - this.attachment, - this.uploaded = false, - }); -} diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart deleted file mode 100644 index f968d850..00000000 --- a/lib/src/message_list_view.dart +++ /dev/null @@ -1,621 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:jiffy/jiffy.dart'; -import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_flutter/src/message_widget.dart'; -import 'package:stream_chat_flutter/src/system_message.dart'; -import 'package:visibility_detector/visibility_detector.dart'; - -import '../stream_chat_flutter.dart'; -import 'date_divider.dart'; -import 'stream_channel.dart'; - -typedef MessageBuilder = Widget Function( - BuildContext, - MessageDetails, - List, -); -typedef ParentMessageBuilder = Widget Function( - BuildContext, - Message, -); -typedef ThreadBuilder = Widget Function(BuildContext context, Message parent); -typedef ThreadTapCallback = void Function(Message, Widget); - -class MessageDetails { - /// True if the message belongs to the current user - bool isMyMessage; - - /// True if the user message is the same of the previous message - bool isLastUser; - - /// True if the user message is the same of the next message - bool isNextUser; - - /// The message - Message message; - - /// The index of the message - int index; - - MessageDetails( - BuildContext context, - this.message, - List messages, - this.index, - ) { - isMyMessage = message.user.id == StreamChat.of(context).user.id; - isLastUser = index + 1 < messages.length && - message.user.id == messages[index + 1]?.user?.id; - isNextUser = - index - 1 >= 0 && message.user.id == messages[index - 1]?.user?.id; - } -} - -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_listview.png) -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_listview_paint.png) -/// -/// It shows the list of messages of the current channel. -/// -/// ```dart -/// class ChannelPage extends StatelessWidget { -/// const ChannelPage({ -/// Key key, -/// }) : super(key: key); -/// -/// @override -/// Widget build(BuildContext context) { -/// return Scaffold( -/// appBar: ChannelHeader(), -/// body: Column( -/// children: [ -/// Expanded( -/// child: MessageListView( -/// threadBuilder: (_, parentMessage) { -/// return ThreadPage( -/// parent: parentMessage, -/// ); -/// }, -/// ), -/// ), -/// MessageInput(), -/// ], -/// ), -/// ); -/// } -/// } -/// ``` -/// -/// -/// Make sure to have a [StreamChannel] ancestor in order to provide the information about the channels. -/// The widget uses a [ListView.custom] to render the list of channels. -/// -/// The widget components render the ui based on the first ancestor of type [StreamChatTheme]. -/// Modify it to change the widget appearance. -class MessageListView extends StatefulWidget { - /// Instantiate a new MessageListView - MessageListView({ - Key key, - this.messageBuilder, - this.parentMessageBuilder, - this.parentMessage, - this.threadBuilder, - this.onThreadTap, - this.dateDividerBuilder, - this.onMessageTap, - this.onSystemMessageTap, - this.onParentMessageTap, - this.scrollPhysics = const AlwaysScrollableScrollPhysics(), - this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, - this.messageFilter, - this.loadingBuilder, - }) : super(key: key); - - /// Function used to build a custom message widget - final MessageBuilder messageBuilder; - - /// Function used to build a custom parent message widget - final ParentMessageBuilder parentMessageBuilder; - - /// Function used to build a custom thread widget - final ThreadBuilder threadBuilder; - - /// The builder used while loading. - final WidgetBuilder loadingBuilder; - - /// Filter applied to the message list before rendering - final bool Function(Message) messageFilter; - - /// Function called when tapping on a thread - /// By default it calls [Navigator.push] using the widget built using [threadBuilder] - final ThreadTapCallback onThreadTap; - - /// The function called when tapping on the message when the message is not failed - final Function(Message) onMessageTap; - - /// The function called when tapping on a system message - final Function(Message) onSystemMessageTap; - - /// The function called when tapping on the parent message when the message is not failed - final Function(Message) onParentMessageTap; - - /// Parent message in case of a thread - final Message parentMessage; - - /// Builder used to render date dividers - final Widget Function(DateTime) dateDividerBuilder; - - /// The ScrollPhysics used by the ListView - final ScrollPhysics scrollPhysics; - - /// The [ScrollViewKeyboardDismissBehavior] used by the ListView - final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; - - @override - _MessageListViewState createState() => _MessageListViewState(); -} - -class _MessageListViewState extends State { - static const _newMessageLoadingOffset = 100; - final ScrollController _scrollController = ScrollController(); - bool _bottomWasVisible = true; - bool _topWasVisible = false; - List _messages = []; - List _newMessageList = []; - Function _onThreadTap; - - @override - Widget build(BuildContext context) { - final streamChannel = StreamChannel.of(context); - - /// TODO: find a better solution when (https://github.com/flutter/flutter/issues/21023) is fixed - return NotificationListener( - onNotification: (_) { - if (_scrollController.offset < 150 && _newMessageList.isNotEmpty) { - setState(() { - _messages.insertAll(0, _newMessageList); - _newMessageList.clear(); - }); - } - return true; - }, - child: ListView.custom( - key: Key('messageListView'), - physics: widget.scrollPhysics, - keyboardDismissBehavior: widget.keyboardDismissBehavior, - controller: _scrollController, - reverse: true, - childrenDelegate: SliverChildBuilderDelegate( - (context, i) { - if (i == _messages.length + 1) { - if (widget.parentMessage != null) { - if (widget.parentMessageBuilder != null) { - return widget.parentMessageBuilder( - context, - widget.parentMessage, - ); - } else { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - buildParentMessage(widget.parentMessage), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 32), - child: Container( - padding: const EdgeInsets.all(8), - child: Text( - 'Start of thread', - textAlign: TextAlign.center, - ), - color: Theme.of(context).accentColor.withAlpha(50), - ), - ), - ], - ); - } - } else { - return SizedBox(); - } - } - - if (i == _messages.length) { - return _buildLoadingIndicator(streamChannel); - } - final message = _messages[i]; - final nextMessage = i > 0 ? _messages[i - 1] : null; - - Widget messageWidget; - - if (i == 0) { - messageWidget = _buildBottomMessage( - context, - message, - _messages, - streamChannel, - ); - } else if (i == _messages.length - 1) { - messageWidget = _buildTopMessage( - context, - message, - _messages, - streamChannel, - ); - } else { - if (widget.messageBuilder != null) { - messageWidget = Builder( - key: ValueKey('MESSAGE-${message.id}'), - builder: (_) => widget.messageBuilder( - context, - MessageDetails( - context, - message, - _messages, - i, - ), - _messages), - ); - } else { - messageWidget = buildMessage(message, _messages, i); - } - } - - if (nextMessage != null && - !Jiffy(message.createdAt.toLocal()) - .isSame(nextMessage.createdAt.toLocal(), Units.DAY)) { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - messageWidget, - Padding( - padding: const EdgeInsets.symmetric(vertical: 12.0), - child: widget.dateDividerBuilder != null - ? widget - .dateDividerBuilder(nextMessage.createdAt.toLocal()) - : DateDivider( - dateTime: nextMessage.createdAt.toLocal(), - ), - ), - ], - ); - } - - return messageWidget; - }, - childCount: _messages.length + 2, - findChildIndexCallback: (key) { - final ValueKey valueKey = key; - final index = _messages - .indexWhere((m) => 'MESSAGE-${m.id}' == valueKey.value); - return index != -1 ? index : null; - }, - ), - ), - ); - } - - Container _buildLoadingIndicator(StreamChannelState streamChannel) { - return Container( - height: 50, - child: StreamBuilder( - stream: streamChannel.queryMessage, - initialData: false, - builder: (context, snapshot) { - if (snapshot.hasError) { - return Container( - color: Color(0xffd0021B).withAlpha(26), - child: Center( - child: Text('Error loading messages'), - ), - ); - } - if (!snapshot.data) { - return Container(); - } - - if (widget.loadingBuilder != null) { - return widget.loadingBuilder(context); - } - - return Center( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: CircularProgressIndicator(), - ), - ); - }), - ); - } - - Widget _buildTopMessage( - BuildContext context, - Message message, - List messages, - StreamChannelState streamChannel, - ) { - Widget messageWidget; - if (widget.messageBuilder != null) { - messageWidget = Builder( - key: ValueKey('TOP-MESSAGE'), - builder: (_) => widget.messageBuilder( - context, - MessageDetails( - context, - message, - _messages, - _messages.length - 1, - ), - _messages, - ), - ); - } else { - messageWidget = buildMessage(message, messages, _messages.length - 1); - } - - return VisibilityDetector( - key: ValueKey('TOP-MESSAGE'), - child: messageWidget, - onVisibilityChanged: (visibility) { - final topIsVisible = visibility.visibleBounds != Rect.zero; - if (topIsVisible && !_topWasVisible) { - if (widget.parentMessage == null) { - streamChannel.queryMessages(); - } else { - streamChannel.getReplies(widget.parentMessage.id); - } - } - _topWasVisible = topIsVisible; - }, - ); - } - - Widget _buildBottomMessage( - BuildContext context, - Message message, - List messages, - StreamChannelState streamChannel, - ) { - Widget messageWidget; - if (widget.messageBuilder != null) { - messageWidget = Builder( - key: ValueKey('BOTTOM-MESSAGE'), - builder: (_) => widget.messageBuilder( - context, - MessageDetails( - context, - message, - _messages, - 0, - ), - _messages, - ), - ); - } else { - messageWidget = buildMessage(message, messages, 0); - } - - return VisibilityDetector( - key: ValueKey('BOTTOM-MESSAGE'), - onVisibilityChanged: (visibility) { - final isVisible = visibility.visibleBounds != Rect.zero; - if (isVisible && - !_bottomWasVisible && - streamChannel.channel.config?.readEvents == true) { - if (streamChannel.channel.state.unreadCount > 0) { - streamChannel.channel.markRead(); - } - } - _bottomWasVisible = isVisible; - }, - child: messageWidget, - ); - } - - Widget buildParentMessage( - Message message, - ) { - final isMyMessage = message.user.id == StreamChat.of(context).user.id; - - return MessageWidget( - showReplyIndicator: false, - message: message, - reverse: isMyMessage, - showUsername: !isMyMessage, - padding: EdgeInsets.only( - top: 8.0, - left: 8.0, - right: 8.0, - bottom: 16.0, - ), - showSendingIndicator: DisplayWidget.hide, - onThreadTap: _onThreadTap, - showEditMessage: false, - showDeleteMessage: false, - borderRadiusGeometry: BorderRadius.only( - topLeft: Radius.circular(16), - bottomLeft: Radius.circular(2), - topRight: Radius.circular(16), - bottomRight: Radius.circular(16), - ), - onMessageTap: widget.onParentMessageTap, - borderSide: isMyMessage ? BorderSide.none : null, - showUserAvatar: DisplayWidget.show, - messageTheme: isMyMessage - ? StreamChatTheme.of(context).ownMessageTheme - : StreamChatTheme.of(context).otherMessageTheme, - ); - } - - Widget buildMessage( - Message message, - List messages, - int index, - ) { - if (message.type == 'system' && message.text?.isNotEmpty == true) { - return SystemMessage( - onMessageTap: widget.onSystemMessageTap, - message: message, - ); - } - - final userId = StreamChat.of(context).user.id; - final isMyMessage = message.user.id == userId; - final isLastUser = index + 1 < messages.length && - message.user.id == messages[index + 1]?.user?.id; - final isNextUser = - index - 1 >= 0 && message.user.id == messages[index - 1]?.user?.id; - - final readList = StreamChannel.of(context) - .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(); - - return MessageWidget( - message: message, - reverse: isMyMessage, - showReactions: !message.isDeleted, - padding: EdgeInsets.only( - left: 8.0, - right: 8.0, - bottom: index == 0 ? 30 : (isNextUser ? 5 : 10), - ), - showUsername: !isMyMessage && !isNextUser, - showSendingIndicator: isMyMessage && - (index == 0 || message.status != MessageSendingStatus.SENT) - ? DisplayWidget.show - : DisplayWidget.hide, - onMessageTap: widget.onMessageTap, - showTimestamp: !isNextUser || readList?.isNotEmpty == true, - showEditMessage: isMyMessage, - showDeleteMessage: isMyMessage, - borderSide: isMyMessage ? BorderSide.none : null, - onThreadTap: _onThreadTap, - attachmentBorderRadiusGeometry: BorderRadius.circular(16), - borderRadiusGeometry: BorderRadius.only( - topLeft: Radius.circular(isLastUser ? 2 : 16), - bottomLeft: Radius.circular(2), - topRight: Radius.circular(16), - bottomRight: Radius.circular(16), - ), - showUserAvatar: isNextUser ? DisplayWidget.hide : DisplayWidget.show, - messageTheme: isMyMessage - ? StreamChatTheme.of(context).ownMessageTheme - : StreamChatTheme.of(context).otherMessageTheme, - readList: readList, - ); - } - - StreamSubscription _streamListener; - - @override - void initState() { - super.initState(); - - final streamChannel = StreamChannel.of(context); - - Stream> stream; - - if (widget.parentMessage == null) { - stream = streamChannel.channel.state.messagesStream; - } else { - streamChannel.getReplies(widget.parentMessage.id); - stream = streamChannel.channel.state.threadsStream - .where((threads) => threads.containsKey(widget.parentMessage.id)) - .map((threads) => threads[widget.parentMessage.id]); - } - - _streamListener = stream.map((messages) { - final filteredMessages = messages - ?.where((m) => - !(m.status == MessageSendingStatus.FAILED && m.isDeleted) && - m.shadowed != true) - ?.toList() ?? - []; - - if (widget.messageFilter != null) { - return messages.where(widget.messageFilter).toList(); - } - - return filteredMessages; - }).listen((newMessages) { - newMessages = newMessages.reversed.toList(); - if (_messages.isEmpty || - newMessages.isEmpty || - newMessages.first.id != _messages.first.id) { - if (!_scrollController.hasClients || - _scrollController.offset < _newMessageLoadingOffset) { - if (streamChannel.channel.state.unreadCount > 0 && - streamChannel.channel.config?.readEvents == true) { - streamChannel.channel.markRead(); - } - setState(() { - _messages = newMessages; - }); - } else if (newMessages.first.user.id == - streamChannel.channel.client.state.user.id) { - _scrollController.jumpTo(0); - WidgetsBinding.instance.addPostFrameCallback((_) { - setState(() { - _messages = newMessages; - }); - }); - } else { - _newMessageList = newMessages; - } - } else { - setState(() { - _messages = newMessages; - }); - } - }); - - _getOnThreadTap(); - } - - void _getOnThreadTap() { - if (widget.onThreadTap != null) { - _onThreadTap = (Message message) { - widget.onThreadTap( - message, - widget.threadBuilder != null - ? widget.threadBuilder(context, message) - : null); - }; - } else if (widget.threadBuilder != null) { - _onThreadTap = (Message message) { - Navigator.push( - context, - MaterialPageRoute(builder: (_) { - return StreamBuilder( - stream: StreamChannel.of(context) - .channel - .state - .messagesStream - .map((messages) => - messages.firstWhere((m) => m.id == message.id)), - initialData: message, - builder: (_, snapshot) { - return StreamChannel( - channel: StreamChannel.of(context).channel, - child: widget.threadBuilder(context, snapshot.data), - ); - }); - }), - ); - }; - } - } - - @override - void dispose() { - _streamListener.cancel(); - super.dispose(); - } -} diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart deleted file mode 100644 index eb13e2ad..00000000 --- a/lib/src/message_widget.dart +++ /dev/null @@ -1,814 +0,0 @@ -import 'dart:math'; -import 'dart:ui'; - -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter_portal/flutter_portal.dart'; -import 'package:jiffy/jiffy.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -import 'message_actions_bottom_sheet.dart'; -import 'message_text.dart'; - -typedef AttachmentBuilder = Widget Function(BuildContext, Message, Attachment); - -/// The display behaviour of a widget -enum DisplayWidget { - /// Hides the widget replacing its space with a spacer - hide, - - /// Hides the widget not replacing its space - gone, - - /// Shows the widget normally - show, -} - -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_widget.png) -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_widget_paint.png) -/// -/// It shows a message with reactions, replies and user avatar. -/// -/// Usually you don't use this widget as it's the default message widget used by [MessageListView]. -/// -/// The widget components render the ui based on the first ancestor of type [StreamChatTheme]. -/// Modify it to change the widget appearance. -class MessageWidget extends StatefulWidget { - /// Function called on mention tap - final void Function(User) onMentionTap; - - /// The function called when tapping on replies - final void Function(Message) onThreadTap; - - /// The function called when tapping on the message when the message is not failed - final void Function(Message) onMessageTap; - - /// The builder of MessageInput used while editing this message - final Widget Function(BuildContext, Message) editMessageInputBuilder; - - /// The builder of the text of the message - final Widget Function(BuildContext, Message) textBuilder; - - /// Function called on long press - final void Function(BuildContext, Message) onMessageActions; - - /// The message - final Message message; - - /// The message theme - final MessageTheme messageTheme; - - /// If true the widget will be mirrored - final bool reverse; - - /// The shape of the message text - final ShapeBorder shape; - - /// The shape of an attachment - final ShapeBorder attachmentShape; - - /// The borderside of the message text - final BorderSide borderSide; - - /// The borderside of an attachment - final BorderSide attachmentBorderSide; - - /// The border radius of the message text - final BorderRadiusGeometry borderRadiusGeometry; - - /// The border radius of an attachment - final BorderRadiusGeometry attachmentBorderRadiusGeometry; - - /// The padding of the widget - final EdgeInsetsGeometry padding; - - /// The internal padding of the message text - final EdgeInsetsGeometry textPadding; - - /// The internal padding of an attachment - final EdgeInsetsGeometry attachmentPadding; - - /// It controls the display behaviour of the user avatar - final DisplayWidget showUserAvatar; - - /// It controls the display behaviour of the sending indicator - final DisplayWidget showSendingIndicator; - - /// If true the widget will show the reactions - final bool showReactions; - - /// If true the widget will show the reply indicator - final bool showReplyIndicator; - - /// The function called when tapping on UserAvatar - final void Function(User) onUserAvatarTap; - - /// The function called when tapping on a link - final void Function(String) onLinkTap; - - final List readList; - - /// If true show the users username next to the timestamp of the message - final bool showUsername; - final bool showTimestamp; - final bool showDeleteMessage; - final bool showEditMessage; - final Map attachmentBuilders; - - MessageWidget({ - Key key, - @required this.message, - @required this.messageTheme, - this.onMessageTap, - this.reverse = false, - this.shape, - this.attachmentShape, - this.borderSide, - this.attachmentBorderSide, - this.borderRadiusGeometry, - this.attachmentBorderRadiusGeometry, - this.onMentionTap, - this.showUserAvatar = DisplayWidget.show, - this.showSendingIndicator = DisplayWidget.show, - this.showReplyIndicator = true, - this.onThreadTap, - this.showUsername = true, - this.showTimestamp = true, - this.showReactions = true, - this.showDeleteMessage = true, - this.showEditMessage = true, - this.onUserAvatarTap, - this.onLinkTap, - this.onMessageActions, - this.editMessageInputBuilder, - this.textBuilder, - Map customAttachmentBuilders, - this.readList, - this.padding, - this.textPadding = const EdgeInsets.all(8.0), - this.attachmentPadding = EdgeInsets.zero, - }) : attachmentBuilders = { - 'image': (context, message, attachment) { - return ImageAttachment( - attachment: attachment, - message: message, - messageTheme: messageTheme, - size: Size( - MediaQuery.of(context).size.width * 0.8, - MediaQuery.of(context).size.height * 0.3, - ), - ); - }, - 'video': (context, message, attachment) { - return VideoAttachment( - attachment: attachment, - messageTheme: messageTheme, - size: Size( - MediaQuery.of(context).size.width * 0.8, - MediaQuery.of(context).size.height * 0.3, - ), - ); - }, - 'giphy': (context, message, attachment) { - return GiphyAttachment( - attachment: attachment, - messageTheme: messageTheme, - message: message, - size: Size( - MediaQuery.of(context).size.width * 0.8, - MediaQuery.of(context).size.height * 0.3, - ), - ); - }, - 'file': (context, message, attachment) { - return FileAttachment( - attachment: attachment, - size: Size( - MediaQuery.of(context).size.width * 0.8, - MediaQuery.of(context).size.height * 0.3, - ), - ); - }, - }..addAll(customAttachmentBuilders ?? {}), - super(key: key); - - @override - _MessageWidgetState createState() => _MessageWidgetState(); -} - -class _MessageWidgetState extends State { - final Map _reactionToEmoji = { - 'love': 'โค๏ธ๏ธ', - 'haha': '๐Ÿ˜‚', - 'like': '๐Ÿ‘', - 'sad': '๐Ÿ˜•', - 'angry': '๐Ÿ˜ก', - 'wow': '๐Ÿ˜ฒ', - }; - - final GlobalKey _reactionPickerKey = GlobalKey(); - double _reactionPadding = 0; - - @override - Widget build(BuildContext context) { - var leftPadding = widget.showUserAvatar != DisplayWidget.gone - ? widget.messageTheme.avatarTheme.constraints.maxWidth + 23.0 - : 12.0; - if (widget.showSendingIndicator == DisplayWidget.gone) { - leftPadding -= 7; - } - return GestureDetector( - onTap: () => onMessageTap(context), - onLongPress: () => onLongPress(context), - behavior: HitTestBehavior.opaque, - child: Container( - width: double.infinity, - child: Portal( - child: Padding( - padding: widget.padding ?? EdgeInsets.all(8), - child: Transform( - alignment: Alignment.center, - transform: Matrix4.rotationY(widget.reverse ? pi : 0), - child: FractionallySizedBox( - alignment: Alignment.centerLeft, - widthFactor: 0.75, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - if (widget.showSendingIndicator == - DisplayWidget.show) - _buildSendingIndicator(), - SizedBox( - width: 2, - ), - if (widget.showSendingIndicator == - DisplayWidget.hide) - SizedBox( - width: 8, - ), - if (widget.showUserAvatar == DisplayWidget.show) - _buildUserAvatar(), - SizedBox( - width: 6, - ), - if (widget.showUserAvatar == DisplayWidget.hide) - SizedBox( - width: widget.messageTheme.avatarTheme - .constraints.maxWidth + - 8, - ), - Flexible( - child: Padding( - padding: widget.showReactions - ? EdgeInsets.only( - top: _reactionPadding, - ) - : EdgeInsets.zero, - child: PortalEntry( - portalAnchor: Alignment(0, 1), - childAnchor: Alignment.topRight, - portal: _buildReactionIndicator(context), - child: (widget.message.isDeleted && - widget.message.status != - MessageSendingStatus - .FAILED_DELETE) - ? Transform( - alignment: Alignment.center, - transform: Matrix4.rotationY( - widget.reverse ? pi : 0), - child: DeletedMessage( - messageTheme: widget.messageTheme, - ), - ) - : Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - ..._parseAttachments(context), - if (widget.message.text - .trim() - .isNotEmpty) - _buildTextBubble(context), - ], - ), - ), - ), - ), - ], - ), - if (widget.showReplyIndicator && - widget.message.replyCount > 0) - _buildReplyIndicator(leftPadding), - ], - ), - if ((widget.message.createdAt != null && - widget.showTimestamp) || - widget.showUsername || - widget.readList?.isNotEmpty == true) - _buildBottomRow(leftPadding), - ], - ), - ), - ), - ), - ), - ), - ); - } - - @override - void didUpdateWidget(MessageWidget oldWidget) { - super.didUpdateWidget(oldWidget); - _updateReactionPadding(); - } - - @override - void initState() { - super.initState(); - _updateReactionPadding(); - } - - void _updateReactionPadding() { - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - if (!mounted) { - return; - } - if (_reactionPickerKey.currentContext != null && - widget.message.reactionCounts != null && - widget.message.reactionCounts.values - .where((element) => element > 0) - .isNotEmpty) { - setState(() { - _reactionPadding = _reactionPickerKey.currentContext.size.height; - }); - } else { - setState(() { - _reactionPadding = 0; - }); - } - }); - } - - Widget _buildReactionsTail(BuildContext context) { - return AnimatedSwitcher( - duration: Duration(milliseconds: 300), - child: widget.message.reactionCounts?.isNotEmpty == true - ? Transform.translate( - offset: Offset(4, 0), - child: CustomPaint( - painter: ReactionBubblePainter( - Theme.of(context).brightness == Brightness.dark - ? Colors.white - : Colors.black, - ), - ), - ) - : SizedBox(), - ); - } - - Padding _buildBottomRow(double leftPadding) { - return Padding( - padding: EdgeInsets.only( - left: leftPadding, - top: 2, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Transform( - alignment: Alignment.center, - transform: Matrix4.rotationY(widget.reverse ? pi : 0), - child: RichText( - text: TextSpan( - style: widget.messageTheme.createdAt, - children: [ - if (widget.showUsername) - TextSpan( - text: widget.message.user.name, - style: TextStyle(fontWeight: FontWeight.bold), - ), - if (widget.message.createdAt != null && widget.showTimestamp) - TextSpan( - text: Jiffy(widget.message.createdAt.toLocal()) - .format(' HH:mm'), - ), - ], - ), - ), - ), - if (widget.readList?.isNotEmpty == true) - SizedBox.fromSize( - size: Size((widget.readList.length * 10.0) + 10, 17), - child: Transform( - alignment: Alignment.center, - transform: Matrix4.rotationY(widget.reverse ? pi : 0), - child: Padding( - padding: const EdgeInsets.only(left: 4.0), - child: _buildReadIndicator(), - ), - ), - ), - ], - ), - ); - } - - Widget _buildReadIndicator() { - var padding = 0.0; - return Stack( - children: widget.readList.map((e) { - padding += 10.0; - return Positioned( - left: padding - 10, - bottom: 0, - top: 0, - child: Material( - color: Colors.white, - shape: CircleBorder(), - child: Padding( - padding: const EdgeInsets.all(1.0), - child: UserAvatar( - user: e.user, - constraints: BoxConstraints.loose(Size.fromRadius(16)), - ), - ), - ), - ); - }).toList(), - ); - } - - Widget _buildReactionIndicator(BuildContext context) { - return AnimatedSwitcher( - key: _reactionPickerKey, - duration: Duration(milliseconds: 300), - child: (widget.showReactions && - widget.message.reactionCounts?.isNotEmpty == true && - !widget.message.isDeleted) - ? Container( - child: GestureDetector( - onTap: () => onLongPress(context), - child: Container( - width: MediaQuery.of(context).size.width * 0.3, - padding: const EdgeInsets.only( - bottom: 4.0, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Transform( - transform: Matrix4.rotationY(widget.reverse ? pi : 0), - alignment: Alignment.center, - child: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: - Theme.of(context).brightness == Brightness.dark - ? Colors.white - : Colors.black, - borderRadius: BorderRadius.all(Radius.circular(14)), - ), - child: _buildReactionsText(context), - ), - ), - _buildReactionsTail(context), - ], - ), - ), - ), - ) - : SizedBox(), - ); - } - - Text _buildReactionsText(BuildContext context) { - return Text( - widget.message.reactionCounts.keys.map((reactionType) { - return _reactionToEmoji[reactionType] ?? '?'; - }).join(' ') + - ' ${widget.message.reactionCounts.values.fold(0, (t, v) => v + t).toString()}', - style: TextStyle( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.black - : Colors.white, - ), - textAlign: TextAlign.justify, - ); - } - - void _showMessageBottomSheet(BuildContext context) { - final channel = StreamChannel.of(context).channel; - showModalBottomSheet( - clipBehavior: Clip.hardEdge, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(32), - topRight: Radius.circular(32), - ), - ), - context: context, - builder: (context) { - return StreamChannel( - channel: channel, - child: MessageActionsBottomSheet( - showDeleteMessage: widget.showDeleteMessage, - message: widget.message, - editMessageInputBuilder: widget.editMessageInputBuilder, - onThreadTap: widget.onThreadTap, - showEditMessage: widget.showEditMessage, - showReactions: widget.showReactions, - showReply: - widget.showReplyIndicator && widget.onThreadTap != null, - ), - ); - }); - } - - List _parseAttachments(BuildContext context) { - return widget.message.attachments?.map((attachment) { - final attachmentBuilder = widget.attachmentBuilders[attachment.type]; - - if (attachmentBuilder == null) { - return SizedBox(); - } - - return Padding( - key: Key(attachment.hashCode.toString()), - padding: EdgeInsets.only( - bottom: 4, - ), - child: Material( - color: _getBackgroundColor(), - clipBehavior: Clip.hardEdge, - shape: widget.attachmentShape ?? - widget.shape ?? - ContinuousRectangleBorder( - side: widget.attachmentBorderSide ?? - widget.borderSide ?? - BorderSide( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white.withAlpha(24) - : Colors.black.withAlpha(24), - ), - borderRadius: widget.attachmentBorderRadiusGeometry ?? - widget.borderRadiusGeometry ?? - BorderRadius.zero, - ), - child: Padding( - padding: widget.attachmentPadding, - child: Transform( - transform: Matrix4.rotationY(widget.reverse ? pi : 0), - alignment: Alignment.center, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - getFailedMessageWidget( - context, - padding: const EdgeInsets.all(8.0), - ), - attachmentBuilder( - context, - widget.message, - attachment, - ), - ], - ), - ), - ), - ), - ); - })?.toList() ?? - []; - } - - void onLongPress(BuildContext context) { - if (widget.message.isEphemeral || - widget.message.status == MessageSendingStatus.SENDING) { - return; - } - - if (widget.onMessageActions != null) { - widget.onMessageActions(context, widget.message); - } else { - _showMessageBottomSheet(context); - } - return; - } - - Widget _buildReplyIndicator(double leftPadding) { - return Padding( - padding: EdgeInsets.only( - left: leftPadding, - ), - child: Transform( - transform: Matrix4.rotationY(widget.reverse ? pi : 0), - alignment: Alignment.center, - child: ReplyIndicator( - message: widget.message, - reversed: widget.reverse, - messageTheme: widget.messageTheme, - onTap: widget.onThreadTap != null - ? () { - widget.onThreadTap(widget.message); - } - : null, - ), - ), - ); - } - - Widget _buildSendingIndicator() { - return Transform.translate( - offset: Offset( - 0, - 4, - ), - child: Transform( - transform: Matrix4.rotationY(widget.reverse ? pi : 0), - alignment: Alignment.center, - child: SendingIndicator( - message: widget.message, - ), - ), - ); - } - - Widget _buildUserAvatar() => Transform( - transform: Matrix4.rotationY(widget.reverse ? pi : 0), - alignment: Alignment.center, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4.0), - child: Transform.translate( - offset: Offset( - 0, widget.messageTheme.avatarTheme.constraints.maxHeight / 2), - child: UserAvatar( - user: widget.message.user, - onTap: widget.onUserAvatarTap, - constraints: widget.messageTheme.avatarTheme.constraints, - ), - ), - ), - ); - - Widget getFailedMessageWidget( - BuildContext context, { - EdgeInsetsGeometry padding, - }) { - Widget failedWidget; - if (widget.message.status == MessageSendingStatus.FAILED) { - failedWidget = Text( - 'MESSAGE FAILED ยท CLICK TO TRY AGAIN', - style: widget.messageTheme.messageText.copyWith( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white.withOpacity(.5) - : Colors.black.withOpacity(.5), - fontSize: 11, - ), - ); - } - if (widget.message.status == MessageSendingStatus.FAILED_UPDATE) { - failedWidget = Text( - 'MESSAGE UPDATE FAILED ยท CLICK TO TRY AGAIN', - style: widget.messageTheme.messageText.copyWith( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white.withOpacity(.5) - : Colors.black.withOpacity(.5), - fontSize: 11, - ), - ); - } - if (widget.message.status == MessageSendingStatus.FAILED_DELETE) { - failedWidget = Text( - 'MESSAGE DELETE FAILED ยท CLICK TO TRY AGAIN', - style: widget.messageTheme.messageText.copyWith( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white.withOpacity(.5) - : Colors.black.withOpacity(.5), - fontSize: 11, - ), - ); - } - - if (failedWidget != null) { - return Padding( - padding: padding ?? EdgeInsets.zero, - child: failedWidget, - ); - } - - return SizedBox(); - } - - Widget _buildTextBubble(BuildContext context) { - return Material( - shape: widget.shape ?? - ContinuousRectangleBorder( - side: widget.borderSide ?? - BorderSide( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white.withAlpha(24) - : Colors.black.withAlpha(24), - ), - borderRadius: widget.borderRadiusGeometry ?? BorderRadius.zero, - ), - color: _getBackgroundColor(), - 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), - _buildText(context), - ], - ), - ), - ), - ); - } - - 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; - } - - void onMessageTap(BuildContext context) { - final channel = StreamChannel.of(context).channel; - if (widget.message.status == MessageSendingStatus.FAILED) { - channel.sendMessage(widget.message); - return; - } - if (widget.message.status == MessageSendingStatus.FAILED_UPDATE) { - StreamChat.of(context).client.updateMessage( - widget.message, - channel.cid, - ); - return; - } - - if (widget.message.status == MessageSendingStatus.FAILED_DELETE) { - StreamChat.of(context).client.deleteMessage( - widget.message, - channel.cid, - ); - return; - } - - if (widget.onMessageTap != null) { - widget.onMessageTap(widget.message); - } - } - - Widget _buildText(BuildContext context) { - return widget.textBuilder != null - ? widget.textBuilder(context, widget.message) - : MessageText( - onLinkTap: widget.onLinkTap, - message: widget.message, - onMentionTap: widget.onMentionTap, - messageTheme: widget.messageTheme, - ); - } -} - -class ReactionBubblePainter extends CustomPainter { - final Color color; - - ReactionBubblePainter(this.color); - - @override - void paint(Canvas canvas, Size size) { - final paint = Paint()..color = color; - final path = Path(); - path.lineTo(-2, -6); - path.lineTo(0, 10); - path.lineTo(10, -6); - path.lineTo(-2, -6); - canvas.drawPath(path, paint); - } - - @override - bool shouldRepaint(CustomPainter oldDelegate) { - return true; - } -} diff --git a/lib/src/reaction_picker.dart b/lib/src/reaction_picker.dart deleted file mode 100644 index 8e2d8b10..00000000 --- a/lib/src/reaction_picker.dart +++ /dev/null @@ -1,93 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../stream_chat_flutter.dart'; - -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/reaction_picker.png) -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/reaction_picker_paint.png) -/// -/// It shows a reaction picker -/// -/// Usually you don't use this widget as it's one of the default widgets used by [MessageWidget.onMessageActions]. -class ReactionPicker extends StatelessWidget { - const ReactionPicker({ - Key key, - @required this.reactionToEmoji, - @required this.message, - @required this.channel, - this.size = 30, - }) : super(key: key); - - final Map reactionToEmoji; - final Message message; - final double size; - final Channel channel; - - @override - Widget build(BuildContext context) { - return Container( - color: Colors.black87, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: reactionToEmoji.keys.map((reactionType) { - var user = StreamChat.of(context).user; - final ownReactionIndex = message.latestReactions?.indexWhere( - (reaction) => - reaction.type == reactionType && - reaction.userId == user.id) ?? - -1; - final totalScore = message.latestReactions - .where((r) => r.type == reactionType) - .map((r) => r.score) - .fold(0, (tot, s) => tot + s); - - return Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - IconButton( - iconSize: size, - icon: Text( - reactionToEmoji[reactionType], - style: TextStyle( - fontSize: size - 10, - ), - ), - onPressed: () { - if (ownReactionIndex != -1) { - removeReaction( - context, message.latestReactions[ownReactionIndex]); - } else { - sendReaction(context, reactionType); - } - }, - ), - totalScore > 0 - ? Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: Text( - totalScore.toString(), - style: TextStyle(color: Colors.white), - ), - ) - : SizedBox(), - ], - ); - }).toList(), - ), - ); - } - - /// Add a reaction to the message - void sendReaction(BuildContext context, String reactionType) { - channel.sendReaction(message, reactionType); - Navigator.of(context).pop(); - } - - /// Remove a reaction from the message - void removeReaction(BuildContext context, Reaction reaction) { - channel.deleteReaction(message, reaction); - Navigator.of(context).pop(); - } -} diff --git a/lib/src/stream_chat.dart b/lib/src/stream_chat.dart deleted file mode 100644 index 040409db..00000000 --- a/lib/src/stream_chat.dart +++ /dev/null @@ -1,238 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; - -/// Widget used to provide information about the chat to the widget tree -/// -/// class MyApp extends StatelessWidget { -/// final Client client; -/// -/// MyApp(this.client); -/// -/// @override -/// Widget build(BuildContext context) { -/// return MaterialApp( -/// home: Container( -/// child: StreamChat( -/// client: client, -/// child: ChannelListPage(), -/// ), -/// ), -/// ); -/// } -/// } -/// -/// Use [StreamChat.of] to get the current [StreamChatState] instance. -class StreamChat extends StatefulWidget { - final Client client; - final Widget child; - final StreamChatThemeData streamChatThemeData; - - StreamChat({ - Key key, - @required this.client, - @required this.child, - this.streamChatThemeData, - }) : super( - key: key, - ); - - @override - StreamChatState createState() => StreamChatState(); - - /// Use this method to get the current [StreamChatState] instance - static StreamChatState of(BuildContext context) { - StreamChatState streamChatState; - - streamChatState = context.findAncestorStateOfType(); - - if (streamChatState == null) { - throw Exception( - 'You must have a StreamChat widget at the top of your widget tree'); - } - - return streamChatState; - } -} - -/// The current state of the StreamChat widget -class StreamChatState extends State with WidgetsBindingObserver { - Client get client => widget.client; - Timer _disconnectTimer; - - @override - Widget build(BuildContext context) { - final theme = _getTheme(context, widget.streamChatThemeData); - return StreamChatTheme( - data: theme, - child: Builder( - builder: (context) { - final materialTheme = Theme.of(context); - final streamTheme = StreamChatTheme.of(context); - return Theme( - data: materialTheme.copyWith( - primaryIconTheme: streamTheme.primaryIconTheme, - accentColor: streamTheme.accentColor, - scaffoldBackgroundColor: streamTheme.backgroundColor, - ), - child: widget.child, - ); - }, - ), - ); - } - - StreamChatThemeData _getTheme( - BuildContext context, - StreamChatThemeData themeData, - ) { - final defaultTheme = StreamChatThemeData.getDefaultTheme(Theme.of(context)); - final theme = defaultTheme.copyWith( - primaryColor: themeData?.primaryColor, - defaultChannelImage: themeData?.defaultChannelImage, - primaryIconTheme: themeData?.primaryIconTheme, - defaultUserImage: themeData?.defaultUserImage, - backgroundColor: themeData?.backgroundColor, - channelTheme: defaultTheme.channelTheme.copyWith( - channelHeaderTheme: - defaultTheme.channelTheme.channelHeaderTheme.copyWith( - color: themeData?.channelTheme?.channelHeaderTheme?.color, - lastMessageAt: - themeData?.channelTheme?.channelHeaderTheme?.lastMessageAt, - title: themeData?.channelTheme?.channelHeaderTheme?.title, - avatarTheme: defaultTheme.channelPreviewTheme.avatarTheme.copyWith( - constraints: themeData - ?.channelTheme?.channelHeaderTheme?.avatarTheme?.constraints, - borderRadius: themeData - ?.channelTheme?.channelHeaderTheme?.avatarTheme?.borderRadius, - ), - ), - inputBackground: themeData?.channelTheme?.inputBackground, - messageInputButtonIconTheme: - themeData?.channelTheme?.messageInputButtonIconTheme, - inputGradient: themeData?.channelTheme?.inputGradient, - messageInputButtonTheme: - themeData?.channelTheme?.messageInputButtonTheme, - messageInputDecoration: themeData?.channelTheme?.messageInputDecoration, - ), - ownMessageTheme: defaultTheme.ownMessageTheme.copyWith( - replies: themeData?.ownMessageTheme?.replies, - createdAt: themeData?.ownMessageTheme?.createdAt, - messageText: themeData?.ownMessageTheme?.messageText, - messageBackgroundColor: - themeData?.ownMessageTheme?.messageBackgroundColor, - messageAuthor: themeData?.ownMessageTheme?.messageAuthor, - messageLinks: themeData?.ownMessageTheme?.messageLinks, - avatarTheme: defaultTheme.ownMessageTheme.avatarTheme.copyWith( - constraints: themeData?.ownMessageTheme?.avatarTheme?.constraints, - borderRadius: themeData?.ownMessageTheme?.avatarTheme?.borderRadius, - ), - ), - otherMessageTheme: defaultTheme.otherMessageTheme.copyWith( - replies: themeData?.otherMessageTheme?.replies, - createdAt: themeData?.otherMessageTheme?.createdAt, - messageText: themeData?.otherMessageTheme?.messageText, - messageBackgroundColor: - themeData?.otherMessageTheme?.messageBackgroundColor, - messageAuthor: themeData?.otherMessageTheme?.messageAuthor, - messageLinks: themeData?.otherMessageTheme?.messageLinks, - avatarTheme: defaultTheme.otherMessageTheme.avatarTheme.copyWith( - constraints: themeData?.otherMessageTheme?.avatarTheme?.constraints, - borderRadius: themeData?.otherMessageTheme?.avatarTheme?.borderRadius, - ), - ), - accentColor: themeData?.accentColor, - secondaryColor: themeData?.secondaryColor, - channelPreviewTheme: defaultTheme.channelPreviewTheme.copyWith( - avatarTheme: defaultTheme.channelPreviewTheme.avatarTheme.copyWith( - constraints: themeData?.channelPreviewTheme?.avatarTheme?.constraints, - borderRadius: - themeData?.channelPreviewTheme?.avatarTheme?.borderRadius, - ), - title: themeData?.channelPreviewTheme?.title, - lastMessageAt: themeData?.channelPreviewTheme?.lastMessageAt, - subtitle: themeData?.channelPreviewTheme?.subtitle, - unreadCounterColor: themeData?.channelPreviewTheme?.unreadCounterColor, - ), - ); - return theme; - } - - /// The current user - User get user => widget.client.state.user; - - /// The current user as a stream - Stream get userStream => widget.client.state.userStream; - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addObserver(this); - } - - StreamSubscription _newMessageSubscription; - - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - if (client.state?.user != null) { - if (state == AppLifecycleState.paused) { - if (client.showLocalNotification != null) { - _newMessageSubscription = client - .on(EventType.messageNew) - .where((e) => e.user?.id != user.id) - .where((e) => e.message.silent != true) - .where((e) => e.message.shadowed != true) - .listen((event) async { - final channel = client.channel( - event.channelType, - id: event.channelId, - ); - - client.showLocalNotification( - event.message, - ChannelModel( - id: channel.id, - createdAt: channel.createdAt, - extraData: channel.extraData, - type: channel.type, - memberCount: channel.memberCount, - frozen: channel.frozen, - cid: channel.cid, - deletedAt: channel.deletedAt, - config: channel.config, - createdBy: channel.createdBy, - updatedAt: channel.updatedAt, - lastMessageAt: channel.lastMessageAt, - ), - ); - }); - _disconnectTimer = Timer(client.backgroundKeepAlive, () { - client.disconnect(); - }); - } else { - client.disconnect(); - } - } else if (state == AppLifecycleState.resumed) { - _newMessageSubscription?.cancel(); - if (_disconnectTimer?.isActive == true) { - _disconnectTimer.cancel(); - } else { - if (client.wsConnectionStatus.value == - ConnectionStatus.disconnected) { - NotificationService.handleIosMessageQueue(client); - client.connect(); - } - } - } - } - } - - @override - void dispose() { - WidgetsBinding.instance.removeObserver(this); - super.dispose(); - } -} diff --git a/lib/src/stream_chat_theme.dart b/lib/src/stream_chat_theme.dart deleted file mode 100644 index 5af16cab..00000000 --- a/lib/src/stream_chat_theme.dart +++ /dev/null @@ -1,523 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_flutter/src/channel_header.dart'; -import 'package:stream_chat_flutter/src/channel_preview.dart'; -import 'package:stream_chat_flutter/src/message_input.dart'; - -/// Inherited widget providing the [StreamChatThemeData] to the widget tree -class StreamChatTheme extends InheritedWidget { - final StreamChatThemeData data; - - StreamChatTheme({ - Key key, - @required this.data, - Widget child, - }) : super( - key: key, - child: child, - ); - - @override - bool updateShouldNotify(StreamChatTheme old) { - return data != old.data; - } - - /// Use this method to get the current [StreamChatThemeData] instance - static StreamChatThemeData of(BuildContext context) { - final streamChatTheme = - context.dependOnInheritedWidgetOfExactType(); - - if (streamChatTheme == null) { - throw Exception( - 'You must have a StreamChatTheme widget at the top of your widget tree', - ); - } - - return streamChatTheme.data; - } -} - -/// Theme data -class StreamChatThemeData { - /// Primary color of the chat widgets - final Color primaryColor; - - /// Secondary color of the chat widgets - final Color secondaryColor; - - /// Accent color of the chat widgets - final Color accentColor; - - /// Background color of the chat widgets - final Color backgroundColor; - - /// Theme of the [ChannelPreview] - final ChannelPreviewTheme channelPreviewTheme; - - /// Theme of the chat widgets dedicated to a channel - final ChannelTheme channelTheme; - - /// Theme of the current user messages - final MessageTheme ownMessageTheme; - - /// Theme of other users messages - final MessageTheme otherMessageTheme; - - /// The widget that will be built when the channel image is unavailable - final Widget Function(BuildContext, Channel) defaultChannelImage; - - /// The widget that will be built when the user image is unavailable - final Widget Function(BuildContext, User) defaultUserImage; - - /// Primary icon theme - final IconThemeData primaryIconTheme; - - /// Create a theme from scratch - StreamChatThemeData({ - this.primaryColor, - this.secondaryColor, - this.accentColor, - this.backgroundColor, - this.channelPreviewTheme, - this.channelTheme, - this.otherMessageTheme, - this.ownMessageTheme, - this.defaultChannelImage, - this.defaultUserImage, - this.primaryIconTheme, - }); - - /// Create a theme from a Material [Theme] - factory StreamChatThemeData.fromTheme(ThemeData theme) { - final defaultTheme = getDefaultTheme(theme); - - return defaultTheme.copyWith( - accentColor: theme.accentColor, - primaryIconTheme: theme.primaryIconTheme, - primaryColor: theme.colorScheme.primary, - secondaryColor: theme.colorScheme.secondary, - backgroundColor: theme.scaffoldBackgroundColor, - channelTheme: defaultTheme.channelTheme.copyWith( - inputGradient: LinearGradient(colors: [ - theme.accentColor.withOpacity(.5), - theme.accentColor, - ]), - ), - ownMessageTheme: defaultTheme.ownMessageTheme.copyWith( - replies: defaultTheme.ownMessageTheme.replies.copyWith( - color: theme.accentColor, - ), - messageLinks: TextStyle( - color: theme.accentColor, - ), - ), - otherMessageTheme: defaultTheme.otherMessageTheme.copyWith( - replies: defaultTheme.otherMessageTheme.replies.copyWith( - color: theme.accentColor, - ), - messageLinks: TextStyle( - color: theme.accentColor, - ), - ), - ); - } - - /// Creates a copy of [StreamChatThemeData] with specified attributes overridden. - StreamChatThemeData copyWith({ - Color primaryColor, - Color secondaryColor, - Color accentColor, - Color backgroundColor, - ChannelPreviewTheme channelPreviewTheme, - ChannelTheme channelTheme, - MessageTheme ownMessageTheme, - MessageTheme otherMessageTheme, - Widget Function(BuildContext, Channel) defaultChannelImage, - Widget Function(BuildContext, User) defaultUserImage, - IconThemeData primaryIconTheme, - }) => - StreamChatThemeData( - primaryColor: primaryColor ?? this.primaryColor, - secondaryColor: secondaryColor ?? this.secondaryColor, - primaryIconTheme: primaryIconTheme ?? this.primaryIconTheme, - accentColor: accentColor ?? this.accentColor, - defaultChannelImage: defaultChannelImage ?? this.defaultChannelImage, - defaultUserImage: defaultUserImage ?? this.defaultUserImage, - backgroundColor: backgroundColor ?? this.backgroundColor, - channelPreviewTheme: channelPreviewTheme?.copyWith( - title: - channelPreviewTheme.title ?? this.channelPreviewTheme.title, - subtitle: channelPreviewTheme.subtitle ?? - this.channelPreviewTheme.subtitle, - lastMessageAt: channelPreviewTheme.lastMessageAt ?? - this.channelPreviewTheme.lastMessageAt, - avatarTheme: channelPreviewTheme.avatarTheme ?? - this.channelPreviewTheme.avatarTheme, - ) ?? - this.channelPreviewTheme, - channelTheme: channelTheme?.copyWith( - channelHeaderTheme: channelTheme.channelHeaderTheme ?? - this.channelTheme.channelHeaderTheme, - messageInputButtonIconTheme: - channelTheme.messageInputButtonIconTheme ?? - this.channelTheme.messageInputButtonIconTheme, - messageInputButtonTheme: channelTheme.messageInputButtonTheme ?? - this.channelTheme.messageInputButtonTheme, - inputGradient: - channelTheme.inputGradient ?? this.channelTheme.inputGradient, - inputBackground: channelTheme.inputBackground ?? - this.channelTheme.inputBackground, - messageInputDecoration: channelTheme.messageInputDecoration ?? - this.channelTheme.messageInputDecoration, - ) ?? - this.channelTheme, - ownMessageTheme: ownMessageTheme?.copyWith( - messageText: ownMessageTheme?.messageText ?? - this.ownMessageTheme.messageText, - messageAuthor: ownMessageTheme?.messageAuthor ?? - this.ownMessageTheme.messageAuthor, - messageLinks: ownMessageTheme?.messageLinks ?? - this.ownMessageTheme.messageLinks, - createdAt: - ownMessageTheme?.createdAt ?? this.ownMessageTheme.createdAt, - replies: ownMessageTheme?.replies ?? this.ownMessageTheme.replies, - messageBackgroundColor: ownMessageTheme?.messageBackgroundColor ?? - this.ownMessageTheme.messageBackgroundColor, - avatarTheme: ownMessageTheme?.avatarTheme ?? - this.ownMessageTheme.avatarTheme, - ) ?? - this.ownMessageTheme, - otherMessageTheme: otherMessageTheme?.copyWith( - messageText: otherMessageTheme?.messageText ?? - this.otherMessageTheme.messageText, - messageAuthor: otherMessageTheme?.messageAuthor ?? - this.otherMessageTheme.messageAuthor, - messageLinks: otherMessageTheme?.messageLinks ?? - this.otherMessageTheme.messageLinks, - createdAt: otherMessageTheme?.createdAt ?? - this.otherMessageTheme.createdAt, - replies: - otherMessageTheme?.replies ?? this.otherMessageTheme.replies, - messageBackgroundColor: - otherMessageTheme?.messageBackgroundColor ?? - this.otherMessageTheme.messageBackgroundColor, - avatarTheme: otherMessageTheme?.avatarTheme ?? - this.otherMessageTheme.avatarTheme, - ) ?? - this.otherMessageTheme, - ); - - /// Get the default Stream Chat theme - static StreamChatThemeData getDefaultTheme(ThemeData theme) { - final accentColor = Color(0xff006cff); - final isDark = theme.brightness == Brightness.dark; - return StreamChatThemeData( - accentColor: accentColor, - primaryColor: isDark ? Colors.black : Colors.white, - primaryIconTheme: - IconThemeData(color: isDark ? Colors.white : Colors.black), - 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), - ), - ), - channelPreviewTheme: ChannelPreviewTheme( - unreadCounterColor: Color(0xffd0021B), - avatarTheme: AvatarTheme( - borderRadius: BorderRadius.circular(20), - constraints: BoxConstraints.tightFor( - height: 40, - width: 40, - ), - ), - title: TextStyle( - fontSize: 14, - color: isDark ? Colors.white : Colors.black, - ), - subtitle: TextStyle( - fontSize: 13, - color: isDark ? Colors.white : Colors.black, - ), - lastMessageAt: TextStyle( - fontSize: 11, - color: isDark - ? Colors.white.withOpacity(.5) - : Colors.black.withOpacity(.5), - ), - ), - channelTheme: ChannelTheme( - messageInputDecoration: InputDecoration( - hintText: 'Write a message', - hintStyle: theme.textTheme.bodyText2, - prefixText: ' ', - border: InputBorder.none, - ), - messageInputButtonIconTheme: theme.iconTheme.copyWith( - color: accentColor, - ), - channelHeaderTheme: ChannelHeaderTheme( - avatarTheme: AvatarTheme( - borderRadius: BorderRadius.circular(20), - constraints: BoxConstraints.tightFor( - height: 40, - width: 40, - ), - ), - color: isDark ? Colors.black : Colors.white, - title: TextStyle( - fontSize: 14, - color: isDark ? Colors.white : Colors.black, - ), - lastMessageAt: TextStyle( - fontSize: 11, - color: isDark - ? Colors.white.withOpacity(.5) - : Colors.black.withOpacity(.5), - ), - ), - inputBackground: - isDark ? Colors.black.withAlpha(12) : Colors.white.withAlpha(12), - inputGradient: LinearGradient(colors: [ - Color(0xFF00AEFF), - Color(0xFF0076FF), - ]), - ), - ownMessageTheme: MessageTheme( - messageText: TextStyle( - fontSize: 15, - color: isDark ? Colors.white : Colors.black, - ), - createdAt: TextStyle( - color: isDark - ? Colors.white.withOpacity(.5) - : Colors.black.withOpacity(.5), - fontSize: 11, - ), - replies: TextStyle( - color: accentColor, - fontWeight: FontWeight.bold, - fontSize: 12, - ), - messageBackgroundColor: Color(0x33ebebeb), - avatarTheme: AvatarTheme( - borderRadius: BorderRadius.circular(20), - constraints: BoxConstraints.tightFor( - height: 32, - width: 32, - ), - ), - messageLinks: TextStyle( - color: accentColor, - ), - ), - otherMessageTheme: MessageTheme( - messageText: TextStyle( - fontSize: 15, - color: isDark ? Colors.white : Colors.black, - ), - createdAt: TextStyle( - color: isDark - ? Colors.white.withOpacity(.5) - : Colors.black.withOpacity(.5), - fontSize: 11, - ), - replies: TextStyle( - color: accentColor, - fontWeight: FontWeight.bold, - fontSize: 12, - ), - messageLinks: TextStyle( - color: accentColor, - ), - messageBackgroundColor: isDark ? Colors.black : Colors.white, - avatarTheme: AvatarTheme( - borderRadius: BorderRadius.circular(20), - constraints: BoxConstraints.tightFor( - height: 32, - width: 32, - ), - ), - ), - ); - } -} - -/// Channel theme data -class ChannelTheme { - /// Theme of the [ChannelHeader] widget - final ChannelHeaderTheme channelHeaderTheme; - - /// IconTheme of the send button in [MessageInput] - final IconThemeData messageInputButtonIconTheme; - - /// Theme of the send button in [MessageInput] - final ButtonThemeData messageInputButtonTheme; - - /// Gradient of [MessageInput] - final Gradient inputGradient; - - /// Background color of [MessageInput] - final Color inputBackground; - - /// InputDecoration of [MessageInput] - final InputDecoration messageInputDecoration; - - ChannelTheme({ - this.channelHeaderTheme, - this.messageInputButtonIconTheme, - this.messageInputButtonTheme, - this.inputBackground, - this.inputGradient, - this.messageInputDecoration, - }); - - /// Creates a copy of [ChannelTheme] with specified attributes overridden. - ChannelTheme copyWith({ - ChannelHeaderTheme channelHeaderTheme, - IconThemeData messageInputButtonIconTheme, - ButtonThemeData messageInputButtonTheme, - Gradient inputGradient, - Color inputBackground, - InputDecoration messageInputDecoration, - }) => - ChannelTheme( - channelHeaderTheme: channelHeaderTheme?.copyWith( - title: channelHeaderTheme?.title ?? this.channelHeaderTheme.title, - lastMessageAt: channelHeaderTheme?.lastMessageAt ?? - this.channelHeaderTheme.lastMessageAt, - avatarTheme: channelHeaderTheme?.avatarTheme ?? - this.channelHeaderTheme.avatarTheme, - color: channelHeaderTheme?.color ?? this.channelHeaderTheme.color, - ) ?? - this.channelHeaderTheme, - messageInputButtonIconTheme: - messageInputButtonIconTheme ?? this.messageInputButtonIconTheme, - messageInputButtonTheme: - messageInputButtonTheme ?? this.messageInputButtonTheme, - inputGradient: inputGradient ?? this.inputGradient, - inputBackground: inputBackground ?? this.inputBackground, - messageInputDecoration: - messageInputDecoration ?? this.messageInputDecoration, - ); -} - -class AvatarTheme { - final BoxConstraints constraints; - final BorderRadius borderRadius; - - AvatarTheme({ - this.constraints, - this.borderRadius, - }); - - AvatarTheme copyWith({ - BoxConstraints constraints, - BorderRadius borderRadius, - }) => - AvatarTheme( - constraints: constraints ?? this.constraints, - borderRadius: borderRadius ?? this.borderRadius, - ); -} - -class MessageTheme { - final TextStyle messageText; - final TextStyle messageAuthor; - final TextStyle messageLinks; - final TextStyle createdAt; - final TextStyle replies; - final Color messageBackgroundColor; - final AvatarTheme avatarTheme; - - const MessageTheme({ - this.replies, - this.messageText, - this.messageAuthor, - this.messageLinks, - this.messageBackgroundColor, - this.avatarTheme, - this.createdAt, - }); - - MessageTheme copyWith({ - TextStyle messageText, - TextStyle messageAuthor, - TextStyle messageLinks, - TextStyle createdAt, - TextStyle replies, - Color messageBackgroundColor, - Color otherMessageBackgroundColor, - AvatarTheme avatarTheme, - }) => - MessageTheme( - messageText: messageText ?? this.messageText, - messageAuthor: messageAuthor ?? this.messageAuthor, - messageLinks: messageLinks ?? this.messageLinks, - createdAt: createdAt ?? this.createdAt, - messageBackgroundColor: - messageBackgroundColor ?? this.messageBackgroundColor, - avatarTheme: avatarTheme ?? this.avatarTheme, - replies: replies ?? this.replies, - ); -} - -class ChannelPreviewTheme { - final TextStyle title; - final TextStyle subtitle; - final TextStyle lastMessageAt; - final AvatarTheme avatarTheme; - final Color unreadCounterColor; - - const ChannelPreviewTheme({ - this.title, - this.subtitle, - this.lastMessageAt, - this.avatarTheme, - this.unreadCounterColor, - }); - - ChannelPreviewTheme copyWith({ - TextStyle title, - TextStyle subtitle, - TextStyle lastMessageAt, - AvatarTheme avatarTheme, - Color unreadCounterColor, - }) => - ChannelPreviewTheme( - title: title ?? this.title, - subtitle: subtitle ?? this.subtitle, - lastMessageAt: lastMessageAt ?? this.lastMessageAt, - avatarTheme: avatarTheme ?? this.avatarTheme, - unreadCounterColor: unreadCounterColor ?? this.unreadCounterColor, - ); -} - -class ChannelHeaderTheme { - final TextStyle title; - final TextStyle lastMessageAt; - final AvatarTheme avatarTheme; - final Color color; - - const ChannelHeaderTheme({ - this.title, - this.lastMessageAt, - this.avatarTheme, - this.color, - }); - - ChannelHeaderTheme copyWith({ - TextStyle title, - TextStyle lastMessageAt, - AvatarTheme avatarTheme, - Color color, - }) => - ChannelHeaderTheme( - title: title ?? this.title, - lastMessageAt: lastMessageAt ?? this.lastMessageAt, - avatarTheme: avatarTheme ?? this.avatarTheme, - color: color ?? this.color, - ); -} diff --git a/lib/src/system_message.dart b/lib/src/system_message.dart deleted file mode 100644 index 7b4dc243..00000000 --- a/lib/src/system_message.dart +++ /dev/null @@ -1,116 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:jiffy/jiffy.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// It shows a date divider depending on the date difference -class SystemMessage extends StatelessWidget { - /// This message - final Message message; - - /// The function called when tapping on the message when the message is not failed - final void Function(Message) onMessageTap; - - const SystemMessage({ - Key key, - @required this.message, - this.onMessageTap, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - final divider = Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Divider(), - ), - ); - - final createdAt = Jiffy(message.createdAt.toLocal()); - final now = DateTime.now(); - final hourInfo = createdAt.format('h:mm a'); - - String dayInfo; - if (Jiffy(createdAt).isSame(now, Units.DAY)) { - dayInfo = 'TODAY'; - } else if (Jiffy(createdAt) - .isSame(now.subtract(Duration(days: 1)), Units.DAY)) { - dayInfo = 'YESTERDAY'; - } else if (Jiffy(createdAt).isAfter( - now.subtract(Duration(days: 7)), - Units.DAY, - )) { - dayInfo = createdAt.format('EEEE').toUpperCase(); - } else if (Jiffy(createdAt).isAfter( - Jiffy(now).subtract(years: 1), - Units.DAY, - )) { - dayInfo = createdAt.format('dd/MM').toUpperCase(); - } else { - dayInfo = createdAt.format('dd/MM/yyyy').toUpperCase(); - } - - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - if (onMessageTap != null) { - onMessageTap(message); - } - }, - child: Container( - width: double.infinity, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - divider, - Padding( - padding: const EdgeInsets.symmetric(horizontal: 32.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - message.text, - style: TextStyle( - fontSize: 10, - color: Theme.of(context) - .textTheme - .headline6 - .color - .withOpacity(.5), - fontWeight: FontWeight.bold, - ), - ), - Text.rich( - TextSpan( - children: [ - TextSpan( - text: dayInfo, - style: TextStyle( - fontWeight: FontWeight.bold, - ), - ), - TextSpan(text: ' AT'), - TextSpan(text: ' $hourInfo'), - ], - style: TextStyle( - fontWeight: FontWeight.normal, - ), - ), - style: TextStyle( - fontSize: 10, - color: Theme.of(context) - .textTheme - .headline6 - .color - .withOpacity(.5), - ), - ), - ], - ), - ), - divider, - ], - ), - ), - ); - } -} diff --git a/pubspec.yaml b/pubspec.yaml deleted file mode 100644 index 849339b8..00000000 --- a/pubspec.yaml +++ /dev/null @@ -1,35 +0,0 @@ -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.21 -repository: https://github.com/GetStream/stream-chat-flutter -issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues - -environment: - sdk: ">=2.3.0 <3.0.0" - -dependencies: - flutter: - sdk: flutter - photo_view: ^0.10.1 - rxdart: ^0.24.1 - jiffy: ^3.0.1 - flutter_portal: ^0.3.0 - cached_network_image: ^2.2.0+1 - flutter_markdown: ^0.5.0 - url_launcher: ^5.4.11 - video_player: ^1.0.0 - chewie: ^0.12.0 - file_picker: ^2.0.12 - image_picker: ^0.6.7+2 - flutter_keyboard_visibility: ^4.0.1 - stream_chat: ^0.2.21+1 - mime: ^0.9.6+3 - visibility_detector: ^0.1.5 - http_parser: ^3.1.4 - -dev_dependencies: - pedantic: ^1.9.0 - flutter_test: - sdk: flutter - mockito: ^4.1.1