diff --git a/lib/components/default_app_bar_search_field.dart b/lib/components/default_app_bar_search_field.dart index 1795b38b..1212f64e 100644 --- a/lib/components/default_app_bar_search_field.dart +++ b/lib/components/default_app_bar_search_field.dart @@ -11,7 +11,7 @@ class DefaultAppBarSearchField extends StatefulWidget { final bool readOnly; final Widget prefixIcon; - const DefaultAppBarSearchField({ + DefaultAppBarSearchField({ Key key, this.searchController, this.onChanged, @@ -25,14 +25,16 @@ class DefaultAppBarSearchField extends StatefulWidget { }) : super(key: key); @override - _DefaultAppBarSearchFieldState createState() => - _DefaultAppBarSearchFieldState(); + DefaultAppBarSearchFieldState createState() => + DefaultAppBarSearchFieldState(); } -class _DefaultAppBarSearchFieldState extends State { - final FocusNode _focusNode = FocusNode(); +class DefaultAppBarSearchFieldState extends State { TextEditingController _searchController; bool _lastTextWasEmpty = false; + final FocusNode _focusNode = FocusNode(); + + void requestFocus() => _focusNode.requestFocus(); void _updateSearchController() { final thisTextIsEmpty = _searchController.text?.isEmpty ?? false; diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 6b4f4d7c..de72f20a 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -1035,6 +1035,11 @@ "type": "text", "placeholders": {} }, + "addNewContact": "Add new contact", + "@addNewContact": { + "type": "text", + "placeholders": {} + }, "newVerificationRequest": "New verification request!", "@newVerificationRequest": { "type": "text", diff --git a/lib/views/home_view.dart b/lib/views/home_view.dart index aa308c43..20f96dbf 100644 --- a/lib/views/home_view.dart +++ b/lib/views/home_view.dart @@ -4,7 +4,6 @@ import 'dart:io'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:adaptive_page_layout/adaptive_page_layout.dart'; import 'package:famedlysdk/famedlysdk.dart'; -import 'package:fluffychat/utils/fluffy_share.dart'; import 'package:fluffychat/views/home_view_parts/discover.dart'; import 'package:fluffychat/views/share_view.dart'; import 'package:flutter/cupertino.dart'; @@ -179,20 +178,30 @@ class _HomeViewState extends State with TickerProviderStateMixin { } } + final StreamController _onAppBarButtonTap = + StreamController.broadcast(); + @override Widget build(BuildContext context) { _onShareContentChanged ??= Matrix.of(context).onShareContentChanged.stream.listen(_onShare); IconData fabIcon; + String title; switch (currentIndex) { case 0: fabIcon = Icons.edit_outlined; + title = L10n.of(context).contacts; break; case 1: fabIcon = Icons.add_outlined; + title = AppConfig.applicationName; break; case 2: fabIcon = Icons.domain_outlined; + title = L10n.of(context).discover; + break; + case 3: + title = L10n.of(context).settings; break; } @@ -201,43 +210,27 @@ class _HomeViewState extends State with TickerProviderStateMixin { AppBar( centerTitle: false, actions: [ - PopupMenuButton( - onSelected: (action) { - switch (action) { - case 'invite': - FluffyShare.share( - L10n.of(context).inviteText( - Matrix.of(context).client.userID, - 'https://matrix.to/#/${Matrix.of(context).client.userID}'), - context); - break; - case 'archive': - AdaptivePageLayout.of(context).pushNamed('/archive'); - break; - } - }, - itemBuilder: (_) => [ - PopupMenuItem( - value: 'invite', - child: Text(L10n.of(context).inviteContact), - ), - PopupMenuItem( - value: 'archive', - child: Text(L10n.of(context).archive), - ), - ], + IconButton( + icon: Icon(currentIndex == 3 + ? Icons.exit_to_app_outlined + : Icons.search_outlined), + onPressed: () => _pageController.indexIsChanging + ? null + : _onAppBarButtonTap.add(currentIndex), ), ], - title: Text(AppConfig.applicationName)), + title: Text(title)), body: TabBarView( controller: _pageController, children: [ - ContactList(), + ContactList(onAppBarButtonTap: _onAppBarButtonTap.stream), ChatList( onCustomAppBar: (appBar) => setState(() => this.appBar = appBar), + onAppBarButtonTap: _onAppBarButtonTap.stream, ), - Discover(server: _server), - Settings(), + Discover( + server: _server, onAppBarButtonTap: _onAppBarButtonTap.stream), + Settings(onAppBarButtonTap: _onAppBarButtonTap.stream), ], ), floatingActionButton: fabIcon == null @@ -258,7 +251,6 @@ class _HomeViewState extends State with TickerProviderStateMixin { showSelectedLabels: true, showUnselectedLabels: false, type: BottomNavigationBarType.fixed, - elevation: 20, backgroundColor: Theme.of(context).appBarTheme.color, onTap: (i) { _pageController.animateTo(i); @@ -275,7 +267,7 @@ class _HomeViewState extends State with TickerProviderStateMixin { ), BottomNavigationBarItem( label: L10n.of(context).discover, - icon: Icon(CupertinoIcons.search_circle), + icon: Icon(CupertinoIcons.compass), ), BottomNavigationBarItem( label: L10n.of(context).settings, diff --git a/lib/views/home_view_parts/chat_list.dart b/lib/views/home_view_parts/chat_list.dart index 446485f8..d55cd265 100644 --- a/lib/views/home_view_parts/chat_list.dart +++ b/lib/views/home_view_parts/chat_list.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:famedlysdk/famedlysdk.dart'; import 'package:fluffychat/components/connection_status_header.dart'; @@ -13,11 +15,13 @@ enum SelectMode { normal, select } class ChatList extends StatefulWidget { final String activeChat; final void Function(AppBar appBar) onCustomAppBar; + final Stream onAppBarButtonTap; const ChatList({ Key key, this.activeChat, this.onCustomAppBar, + this.onAppBarButtonTap, }) : super(key: key); @override _ChatListState createState() => _ChatListState(); @@ -28,6 +32,32 @@ class _ChatListState extends State { final TextEditingController searchController = TextEditingController(); final _selectedRoomIds = {}; + final ScrollController _scrollController = ScrollController(); + StreamSubscription _onAppBarButtonTapSub; + final GlobalKey _searchField = GlobalKey(); + + @override + void initState() { + _onAppBarButtonTapSub = + widget.onAppBarButtonTap.where((i) => i == 1).listen((_) async { + await _scrollController.animateTo( + _scrollController.position.minScrollExtent, + duration: Duration(milliseconds: 200), + curve: Curves.ease, + ); + WidgetsBinding.instance.addPostFrameCallback( + (_) => _searchField.currentState.requestFocus(), + ); + }); + super.initState(); + } + + @override + void dispose() { + _onAppBarButtonTapSub?.cancel(); + super.dispose(); + } + void _toggleSelection(String roomId) { setState(() => _selectedRoomIds.contains(roomId) ? _selectedRoomIds.remove(roomId) @@ -198,11 +228,13 @@ class _ChatListState extends State { } final totalCount = rooms.length; return ListView.builder( + controller: _scrollController, itemCount: totalCount + 1, itemBuilder: (BuildContext context, int i) => i == 0 ? Padding( padding: EdgeInsets.all(12), child: DefaultAppBarSearchField( + key: _searchField, hintText: L10n.of(context).search, prefixIcon: Icon(Icons.search_outlined), searchController: searchController, diff --git a/lib/views/home_view_parts/contact_list.dart b/lib/views/home_view_parts/contact_list.dart index acba5378..d606f361 100644 --- a/lib/views/home_view_parts/contact_list.dart +++ b/lib/views/home_view_parts/contact_list.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:adaptive_page_layout/adaptive_page_layout.dart'; import 'package:fluffychat/components/avatar.dart'; import 'package:fluffychat/components/default_app_bar_search_field.dart'; @@ -8,65 +10,98 @@ import '../../utils/client_presence_extension.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; class ContactList extends StatefulWidget { + final Stream onAppBarButtonTap; + + const ContactList({Key key, this.onAppBarButtonTap}) : super(key: key); @override _ContactListState createState() => _ContactListState(); } class _ContactListState extends State { String _searchQuery = ''; + final ScrollController _scrollController = ScrollController(); + StreamSubscription _onAppBarButtonTapSub; + final GlobalKey _searchField = GlobalKey(); + + @override + void initState() { + _onAppBarButtonTapSub = + widget.onAppBarButtonTap.where((i) => i == 0).listen((_) async { + await _scrollController.animateTo( + _scrollController.position.minScrollExtent, + duration: Duration(milliseconds: 200), + curve: Curves.ease, + ); + WidgetsBinding.instance.addPostFrameCallback( + (_) => _searchField.currentState.requestFocus(), + ); + }); + super.initState(); + } + + @override + void dispose() { + _onAppBarButtonTapSub?.cancel(); + super.dispose(); + } + @override Widget build(BuildContext context) { - return ListView(children: [ - Padding( - padding: EdgeInsets.all(12), - child: DefaultAppBarSearchField( - hintText: L10n.of(context).search, - prefixIcon: Icon(Icons.search_outlined), - onChanged: (t) => setState(() => _searchQuery = t), - padding: EdgeInsets.zero, + return ListView( + controller: _scrollController, + children: [ + Padding( + padding: EdgeInsets.all(12), + child: DefaultAppBarSearchField( + key: _searchField, + hintText: L10n.of(context).search, + prefixIcon: Icon(Icons.search_outlined), + onChanged: (t) => setState(() => _searchQuery = t), + padding: EdgeInsets.zero, + ), ), - ), - ListTile( - leading: CircleAvatar( - backgroundColor: Theme.of(context).primaryColor, - foregroundColor: Colors.white, - child: Icon(Icons.add_outlined), - radius: Avatar.defaultSize / 2, + ListTile( + leading: CircleAvatar( + backgroundColor: Theme.of(context).primaryColor, + foregroundColor: Colors.white, + child: Icon(Icons.add_outlined), + radius: Avatar.defaultSize / 2, + ), + title: Text(L10n.of(context).addNewContact), + onTap: () => + AdaptivePageLayout.of(context).pushNamed('/newprivatechat'), ), - title: Text('Add new contact'), - onTap: () => - AdaptivePageLayout.of(context).pushNamed('/newprivatechat'), - ), - Divider(height: 1), - StreamBuilder( - stream: Matrix.of(context).client.onSync.stream, - builder: (context, snapshot) { - final contactList = Matrix.of(context) - .client - .contactList - .where((p) => p.senderId - .toLowerCase() - .contains(_searchQuery.toLowerCase())) - .toList(); - if (contactList.isEmpty) { - return Container( - padding: EdgeInsets.all(16), - alignment: Alignment.center, - child: Text( - 'No contacts found...', - textAlign: TextAlign.center, - ), + Divider(height: 1), + StreamBuilder( + stream: Matrix.of(context).client.onSync.stream, + builder: (context, snapshot) { + final contactList = Matrix.of(context) + .client + .contactList + .where((p) => p.senderId + .toLowerCase() + .contains(_searchQuery.toLowerCase())) + .toList(); + if (contactList.isEmpty) { + return Container( + padding: EdgeInsets.all(16), + alignment: Alignment.center, + child: Text( + 'No contacts found...', + textAlign: TextAlign.center, + ), + ); + } + return ListView.builder( + physics: NeverScrollableScrollPhysics(), + shrinkWrap: true, + padding: EdgeInsets.only(bottom: 24), + itemCount: contactList.length, + itemBuilder: (context, i) => + ContactListTile(contact: contactList[i]), ); - } - return ListView.builder( - physics: NeverScrollableScrollPhysics(), - shrinkWrap: true, - padding: EdgeInsets.only(bottom: 24), - itemCount: contactList.length, - itemBuilder: (context, i) => - ContactListTile(contact: contactList[i]), - ); - }), - ]); + }), + ], + ); } } diff --git a/lib/views/home_view_parts/discover.dart b/lib/views/home_view_parts/discover.dart index 1127dfd8..9571b52d 100644 --- a/lib/views/home_view_parts/discover.dart +++ b/lib/views/home_view_parts/discover.dart @@ -14,11 +14,13 @@ class Discover extends StatefulWidget { final String alias; final String server; + final Stream onAppBarButtonTap; const Discover({ Key key, this.alias, this.server, + this.onAppBarButtonTap, }) : super(key: key); @override _DiscoverState createState() => _DiscoverState(); @@ -29,6 +31,16 @@ class _DiscoverState extends State { Timer _coolDown; String _genericSearchTerm; + final ScrollController _scrollController = ScrollController(); + StreamSubscription _onAppBarButtonTapSub; + final GlobalKey _searchField = GlobalKey(); + + @override + void dispose() { + _onAppBarButtonTapSub?.cancel(); + super.dispose(); + } + void _search(BuildContext context, String query) async { _coolDown?.cancel(); _coolDown = Timer( @@ -86,6 +98,17 @@ class _DiscoverState extends State { @override void initState() { _genericSearchTerm = widget.alias; + _onAppBarButtonTapSub = + widget.onAppBarButtonTap.where((i) => i == 2).listen((_) async { + await _scrollController.animateTo( + _scrollController.position.minScrollExtent, + duration: Duration(milliseconds: 200), + curve: Curves.ease, + ); + WidgetsBinding.instance.addPostFrameCallback( + (_) => _searchField.currentState.requestFocus(), + ); + }); super.initState(); } @@ -121,10 +144,12 @@ class _DiscoverState extends State { return res; }); return ListView( + controller: _scrollController, children: [ Padding( padding: EdgeInsets.all(12), child: DefaultAppBarSearchField( + key: _searchField, hintText: L10n.of(context).search, prefixIcon: Icon(Icons.search_outlined), onChanged: (t) => _search(context, t), diff --git a/lib/views/home_view_parts/settings.dart b/lib/views/home_view_parts/settings.dart index 2b7d9743..ddaca2b0 100644 --- a/lib/views/home_view_parts/settings.dart +++ b/lib/views/home_view_parts/settings.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:adaptive_page_layout/adaptive_page_layout.dart'; import 'package:fluffychat/components/dialogs/bootstrap_dialog.dart'; @@ -26,6 +28,9 @@ import '../../app_config.dart'; import '../../config/setting_keys.dart'; class Settings extends StatefulWidget { + final Stream onAppBarButtonTap; + + const Settings({Key key, this.onAppBarButtonTap}) : super(key: key); @override _SettingsState createState() => _SettingsState(); } @@ -37,6 +42,21 @@ class _SettingsState extends State { bool crossSigningCached; Future megolmBackupCachedFuture; bool megolmBackupCached; + StreamSubscription _onAppBarButtonTapSub; + + @override + void initState() { + _onAppBarButtonTapSub = widget.onAppBarButtonTap + .where((i) => i == 3) + .listen((_) => logoutAction(context)); + super.initState(); + } + + @override + void dispose() { + _onAppBarButtonTapSub?.cancel(); + super.dispose(); + } void logoutAction(BuildContext context) async { if (await showOkCancelAlertDialog(