From 3638e364712e454235e8a4353e14d35afa4e506e Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 21 Jun 2021 20:38:43 +0200 Subject: [PATCH] extract main.dart widgets --- .../stream_chat_v1/lib/channel_list_page.dart | 199 +++++ packages/stream_chat_v1/lib/channel_page.dart | 183 +++++ .../stream_chat_v1/lib/chat_info_screen.dart | 2 +- .../lib/group_chat_details_screen.dart | 2 +- .../stream_chat_v1/lib/group_info_screen.dart | 2 +- packages/stream_chat_v1/lib/home_page.dart | 241 ++++++ packages/stream_chat_v1/lib/main.dart | 738 ------------------ .../stream_chat_v1/lib/new_chat_screen.dart | 2 +- .../stream_chat_v1/lib/routes/app_routes.dart | 26 +- packages/stream_chat_v1/lib/thread_page.dart | 70 ++ .../lib/user_mentions_page.dart | 88 +++ 11 files changed, 800 insertions(+), 753 deletions(-) create mode 100644 packages/stream_chat_v1/lib/channel_list_page.dart create mode 100644 packages/stream_chat_v1/lib/channel_page.dart create mode 100644 packages/stream_chat_v1/lib/home_page.dart create mode 100644 packages/stream_chat_v1/lib/thread_page.dart create mode 100644 packages/stream_chat_v1/lib/user_mentions_page.dart 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..c6529d5 --- /dev/null +++ b/packages/stream_chat_v1/lib/channel_list_page.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 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]), + 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_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 0fbd4ea..6517ef3 100644 --- a/packages/stream_chat_v1/lib/chat_info_screen.dart +++ b/packages/stream_chat_v1/lib/chat_info_screen.dart @@ -6,7 +6,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'channel_file_display_screen.dart'; import 'channel_media_display_screen.dart'; -import 'main.dart'; +import 'channel_page.dart'; import 'pinned_messages_screen.dart'; import 'routes/routes.dart'; 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 5ec1ec2..1d9572e 100644 --- a/packages/stream_chat_v1/lib/group_info_screen.dart +++ b/packages/stream_chat_v1/lib/group_info_screen.dart @@ -10,8 +10,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 'chat_info_screen.dart'; -import 'main.dart'; import 'pinned_messages_screen.dart'; import 'routes/routes.dart'; 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..af5f68d --- /dev/null +++ b/packages/stream_chat_v1/lib/home_page.dart @@ -0,0 +1,241 @@ +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_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_page.dart'; + +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: 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: [ + ChannelListPage(), + UserMentionsPage(), + ], + ), + ); + } +} + +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(); + } + + StreamChat.of(context).client.disconnectUser(); + + await Navigator.pushNamedAndRemoveUntil( + context, + Routes.APP, + ModalRoute.withName(Routes.APP), + ); + }, + 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/main.dart b/packages/stream_chat_v1/lib/main.dart index b0a0327..05af49c 100644 --- a/packages/stream_chat_v1/lib/main.dart +++ b/packages/stream_chat_v1/lib/main.dart @@ -1,9 +1,6 @@ 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:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -20,7 +17,6 @@ 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, @@ -273,740 +269,6 @@ class _MyAppState extends State with TickerProviderStateMixin { } } -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.disconnectUser(); - - 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]), - 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; diff --git a/packages/stream_chat_v1/lib/new_chat_screen.dart b/packages/stream_chat_v1/lib/new_chat_screen.dart index e70c492..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 { diff --git a/packages/stream_chat_v1/lib/routes/app_routes.dart b/packages/stream_chat_v1/lib/routes/app_routes.dart index a8ce583..4bc157e 100644 --- a/packages/stream_chat_v1/lib/routes/app_routes.dart +++ b/packages/stream_chat_v1/lib/routes/app_routes.dart @@ -1,14 +1,17 @@ -import 'routes.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 @@ -42,12 +45,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, ), ); }); 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, + ), + ); + }, + ), + ); + } +}