diff --git a/lib/components/contacts_list.dart b/lib/components/contacts_list.dart new file mode 100644 index 00000000..01dd3f83 --- /dev/null +++ b/lib/components/contacts_list.dart @@ -0,0 +1,115 @@ +import 'dart:async'; + +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:fluffychat/components/avatar.dart'; +import 'package:fluffychat/components/matrix.dart'; +import 'package:flutter/material.dart'; +import '../utils/client_presence_extension.dart'; +import '../utils/presence_extension.dart'; +import 'package:adaptive_page_layout/adaptive_page_layout.dart'; + +class ContactsList extends StatefulWidget { + final TextEditingController searchController; + + const ContactsList({ + Key key, + @required this.searchController, + }) : super(key: key); + @override + _ContactsState createState() => _ContactsState(); +} + +class _ContactsState extends State { + StreamSubscription _onSync; + + @override + void dispose() { + _onSync?.cancel(); + super.dispose(); + } + + DateTime _lastSetState = DateTime.now(); + Timer _coolDown; + + void _updateView() { + _lastSetState = DateTime.now(); + setState(() => null); + } + + @override + Widget build(BuildContext context) { + final client = Matrix.of(context).client; + _onSync ??= client.onSync.stream.listen((_) { + if (DateTime.now().millisecondsSinceEpoch - + _lastSetState.millisecondsSinceEpoch < + 1000) { + _coolDown?.cancel(); + _coolDown = Timer(Duration(seconds: 1), _updateView); + } else { + _updateView(); + } + }); + final contactList = Matrix.of(context) + .client + .contactList + .where((p) => p.senderId + .toLowerCase() + .contains(widget.searchController.text.toLowerCase())) + .toList(); + if (client.presences[client.userID]?.presence?.statusMsg?.isNotEmpty ?? + false) { + contactList.insert(0, client.presences[client.userID]); + } + return ListView.builder( + itemCount: contactList.length, + itemBuilder: (_, i) => _ContactListTile(contact: contactList[i]), + ); + } +} + +class _ContactListTile extends StatelessWidget { + final Presence contact; + + const _ContactListTile({Key key, @required this.contact}) : super(key: key); + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: + Matrix.of(context).client.getProfileFromUserId(contact.senderId), + builder: (context, snapshot) { + final displayname = + snapshot.data?.displayname ?? contact.senderId.localpart; + final avatarUrl = snapshot.data?.avatarUrl; + return ListTile( + leading: Container( + width: Avatar.defaultSize, + height: Avatar.defaultSize, + child: Stack( + children: [ + Center(child: Avatar(avatarUrl, displayname)), + Align( + alignment: Alignment.bottomRight, + child: Icon( + Icons.circle, + color: contact.color, + size: 12, + ), + ), + ], + ), + ), + title: Text(displayname), + subtitle: Text(contact.getLocalizedStatusMessage(context), + style: contact.presence.statusMsg?.isNotEmpty ?? false + ? TextStyle( + color: Theme.of(context).accentColor, + fontWeight: FontWeight.bold, + ) + : null), + onTap: () => AdaptivePageLayout.of(context) + .pushNamedAndRemoveUntilIsFirst( + '/rooms/${Matrix.of(context).client.getDirectChatFromUserId(contact.senderId)}'), + ); + }); + } +} diff --git a/lib/components/default_bottom_navigation_bar.dart b/lib/components/default_bottom_navigation_bar.dart deleted file mode 100644 index 5bd96f7a..00000000 --- a/lib/components/default_bottom_navigation_bar.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:adaptive_page_layout/adaptive_page_layout.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; - -class DefaultBottomNavigationBar extends StatelessWidget { - final int currentIndex; - - const DefaultBottomNavigationBar({Key key, this.currentIndex = 1}) - : super(key: key); - @override - Widget build(BuildContext context) { - return BottomNavigationBar( - onTap: (i) { - if (i == currentIndex) return; - switch (i) { - case 0: - AdaptivePageLayout.of(context) - .pushNamedAndRemoveUntilIsFirst('/contacts'); - break; - case 1: - AdaptivePageLayout.of(context).pushNamedAndRemoveAllOthers('/'); - break; - case 2: - AdaptivePageLayout.of(context) - .pushNamedAndRemoveUntilIsFirst('/discover'); - break; - } - }, - backgroundColor: Theme.of(context).scaffoldBackgroundColor, - selectedItemColor: Theme.of(context).accentColor, - currentIndex: currentIndex, - type: BottomNavigationBarType.fixed, - showUnselectedLabels: true, - items: [ - BottomNavigationBarItem( - icon: Icon(currentIndex == 0 ? Icons.people : Icons.people_outlined), - label: L10n.of(context).friends, - ), - BottomNavigationBarItem( - icon: Icon(currentIndex == 1 - ? CupertinoIcons.chat_bubble_2_fill - : CupertinoIcons.chat_bubble_2), - label: L10n.of(context).messages, - ), - BottomNavigationBarItem( - icon: Icon(currentIndex == 2 - ? CupertinoIcons.compass_fill - : CupertinoIcons.compass), - label: L10n.of(context).discover, - ), - ], - ); - } -} diff --git a/lib/config/routes.dart b/lib/config/routes.dart index 6e7834b2..a626caf6 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -5,8 +5,6 @@ import 'package:fluffychat/views/archive.dart'; import 'package:fluffychat/views/chat.dart'; import 'package:fluffychat/views/chat_details.dart'; import 'package:fluffychat/views/chat_encryption_settings.dart'; -import 'package:fluffychat/views/contacts.dart'; -import 'package:fluffychat/views/discover.dart'; import 'package:fluffychat/views/chat_list.dart'; import 'package:fluffychat/views/chat_permissions_settings.dart'; import 'package:fluffychat/views/empty_page.dart'; @@ -17,6 +15,7 @@ import 'package:fluffychat/views/log_view.dart'; import 'package:fluffychat/views/login.dart'; import 'package:fluffychat/views/new_group.dart'; import 'package:fluffychat/views/new_private_chat.dart'; +import 'package:fluffychat/views/search_view.dart'; import 'package:fluffychat/views/settings.dart'; import 'package:fluffychat/views/settings_3pid.dart'; import 'package:fluffychat/views/settings_devices.dart'; @@ -142,22 +141,16 @@ class FluffyRoutes { leftView: (_) => ChatList(), mainView: (_) => NewPrivateChat(), ); - case 'contacts': - return ViewData( - mainView: (_) => Contacts(), - emptyView: (_) => - activeRoomId != null ? Chat(activeRoomId) : EmptyPage(), - ); - case 'discover': + case 'search': if (parts.length == 3) { return ViewData( - mainView: (_) => Discover(alias: parts[2]), + mainView: (_) => SearchView(alias: parts[2]), emptyView: (_) => activeRoomId != null ? Chat(activeRoomId) : EmptyPage(), ); } return ViewData( - mainView: (_) => Discover(), + mainView: (_) => SearchView(), emptyView: (_) => activeRoomId != null ? Chat(activeRoomId) : EmptyPage(), ); diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index c371a5ce..fd77d55c 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -11,6 +11,21 @@ "type": "text", "placeholders": {} }, + "chats": "Chats", + "@chats": { + "type": "text", + "placeholders": {} + }, + "people": "People", + "@people": { + "type": "text", + "placeholders": {} + }, + "publicGroups": "Public Groups", + "@publicGroups": { + "type": "text", + "placeholders": {} + }, "acceptedTheInvitation": "{username} accepted the invitation", "@acceptedTheInvitation": { "type": "text", diff --git a/lib/main.dart b/lib/main.dart index 22d29895..f7634723 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -81,7 +81,7 @@ class App extends StatelessWidget { Matrix.of(context).loginState == LoginState.logged && !{ '/', - '/discover', + '/search', '/contacts', }.contains(settings.name) ? CupertinoPageRoute(builder: builder) diff --git a/lib/utils/url_launcher.dart b/lib/utils/url_launcher.dart index 68f17f97..06a565bb 100644 --- a/lib/utils/url_launcher.dart +++ b/lib/utils/url_launcher.dart @@ -99,7 +99,7 @@ class UrlLauncher { } } else { await AdaptivePageLayout.of(context) - .pushNamedAndRemoveUntilIsFirst('/discover/$roomIdOrAlias'); + .pushNamedAndRemoveUntilIsFirst('/search/$roomIdOrAlias'); } } else if (identityParts.primaryIdentifier.sigil == '@') { final user = User( diff --git a/lib/views/chat_list.dart b/lib/views/chat_list.dart index 3e27d187..279bc9be 100644 --- a/lib/views/chat_list.dart +++ b/lib/views/chat_list.dart @@ -4,11 +4,9 @@ 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/components/avatar.dart'; import 'package:fluffychat/components/connection_status_header.dart'; -import 'package:fluffychat/components/default_app_bar_search_field.dart'; -import 'package:fluffychat/components/default_bottom_navigation_bar.dart'; import 'package:fluffychat/components/list_items/chat_list_item.dart'; +import 'package:fluffychat/utils/fluffy_share.dart'; import 'package:flutter/cupertino.dart'; import 'package:fluffychat/app_config.dart'; import 'package:fluffychat/utils/platform_infos.dart'; @@ -22,6 +20,7 @@ import '../utils/url_launcher.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; enum SelectMode { normal, share, select } +enum PopupMenuAction { settings, invite, newGroup } class ChatList extends StatefulWidget { final String activeChat; @@ -38,9 +37,6 @@ class _ChatListState extends State { StreamSubscription _intentFileStreamSubscription; AppBar appBar; - - bool get searchMode => searchController.text?.isNotEmpty ?? false; - final TextEditingController searchController = TextEditingController(); final _selectedRoomIds = {}; final ScrollController _scrollController = ScrollController(); @@ -172,8 +168,6 @@ class _ChatListState extends State { return true; } - final GlobalKey _searchFieldKey = GlobalKey(); - @override Widget build(BuildContext context) { return StreamBuilder( @@ -187,24 +181,8 @@ class _ChatListState extends State { return Scaffold( appBar: appBar ?? AppBar( - elevation: 1, leading: selectMode == SelectMode.normal - ? Center( - child: InkWell( - borderRadius: BorderRadius.circular(32), - onTap: () => AdaptivePageLayout.of(context) - .pushNamedAndRemoveUntilIsFirst('/settings'), - child: FutureBuilder( - future: Matrix.of(context).client.ownProfile, - builder: (_, snapshot) => Avatar( - snapshot.data?.avatarUrl ?? Uri.parse(''), - snapshot.data?.displayname ?? - Matrix.of(context).client.userID.localpart, - size: 32, - ), - ), - ), - ) + ? null : IconButton( tooltip: L10n.of(context).cancel, icon: Icon(Icons.close_outlined), @@ -259,24 +237,64 @@ class _ChatListState extends State { IconButton( icon: Icon(Icons.search_outlined), tooltip: L10n.of(context).search, - onPressed: Matrix.of(context) - .client - .rooms - .isEmpty - ? null - : () async { - await _scrollController.animateTo( - _scrollController - .position.minScrollExtent, - duration: Duration(milliseconds: 200), - curve: Curves.ease, - ); - WidgetsBinding.instance - .addPostFrameCallback( - (_) => _searchFieldKey.currentState - .requestFocus(), - ); - }, + onPressed: () => AdaptivePageLayout.of(context) + .pushNamed('/search'), + ), + PopupMenuButton( + onSelected: (action) { + switch (action) { + case PopupMenuAction.settings: + AdaptivePageLayout.of(context) + .pushNamed('/settings'); + break; + case PopupMenuAction.invite: + FluffyShare.share( + L10n.of(context).inviteText( + Matrix.of(context).client.userID, + 'https://matrix.to/#/${Matrix.of(context).client.userID}'), + context); + break; + case PopupMenuAction.newGroup: + AdaptivePageLayout.of(context) + .pushNamed('/newgroup'); + break; + } + }, + itemBuilder: (_) => [ + PopupMenuItem( + value: PopupMenuAction.newGroup, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.group_add_outlined), + SizedBox(width: 12), + Text(L10n.of(context).createNewGroup), + ], + ), + ), + PopupMenuItem( + value: PopupMenuAction.invite, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.share_outlined), + SizedBox(width: 12), + Text(L10n.of(context).inviteContact), + ], + ), + ), + PopupMenuItem( + value: PopupMenuAction.settings, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.settings_outlined), + SizedBox(width: 12), + Text(L10n.of(context).settings), + ], + ), + ), + ], ), ], title: Text(selectMode == SelectMode.share @@ -302,29 +320,20 @@ class _ChatListState extends State { if (snapshot.hasData) { var rooms = List.from( Matrix.of(context).client.rooms); - rooms.removeWhere((room) => - room.lastEvent == null || - (searchMode && - !room.displayname.toLowerCase().contains( - searchController.text.toLowerCase() ?? - ''))); - if (rooms.isEmpty && (!searchMode)) { + rooms.removeWhere((room) => room.lastEvent == null); + if (rooms.isEmpty) { return Column( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ Icon( - searchMode - ? Icons.search_outlined - : Icons.maps_ugc_outlined, + Icons.maps_ugc_outlined, size: 80, color: Colors.grey, ), Center( child: Text( - searchMode - ? L10n.of(context).noRoomsFound - : L10n.of(context).startYourFirstChat, + L10n.of(context).startYourFirstChat, textAlign: TextAlign.start, style: TextStyle( color: Colors.grey, @@ -338,33 +347,19 @@ 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: _searchFieldKey, - hintText: L10n.of(context).search, - prefixIcon: Icon(Icons.search_outlined), - searchController: searchController, - onChanged: (_) => setState(() => null), - padding: EdgeInsets.zero, - ), - ) - : ChatListItem( - rooms[i - 1], - selected: _selectedRoomIds - .contains(rooms[i - 1].id), - onTap: selectMode == SelectMode.select - ? () => - _toggleSelection(rooms[i - 1].id) - : null, - onLongPress: () => - _toggleSelection(rooms[i - 1].id), - activeChat: - widget.activeChat == rooms[i - 1].id, - ), + itemCount: totalCount, + itemBuilder: (BuildContext context, int i) => + ChatListItem( + rooms[i], + selected: + _selectedRoomIds.contains(rooms[i].id), + onTap: selectMode == SelectMode.select + ? () => _toggleSelection(rooms[i].id) + : null, + onLongPress: () => + _toggleSelection(rooms[i].id), + activeChat: widget.activeChat == rooms[i].id, + ), ); } else { return Center( @@ -383,9 +378,6 @@ class _ChatListState extends State { child: Icon(Icons.add_outlined), ) : null, - bottomNavigationBar: selectMode == SelectMode.normal - ? DefaultBottomNavigationBar(currentIndex: 1) - : null, ); }); } diff --git a/lib/views/contacts.dart b/lib/views/contacts.dart deleted file mode 100644 index d0a69133..00000000 --- a/lib/views/contacts.dart +++ /dev/null @@ -1,210 +0,0 @@ -import 'dart:async'; - -import 'package:adaptive_dialog/adaptive_dialog.dart'; -import 'package:famedlysdk/famedlysdk.dart'; -import 'package:fluffychat/components/avatar.dart'; -import 'package:fluffychat/components/default_app_bar_search_field.dart'; -import 'package:fluffychat/components/default_bottom_navigation_bar.dart'; -import 'package:fluffychat/components/matrix.dart'; -import 'package:fluffychat/utils/fluffy_share.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; -import '../utils/client_presence_extension.dart'; -import '../utils/presence_extension.dart'; -import 'package:adaptive_page_layout/adaptive_page_layout.dart'; - -class Contacts extends StatefulWidget { - @override - _ContactsState createState() => _ContactsState(); -} - -class _ContactsState extends State { - StreamSubscription _onSync; - final TextEditingController _controller = TextEditingController(); - - @override - void dispose() { - _onSync?.cancel(); - super.dispose(); - } - - DateTime _lastSetState = DateTime.now(); - Timer _coolDown; - - void _updateView() { - _lastSetState = DateTime.now(); - setState(() => null); - } - - void _setStatus(BuildContext context) async { - final input = await showTextInputDialog( - context: context, - title: L10n.of(context).setStatus, - okLabel: L10n.of(context).ok, - cancelLabel: L10n.of(context).cancel, - useRootNavigator: false, - textFields: [ - DialogTextField( - hintText: L10n.of(context).statusExampleMessage, - ), - ]); - if (input == null) return; - await showFutureLoadingDialog( - context: context, - future: () => Matrix.of(context).client.sendPresence( - Matrix.of(context).client.userID, - PresenceType.online, - statusMsg: input.single, - ), - ); - } - - @override - Widget build(BuildContext context) { - final client = Matrix.of(context).client; - _onSync ??= client.onSync.stream.listen((_) { - if (DateTime.now().millisecondsSinceEpoch - - _lastSetState.millisecondsSinceEpoch < - 1000) { - _coolDown?.cancel(); - _coolDown = Timer(Duration(seconds: 1), _updateView); - } else { - _updateView(); - } - }); - final contactList = Matrix.of(context) - .client - .contactList - .where((p) => - p.senderId.toLowerCase().contains(_controller.text.toLowerCase())) - .toList(); - if (client.presences[client.userID]?.presence?.statusMsg?.isNotEmpty ?? - false) { - contactList.insert(0, client.presences[client.userID]); - } - return Scaffold( - appBar: AppBar( - automaticallyImplyLeading: false, - elevation: 1, - title: Text(L10n.of(context).friends), - actions: [ - TextButton.icon( - label: Text( - L10n.of(context).status, - style: TextStyle(color: Theme.of(context).accentColor), - ), - icon: - Icon(Icons.edit_outlined, color: Theme.of(context).accentColor), - onPressed: () => _setStatus(context), - ), - ], - ), - body: ListView.builder( - itemCount: contactList.length + 1, - itemBuilder: (_, i) => i == 0 - ? Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: EdgeInsets.all(12), - child: DefaultAppBarSearchField( - hintText: L10n.of(context).search, - prefixIcon: Icon(Icons.search_outlined), - searchController: _controller, - onChanged: (_) => setState(() => null), - padding: EdgeInsets.zero, - ), - ), - ListTile( - leading: CircleAvatar( - radius: Avatar.defaultSize / 2, - backgroundColor: Theme.of(context).primaryColor, - foregroundColor: Colors.white, - child: Icon(Icons.person_add_outlined), - ), - title: Text(L10n.of(context).addNewFriend), - onTap: () => AdaptivePageLayout.of(context) - .pushNamed('/newprivatechat'), - ), - Divider(height: 1), - if (contactList.isEmpty) - Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox(height: 16), - Icon( - Icons.share_outlined, - size: 80, - color: Colors.grey, - ), - Center( - child: OutlinedButton( - onPressed: () => FluffyShare.share( - L10n.of(context).inviteText(client.userID, - 'https://matrix.to/#/${client.userID}'), - context), - child: Text( - L10n.of(context).inviteContact, - style: TextStyle( - color: Theme.of(context).accentColor), - ), - ), - ), - ], - ), - ], - ) - : _ContactListTile(contact: contactList[i - 1]), - ), - bottomNavigationBar: DefaultBottomNavigationBar(currentIndex: 0), - ); - } -} - -class _ContactListTile extends StatelessWidget { - final Presence contact; - - const _ContactListTile({Key key, @required this.contact}) : super(key: key); - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: - Matrix.of(context).client.getProfileFromUserId(contact.senderId), - builder: (context, snapshot) { - final displayname = - snapshot.data?.displayname ?? contact.senderId.localpart; - final avatarUrl = snapshot.data?.avatarUrl; - return ListTile( - leading: Container( - width: Avatar.defaultSize, - height: Avatar.defaultSize, - child: Stack( - children: [ - Center(child: Avatar(avatarUrl, displayname)), - Align( - alignment: Alignment.bottomRight, - child: Icon( - Icons.circle, - color: contact.color, - size: 12, - ), - ), - ], - ), - ), - title: Text(displayname), - subtitle: Text(contact.getLocalizedStatusMessage(context), - style: contact.presence.statusMsg?.isNotEmpty ?? false - ? TextStyle( - color: Theme.of(context).accentColor, - fontWeight: FontWeight.bold, - ) - : null), - onTap: () => AdaptivePageLayout.of(context).pushNamed( - '/rooms/${Matrix.of(context).client.getDirectChatFromUserId(contact.senderId)}'), - ); - }); - } -} diff --git a/lib/views/discover.dart b/lib/views/discover.dart index 55a0d04d..3a03e4be 100644 --- a/lib/views/discover.dart +++ b/lib/views/discover.dart @@ -5,7 +5,6 @@ import 'package:adaptive_page_layout/adaptive_page_layout.dart'; import 'package:famedlysdk/famedlysdk.dart'; import 'package:fluffychat/components/avatar.dart'; import 'package:fluffychat/components/default_app_bar_search_field.dart'; -import 'package:fluffychat/components/default_bottom_navigation_bar.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:fluffychat/components/matrix.dart'; import 'package:flutter/material.dart'; @@ -296,7 +295,6 @@ class _DiscoverState extends State { }), ], ), - bottomNavigationBar: DefaultBottomNavigationBar(currentIndex: 2), ); } } diff --git a/lib/views/new_private_chat.dart b/lib/views/new_private_chat.dart index 4ba63dbc..544f2631 100644 --- a/lib/views/new_private_chat.dart +++ b/lib/views/new_private_chat.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:adaptive_page_layout/adaptive_page_layout.dart'; import 'package:famedlysdk/famedlysdk.dart'; import 'package:fluffychat/components/avatar.dart'; +import 'package:fluffychat/components/contacts_list.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:fluffychat/components/matrix.dart'; import 'package:fluffychat/utils/fluffy_share.dart'; @@ -108,7 +109,7 @@ class _NewPrivateChatState extends State { key: _formKey, child: TextFormField( controller: controller, - autofocus: true, + //autofocus: true, autocorrect: false, onChanged: (String text) => searchUserWithCoolDown(context), textInputAction: TextInputAction.go, @@ -150,13 +151,35 @@ class _NewPrivateChatState extends State { ) : Icon(Icons.account_circle_outlined), prefixText: '@', + suffixIcon: IconButton( + onPressed: () => submitAction(context), + icon: Icon(Icons.arrow_forward_outlined), + ), hintText: '${L10n.of(context).username.toLowerCase()}', ), ), ), ), Divider(height: 1), - if (foundProfiles.isNotEmpty && !correctMxId) + ListTile( + leading: CircleAvatar( + radius: Avatar.defaultSize / 2, + foregroundColor: Theme.of(context).accentColor, + backgroundColor: Theme.of(context).secondaryHeaderColor, + child: Icon(Icons.share_outlined), + ), + onTap: () => FluffyShare.share( + L10n.of(context).inviteText(Matrix.of(context).client.userID, + 'https://matrix.to/#/${Matrix.of(context).client.userID}'), + context), + title: Text('${L10n.of(context).yourOwnUsername}:'), + subtitle: Text( + Matrix.of(context).client.userID, + style: TextStyle(color: Theme.of(context).accentColor), + ), + ), + Divider(height: 1), + if (foundProfiles.isNotEmpty) Expanded( child: ListView.builder( itemCount: foundProfiles.length, @@ -190,38 +213,12 @@ class _NewPrivateChatState extends State { }, ), ), - if (foundProfiles.isEmpty || correctMxId) - ListTile( - trailing: Icon(Icons.share_outlined), - onTap: () => FluffyShare.share( - L10n.of(context).inviteText(Matrix.of(context).client.userID, - 'https://matrix.to/#/${Matrix.of(context).client.userID}'), - context), - title: Text( - '${L10n.of(context).yourOwnUsername}:', - style: TextStyle( - fontStyle: FontStyle.italic, - ), - ), - subtitle: Text( - Matrix.of(context).client.userID, - style: TextStyle( - fontSize: 16, - color: Theme.of(context).primaryColor, - ), - ), - ), - Divider(height: 1), - if (foundProfiles.isEmpty || correctMxId) + if (foundProfiles.isEmpty) Expanded( - child: Image.asset('assets/private_chat_wallpaper.png'), + child: ContactsList(searchController: controller), ), ], ), - floatingActionButton: FloatingActionButton( - onPressed: () => submitAction(context), - child: Icon(Icons.arrow_forward_outlined), - ), ); } } diff --git a/lib/views/search_view.dart b/lib/views/search_view.dart new file mode 100644 index 00000000..a040350f --- /dev/null +++ b/lib/views/search_view.dart @@ -0,0 +1,393 @@ +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/components/avatar.dart'; +import 'package:fluffychat/components/contacts_list.dart'; +import 'package:fluffychat/components/default_app_bar_search_field.dart'; +import 'package:fluffychat/components/list_items/chat_list_item.dart'; +import 'package:fluffychat/components/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 '../utils/localized_exception_extension.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(() {}); + } + + @override + void initState() { + _genericSearchTerm = widget.alias; + super.initState(); + } + + @override + Widget build(BuildContext context) { + final server = _genericSearchTerm?.isValidMatrixId ?? false + ? _genericSearchTerm.domain + : _server; + if (_lastServer != server) { + _lastServer = server; + _publicRoomsResponse = null; + } + _publicRoomsResponse ??= Matrix.of(context) + .client + .searchPublicRooms( + server: server, + genericSearchTerm: _genericSearchTerm, + ) + .catchError((error) { + if (widget.alias == null) { + throw error; + } + return PublicRoomsResponse.fromJson({ + 'chunk': [], + }); + }).then((PublicRoomsResponse res) { + if (widget.alias != null && + !res.chunk.any((room) => + (room.aliases?.contains(widget.alias) ?? false) || + room.canonicalAlias == widget.alias)) { + // we have to tack on the original alias + res.chunk.add(PublicRoom.fromJson({ + 'aliases': [widget.alias], + 'name': widget.alias, + })); + } + return res; + }); + + final rooms = List.from(Matrix.of(context).client.rooms); + rooms.removeWhere( + (room) => + room.lastEvent == null || + !room.displayname + .toLowerCase() + .contains(_controller.text.toLowerCase()), + ); + return DefaultTabController( + length: 3, + initialIndex: 1, + child: Scaffold( + appBar: AppBar( + leading: BackButton(), + titleSpacing: 0, + title: DefaultAppBarSearchField( + autofocus: true, + hintText: L10n.of(context).search, + searchController: _controller, + suffix: Icon(Icons.search_outlined), + onChanged: (t) => _search(context, t), + ), + bottom: TabBar( + indicatorColor: Theme.of(context).accentColor, + labelColor: Theme.of(context).accentColor, + unselectedLabelColor: Theme.of(context).textTheme.bodyText1.color, + labelStyle: TextStyle(fontSize: 16), + labelPadding: EdgeInsets.symmetric( + horizontal: 8, + vertical: 0, + ), + tabs: [ + Tab(child: Text(L10n.of(context).publicGroups, maxLines: 1)), + Tab(child: Text(L10n.of(context).chats, maxLines: 1)), + Tab(child: Text(L10n.of(context).people, maxLines: 1)), + ], + ), + ), + body: TabBarView( + children: [ + ListView( + children: [ + SizedBox(height: 12), + ListTile( + leading: CircleAvatar( + foregroundColor: Theme.of(context).accentColor, + backgroundColor: Theme.of(context).secondaryHeaderColor, + child: Icon(Icons.edit_outlined), + ), + title: Text(L10n.of(context).changeTheServer), + onTap: () => _setServer(context), + ), + FutureBuilder( + future: _publicRoomsResponse, + builder: (BuildContext context, + AsyncSnapshot snapshot) { + if (snapshot.hasError) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: 32), + Icon( + Icons.error_outlined, + size: 80, + color: Colors.grey, + ), + Center( + child: Text( + snapshot.error.toLocalizedString(context), + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.grey, + fontSize: 16, + ), + ), + ), + ], + ); + } + if (snapshot.connectionState != ConnectionState.done) { + return Center(child: CircularProgressIndicator()); + } + final publicRoomsResponse = snapshot.data; + if (publicRoomsResponse.chunk.isEmpty) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: 32), + Icon( + Icons.search_outlined, + size: 80, + color: Colors.grey, + ), + Center( + child: Text( + L10n.of(context).noPublicRoomsFound, + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.grey, + fontSize: 16, + ), + ), + ), + ], + ); + } + return GridView.builder( + shrinkWrap: true, + padding: EdgeInsets.all(12), + physics: NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + childAspectRatio: 1, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + ), + itemCount: publicRoomsResponse.chunk.length, + itemBuilder: (BuildContext context, int i) => Material( + elevation: 2, + borderRadius: BorderRadius.circular(16), + child: InkWell( + onTap: () => _joinGroupAction( + context, + publicRoomsResponse.chunk[i], + ), + borderRadius: BorderRadius.circular(16), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Avatar( + Uri.parse(publicRoomsResponse + .chunk[i].avatarUrl ?? + ''), + publicRoomsResponse.chunk[i].name), + Text( + publicRoomsResponse.chunk[i].name, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + maxLines: 1, + textAlign: TextAlign.center, + ), + Text( + L10n.of(context).countParticipants( + publicRoomsResponse + .chunk[i].numJoinedMembers ?? + 0), + style: TextStyle(fontSize: 10.5), + maxLines: 1, + textAlign: TextAlign.center, + ), + Text( + publicRoomsResponse.chunk[i].topic ?? + L10n.of(context).noDescription, + maxLines: 4, + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + ), + ); + }), + ], + ), + ListView.builder( + itemCount: rooms.length, + itemBuilder: (_, i) => ChatListItem(rooms[i]), + ), + foundProfiles.isNotEmpty + ? ListView.builder( + itemCount: foundProfiles.length, + itemBuilder: (BuildContext context, int i) { + var foundProfile = foundProfiles[i]; + return ListTile( + onTap: () { + setState(() { + _controller.text = currentSearchTerm = + foundProfile.userId.substring(1); + }); + }, + leading: Avatar( + foundProfile.avatarUrl, + foundProfile.displayname ?? foundProfile.userId, + //size: 24, + ), + title: Text( + foundProfile.displayname ?? + foundProfile.userId.localpart, + style: TextStyle(), + maxLines: 1, + ), + subtitle: Text( + foundProfile.userId, + maxLines: 1, + style: TextStyle( + fontSize: 12, + ), + ), + ); + }, + ) + : ContactsList(searchController: _controller), + ], + ), + ), + ); + } +}