From 694cce20b9b19d653c09f076e777dcdb6cea1c4b Mon Sep 17 00:00:00 2001 From: Christian Pauly Date: Sat, 17 Apr 2021 11:24:00 +0200 Subject: [PATCH] refactor: MVC search --- lib/config/routes.dart | 6 +- lib/views/search.dart | 143 ++++++++++++++++++++++++++++ lib/views/ui/search_ui.dart | 183 ++++++------------------------------ 3 files changed, 174 insertions(+), 158 deletions(-) create mode 100644 lib/views/search.dart diff --git a/lib/config/routes.dart b/lib/config/routes.dart index c19b5d9a8..646138bc0 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -17,7 +17,7 @@ import 'package:fluffychat/views/widgets/log_view.dart'; import 'package:fluffychat/views/ui/login_ui.dart'; import 'package:fluffychat/views/new_group.dart'; import 'package:fluffychat/views/new_private_chat.dart'; -import 'package:fluffychat/views/ui/search_ui.dart'; +import 'package:fluffychat/views/search.dart'; import 'package:fluffychat/views/ui/settings_ui.dart'; import 'package:fluffychat/views/ui/settings_3pid_ui.dart'; import 'package:fluffychat/views/device_settings.dart'; @@ -137,11 +137,11 @@ class FluffyRoutes { case 'search': if (parts.length == 3) { return ViewData( - mainView: (_) => SearchView(alias: parts[2]), + mainView: (_) => Search(alias: parts[2]), emptyView: (_) => EmptyPage()); } return ViewData( - mainView: (_) => SearchView(), emptyView: (_) => EmptyPage()); + mainView: (_) => Search(), emptyView: (_) => EmptyPage()); case 'settings': if (parts.length == 3) { final action = parts[2]; diff --git a/lib/views/search.dart b/lib/views/search.dart new file mode 100644 index 000000000..b152e2a6e --- /dev/null +++ b/lib/views/search.dart @@ -0,0 +1,143 @@ +import 'dart:async'; + +import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:adaptive_page_layout/adaptive_page_layout.dart'; +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:fluffychat/views/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:future_loading_dialog/future_loading_dialog.dart'; +import 'ui/search_ui.dart'; + +class Search extends StatefulWidget { + final String alias; + + const Search({Key key, this.alias}) : super(key: key); + + @override + SearchController createState() => SearchController(); +} + +class SearchController extends State { + final TextEditingController controller = TextEditingController(); + Future publicRoomsResponse; + String lastServer; + Timer _coolDown; + String genericSearchTerm; + + void search(String query) async { + setState(() => null); + _coolDown?.cancel(); + _coolDown = Timer( + Duration(milliseconds: 500), + () => setState(() { + genericSearchTerm = query; + publicRoomsResponse = null; + searchUser(context, controller.text); + }), + ); + } + + Future _joinRoomAndWait( + BuildContext context, + String roomId, + String alias, + ) async { + if (Matrix.of(context).client.getRoomById(roomId) != null) { + return roomId; + } + final newRoomId = await Matrix.of(context) + .client + .joinRoomOrAlias(alias?.isNotEmpty ?? false ? alias : roomId); + await Matrix.of(context) + .client + .onRoomUpdate + .stream + .firstWhere((r) => r.id == newRoomId); + return newRoomId; + } + + void joinGroupAction(PublicRoom room) async { + if (await showOkCancelAlertDialog( + context: context, + okLabel: L10n.of(context).joinRoom, + title: '${room.name} (${room.numJoinedMembers ?? 0})', + message: room.topic ?? L10n.of(context).noDescription, + cancelLabel: L10n.of(context).cancel, + useRootNavigator: false, + ) == + OkCancelResult.cancel) { + return; + } + final success = await showFutureLoadingDialog( + context: context, + future: () => _joinRoomAndWait( + context, + room.roomId, + room.canonicalAlias ?? room.aliases.first, + ), + ); + if (success.error == null) { + await AdaptivePageLayout.of(context) + .pushNamedAndRemoveUntilIsFirst('/rooms/${success.result}'); + } + } + + String server; + + void setServer() async { + final newServer = await showTextInputDialog( + title: L10n.of(context).changeTheHomeserver, + context: context, + okLabel: L10n.of(context).ok, + cancelLabel: L10n.of(context).cancel, + useRootNavigator: false, + textFields: [ + DialogTextField( + prefixText: 'https://', + hintText: Matrix.of(context).client.homeserver.host, + initialText: server, + keyboardType: TextInputType.url, + ) + ]); + if (newServer == null) return; + setState(() { + server = newServer.single; + }); + } + + String currentSearchTerm; + List foundProfiles = []; + + void searchUser(BuildContext context, String text) async { + if (text.isEmpty) { + setState(() { + foundProfiles = []; + }); + } + currentSearchTerm = text; + if (currentSearchTerm.isEmpty) return; + final matrix = Matrix.of(context); + UserSearchResult response; + try { + response = await matrix.client.searchUser(text, limit: 10); + } catch (_) {} + foundProfiles = List.from(response?.results ?? []); + if (foundProfiles.isEmpty && text.isValidMatrixId && text.sigil == '@') { + foundProfiles.add(Profile.fromJson({ + 'displayname': text.localpart, + 'user_id': text, + })); + } + setState(() {}); + } + + @override + void initState() { + genericSearchTerm = widget.alias; + super.initState(); + } + + @override + Widget build(BuildContext context) => SearchUI(this); +} diff --git a/lib/views/ui/search_ui.dart b/lib/views/ui/search_ui.dart index 550ac7e91..c17f78ab6 100644 --- a/lib/views/ui/search_ui.dart +++ b/lib/views/ui/search_ui.dart @@ -1,6 +1,3 @@ -import 'dart:async'; - -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:adaptive_page_layout/adaptive_page_layout.dart'; import 'package:famedlysdk/famedlysdk.dart'; import 'package:fluffychat/views/widgets/avatar.dart'; @@ -12,167 +9,44 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; import '../../utils/localized_exception_extension.dart'; +import '../search.dart'; -class SearchView extends StatefulWidget { - final String alias; - - const SearchView({Key key, this.alias}) : super(key: key); - - @override - _SearchViewState createState() => _SearchViewState(); -} - -class _SearchViewState extends State { - final TextEditingController _controller = TextEditingController(); - Future _publicRoomsResponse; - String _lastServer; - Timer _coolDown; - String _genericSearchTerm; - - void _search(BuildContext context, String query) async { - setState(() => null); - _coolDown?.cancel(); - _coolDown = Timer( - Duration(milliseconds: 500), - () => setState(() { - _genericSearchTerm = query; - _publicRoomsResponse = null; - searchUser(context, _controller.text); - }), - ); - } - - Future _joinRoomAndWait( - BuildContext context, - String roomId, - String alias, - ) async { - if (Matrix.of(context).client.getRoomById(roomId) != null) { - return roomId; - } - final newRoomId = await Matrix.of(context) - .client - .joinRoomOrAlias(alias?.isNotEmpty ?? false ? alias : roomId); - await Matrix.of(context) - .client - .onRoomUpdate - .stream - .firstWhere((r) => r.id == newRoomId); - return newRoomId; - } - - void _joinGroupAction(BuildContext context, PublicRoom room) async { - if (await showOkCancelAlertDialog( - context: context, - okLabel: L10n.of(context).joinRoom, - title: '${room.name} (${room.numJoinedMembers ?? 0})', - message: room.topic ?? L10n.of(context).noDescription, - cancelLabel: L10n.of(context).cancel, - useRootNavigator: false, - ) == - OkCancelResult.cancel) { - return; - } - final success = await showFutureLoadingDialog( - context: context, - future: () => _joinRoomAndWait( - context, - room.roomId, - room.canonicalAlias ?? room.aliases.first, - ), - ); - if (success.error == null) { - await AdaptivePageLayout.of(context) - .pushNamedAndRemoveUntilIsFirst('/rooms/${success.result}'); - } - } - - String _server; - - void _setServer(BuildContext context) async { - final newServer = await showTextInputDialog( - title: L10n.of(context).changeTheHomeserver, - context: context, - okLabel: L10n.of(context).ok, - cancelLabel: L10n.of(context).cancel, - useRootNavigator: false, - textFields: [ - DialogTextField( - prefixText: 'https://', - hintText: Matrix.of(context).client.homeserver.host, - initialText: _server, - keyboardType: TextInputType.url, - ) - ]); - if (newServer == null) return; - setState(() { - _server = newServer.single; - }); - } - - String currentSearchTerm; - List foundProfiles = []; - - void searchUser(BuildContext context, String text) async { - if (text.isEmpty) { - setState(() { - foundProfiles = []; - }); - } - currentSearchTerm = text; - if (currentSearchTerm.isEmpty) return; - final matrix = Matrix.of(context); - UserSearchResult response; - try { - response = await matrix.client.searchUser(text, limit: 10); - } catch (_) {} - foundProfiles = List.from(response?.results ?? []); - if (foundProfiles.isEmpty && text.isValidMatrixId && text.sigil == '@') { - foundProfiles.add(Profile.fromJson({ - 'displayname': text.localpart, - 'user_id': text, - })); - } - setState(() {}); - } +class SearchUI extends StatelessWidget { + final SearchController controller; - @override - void initState() { - _genericSearchTerm = widget.alias; - super.initState(); - } + const SearchUI(this.controller, {Key key}) : super(key: key); @override Widget build(BuildContext context) { - final server = _genericSearchTerm?.isValidMatrixId ?? false - ? _genericSearchTerm.domain - : _server; - if (_lastServer != server) { - _lastServer = server; - _publicRoomsResponse = null; + final server = controller.genericSearchTerm?.isValidMatrixId ?? false + ? controller.genericSearchTerm.domain + : controller.server; + if (controller.lastServer != server) { + controller.lastServer = server; + controller.publicRoomsResponse = null; } - _publicRoomsResponse ??= Matrix.of(context) + controller.publicRoomsResponse ??= Matrix.of(context) .client .searchPublicRooms( server: server, - genericSearchTerm: _genericSearchTerm, + genericSearchTerm: controller.genericSearchTerm, ) .catchError((error) { - if (widget.alias == null) { + if (controller.widget.alias == null) { throw error; } return PublicRoomsResponse.fromJson({ 'chunk': [], }); }).then((PublicRoomsResponse res) { - if (widget.alias != null && + if (controller.widget.alias != null && !res.chunk.any((room) => - (room.aliases?.contains(widget.alias) ?? false) || - room.canonicalAlias == widget.alias)) { + (room.aliases?.contains(controller.widget.alias) ?? false) || + room.canonicalAlias == controller.widget.alias)) { // we have to tack on the original alias res.chunk.add(PublicRoom.fromJson({ - 'aliases': [widget.alias], - 'name': widget.alias, + 'aliases': [controller.widget.alias], + 'name': controller.widget.alias, })); } return res; @@ -184,7 +58,7 @@ class _SearchViewState extends State { room.lastEvent == null || !room.displayname .toLowerCase() - .contains(_controller.text.toLowerCase()), + .contains(controller.controller.text.toLowerCase()), ); return DefaultTabController( length: 3, @@ -196,9 +70,9 @@ class _SearchViewState extends State { title: DefaultAppBarSearchField( autofocus: true, hintText: L10n.of(context).search, - searchController: _controller, + searchController: controller.controller, suffix: Icon(Icons.search_outlined), - onChanged: (t) => _search(context, t), + onChanged: controller.search, ), bottom: TabBar( indicatorColor: Theme.of(context).accentColor, @@ -228,10 +102,10 @@ class _SearchViewState extends State { child: Icon(Icons.edit_outlined), ), title: Text(L10n.of(context).changeTheServer), - onTap: () => _setServer(context), + onTap: controller.setServer, ), FutureBuilder( - future: _publicRoomsResponse, + future: controller.publicRoomsResponse, builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasError) { @@ -299,8 +173,7 @@ class _SearchViewState extends State { elevation: 2, borderRadius: BorderRadius.circular(16), child: InkWell( - onTap: () => _joinGroupAction( - context, + onTap: () => controller.joinGroupAction( publicRoomsResponse.chunk[i], ), borderRadius: BorderRadius.circular(16), @@ -351,11 +224,11 @@ class _SearchViewState extends State { itemCount: rooms.length, itemBuilder: (_, i) => ChatListItem(rooms[i]), ), - foundProfiles.isNotEmpty + controller.foundProfiles.isNotEmpty ? ListView.builder( - itemCount: foundProfiles.length, + itemCount: controller.foundProfiles.length, itemBuilder: (BuildContext context, int i) { - final foundProfile = foundProfiles[i]; + final foundProfile = controller.foundProfiles[i]; return ListTile( onTap: () async { final roomID = await showFutureLoadingDialog( @@ -390,7 +263,7 @@ class _SearchViewState extends State { ); }, ) - : ContactsList(searchController: _controller), + : ContactsList(searchController: controller.controller), ], ), ),