diff --git a/packages/stream_chat_v1/lib/advanced_options_page.dart b/packages/stream_chat_v1/lib/advanced_options_page.dart index 29fe270..4a97c68 100644 --- a/packages/stream_chat_v1/lib/advanced_options_page.dart +++ b/packages/stream_chat_v1/lib/advanced_options_page.dart @@ -1,3 +1,4 @@ +import 'package:example/home_page.dart'; import 'package:example/routes/routes.dart'; import 'package:example/stream_version.dart'; import 'package:flutter/material.dart'; @@ -306,7 +307,6 @@ class _AdvancedOptionsPageState extends State { key: kStreamToken, value: userToken, ); - await client.disconnect(); } catch (e) { var errorText = 'Error connecting, retry'; if (e is Map) { @@ -317,15 +317,14 @@ class _AdvancedOptionsPageState extends State { _apiKeyError = errorText.toUpperCase(); }); loading = false; - await client.disconnect(); return; } loading = false; await Navigator.pushNamedAndRemoveUntil( context, - Routes.APP, - ModalRoute.withName(Routes.APP), - arguments: client, + Routes.HOME, + ModalRoute.withName(Routes.HOME), + arguments: HomePageArgs(client), ); } }, diff --git a/packages/stream_chat_v1/lib/channel_list.dart b/packages/stream_chat_v1/lib/channel_list.dart new file mode 100644 index 0000000..e88c9a1 --- /dev/null +++ b/packages/stream_chat_v1/lib/channel_list.dart @@ -0,0 +1,199 @@ +import 'dart:async'; + +import 'package:example/routes/routes.dart'; +import 'package:example/search_text_field.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'channel_page.dart'; +import 'chat_info_screen.dart'; +import 'group_info_screen.dart'; + +class ChannelList extends StatefulWidget { + @override + _ChannelList createState() => _ChannelList(); +} + +class _ChannelList extends State { + TextEditingController? _controller; + + String _channelQuery = ''; + + bool _isSearchActive = false; + + Timer? _debounce; + + void _channelQueryListener() { + if (_debounce?.isActive ?? false) _debounce!.cancel(); + _debounce = Timer(const Duration(milliseconds: 350), () { + if (mounted) { + setState(() { + _channelQuery = _controller!.text; + _isSearchActive = _channelQuery.isNotEmpty; + }); + } + }); + } + + @override + void initState() { + super.initState(); + _controller = TextEditingController()..addListener(_channelQueryListener); + } + + @override + void dispose() { + _controller?.removeListener(_channelQueryListener); + _controller?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final user = StreamChat.of(context).user; + return WillPopScope( + onWillPop: () async { + if (_isSearchActive) { + _controller!.clear(); + setState(() => _isSearchActive = false); + return false; + } + return true; + }, + child: ChannelsBloc( + child: MessageSearchBloc( + child: NestedScrollView( + floatHeaderSlivers: true, + headerSliverBuilder: (_, __) => [ + SliverToBoxAdapter( + child: SearchTextField( + controller: _controller, + showCloseButton: _isSearchActive, + ), + ), + ], + body: AnimatedSwitcher( + duration: const Duration(milliseconds: 350), + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onPanDown: (_) => FocusScope.of(context).unfocus(), + child: _isSearchActive + ? MessageSearchListView( + showErrorTile: true, + messageQuery: _channelQuery, + filters: Filter.in_('members', [user!.id]), + sortOptions: [ + SortOption( + 'created_at', + direction: SortOption.ASC, + ), + ], + pullToRefresh: false, + paginationParams: PaginationParams(limit: 20), + emptyBuilder: (_) { + return LayoutBuilder( + builder: (context, viewportConstraints) { + return SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: viewportConstraints.maxHeight, + ), + child: Center( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(24), + child: StreamSvgIcon.search( + size: 96, + color: Colors.grey, + ), + ), + Text( + 'No results...', + ), + ], + ), + ), + ), + ); + }, + ); + }, + onItemTap: (messageResponse) async { + FocusScope.of(context).requestFocus(FocusNode()); + final client = StreamChat.of(context).client; + final message = messageResponse.message; + final channel = client.channel( + messageResponse.channel!.type, + id: messageResponse.channel!.id, + ); + if (channel.state == null) { + await channel.watch(); + } + Navigator.pushNamed( + context, + Routes.CHANNEL_PAGE, + arguments: ChannelPageArgs( + channel: channel, + initialMessage: message, + ), + ); + }, + ) + : ChannelListView( + onStartChatPressed: () { + Navigator.pushNamed(context, Routes.NEW_CHAT); + }, + swipeToAction: true, + filter: Filter.in_('members', [user!.id]), + presence: true, + pagination: PaginationParams( + limit: 20, + ), + channelWidget: ChannelPage(), + onViewInfoTap: (channel) { + Navigator.pop(context); + if (channel.memberCount == 2 && channel.isDistinct) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: channel, + child: ChatInfoScreen( + messageTheme: StreamChatTheme.of(context) + .ownMessageTheme, + user: channel.state!.members + .where((m) => + m.userId != + channel.client.state.user!.id) + .first + .user, + ), + ), + ), + ); + } else { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: channel, + child: GroupInfoScreen( + messageTheme: StreamChatTheme.of(context) + .ownMessageTheme, + ), + ), + ), + ); + } + }, + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/packages/stream_chat_v1/lib/channel_list_page.dart b/packages/stream_chat_v1/lib/channel_list_page.dart new file mode 100644 index 0000000..baf6eca --- /dev/null +++ b/packages/stream_chat_v1/lib/channel_list_page.dart @@ -0,0 +1,281 @@ +import 'dart:async'; + +import 'package:example/routes/routes.dart'; +import 'package:example/user_mentions_page.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_app_badger/flutter_app_badger.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:streaming_shared_preferences/streaming_shared_preferences.dart'; + +import 'channel_list.dart'; + +class ChannelListPage extends StatefulWidget { + const ChannelListPage({ + Key? key, + }) : super(key: key); + + @override + _ChannelListPageState createState() => _ChannelListPageState(); +} + +class _ChannelListPageState extends State { + int _currentIndex = 0; + + bool _isSelected(int index) => _currentIndex == index; + + List get _navBarItems { + return [ + BottomNavigationBarItem( + icon: Stack( + clipBehavior: Clip.none, + children: [ + StreamSvgIcon.message( + color: _isSelected(0) + ? StreamChatTheme.of(context).colorTheme.black + : Colors.grey, + ), + Positioned( + top: -3, + right: -16, + child: UnreadIndicator(), + ), + ], + ), + label: 'Chats', + ), + BottomNavigationBarItem( + icon: Stack( + clipBehavior: Clip.none, + children: [ + StreamSvgIcon.mentions( + color: _isSelected(1) + ? StreamChatTheme.of(context).colorTheme.black + : Colors.grey, + ), + ], + ), + label: 'Mentions', + ), + ]; + } + + @override + Widget build(BuildContext context) { + final user = StreamChat.of(context).user; + if (user == null) { + return Offstage(); + } + return Scaffold( + backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow, + appBar: ChannelListHeader( + onNewChatButtonTap: () { + Navigator.pushNamed(context, Routes.NEW_CHAT); + }, + preNavigationCallback: () { + FocusScope.of(context).requestFocus(FocusNode()); + }, + ), + drawer: LeftDrawer( + user: user, + ), + drawerEdgeDragWidth: 50, + bottomNavigationBar: BottomNavigationBar( + backgroundColor: StreamChatTheme.of(context).colorTheme.white, + currentIndex: _currentIndex, + items: _navBarItems, + selectedLabelStyle: StreamChatTheme.of(context).textTheme.footnoteBold, + unselectedLabelStyle: + StreamChatTheme.of(context).textTheme.footnoteBold, + type: BottomNavigationBarType.fixed, + selectedItemColor: StreamChatTheme.of(context).colorTheme.black, + unselectedItemColor: Colors.grey, + onTap: (index) { + setState(() => _currentIndex = index); + }, + ), + body: IndexedStack( + index: _currentIndex, + children: [ + ChannelList(), + UserMentionsPage(), + ], + ), + ); + } + + StreamSubscription? badgeListener; + + @override + void initState() { + if (!kIsWeb) { + badgeListener = StreamChat.of(context) + .client + .state + .totalUnreadCountStream + .listen((count) { + if (count > 0) { + FlutterAppBadger.updateBadgeCount(count); + } else { + FlutterAppBadger.removeBadge(); + } + }); + } + super.initState(); + } + + @override + void dispose() { + badgeListener?.cancel(); + super.dispose(); + } +} + +class LeftDrawer extends StatelessWidget { + const LeftDrawer({ + Key? key, + required this.user, + }) : super(key: key); + + final User user; + + @override + Widget build(BuildContext context) { + return Drawer( + child: Container( + color: StreamChatTheme.of(context).colorTheme.white, + child: SafeArea( + child: Padding( + padding: EdgeInsets.only( + top: MediaQuery.of(context).viewPadding.top + 8, + ), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only( + bottom: 20.0, + left: 8, + ), + child: Row( + children: [ + UserAvatar( + user: user, + showOnlineStatus: false, + constraints: BoxConstraints.tight(Size.fromRadius(20)), + ), + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Text( + user.name, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + ListTile( + leading: StreamSvgIcon.penWrite( + color: StreamChatTheme.of(context) + .colorTheme + .black + .withOpacity(.5), + ), + onTap: () { + Navigator.popAndPushNamed( + context, + Routes.NEW_CHAT, + ); + }, + title: Text( + 'New direct message', + style: TextStyle( + fontSize: 14.5, + ), + ), + ), + ListTile( + leading: StreamSvgIcon.contacts( + color: StreamChatTheme.of(context) + .colorTheme + .black + .withOpacity(.5), + ), + onTap: () { + Navigator.popAndPushNamed( + context, + Routes.NEW_GROUP_CHAT, + ); + }, + title: Text( + 'New group', + style: TextStyle( + fontSize: 14.5, + ), + ), + ), + Expanded( + child: Container( + alignment: Alignment.bottomCenter, + child: ListTile( + onTap: () async { + Navigator.pop(context); + + if (!kIsWeb) { + final secureStorage = FlutterSecureStorage(); + await secureStorage.deleteAll(); + } + + final client = StreamChat.of(context).client; + client.disconnectUser(); + await client.dispose(); + + await Navigator.of( + context, + rootNavigator: true, + ).pushNamedAndRemoveUntil( + Routes.CHOOSE_USER, + ModalRoute.withName(Routes.CHOOSE_USER), + ); + }, + leading: StreamSvgIcon.user( + color: StreamChatTheme.of(context) + .colorTheme + .black + .withOpacity(.5), + ), + title: Text( + 'Sign out', + style: TextStyle( + fontSize: 14.5, + ), + ), + trailing: IconButton( + icon: StreamSvgIcon.iconMoon( + size: 24, + ), + color: StreamChatTheme.of(context).colorTheme.grey, + onPressed: () async { + final sp = await StreamingSharedPreferences.instance; + sp.setInt( + 'theme', + Theme.of(context).brightness == Brightness.dark + ? 1 + : -1, + ); + }, + ), + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/packages/stream_chat_v1/lib/channel_media_display_screen.dart b/packages/stream_chat_v1/lib/channel_media_display_screen.dart index 5c33622..15f8fd6 100644 --- a/packages/stream_chat_v1/lib/channel_media_display_screen.dart +++ b/packages/stream_chat_v1/lib/channel_media_display_screen.dart @@ -71,21 +71,7 @@ class _ChannelMediaDisplayScreenState extends State { fontSize: 16.0, ), ), - leading: Center( - child: InkWell( - onTap: () { - Navigator.of(context).pop(); - }, - child: Container( - width: 24.0, - height: 24.0, - child: StreamSvgIcon.left( - color: StreamChatTheme.of(context).colorTheme.black, - size: 24.0, - ), - ), - ), - ), + leading: StreamBackButton(), backgroundColor: StreamChatTheme.of(context).colorTheme.white, ), body: _buildMediaGrid(), diff --git a/packages/stream_chat_v1/lib/channel_page.dart b/packages/stream_chat_v1/lib/channel_page.dart new file mode 100644 index 0000000..1ff592c --- /dev/null +++ b/packages/stream_chat_v1/lib/channel_page.dart @@ -0,0 +1,183 @@ +import 'package:collection/collection.dart'; +import 'package:example/routes/routes.dart'; +import 'package:example/thread_page.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'chat_info_screen.dart'; +import 'group_info_screen.dart'; + +class ChannelPageArgs { + final Channel? channel; + final Message? initialMessage; + + const ChannelPageArgs({ + this.channel, + this.initialMessage, + }); +} + +class ChannelPage extends StatefulWidget { + final int? initialScrollIndex; + final double? initialAlignment; + final bool highlightInitialMessage; + + const ChannelPage({ + Key? key, + this.initialScrollIndex, + this.initialAlignment, + this.highlightInitialMessage = false, + }) : super(key: key); + + @override + _ChannelPageState createState() => _ChannelPageState(); +} + +class _ChannelPageState extends State { + Message? _quotedMessage; + FocusNode? _focusNode; + + @override + void initState() { + _focusNode = FocusNode(); + super.initState(); + } + + @override + void dispose() { + _focusNode!.dispose(); + super.dispose(); + } + + void _reply(Message message) { + setState(() => _quotedMessage = message); + WidgetsBinding.instance!.addPostFrameCallback((timeStamp) { + _focusNode!.requestFocus(); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow, + appBar: ChannelHeader( + showTypingIndicator: false, + onImageTap: () async { + var channel = StreamChannel.of(context).channel; + + if (channel.memberCount == 2 && channel.isDistinct) { + final currentUser = StreamChat.of(context).user; + final otherUser = channel.state!.members.firstWhereOrNull( + (element) => element.user!.id != currentUser!.id, + ); + if (otherUser != null) { + final pop = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StreamChannel( + child: ChatInfoScreen( + messageTheme: StreamChatTheme.of(context).ownMessageTheme, + user: otherUser.user, + ), + channel: channel, + ), + ), + ); + + if (pop == true) { + Navigator.pop(context); + } + } + } else { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StreamChannel( + child: GroupInfoScreen( + messageTheme: StreamChatTheme.of(context).ownMessageTheme, + ), + channel: channel, + ), + ), + ); + } + }, + ), + body: Column( + children: [ + Expanded( + child: Stack( + children: [ + MessageListView( + initialScrollIndex: widget.initialScrollIndex, + initialAlignment: widget.initialAlignment, + highlightInitialMessage: widget.highlightInitialMessage, + onMessageSwiped: _reply, + onReplyTap: _reply, + threadBuilder: (_, parentMessage) { + return ThreadPage( + parent: parentMessage, + ); + }, + onShowMessage: (m, c) async { + final client = StreamChat.of(context).client; + final message = m; + final channel = client.channel( + c.type, + id: c.id, + ); + if (channel.state == null) { + await channel.watch(); + } + Navigator.pushReplacementNamed( + context, + Routes.CHANNEL_PAGE, + arguments: ChannelPageArgs( + channel: channel, + initialMessage: message, + ), + ); + }, + pinPermissions: ['owner', 'admin', 'member'], + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + alignment: Alignment.centerLeft, + color: StreamChatTheme.of(context) + .colorTheme + .whiteSnow + .withOpacity(.9), + child: TypingIndicator( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + style: StreamChatTheme.of(context) + .textTheme + .footnote + .copyWith( + color: + StreamChatTheme.of(context).colorTheme.grey), + ), + ), + ), + ], + ), + ), + MessageInput( + focusNode: _focusNode, + quotedMessage: _quotedMessage, + onQuotedMessageCleared: () { + setState(() => _quotedMessage = null); + _focusNode!.unfocus(); + }, + ), + ], + ), + ); + } +} diff --git a/packages/stream_chat_v1/lib/chat_info_screen.dart b/packages/stream_chat_v1/lib/chat_info_screen.dart index eca16bb..6517ef3 100644 --- a/packages/stream_chat_v1/lib/chat_info_screen.dart +++ b/packages/stream_chat_v1/lib/chat_info_screen.dart @@ -6,8 +6,8 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'channel_file_display_screen.dart'; import 'channel_media_display_screen.dart'; +import 'channel_page.dart'; import 'pinned_messages_screen.dart'; -import 'main.dart'; import 'routes/routes.dart'; /// Detail screen for a 1:1 chat correspondence @@ -615,7 +615,7 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> { title = 'No title'; } } else { - title = extraData['name']; + title = extraData['name'] as String; } return Column( @@ -633,7 +633,7 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> { ), Expanded( child: Text( - title!, + title, style: textStyle, )), Padding( diff --git a/packages/stream_chat_v1/lib/choose_user_page.dart b/packages/stream_chat_v1/lib/choose_user_page.dart index a6dd5e1..e7be235 100644 --- a/packages/stream_chat_v1/lib/choose_user_page.dart +++ b/packages/stream_chat_v1/lib/choose_user_page.dart @@ -1,4 +1,5 @@ import 'package:example/app_config.dart'; +import 'package:example/home_page.dart'; import 'package:example/stream_version.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -6,6 +7,7 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'main.dart'; import 'routes/routes.dart'; const kStreamApiKey = 'STREAM_API_KEY'; @@ -89,8 +91,11 @@ class ChooseUserPage extends StatelessWidget { ), ); - final client = StreamChat.of(context).client; - client.apiKey = kDefaultStreamApiKey; + final client = StreamChatClient( + kDefaultStreamApiKey, + logLevel: Level.INFO, + )..chatPersistenceClient = chatPersistentClient; + await client.connectUser( user, token, @@ -115,6 +120,7 @@ class ChooseUserPage extends StatelessWidget { context, Routes.HOME, ModalRoute.withName(Routes.HOME), + arguments: HomePageArgs(client), ); }, leading: UserAvatar( diff --git a/packages/stream_chat_v1/lib/group_chat_details_screen.dart b/packages/stream_chat_v1/lib/group_chat_details_screen.dart index a1a92b8..c59f744 100644 --- a/packages/stream_chat_v1/lib/group_chat_details_screen.dart +++ b/packages/stream_chat_v1/lib/group_chat_details_screen.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:uuid/uuid.dart'; -import 'main.dart'; +import 'channel_page.dart'; import 'routes/routes.dart'; class GroupChatDetailsScreen extends StatefulWidget { diff --git a/packages/stream_chat_v1/lib/group_info_screen.dart b/packages/stream_chat_v1/lib/group_info_screen.dart index 6c45757..1d9572e 100644 --- a/packages/stream_chat_v1/lib/group_info_screen.dart +++ b/packages/stream_chat_v1/lib/group_info_screen.dart @@ -10,9 +10,9 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'channel_file_display_screen.dart'; import 'channel_media_display_screen.dart'; -import 'pinned_messages_screen.dart'; +import 'channel_page.dart'; import 'chat_info_screen.dart'; -import 'main.dart'; +import 'pinned_messages_screen.dart'; import 'routes/routes.dart'; class GroupInfoScreen extends StatefulWidget { @@ -61,7 +61,9 @@ class _GroupInfoScreenState extends State { super.initState(); var channel = StreamChannel.of(context); _nameController = TextEditingController.fromValue( - TextEditingValue(text: channel.channel.extraData['name'] ?? '')); + TextEditingValue( + text: (channel.channel.extraData['name'] as String?) ?? ''), + ); _searchController = TextEditingController()..addListener(_userNameListener); _nameController!.addListener(() { @@ -333,7 +335,7 @@ class _GroupInfoScreenState extends State { Widget _buildNameTile() { var channel = StreamChannel.of(context).channel; - var channelName = channel.extraData['name'] ?? ''; + var channelName = (channel.extraData['name'] as String?) ?? ''; return Material( color: StreamChatTheme.of(context).colorTheme.whiteSnow, @@ -377,8 +379,7 @@ class _GroupInfoScreenState extends State { ), ), ), - if ((channelName == null) || - (channelName != _nameController!.text.trim())) + if (channelName != _nameController!.text.trim()) Row( mainAxisSize: MainAxisSize.min, children: [ diff --git a/packages/stream_chat_v1/lib/home_page.dart b/packages/stream_chat_v1/lib/home_page.dart new file mode 100644 index 0000000..7746729 --- /dev/null +++ b/packages/stream_chat_v1/lib/home_page.dart @@ -0,0 +1,31 @@ +import 'package:example/routes/app_routes.dart'; +import 'package:example/routes/routes.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +class HomePageArgs { + final StreamChatClient chatClient; + + HomePageArgs(this.chatClient); +} + +class HomePage extends StatelessWidget { + HomePage({ + Key? key, + required this.chatClient, + }) : super(key: key); + + final StreamChatClient chatClient; + + @override + Widget build(BuildContext context) { + return StreamChat( + client: chatClient, + child: Navigator( + onGenerateRoute: AppRoutes.generateRoute, + initialRoute: Routes.CHANNEL_LIST_PAGE, + ), + ); + } +} diff --git a/packages/stream_chat_v1/lib/main.dart b/packages/stream_chat_v1/lib/main.dart index c4d16b1..e53e8c0 100644 --- a/packages/stream_chat_v1/lib/main.dart +++ b/packages/stream_chat_v1/lib/main.dart @@ -1,26 +1,20 @@ import 'dart:async'; -import 'package:collection/collection.dart' show IterableExtension; -import 'package:example/chat_info_screen.dart'; import 'package:example/choose_user_page.dart'; -import 'package:example/group_info_screen.dart'; +import 'package:example/home_page.dart'; +import 'package:example/splash_screen.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_app_badger/flutter_app_badger.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:lottie/lottie.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_persistence/stream_chat_persistence.dart'; import 'package:streaming_shared_preferences/streaming_shared_preferences.dart'; import 'app_config.dart'; -import 'notifications_service.dart'; import 'routes/app_routes.dart'; import 'routes/routes.dart'; -import 'search_text_field.dart'; final chatPersistentClient = StreamChatPersistenceClient( logLevel: Level.SEVERE, @@ -28,8 +22,6 @@ final chatPersistentClient = StreamChatPersistenceClient( ); void main() async { - WidgetsFlutterBinding.ensureInitialized(); - runApp(MyApp()); } @@ -38,13 +30,9 @@ class MyApp extends StatefulWidget { _MyAppState createState() => _MyAppState(); } -class _MyAppState extends State with TickerProviderStateMixin { +class _MyAppState extends State + with SplashScreenStateMixin, TickerProviderStateMixin { InitData? _initData; - bool _animCompleted = false; - Animation? _animation, _scaleAnimation; - AnimationController? _animationController, _scaleAnimationController; - Animation? _colorAnimation; - late int timeOfStartMs; Future _initConnection() async { String? apiKey, userId, token; @@ -61,7 +49,7 @@ class _MyAppState extends State with TickerProviderStateMixin { logLevel: Level.INFO, )..chatPersistenceClient = chatPersistentClient; - if (userId != null) { + if (userId != null && token != null) { await client.connectUser( User(id: userId), token, @@ -73,56 +61,9 @@ class _MyAppState extends State with TickerProviderStateMixin { return InitData(client, prefs); } - void _createAnimations() { - _scaleAnimationController = AnimationController( - vsync: this, - value: 0, - duration: Duration( - milliseconds: 500, - ), - ); - _scaleAnimation = Tween( - begin: 1.0, - end: 1.5, - ).animate(CurvedAnimation( - parent: _scaleAnimationController!, - curve: Curves.easeInOutBack, - )); - - _animationController = AnimationController( - vsync: this, - duration: Duration( - milliseconds: 1000, - ), - ); - _animation = Tween( - begin: 0.0, - end: 1000.0, - ).animate(CurvedAnimation( - parent: _animationController!, - curve: Curves.easeInOut, - )); - _colorAnimation = ColorTween( - begin: Color(0xff005FFF), - end: Color(0xff005FFF), - ).animate(CurvedAnimation( - parent: _animationController!, - curve: Curves.easeInOut, - )); - _colorAnimation = ColorTween( - begin: Color(0xff005FFF), - end: Colors.transparent, - ).animate(CurvedAnimation( - parent: _animationController!, - curve: Curves.easeInOut, - )); - } - @override void initState() { - timeOfStartMs = DateTime.now().millisecondsSinceEpoch; - - _createAnimations(); + final timeOfStartMs = DateTime.now().millisecondsSinceEpoch; _initConnection().then( (initData) { @@ -130,97 +71,23 @@ class _MyAppState extends State with TickerProviderStateMixin { _initData = initData; }); - var now = DateTime.now().millisecondsSinceEpoch; + final now = DateTime.now().millisecondsSinceEpoch; if (now - timeOfStartMs > 1500) { SchedulerBinding.instance!.addPostFrameCallback((timeStamp) { - _scaleAnimationController?.forward().whenComplete(() { - _animationController?.forward(); - }); + forwardAnimations(); }); } else { Future.delayed(Duration(milliseconds: 1500)).then((value) { - _scaleAnimationController?.forward().whenComplete(() { - _animationController?.forward(); - }); - }); - } - - if (!kIsWeb) { - _initData!.client.state.totalUnreadCountStream.listen((count) { - if (count! > 0) { - FlutterAppBadger.updateBadgeCount(count); - } else { - FlutterAppBadger.removeBadge(); - } + forwardAnimations(); }); } }, ); - _animationController?.addStatusListener((status) { - if (status == AnimationStatus.completed) { - setState(() { - _animCompleted = true; - }); - } - }); super.initState(); } - Widget _buildAnimation() { - return MaterialApp( - home: Stack( - clipBehavior: Clip.none, - alignment: Alignment.center, - children: [ - AnimatedBuilder( - animation: _scaleAnimation!, - builder: (context, _) { - return Transform.scale( - scale: _scaleAnimation!.value, - child: AnimatedBuilder( - animation: _colorAnimation!, - builder: (context, snapshot) { - return Container( - alignment: Alignment.center, - constraints: BoxConstraints.expand(), - color: _colorAnimation == null - ? Color(0xff005FFF) - : _colorAnimation!.value, - child: !_animationController!.isAnimating - ? Lottie.asset( - 'assets/floating_boat.json', - alignment: Alignment.center, - ) - : SizedBox(), - ); - }), - ); - }, - ), - AnimatedBuilder( - animation: _animation!, - builder: (context, snapshot) { - return Transform.scale( - scale: _animation!.value, - child: Container( - width: 1.0, - height: 1.0, - decoration: BoxDecoration( - color: Colors.white - .withOpacity(1 - _animationController!.value), - shape: BoxShape.circle, - ), - ), - ); - }, - ), - ], - ), - ); - } - @override Widget build(BuildContext context) { return Stack( @@ -233,26 +100,6 @@ class _MyAppState extends State with TickerProviderStateMixin { defaultValue: 0, ), builder: (context, snapshot) => MaterialApp( - builder: (context, child) { - return StreamChat( - client: _initData!.client, - onBackgroundEventReceived: (e) => showLocalNotification( - e, _initData!.client.state.user!.id), - child: Builder( - builder: (context) => AnnotatedRegion( - child: child!, - value: SystemUiOverlayStyle( - systemNavigationBarColor: - StreamChatTheme.of(context).colorTheme.white, - systemNavigationBarIconBrightness: - Theme.of(context).brightness == Brightness.dark - ? Brightness.light - : Brightness.dark, - ), - ), - ), - ); - }, theme: ThemeData.light(), darkTheme: ThemeData.dark(), themeMode: { @@ -260,806 +107,46 @@ class _MyAppState extends State with TickerProviderStateMixin { 0: ThemeMode.system, 1: ThemeMode.light, }[snapshot], + builder: (context, child) => StreamChatTheme( + data: StreamChatThemeData( + brightness: Theme.of(context).brightness, + ), + child: child!, + ), onGenerateRoute: AppRoutes.generateRoute, + onGenerateInitialRoutes: (initialRouteName) { + if (initialRouteName == Routes.HOME) { + return [ + AppRoutes.generateRoute( + RouteSettings( + name: Routes.HOME, + arguments: HomePageArgs(_initData!.client), + ), + )! + ]; + } + return [ + AppRoutes.generateRoute( + RouteSettings( + name: Routes.CHOOSE_USER, + ), + )! + ]; + }, initialRoute: _initData!.client.state.user == null ? Routes.CHOOSE_USER : Routes.HOME, ), ), - if (!_animCompleted) _buildAnimation(), + if (!animationCompleted) buildAnimation(), ], ); } } -class HomePage extends StatefulWidget { - @override - _HomePageState createState() => _HomePageState(); -} - -class _HomePageState extends State { - int _currentIndex = 0; - - bool _isSelected(int index) => _currentIndex == index; - - List get _navBarItems { - return [ - BottomNavigationBarItem( - icon: Stack( - clipBehavior: Clip.none, - children: [ - StreamSvgIcon.message( - color: _isSelected(0) - ? StreamChatTheme.of(context).colorTheme.black - : Colors.grey, - ), - Positioned( - top: -3, - right: -16, - child: UnreadIndicator(), - ), - ], - ), - label: 'Chats', - ), - BottomNavigationBarItem( - icon: Stack( - clipBehavior: Clip.none, - children: [ - StreamSvgIcon.mentions( - color: _isSelected(1) - ? StreamChatTheme.of(context).colorTheme.black - : Colors.grey, - ), - ], - ), - label: 'Mentions', - ), - ]; - } - - @override - Widget build(BuildContext context) { - final user = StreamChat.of(context).user!; - return Scaffold( - backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow, - appBar: ChannelListHeader( - onNewChatButtonTap: () { - Navigator.pushNamed(context, Routes.NEW_CHAT); - }, - preNavigationCallback: () { - FocusScope.of(context).requestFocus(FocusNode()); - }, - ), - drawer: _buildDrawer(context, user), - drawerEdgeDragWidth: 50, - bottomNavigationBar: BottomNavigationBar( - backgroundColor: StreamChatTheme.of(context).colorTheme.white, - currentIndex: _currentIndex, - items: _navBarItems, - selectedLabelStyle: StreamChatTheme.of(context).textTheme.footnoteBold, - unselectedLabelStyle: - StreamChatTheme.of(context).textTheme.footnoteBold, - type: BottomNavigationBarType.fixed, - selectedItemColor: StreamChatTheme.of(context).colorTheme.black, - unselectedItemColor: Colors.grey, - onTap: (index) { - setState(() => _currentIndex = index); - }, - ), - body: IndexedStack( - index: _currentIndex, - children: [ - ChannelListPage(), - UserMentionPage(), - ], - ), - ); - } - - Drawer _buildDrawer(BuildContext context, User user) { - return Drawer( - child: Container( - color: StreamChatTheme.of(context).colorTheme.white, - child: SafeArea( - child: Padding( - padding: EdgeInsets.only( - top: MediaQuery.of(context).viewPadding.top + 8, - ), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.only( - bottom: 20.0, - left: 8, - ), - child: Row( - children: [ - UserAvatar( - user: user, - showOnlineStatus: false, - constraints: BoxConstraints.tight(Size.fromRadius(20)), - ), - Padding( - padding: const EdgeInsets.only(left: 16.0), - child: Text( - user.name, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - ), - ListTile( - leading: StreamSvgIcon.penWrite( - color: StreamChatTheme.of(context) - .colorTheme - .black - .withOpacity(.5), - ), - onTap: () { - Navigator.popAndPushNamed( - context, - Routes.NEW_CHAT, - ); - }, - title: Text( - 'New direct message', - style: TextStyle( - fontSize: 14.5, - ), - ), - ), - ListTile( - leading: StreamSvgIcon.contacts( - color: StreamChatTheme.of(context) - .colorTheme - .black - .withOpacity(.5), - ), - onTap: () { - Navigator.popAndPushNamed( - context, - Routes.NEW_GROUP_CHAT, - ); - }, - title: Text( - 'New group', - style: TextStyle( - fontSize: 14.5, - ), - ), - ), - Expanded( - child: Container( - alignment: Alignment.bottomCenter, - child: ListTile( - onTap: () async { - Navigator.pop(context); - - if (!kIsWeb) { - final secureStorage = FlutterSecureStorage(); - await secureStorage.deleteAll(); - } - - StreamChat.of(context).client.disconnect( - clearUser: true, - ); - - await Navigator.pushReplacementNamed( - context, - Routes.CHOOSE_USER, - ); - }, - leading: StreamSvgIcon.user( - color: StreamChatTheme.of(context) - .colorTheme - .black - .withOpacity(.5), - ), - title: Text( - 'Sign out', - style: TextStyle( - fontSize: 14.5, - ), - ), - trailing: IconButton( - icon: StreamSvgIcon.iconMoon( - size: 24, - ), - color: StreamChatTheme.of(context).colorTheme.grey, - onPressed: () async { - final sp = await StreamingSharedPreferences.instance; - sp.setInt( - 'theme', - Theme.of(context).brightness == Brightness.dark - ? 1 - : -1, - ); - }, - ), - ), - ), - ), - ], - ), - ), - ), - ), - ); - } -} - -class UserMentionPage extends StatelessWidget { - @override - Widget build(BuildContext context) { - final user = StreamChat.of(context).user!; - return MessageSearchBloc( - child: MessageSearchListView( - filters: Filter.in_('members', [user.id]), - messageFilters: Filter.custom( - operator: r'$contains', - key: 'mentioned_users.id', - value: user.id, - ), - sortOptions: [ - SortOption( - 'created_at', - direction: SortOption.ASC, - ), - ], - paginationParams: PaginationParams(limit: 20), - showResultCount: false, - emptyBuilder: (_) { - return LayoutBuilder( - builder: (context, viewportConstraints) { - return SingleChildScrollView( - physics: AlwaysScrollableScrollPhysics(), - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: viewportConstraints.maxHeight, - ), - child: Center( - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(24), - child: StreamSvgIcon.mentions( - size: 96, - color: StreamChatTheme.of(context) - .colorTheme - .greyGainsboro, - ), - ), - Text( - 'No mentions exist yet...', - style: StreamChatTheme.of(context) - .textTheme - .body - .copyWith( - color: - StreamChatTheme.of(context).colorTheme.grey, - ), - ), - ], - ), - ), - ), - ); - }, - ); - }, - onItemTap: (messageResponse) async { - final client = StreamChat.of(context).client; - final message = messageResponse.message; - final channel = client.channel( - messageResponse.channel!.type, - id: messageResponse.channel!.id, - ); - if (channel.state == null) { - await channel.watch(); - } - Navigator.pushNamed( - context, - Routes.CHANNEL_PAGE, - arguments: ChannelPageArgs( - channel: channel, - initialMessage: message, - ), - ); - }, - ), - ); - } -} - -class ChannelListPage extends StatefulWidget { - @override - _ChannelListPageState createState() => _ChannelListPageState(); -} - -class _ChannelListPageState extends State { - TextEditingController? _controller; - - String _channelQuery = ''; - - bool _isSearchActive = false; - - Timer? _debounce; - - void _channelQueryListener() { - if (_debounce?.isActive ?? false) _debounce!.cancel(); - _debounce = Timer(const Duration(milliseconds: 350), () { - if (mounted) { - setState(() { - _channelQuery = _controller!.text; - _isSearchActive = _channelQuery.isNotEmpty; - }); - } - }); - } - - @override - void initState() { - super.initState(); - _controller = TextEditingController()..addListener(_channelQueryListener); - } - - @override - void dispose() { - _controller?.removeListener(_channelQueryListener); - _controller?.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final user = StreamChat.of(context).user; - return WillPopScope( - onWillPop: () async { - if (_isSearchActive) { - _controller!.clear(); - setState(() => _isSearchActive = false); - return false; - } - return true; - }, - child: ChannelsBloc( - child: MessageSearchBloc( - child: NestedScrollView( - floatHeaderSlivers: true, - headerSliverBuilder: (_, __) => [ - SliverToBoxAdapter( - child: SearchTextField( - controller: _controller, - showCloseButton: _isSearchActive, - ), - ), - ], - body: AnimatedSwitcher( - duration: const Duration(milliseconds: 350), - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onPanDown: (_) => FocusScope.of(context).unfocus(), - child: _isSearchActive - ? MessageSearchListView( - showErrorTile: true, - messageQuery: _channelQuery, - filters: Filter.in_('members', [user!.id]), - sortOptions: [ - SortOption( - 'created_at', - direction: SortOption.ASC, - ), - ], - pullToRefresh: false, - paginationParams: PaginationParams(limit: 20), - emptyBuilder: (_) { - return LayoutBuilder( - builder: (context, viewportConstraints) { - return SingleChildScrollView( - physics: AlwaysScrollableScrollPhysics(), - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: viewportConstraints.maxHeight, - ), - child: Center( - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(24), - child: StreamSvgIcon.search( - size: 96, - color: Colors.grey, - ), - ), - Text( - 'No results...', - ), - ], - ), - ), - ), - ); - }, - ); - }, - onItemTap: (messageResponse) async { - FocusScope.of(context).requestFocus(FocusNode()); - final client = StreamChat.of(context).client; - final message = messageResponse.message; - final channel = client.channel( - messageResponse.channel!.type, - id: messageResponse.channel!.id, - ); - if (channel.state == null) { - await channel.watch(); - } - Navigator.pushNamed( - context, - Routes.CHANNEL_PAGE, - arguments: ChannelPageArgs( - channel: channel, - initialMessage: message, - ), - ); - }, - ) - : ChannelListView( - onStartChatPressed: () { - Navigator.pushNamed(context, Routes.NEW_CHAT); - }, - swipeToAction: true, - filter: Filter.in_('members', [user!.id]), - options: { - 'presence': true, - }, - pagination: PaginationParams( - limit: 20, - ), - channelWidget: ChannelPage(), - onViewInfoTap: (channel) { - Navigator.pop(context); - if (channel.memberCount == 2 && channel.isDistinct) { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => StreamChannel( - channel: channel, - child: ChatInfoScreen( - messageTheme: StreamChatTheme.of(context) - .ownMessageTheme, - user: channel.state!.members - .where((m) => - m.userId != - channel.client.state.user!.id) - .first - .user, - ), - ), - ), - ); - } else { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => StreamChannel( - channel: channel, - child: GroupInfoScreen( - messageTheme: StreamChatTheme.of(context) - .ownMessageTheme, - ), - ), - ), - ); - } - }, - ), - ), - ), - ), - ), - ), - ); - } -} - -class ChannelPageArgs { - final Channel? channel; - final Message? initialMessage; - - const ChannelPageArgs({ - this.channel, - this.initialMessage, - }); -} - -class ChannelPage extends StatefulWidget { - final int? initialScrollIndex; - final double? initialAlignment; - final bool highlightInitialMessage; - - const ChannelPage({ - Key? key, - this.initialScrollIndex, - this.initialAlignment, - this.highlightInitialMessage = false, - }) : super(key: key); - - @override - _ChannelPageState createState() => _ChannelPageState(); -} - -class _ChannelPageState extends State { - Message? _quotedMessage; - FocusNode? _focusNode; - - @override - void initState() { - _focusNode = FocusNode(); - super.initState(); - } - - @override - void dispose() { - _focusNode!.dispose(); - super.dispose(); - } - - void _reply(Message message) { - setState(() => _quotedMessage = message); - WidgetsBinding.instance!.addPostFrameCallback((timeStamp) { - _focusNode!.requestFocus(); - }); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow, - appBar: ChannelHeader( - showTypingIndicator: false, - onImageTap: () async { - var channel = StreamChannel.of(context).channel; - - if (channel.memberCount == 2 && channel.isDistinct) { - final currentUser = StreamChat.of(context).user; - final otherUser = channel.state!.members.firstWhereOrNull( - (element) => element.user!.id != currentUser!.id, - ); - if (otherUser != null) { - final pop = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => StreamChannel( - child: ChatInfoScreen( - messageTheme: StreamChatTheme.of(context).ownMessageTheme, - user: otherUser.user, - ), - channel: channel, - ), - ), - ); - - if (pop == true) { - Navigator.pop(context); - } - } - } else { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => StreamChannel( - child: GroupInfoScreen( - messageTheme: StreamChatTheme.of(context).ownMessageTheme, - ), - channel: channel, - ), - ), - ); - } - }, - ), - body: Column( - children: [ - Expanded( - child: Stack( - children: [ - MessageListView( - initialScrollIndex: widget.initialScrollIndex, - initialAlignment: widget.initialAlignment, - highlightInitialMessage: widget.highlightInitialMessage, - onMessageSwiped: _reply, - onReplyTap: _reply, - threadBuilder: (_, parentMessage) { - return ThreadPage( - parent: parentMessage, - ); - }, - onShowMessage: (m, c) async { - final client = StreamChat.of(context).client; - final message = m; - final channel = client.channel( - c.type, - id: c.id, - ); - if (channel.state == null) { - await channel.watch(); - } - Navigator.pushReplacementNamed( - context, - Routes.CHANNEL_PAGE, - arguments: ChannelPageArgs( - channel: channel, - initialMessage: message, - ), - ); - }, - pinPermissions: ['owner', 'admin', 'member'], - ), - Positioned( - bottom: 0, - left: 0, - right: 0, - child: Container( - alignment: Alignment.centerLeft, - color: StreamChatTheme.of(context) - .colorTheme - .whiteSnow - .withOpacity(.9), - child: TypingIndicator( - alignment: Alignment.centerLeft, - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), - style: StreamChatTheme.of(context) - .textTheme - .footnote - .copyWith( - color: - StreamChatTheme.of(context).colorTheme.grey), - ), - ), - ), - ], - ), - ), - MessageInput( - focusNode: _focusNode, - quotedMessage: _quotedMessage, - onQuotedMessageCleared: () { - setState(() => _quotedMessage = null); - _focusNode!.unfocus(); - }, - ), - ], - ), - ); - } -} - -class ThreadPage extends StatefulWidget { - final Message? parent; - final int? initialScrollIndex; - final double? initialAlignment; - - ThreadPage({ - Key? key, - this.parent, - this.initialScrollIndex, - this.initialAlignment, - }) : super(key: key); - - @override - _ThreadPageState createState() => _ThreadPageState(); -} - -class _ThreadPageState extends State { - Message? _quotedMessage; - FocusNode _focusNode = FocusNode(); - - @override - void dispose() { - _focusNode.dispose(); - super.dispose(); - } - - void _reply(Message message) { - setState(() => _quotedMessage = message); - WidgetsBinding.instance!.addPostFrameCallback((timeStamp) { - _focusNode.requestFocus(); - }); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow, - appBar: ThreadHeader( - parent: widget.parent!, - ), - body: Column( - children: [ - Expanded( - child: MessageListView( - parentMessage: widget.parent, - initialScrollIndex: widget.initialScrollIndex, - initialAlignment: widget.initialAlignment, - onMessageSwiped: _reply, - onReplyTap: _reply, - pinPermissions: ['owner', 'admin', 'member'], - ), - ), - if (widget.parent!.type != 'deleted') - MessageInput( - parentMessage: widget.parent, - focusNode: _focusNode, - quotedMessage: _quotedMessage, - onQuotedMessageCleared: () { - setState(() => _quotedMessage = null); - _focusNode.unfocus(); - }, - ), - ], - ), - ); - } -} - class InitData { final StreamChatClient client; final StreamingSharedPreferences preferences; InitData(this.client, this.preferences); } - -class HolePainter extends CustomPainter { - HolePainter({ - required this.color, - required this.holeSize, - }); - - Color color; - double holeSize; - - @override - void paint(Canvas canvas, Size size) { - double radius = holeSize / 2; - Rect rect = Rect.fromLTWH(0, 0, size.width, size.height); - Rect outerCircleRect = Rect.fromCircle( - center: Offset(size.width / 2, size.height / 2), radius: radius); - Rect innerCircleRect = Rect.fromCircle( - center: Offset(size.width / 2, size.height / 2), radius: radius / 2); - - Path transparentHole = Path.combine( - PathOperation.difference, - Path()..addRect(rect), - Path() - ..addOval(outerCircleRect) - ..close(), - ); - - Path halfTransparentRing = Path.combine( - PathOperation.difference, - Path() - ..addOval(outerCircleRect) - ..close(), - Path() - ..addOval(innerCircleRect) - ..close(), - ); - - canvas.drawPath(transparentHole, Paint()..color = color); - canvas.drawPath( - halfTransparentRing, Paint()..color = color.withOpacity(0.5)); - } - - @override - bool shouldRepaint(CustomPainter oldDelegate) { - return true; - } -} diff --git a/packages/stream_chat_v1/lib/new_chat_screen.dart b/packages/stream_chat_v1/lib/new_chat_screen.dart index d708b00..1d7f024 100644 --- a/packages/stream_chat_v1/lib/new_chat_screen.dart +++ b/packages/stream_chat_v1/lib/new_chat_screen.dart @@ -3,8 +3,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'channel_page.dart'; import 'chips_input_text_field.dart'; -import 'main.dart'; import 'routes/routes.dart'; class NewChatScreen extends StatefulWidget { @@ -66,10 +66,8 @@ class _NewChatScreenState extends State { final chatState = StreamChat.of(context); final res = await chatState.client.queryChannelsOnline( - options: { - 'state': false, - 'watch': false, - }, + state: false, + watch: false, filter: Filter.raw(value: { 'members': [ ..._selectedUsers.map((e) => e.id), diff --git a/packages/stream_chat_v1/lib/pinned_messages_screen.dart b/packages/stream_chat_v1/lib/pinned_messages_screen.dart index 82dd532..d8f756b 100644 --- a/packages/stream_chat_v1/lib/pinned_messages_screen.dart +++ b/packages/stream_chat_v1/lib/pinned_messages_screen.dart @@ -70,21 +70,7 @@ class _PinnedMessagesScreenState extends State { fontSize: 16.0, ), ), - leading: Center( - child: InkWell( - onTap: () { - Navigator.of(context).pop(); - }, - child: Container( - width: 24.0, - height: 24.0, - child: StreamSvgIcon.left( - color: StreamChatTheme.of(context).colorTheme.black, - size: 24.0, - ), - ), - ), - ), + leading: StreamBackButton(), backgroundColor: StreamChatTheme.of(context).colorTheme.white, ), body: _buildMediaGrid(), diff --git a/packages/stream_chat_v1/lib/routes/app_routes.dart b/packages/stream_chat_v1/lib/routes/app_routes.dart index a8ce583..145fa8e 100644 --- a/packages/stream_chat_v1/lib/routes/app_routes.dart +++ b/packages/stream_chat_v1/lib/routes/app_routes.dart @@ -1,14 +1,18 @@ -import 'routes.dart'; +import 'package:example/channel_list_page.dart'; import 'package:flutter/material.dart'; -import '../choose_user_page.dart'; -import '../advanced_options_page.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import '../main.dart'; -import '../group_chat_details_screen.dart'; -import '../new_group_chat_screen.dart'; -import '../new_chat_screen.dart'; + +import '../advanced_options_page.dart'; +import '../channel_page.dart'; import '../chat_info_screen.dart'; +import '../choose_user_page.dart'; +import '../group_chat_details_screen.dart'; import '../group_info_screen.dart'; +import '../home_page.dart'; +import '../main.dart'; +import '../new_chat_screen.dart'; +import '../new_group_chat_screen.dart'; +import 'routes.dart'; class AppRoutes { /// Add entry for new route here @@ -25,7 +29,10 @@ class AppRoutes { return MaterialPageRoute( settings: const RouteSettings(name: Routes.HOME), builder: (_) { - return HomePage(); + final homePageArgs = args as HomePageArgs; + return HomePage( + chatClient: homePageArgs.chatClient, + ); }); case Routes.CHOOSE_USER: return MaterialPageRoute( @@ -42,12 +49,13 @@ class AppRoutes { return MaterialPageRoute( settings: const RouteSettings(name: Routes.CHANNEL_PAGE), builder: (_) { - final arg = args as ChannelPageArgs; + final channelPageArgs = args as ChannelPageArgs; return StreamChannel( - channel: arg.channel!, - initialMessageId: arg.initialMessage?.id, + channel: channelPageArgs.channel!, + initialMessageId: channelPageArgs.initialMessage?.id, child: ChannelPage( - highlightInitialMessage: arg.initialMessage != null, + highlightInitialMessage: + channelPageArgs.initialMessage != null, ), ); }); @@ -88,6 +96,12 @@ class AppRoutes { messageTheme: StreamChatTheme.of(context).ownMessageTheme, ); }); + case Routes.CHANNEL_LIST_PAGE: + return MaterialPageRoute( + settings: const RouteSettings(name: Routes.CHANNEL_LIST_PAGE), + builder: (context) { + return ChannelListPage(); + }); // Default case, should not reach here. default: return null; diff --git a/packages/stream_chat_v1/lib/routes/routes.dart b/packages/stream_chat_v1/lib/routes/routes.dart index 2280ca3..239bd28 100644 --- a/packages/stream_chat_v1/lib/routes/routes.dart +++ b/packages/stream_chat_v1/lib/routes/routes.dart @@ -10,4 +10,5 @@ class Routes { static const String NEW_GROUP_CHAT_DETAILS = '/new_group_chat_details'; static const String CHAT_INFO_SCREEN = '/chat_info_screen'; static const String GROUP_INFO_SCREEN = '/group_info_screen'; + static const String CHANNEL_LIST_PAGE = '/channel_list_page'; } diff --git a/packages/stream_chat_v1/lib/splash_screen.dart b/packages/stream_chat_v1/lib/splash_screen.dart new file mode 100644 index 0000000..2c89228 --- /dev/null +++ b/packages/stream_chat_v1/lib/splash_screen.dart @@ -0,0 +1,122 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:lottie/lottie.dart'; + +mixin SplashScreenStateMixin on State + implements TickerProvider { + late Animation animation, scaleAnimation; + late AnimationController _animationController, _scaleAnimationController; + late Animation colorAnimation; + bool animationCompleted = false; + + void _createAnimations() { + _scaleAnimationController = AnimationController( + vsync: this, + value: 0, + duration: Duration( + milliseconds: 500, + ), + ); + scaleAnimation = Tween( + begin: 1.0, + end: 1.5, + ).animate(CurvedAnimation( + parent: _scaleAnimationController, + curve: Curves.easeInOutBack, + )); + + _animationController = AnimationController( + vsync: this, + duration: Duration( + milliseconds: 1000, + ), + ); + animation = Tween( + begin: 0.0, + end: 1000.0, + ).animate(CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOut, + )); + colorAnimation = ColorTween( + begin: Color(0xff005FFF), + end: Color(0xff005FFF), + ).animate(CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOut, + )); + colorAnimation = ColorTween( + begin: Color(0xff005FFF), + end: Colors.transparent, + ).animate(CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOut, + )); + } + + void forwardAnimations() { + _scaleAnimationController.forward().whenComplete(() { + _animationController.forward(); + }); + } + + Widget buildAnimation() => Stack( + clipBehavior: Clip.none, + alignment: Alignment.center, + children: [ + AnimatedBuilder( + animation: scaleAnimation, + builder: (context, _) { + return Transform.scale( + scale: scaleAnimation.value, + child: AnimatedBuilder( + animation: colorAnimation, + builder: (context, snapshot) { + return Container( + alignment: Alignment.center, + constraints: BoxConstraints.expand(), + color: colorAnimation.value, + child: !_animationController.isAnimating + ? Lottie.asset( + 'assets/floating_boat.json', + alignment: Alignment.center, + ) + : SizedBox(), + ); + }), + ); + }, + ), + AnimatedBuilder( + animation: animation, + builder: (context, snapshot) { + return Transform.scale( + scale: animation.value, + child: Container( + width: 1.0, + height: 1.0, + decoration: BoxDecoration( + color: Colors.white + .withOpacity(1 - _animationController.value), + shape: BoxShape.circle, + ), + ), + ); + }, + ), + ], + ); + + @override + void initState() { + _createAnimations(); + _animationController.addStatusListener((status) { + if (status == AnimationStatus.completed) { + setState(() { + animationCompleted = true; + }); + } + }); + super.initState(); + } +} diff --git a/packages/stream_chat_v1/lib/thread_page.dart b/packages/stream_chat_v1/lib/thread_page.dart new file mode 100644 index 0000000..b9799c7 --- /dev/null +++ b/packages/stream_chat_v1/lib/thread_page.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +class ThreadPage extends StatefulWidget { + final Message? parent; + final int? initialScrollIndex; + final double? initialAlignment; + + ThreadPage({ + Key? key, + this.parent, + this.initialScrollIndex, + this.initialAlignment, + }) : super(key: key); + + @override + _ThreadPageState createState() => _ThreadPageState(); +} + +class _ThreadPageState extends State { + Message? _quotedMessage; + FocusNode _focusNode = FocusNode(); + + @override + void dispose() { + _focusNode.dispose(); + super.dispose(); + } + + void _reply(Message message) { + setState(() => _quotedMessage = message); + WidgetsBinding.instance!.addPostFrameCallback((timeStamp) { + _focusNode.requestFocus(); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow, + appBar: ThreadHeader( + parent: widget.parent!, + ), + body: Column( + children: [ + Expanded( + child: MessageListView( + parentMessage: widget.parent, + initialScrollIndex: widget.initialScrollIndex, + initialAlignment: widget.initialAlignment, + onMessageSwiped: _reply, + onReplyTap: _reply, + pinPermissions: ['owner', 'admin', 'member'], + ), + ), + if (widget.parent!.type != 'deleted') + MessageInput( + parentMessage: widget.parent, + focusNode: _focusNode, + quotedMessage: _quotedMessage, + onQuotedMessageCleared: () { + setState(() => _quotedMessage = null); + _focusNode.unfocus(); + }, + ), + ], + ), + ); + } +} diff --git a/packages/stream_chat_v1/lib/user_mentions_page.dart b/packages/stream_chat_v1/lib/user_mentions_page.dart new file mode 100644 index 0000000..837aa19 --- /dev/null +++ b/packages/stream_chat_v1/lib/user_mentions_page.dart @@ -0,0 +1,88 @@ +import 'package:example/routes/routes.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'channel_page.dart'; + +class UserMentionsPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + final user = StreamChat.of(context).user!; + return MessageSearchBloc( + child: MessageSearchListView( + filters: Filter.in_('members', [user.id]), + messageFilters: Filter.custom( + operator: r'$contains', + key: 'mentioned_users.id', + value: user.id, + ), + sortOptions: [ + SortOption( + 'created_at', + direction: SortOption.ASC, + ), + ], + paginationParams: PaginationParams(limit: 20), + showResultCount: false, + emptyBuilder: (_) { + return LayoutBuilder( + builder: (context, viewportConstraints) { + return SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: viewportConstraints.maxHeight, + ), + child: Center( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(24), + child: StreamSvgIcon.mentions( + size: 96, + color: StreamChatTheme.of(context) + .colorTheme + .greyGainsboro, + ), + ), + Text( + 'No mentions exist yet...', + style: StreamChatTheme.of(context) + .textTheme + .body + .copyWith( + color: + StreamChatTheme.of(context).colorTheme.grey, + ), + ), + ], + ), + ), + ), + ); + }, + ); + }, + onItemTap: (messageResponse) async { + final client = StreamChat.of(context).client; + final message = messageResponse.message; + final channel = client.channel( + messageResponse.channel!.type, + id: messageResponse.channel!.id, + ); + if (channel.state == null) { + await channel.watch(); + } + Navigator.pushNamed( + context, + Routes.CHANNEL_PAGE, + arguments: ChannelPageArgs( + channel: channel, + initialMessage: message, + ), + ); + }, + ), + ); + } +}