From ebe246d388c6fbfb4495ae5f15e9fa098d6960ec Mon Sep 17 00:00:00 2001 From: Christian Pauly Date: Sat, 13 Feb 2021 11:55:22 +0100 Subject: [PATCH] change: Revert new design --- lib/components/horizontal_stories_list.dart | 158 ++++++ .../list_items/contact_list_tile.dart | 71 --- lib/config/routes.dart | 56 +- lib/config/themes.dart | 2 - lib/views/chat_list.dart | 486 ++++++++++++++++++ lib/views/discover.dart | 289 +++++++++++ lib/views/home_view.dart | 291 ----------- lib/views/home_view_parts/chat_list.dart | 273 ---------- lib/views/home_view_parts/contact_list.dart | 154 ------ lib/views/home_view_parts/discover.dart | 281 ---------- lib/views/{home_view_parts => }/settings.dart | 418 +++++++-------- lib/views/share_view.dart | 35 -- 12 files changed, 1179 insertions(+), 1335 deletions(-) create mode 100644 lib/components/horizontal_stories_list.dart delete mode 100644 lib/components/list_items/contact_list_tile.dart create mode 100644 lib/views/chat_list.dart create mode 100644 lib/views/discover.dart delete mode 100644 lib/views/home_view.dart delete mode 100644 lib/views/home_view_parts/chat_list.dart delete mode 100644 lib/views/home_view_parts/contact_list.dart delete mode 100644 lib/views/home_view_parts/discover.dart rename lib/views/{home_view_parts => }/settings.dart (54%) delete mode 100644 lib/views/share_view.dart diff --git a/lib/components/horizontal_stories_list.dart b/lib/components/horizontal_stories_list.dart new file mode 100644 index 00000000..1fd7a8fe --- /dev/null +++ b/lib/components/horizontal_stories_list.dart @@ -0,0 +1,158 @@ +import 'dart:async'; + +import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:fluffychat/components/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:adaptive_page_layout/adaptive_page_layout.dart'; +import '../utils/client_presence_extension.dart'; +import '../utils/presence_extension.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'avatar.dart'; + +class HorizontalStoriesList extends StatefulWidget { + final String searchQuery; + + const HorizontalStoriesList({Key key, this.searchQuery = ''}) + : super(key: key); + @override + _HorizontalStoriesListState createState() => _HorizontalStoriesListState(); +} + +class _HorizontalStoriesListState extends State { + StreamSubscription _onSync; + + @override + void dispose() { + _onSync?.cancel(); + super.dispose(); + } + + DateTime _lastSetState = DateTime.now(); + Timer _coolDown; + + void _updateView() { + _lastSetState = DateTime.now(); + setState(() => null); + } + + static const double height = 68.0; + + @override + Widget build(BuildContext context) { + _onSync ??= Matrix.of(context).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.searchQuery.toLowerCase())) + .toList(); + return AnimatedContainer( + height: height, + duration: Duration(milliseconds: 300), + child: contactList.isEmpty + ? null + : ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: contactList.length, + itemBuilder: (context, i) => + _StoriesListTile(story: contactList[i]), + ), + ); + } +} + +class _StoriesListTile extends StatelessWidget { + final Presence story; + + const _StoriesListTile({ + Key key, + @required this.story, + }) : super(key: key); + @override + Widget build(BuildContext context) { + final hasStatusMessage = story.presence.statusMsg?.isNotEmpty ?? false; + return FutureBuilder( + future: Matrix.of(context).client.getProfileFromUserId(story.senderId), + builder: (context, snapshot) { + final displayname = + snapshot.data?.displayname ?? story.senderId.localpart; + final avatarUrl = snapshot.data?.avatarUrl; + return Container( + width: Avatar.defaultSize + 32, + height: _HorizontalStoriesListState.height, + child: InkWell( + borderRadius: BorderRadius.circular(8), + onTap: () async { + if (story.senderId == Matrix.of(context).client.userID) { + await showOkAlertDialog( + context: context, + title: displayname, + message: story.presence.statusMsg, + okLabel: L10n.of(context).close, + ); + return; + } + if (hasStatusMessage) { + if (OkCancelResult.ok != + await showOkCancelAlertDialog( + context: context, + title: displayname, + message: story.presence.statusMsg, + okLabel: L10n.of(context).sendAMessage, + cancelLabel: L10n.of(context).close, + )) { + return; + } + } + final roomId = await Matrix.of(context) + .client + .startDirectChat(story.senderId); + await AdaptivePageLayout.of(context) + .pushNamedAndRemoveUntilIsFirst('/rooms/${roomId}'); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: Avatar.defaultSize, + height: Avatar.defaultSize, + child: Stack( + children: [ + Center(child: Avatar(avatarUrl, displayname)), + Align( + alignment: Alignment.bottomRight, + child: Icon( + Icons.circle, + color: story.color, + size: 12, + ), + ), + ], + ), + ), + SizedBox(height: 4), + Text(displayname.split(' ').first, + style: TextStyle( + fontWeight: hasStatusMessage ? FontWeight.bold : null, + color: hasStatusMessage + ? Theme.of(context).accentColor + : null, + )), + ], + ), + ), + ); + }); + } +} diff --git a/lib/components/list_items/contact_list_tile.dart b/lib/components/list_items/contact_list_tile.dart deleted file mode 100644 index c0bd15b5..00000000 --- a/lib/components/list_items/contact_list_tile.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:adaptive_page_layout/adaptive_page_layout.dart'; -import 'package:famedlysdk/famedlysdk.dart'; -import 'package:fluffychat/components/avatar.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; - -import '../../utils/presence_extension.dart'; -import '../matrix.dart'; - -class ContactListTile extends StatelessWidget { - final Presence contact; - - const ContactListTile({Key key, @required this.contact}) : super(key: key); - @override - Widget build(BuildContext context) { - var statusMsg = contact.presence?.statusMsg?.isNotEmpty ?? false - ? contact.presence.statusMsg - : null; - 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: Avatar(avatarUrl, displayname), - title: Row( - children: [ - Icon(Icons.circle, color: contact.color, size: 10), - SizedBox(width: 4), - Expanded( - child: Text( - displayname, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - subtitle: statusMsg == null - ? Text(contact.getLocalizedLastActiveAgo(context)) - : Row( - children: [ - Icon(Icons.edit_outlined, - color: Theme.of(context).accentColor, size: 12), - SizedBox(width: 2), - Expanded( - child: Text( - statusMsg, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: - Theme.of(context).textTheme.bodyText1.color, - ), - ), - ), - ], - ), - onTap: () async { - if (contact.senderId == Matrix.of(context).client.userID) { - return; - } - final roomId = await User(contact.senderId, - room: Room(id: '', client: Matrix.of(context).client)) - .startDirectChat(); - await AdaptivePageLayout.of(context) - .pushNamedAndRemoveUntilIsFirst('/rooms/${roomId}'); - }); - }); - } -} diff --git a/lib/config/routes.dart b/lib/config/routes.dart index 4b5c447d..5677e8e7 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -5,7 +5,8 @@ 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/home_view.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'; import 'package:fluffychat/views/homeserver_picker.dart'; @@ -15,6 +16,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/settings.dart'; import 'package:fluffychat/views/settings_3pid.dart'; import 'package:fluffychat/views/settings_devices.dart'; import 'package:fluffychat/views/settings_emotes.dart'; @@ -62,14 +64,14 @@ class FluffyRoutes { switch (parts[1]) { case '': return ViewData( - mainView: (_) => HomeView(), + mainView: (_) => ChatList(), emptyView: (_) => EmptyPage(), ); case 'rooms': final roomId = parts[2]; if (parts.length == 3) { return ViewData( - leftView: (_) => HomeView(activeChat: roomId), + leftView: (_) => ChatList(activeChat: roomId), mainView: (_) => Chat(roomId), ); } else if (parts.length == 4) { @@ -77,44 +79,44 @@ class FluffyRoutes { switch (action) { case 'details': return ViewData( - leftView: (_) => HomeView(activeChat: roomId), + leftView: (_) => ChatList(activeChat: roomId), mainView: (_) => Chat(roomId), rightView: (_) => ChatDetails(roomId), ); case 'encryption': return ViewData( - leftView: (_) => HomeView(activeChat: roomId), + leftView: (_) => ChatList(activeChat: roomId), mainView: (_) => Chat(roomId), rightView: (_) => ChatEncryptionSettings(roomId), ); case 'permissions': return ViewData( - leftView: (_) => HomeView(activeChat: roomId), + leftView: (_) => ChatList(activeChat: roomId), mainView: (_) => Chat(roomId), rightView: (_) => ChatPermissionsSettings(roomId), ); case 'invite': return ViewData( - leftView: (_) => HomeView(activeChat: roomId), + leftView: (_) => ChatList(activeChat: roomId), mainView: (_) => Chat(roomId), rightView: (_) => InvitationSelection(roomId), ); case 'emotes': return ViewData( - leftView: (_) => HomeView(activeChat: roomId), + leftView: (_) => ChatList(activeChat: roomId), mainView: (_) => Chat(roomId), rightView: (_) => MultipleEmotesSettings(roomId), ); default: return ViewData( - leftView: (_) => HomeView(activeChat: roomId), + leftView: (_) => ChatList(activeChat: roomId), mainView: (_) => Chat(roomId, scrollToEventId: action.sigil == '\$' ? action : null), ); } } return ViewData( - mainView: (_) => HomeView(), + mainView: (_) => ChatList(), emptyView: (_) => EmptyPage(), ); case 'archive': @@ -128,31 +130,42 @@ class FluffyRoutes { ); case 'newgroup': return ViewData( - leftView: (_) => HomeView(), + leftView: (_) => ChatList(), mainView: (_) => NewGroup(), ); case 'newprivatechat': return ViewData( - leftView: (_) => HomeView(), + leftView: (_) => ChatList(), mainView: (_) => NewPrivateChat(), ); + case 'discover': + if (parts.length == 3) { + return ViewData( + mainView: (_) => Discover(alias: parts[2]), + emptyView: (_) => EmptyPage(), + ); + } + return ViewData( + mainView: (_) => Discover(), + emptyView: (_) => EmptyPage(), + ); case 'settings': if (parts.length == 3) { final action = parts[2]; switch (action) { case '3pid': return ViewData( - leftView: (_) => HomeView(), + leftView: (_) => Settings(), mainView: (_) => Settings3Pid(), ); case 'devices': return ViewData( - leftView: (_) => HomeView(), + leftView: (_) => Settings(), mainView: (_) => DevicesSettings(), ); case 'emotes': return ViewData( - leftView: (_) => HomeView(), + leftView: (_) => Settings(), mainView: (_) => EmotesSettings( room: ((settings.arguments ?? {}) as Map)['room'], stateKey: ((settings.arguments ?? {}) as Map)['stateKey'], @@ -160,25 +173,30 @@ class FluffyRoutes { ); case 'ignore': return ViewData( - leftView: (_) => HomeView(), + leftView: (_) => Settings(), mainView: (_) => SettingsIgnoreList( initialUserId: settings.arguments, ), ); case 'notifications': return ViewData( - leftView: (_) => HomeView(), + leftView: (_) => Settings(), mainView: (_) => SettingsNotifications(), ); case 'style': return ViewData( - leftView: (_) => HomeView(), + leftView: (_) => Settings(), mainView: (_) => SettingsStyle(), ); } + } else { + return ViewData( + mainView: (_) => Settings(), + emptyView: (_) => EmptyPage(), + ); } return ViewData( - mainView: (_) => HomeView(), + mainView: (_) => ChatList(), emptyView: (_) => EmptyPage(), ); } diff --git a/lib/config/themes.dart b/lib/config/themes.dart index 7b9bfc1d..8e1aca83 100644 --- a/lib/config/themes.dart +++ b/lib/config/themes.dart @@ -67,7 +67,6 @@ abstract class FluffyThemes { fillColor: lighten(AppConfig.primaryColor, .51), ), appBarTheme: AppBarTheme( - elevation: 1, brightness: Brightness.light, color: Colors.white, textTheme: TextTheme( @@ -117,7 +116,6 @@ abstract class FluffyThemes { ), ), appBarTheme: AppBarTheme( - elevation: 1, brightness: Brightness.dark, color: Color(0xff1D1D1D), textTheme: TextTheme( diff --git a/lib/views/chat_list.dart b/lib/views/chat_list.dart new file mode 100644 index 00000000..1c93c2f6 --- /dev/null +++ b/lib/views/chat_list.dart @@ -0,0 +1,486 @@ +import 'dart:async'; +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/connection_status_header.dart'; +import 'package:fluffychat/components/default_app_bar_search_field.dart'; +import 'package:fluffychat/components/horizontal_stories_list.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'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:future_loading_dialog/future_loading_dialog.dart'; +import 'package:receive_sharing_intent/receive_sharing_intent.dart'; +import '../components/matrix.dart'; +import '../utils/matrix_file_extension.dart'; +import '../utils/url_launcher.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +enum SelectMode { normal, share, select } + +class ChatList extends StatefulWidget { + final String activeChat; + + const ChatList({this.activeChat, Key key}) : super(key: key); + + @override + _ChatListState createState() => _ChatListState(); +} + +class _ChatListState extends State { + StreamSubscription _intentDataStreamSubscription; + + StreamSubscription _intentFileStreamSubscription; + + AppBar appBar; + + bool get searchMode => searchController.text?.isNotEmpty ?? false; + final TextEditingController searchController = TextEditingController(); + final _selectedRoomIds = {}; + + final ScrollController _scrollController = ScrollController(); + + void _processIncomingSharedFiles(List files) { + if (files?.isEmpty ?? true) return; + AdaptivePageLayout.of(context).popUntilIsFirst(); + final file = File(files.first.path); + + Matrix.of(context).shareContent = { + 'msgtype': 'chat.fluffy.shared_file', + 'file': MatrixFile( + bytes: file.readAsBytesSync(), + name: file.path, + ).detectFileType, + }; + } + + void _processIncomingSharedText(String text) { + if (text == null) return; + AdaptivePageLayout.of(context).popUntilIsFirst(); + if (text.toLowerCase().startsWith(AppConfig.inviteLinkPrefix) || + (text.toLowerCase().startsWith(AppConfig.schemePrefix) && + !RegExp(r'\s').hasMatch(text))) { + UrlLauncher(context, text).openMatrixToUrl(); + return; + } + Matrix.of(context).shareContent = { + 'msgtype': 'm.text', + 'body': text, + }; + } + + void _initReceiveSharingIntent() { + if (!PlatformInfos.isMobile) return; + + // For sharing images coming from outside the app while the app is in the memory + _intentFileStreamSubscription = ReceiveSharingIntent.getMediaStream() + .listen(_processIncomingSharedFiles, onError: print); + + // For sharing images coming from outside the app while the app is closed + ReceiveSharingIntent.getInitialMedia().then(_processIncomingSharedFiles); + + // For sharing or opening urls/text coming from outside the app while the app is in the memory + _intentDataStreamSubscription = ReceiveSharingIntent.getTextStream() + .listen(_processIncomingSharedText, onError: print); + + // For sharing or opening urls/text coming from outside the app while the app is closed + ReceiveSharingIntent.getInitialText().then(_processIncomingSharedText); + } + + @override + void initState() { + _initReceiveSharingIntent(); + super.initState(); + } + + @override + void dispose() { + _intentDataStreamSubscription?.cancel(); + _intentFileStreamSubscription?.cancel(); + super.dispose(); + } + + void _onPopupMenuButtonSelect(ChatListPopupMenuItemActions action) { + switch (action) { + case ChatListPopupMenuItemActions.createGroup: + AdaptivePageLayout.of(context).pushNamed('/newgroup'); + break; + case ChatListPopupMenuItemActions.discover: + AdaptivePageLayout.of(context).pushNamed('/discover'); + break; + case ChatListPopupMenuItemActions.setStatus: + _setStatus(); + break; + case ChatListPopupMenuItemActions.inviteContact: + FluffyShare.share( + L10n.of(context).inviteText(Matrix.of(context).client.userID, + 'https://matrix.to/#/${Matrix.of(context).client.userID}'), + context); + break; + case ChatListPopupMenuItemActions.settings: + AdaptivePageLayout.of(context).pushNamed('/settings'); + break; + } + } + + void _setStatus() async { + final input = await showTextInputDialog( + context: context, + title: L10n.of(context).setStatus, + 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, + ), + ); + } + + void _toggleSelection(String roomId) { + setState(() => _selectedRoomIds.contains(roomId) + ? _selectedRoomIds.remove(roomId) + : _selectedRoomIds.add(roomId)); + } + + Future _toggleUnread(BuildContext context) { + final room = Matrix.of(context).client.getRoomById(_selectedRoomIds.single); + return showFutureLoadingDialog( + context: context, + future: () => room.setUnread(!room.isUnread), + ); + } + + Future _toggleFavouriteRoom(BuildContext context) { + final room = Matrix.of(context).client.getRoomById(_selectedRoomIds.single); + return showFutureLoadingDialog( + context: context, + future: () => room.setFavourite(!room.isFavourite), + ); + } + + Future _toggleMuted(BuildContext context) { + final room = Matrix.of(context).client.getRoomById(_selectedRoomIds.single); + return showFutureLoadingDialog( + context: context, + future: () => room.setPushRuleState( + room.pushRuleState == PushRuleState.notify + ? PushRuleState.mentions_only + : PushRuleState.notify), + ); + } + + Future _archiveAction(BuildContext context) async { + final confirmed = await showOkCancelAlertDialog( + context: context, + title: L10n.of(context).areYouSure, + ) == + OkCancelResult.ok; + if (!confirmed) return; + await showFutureLoadingDialog( + context: context, + future: () => _archiveSelectedRooms(context), + ); + setState(() => null); + } + + Future _archiveSelectedRooms(BuildContext context) async { + final client = Matrix.of(context).client; + while (_selectedRoomIds.isNotEmpty) { + final roomId = _selectedRoomIds.first; + await client.getRoomById(roomId).leave(); + _toggleSelection(roomId); + } + } + + Future waitForFirstSync(BuildContext context) async { + var client = Matrix.of(context).client; + if (client.prevBatch?.isEmpty ?? true) { + await client.onFirstSync.stream.first; + } + return true; + } + + final GlobalKey _searchFieldKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: Matrix.of(context).onShareContentChanged.stream, + builder: (_, __) { + final selectMode = Matrix.of(context).shareContent != null + ? SelectMode.share + : _selectedRoomIds.isEmpty + ? SelectMode.normal + : SelectMode.select; + return Scaffold( + appBar: appBar ?? + AppBar( + leading: selectMode == SelectMode.normal + ? null + : IconButton( + icon: Icon(Icons.close_outlined), + onPressed: () => selectMode == SelectMode.share + ? setState( + () => Matrix.of(context).shareContent = null) + : setState(() => _selectedRoomIds.clear()), + ), + centerTitle: false, + actions: selectMode == SelectMode.share + ? null + : selectMode == SelectMode.select + ? [ + if (_selectedRoomIds.length == 1) + IconButton( + tooltip: L10n.of(context).toggleUnread, + icon: Icon(Matrix.of(context) + .client + .getRoomById(_selectedRoomIds.single) + .isUnread + ? Icons.mark_chat_read_outlined + : Icons.mark_chat_unread_outlined), + onPressed: () => _toggleUnread(context), + ), + if (_selectedRoomIds.length == 1) + IconButton( + tooltip: L10n.of(context).toggleFavorite, + icon: Icon(Icons.push_pin_outlined), + onPressed: () => + _toggleFavouriteRoom(context), + ), + if (_selectedRoomIds.length == 1) + IconButton( + icon: Icon(Matrix.of(context) + .client + .getRoomById( + _selectedRoomIds.single) + .pushRuleState == + PushRuleState.notify + ? Icons.notifications_off_outlined + : Icons.notifications_outlined), + tooltip: L10n.of(context).toggleMuted, + onPressed: () => _toggleMuted(context), + ), + IconButton( + icon: Icon(Icons.archive_outlined), + tooltip: L10n.of(context).archive, + onPressed: () => _archiveAction(context), + ), + ] + : [ + IconButton( + icon: Icon(Icons.search_outlined), + onPressed: () async { + await _scrollController.animateTo( + _scrollController.position.minScrollExtent, + duration: Duration(milliseconds: 200), + curve: Curves.ease, + ); + WidgetsBinding.instance.addPostFrameCallback( + (_) => _searchFieldKey.currentState + .requestFocus(), + ); + }, + ), + PopupMenuButton( + onSelected: _onPopupMenuButtonSelect, + itemBuilder: (_) => [ + PopupMenuItem( + value: ChatListPopupMenuItemActions + .createGroup, + child: Row( + children: [ + Icon(Icons.group_add_outlined), + SizedBox(width: 12), + Text(L10n.of(context).createNewGroup), + ], + ), + ), + PopupMenuItem( + value: + ChatListPopupMenuItemActions.discover, + child: Row( + children: [ + Icon(Icons.group_work_outlined), + SizedBox(width: 12), + Text(L10n.of(context).discoverGroups), + ], + ), + ), + PopupMenuItem( + value: + ChatListPopupMenuItemActions.setStatus, + child: Row( + children: [ + Icon(Icons.edit_outlined), + SizedBox(width: 12), + Text(L10n.of(context).setStatus), + ], + ), + ), + PopupMenuItem( + value: ChatListPopupMenuItemActions + .inviteContact, + child: Row( + children: [ + Icon(Icons.share_outlined), + SizedBox(width: 12), + Text(L10n.of(context).inviteContact), + ], + ), + ), + PopupMenuItem( + value: + ChatListPopupMenuItemActions.settings, + child: Row( + children: [ + Icon(Icons.settings_outlined), + SizedBox(width: 12), + Text(L10n.of(context).settings), + ], + ), + ), + ], + ), + ], + title: Text(selectMode == SelectMode.share + ? L10n.of(context).share + : selectMode == SelectMode.select + ? L10n.of(context).numberSelected( + _selectedRoomIds.length.toString()) + : AppConfig.applicationName), + ), + body: Column(children: [ + ConnectionStatusHeader(), + Expanded( + child: StreamBuilder( + stream: Matrix.of(context) + .client + .onSync + .stream + .where((s) => s.hasRoomUpdate), + builder: (context, snapshot) { + return FutureBuilder( + future: waitForFirstSync(context), + builder: (BuildContext context, snapshot) { + 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)) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + searchMode + ? Icons.search_outlined + : Icons.maps_ugc_outlined, + size: 80, + color: Colors.grey, + ), + Center( + child: Text( + searchMode + ? L10n.of(context).noRoomsFound + : L10n.of(context).startYourFirstChat, + textAlign: TextAlign.start, + style: TextStyle( + color: Colors.grey, + fontSize: 16, + ), + ), + ), + ], + ); + } + final totalCount = rooms.length; + return ListView.builder( + controller: _scrollController, + itemCount: totalCount + 1, + itemBuilder: (BuildContext context, int i) => i == + 0 + ? Column( + mainAxisSize: MainAxisSize.min, + children: [ + 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, + ), + ), + if (selectMode == SelectMode.normal) + Padding( + padding: + const EdgeInsets.only(top: 4.0), + child: HorizontalStoriesList( + searchQuery: + searchController.text, + ), + ), + ], + ) + : 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, + ), + ); + } else { + return Center( + child: CircularProgressIndicator(), + ); + } + }, + ); + }), + ), + ]), + floatingActionButton: FloatingActionButton( + child: Icon(Icons.add_outlined), + onPressed: () => AdaptivePageLayout.of(context) + .pushNamedAndRemoveUntilIsFirst('/newprivatechat'), + ), + ); + }); + } +} + +enum ChatListPopupMenuItemActions { + createGroup, + discover, + setStatus, + inviteContact, + settings, +} diff --git a/lib/views/discover.dart b/lib/views/discover.dart new file mode 100644 index 00000000..4f8ed2c8 --- /dev/null +++ b/lib/views/discover.dart @@ -0,0 +1,289 @@ +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/default_app_bar_search_field.dart'; +import 'package:future_loading_dialog/future_loading_dialog.dart'; +import 'package:fluffychat/components/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import '../utils/localized_exception_extension.dart'; + +class Discover extends StatefulWidget { + final String alias; + + const Discover({ + Key key, + this.alias, + }) : super(key: key); + @override + _DiscoverState createState() => _DiscoverState(); +} + +class _DiscoverState extends State { + Future _publicRoomsResponse; + String _lastServer; + Timer _coolDown; + String _genericSearchTerm; + + void _search(BuildContext context, String query) async { + _coolDown?.cancel(); + _coolDown = Timer( + Duration(milliseconds: 500), + () => setState(() { + _genericSearchTerm = query; + _publicRoomsResponse = null; + }), + ); + } + + 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, + ) == + 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, + textFields: [ + DialogTextField( + prefixText: 'https://', + hintText: Matrix.of(context).client.homeserver.host, + initialText: _server, + keyboardType: TextInputType.url, + ) + ]); + if (newServer == null) return; + setState(() { + _server = newServer.single; + }); + } + + @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; + }); + return Scaffold( + appBar: AppBar( + title: Text(L10n.of(context).discoverGroups), + actions: [ + FlatButton( + child: Text( + server ?? Matrix.of(context).client.userID.domain, + style: TextStyle(color: Theme.of(context).primaryColor), + ), + onPressed: () => _setServer(context), + ), + ], + ), + body: ListView( + children: [ + Padding( + padding: EdgeInsets.all(12), + child: DefaultAppBarSearchField( + hintText: L10n.of(context).search, + prefixIcon: Icon(Icons.search_outlined), + onChanged: (t) => _search(context, t), + padding: EdgeInsets.zero, + ), + ), + 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, + ), + ], + ), + ), + ), + ), + ); + }), + ], + ), + ); + } +} diff --git a/lib/views/home_view.dart b/lib/views/home_view.dart deleted file mode 100644 index 1067c902..00000000 --- a/lib/views/home_view.dart +++ /dev/null @@ -1,291 +0,0 @@ -import 'dart:async'; -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/views/home_view_parts/discover.dart'; -import 'package:fluffychat/views/share_view.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:fluffychat/app_config.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; -import 'package:receive_sharing_intent/receive_sharing_intent.dart'; -import '../components/matrix.dart'; -import '../utils/matrix_file_extension.dart'; -import '../utils/url_launcher.dart'; -import 'home_view_parts/chat_list.dart'; -import 'home_view_parts/settings.dart'; -import 'home_view_parts/contact_list.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; - -enum SelectMode { normal, share, select } - -class HomeView extends StatefulWidget { - final String activeChat; - - const HomeView({this.activeChat, Key key}) : super(key: key); - - @override - _HomeViewState createState() => _HomeViewState(); -} - -class _HomeViewState extends State with TickerProviderStateMixin { - @override - void initState() { - _initReceiveSharingIntent(); - _pageController = TabController(length: 4, vsync: this, initialIndex: 1); - _pageController.addListener(_updateCurrentIndex); - super.initState(); - } - - void _updateCurrentIndex() => - setState(() => currentIndex = _pageController.index); - - int currentIndex = 1; - - StreamSubscription _intentDataStreamSubscription; - - StreamSubscription _intentFileStreamSubscription; - - StreamSubscription _onShareContentChanged; - - AppBar appBar; - - TabController _pageController; - - void _onShare(Map content) { - if (content != null) { - WidgetsBinding.instance.addPostFrameCallback( - (_) => Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => ShareView(), - ), - ), - ); - } - } - - void _processIncomingSharedFiles(List files) { - if (files?.isEmpty ?? true) return; - AdaptivePageLayout.of(context).popUntilIsFirst(); - final file = File(files.first.path); - - Matrix.of(context).shareContent = { - 'msgtype': 'chat.fluffy.shared_file', - 'file': MatrixFile( - bytes: file.readAsBytesSync(), - name: file.path, - ).detectFileType, - }; - } - - void _processIncomingSharedText(String text) { - if (text == null) return; - AdaptivePageLayout.of(context).popUntilIsFirst(); - if (text.toLowerCase().startsWith(AppConfig.inviteLinkPrefix) || - (text.toLowerCase().startsWith(AppConfig.schemePrefix) && - !RegExp(r'\s').hasMatch(text))) { - UrlLauncher(context, text).openMatrixToUrl(); - return; - } - Matrix.of(context).shareContent = { - 'msgtype': 'm.text', - 'body': text, - }; - } - - void _initReceiveSharingIntent() { - if (!PlatformInfos.isMobile) return; - - // For sharing images coming from outside the app while the app is in the memory - _intentFileStreamSubscription = ReceiveSharingIntent.getMediaStream() - .listen(_processIncomingSharedFiles, onError: print); - - // For sharing images coming from outside the app while the app is closed - ReceiveSharingIntent.getInitialMedia().then(_processIncomingSharedFiles); - - // For sharing or opening urls/text coming from outside the app while the app is in the memory - _intentDataStreamSubscription = ReceiveSharingIntent.getTextStream() - .listen(_processIncomingSharedText, onError: print); - - // For sharing or opening urls/text coming from outside the app while the app is closed - ReceiveSharingIntent.getInitialText().then(_processIncomingSharedText); - } - - @override - void dispose() { - _intentDataStreamSubscription?.cancel(); - _intentFileStreamSubscription?.cancel(); - _pageController.removeListener(_updateCurrentIndex); - super.dispose(); - } - - String _server; - - void _setServer(BuildContext context) async { - final newServer = await showTextInputDialog( - title: L10n.of(context).changeTheHomeserver, - context: context, - textFields: [ - DialogTextField( - prefixText: 'https://', - hintText: Matrix.of(context).client.homeserver.host, - initialText: _server, - keyboardType: TextInputType.url, - ) - ]); - if (newServer == null) return; - setState(() { - _server = newServer.single; - }); - } - - void _setStatus() async { - final input = await showTextInputDialog( - context: context, - title: L10n.of(context).setStatus, - 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, - ), - ); - } - - void _onFabTab() { - switch (currentIndex) { - case 0: - _setStatus(); - break; - case 1: - AdaptivePageLayout.of(context) - .pushNamedAndRemoveUntilIsFirst('/newprivatechat'); - break; - case 2: - _setServer(context); - break; - } - } - - 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; - } - - return Scaffold( - appBar: appBar ?? - AppBar( - centerTitle: false, - actions: [ - IconButton( - icon: Icon(currentIndex == 3 - ? Icons.exit_to_app_outlined - : Icons.search_outlined), - onPressed: () => _pageController.indexIsChanging - ? null - : _onAppBarButtonTap.add(currentIndex), - ), - ], - title: Text(title), - ), - body: Column( - children: [ - Expanded( - child: TabBarView( - controller: _pageController, - children: [ - ContactList(onAppBarButtonTap: _onAppBarButtonTap.stream), - ChatList( - onCustomAppBar: (appBar) => - setState(() => this.appBar = appBar), - onAppBarButtonTap: _onAppBarButtonTap.stream, - ), - Discover( - server: _server, - onAppBarButtonTap: _onAppBarButtonTap.stream), - Settings(onAppBarButtonTap: _onAppBarButtonTap.stream), - ], - ), - ), - Divider(height: 1), - ], - ), - floatingActionButton: fabIcon == null - ? null - : FloatingActionButton( - child: Icon(fabIcon), - onPressed: _onFabTab, - foregroundColor: - currentIndex == 2 ? Theme.of(context).accentColor : null, - backgroundColor: currentIndex == 2 - ? Theme.of(context).scaffoldBackgroundColor - : null, - ), - floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, - bottomNavigationBar: BottomNavigationBar( - elevation: 0, - unselectedItemColor: Theme.of(context).textTheme.bodyText1.color, - currentIndex: currentIndex, - showSelectedLabels: true, - showUnselectedLabels: false, - type: BottomNavigationBarType.fixed, - backgroundColor: Theme.of(context).appBarTheme.color, - onTap: (i) { - _pageController.animateTo(i); - setState(() => currentIndex = i); - }, - items: [ - BottomNavigationBarItem( - label: L10n.of(context).contacts, - icon: Icon(Icons.people_outlined), - ), - BottomNavigationBarItem( - label: L10n.of(context).messages, - icon: Icon(CupertinoIcons.chat_bubble_2), - ), - BottomNavigationBarItem( - label: L10n.of(context).discover, - icon: Icon(CupertinoIcons.compass), - ), - BottomNavigationBarItem( - label: L10n.of(context).settings, - icon: Icon(Icons.settings_outlined), - ), - ], - ), - ); - } -} diff --git a/lib/views/home_view_parts/chat_list.dart b/lib/views/home_view_parts/chat_list.dart deleted file mode 100644 index 9eb38c1c..00000000 --- a/lib/views/home_view_parts/chat_list.dart +++ /dev/null @@ -1,273 +0,0 @@ -import 'dart:async'; - -import 'package:adaptive_dialog/adaptive_dialog.dart'; -import 'package:famedlysdk/famedlysdk.dart'; -import 'package:fluffychat/components/connection_status_header.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'; - -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(); -} - -class _ChatListState extends State { - bool get searchMode => searchController.text?.isNotEmpty ?? false; - 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) - : _selectedRoomIds.add(roomId)); - widget.onCustomAppBar( - _selectedRoomIds.isEmpty - ? null - : AppBar( - centerTitle: false, - leading: IconButton( - icon: Icon(Icons.close_outlined), - onPressed: () { - _selectedRoomIds.clear(); - widget.onCustomAppBar(null); - }, - ), - title: Text( - L10n.of(context) - .numberSelected(_selectedRoomIds.length.toString()), - ), - actions: [ - if (_selectedRoomIds.length == 1) - IconButton( - tooltip: L10n.of(context).toggleUnread, - icon: Icon(Matrix.of(context) - .client - .getRoomById(_selectedRoomIds.single) - .isUnread - ? Icons.mark_chat_read_outlined - : Icons.mark_chat_unread_outlined), - onPressed: () => _toggleUnread(context), - ), - if (_selectedRoomIds.length == 1) - IconButton( - tooltip: L10n.of(context).toggleFavorite, - icon: Icon(Icons.push_pin_outlined), - onPressed: () => _toggleFavouriteRoom(context), - ), - if (_selectedRoomIds.length == 1) - IconButton( - icon: Icon(Matrix.of(context) - .client - .getRoomById(_selectedRoomIds.single) - .pushRuleState == - PushRuleState.notify - ? Icons.notifications_off_outlined - : Icons.notifications_outlined), - tooltip: L10n.of(context).toggleMuted, - onPressed: () => _toggleMuted(context), - ), - IconButton( - icon: Icon(Icons.archive_outlined), - tooltip: L10n.of(context).archive, - onPressed: () => _archiveAction(context), - ), - ], - ), - ); - } - - Future _toggleUnread(BuildContext context) { - final room = Matrix.of(context).client.getRoomById(_selectedRoomIds.single); - return showFutureLoadingDialog( - context: context, - future: () => room.setUnread(!room.isUnread), - ); - } - - Future _toggleFavouriteRoom(BuildContext context) { - final room = Matrix.of(context).client.getRoomById(_selectedRoomIds.single); - return showFutureLoadingDialog( - context: context, - future: () => room.setFavourite(!room.isFavourite), - ); - } - - Future _toggleMuted(BuildContext context) { - final room = Matrix.of(context).client.getRoomById(_selectedRoomIds.single); - return showFutureLoadingDialog( - context: context, - future: () => room.setPushRuleState( - room.pushRuleState == PushRuleState.notify - ? PushRuleState.mentions_only - : PushRuleState.notify), - ); - } - - Future _archiveAction(BuildContext context) async { - final confirmed = await showOkCancelAlertDialog( - context: context, - title: L10n.of(context).areYouSure, - ) == - OkCancelResult.ok; - if (!confirmed) return; - await showFutureLoadingDialog( - context: context, - future: () => _archiveSelectedRooms(context), - ); - setState(() => null); - } - - Future _archiveSelectedRooms(BuildContext context) async { - final client = Matrix.of(context).client; - while (_selectedRoomIds.isNotEmpty) { - final roomId = _selectedRoomIds.first; - await client.getRoomById(roomId).leave(); - _toggleSelection(roomId); - } - } - - Future waitForFirstSync(BuildContext context) async { - var client = Matrix.of(context).client; - if (client.prevBatch?.isEmpty ?? true) { - await client.onFirstSync.stream.first; - } - return true; - } - - @override - Widget build(BuildContext context) { - final selectMode = - _selectedRoomIds.isEmpty ? SelectMode.normal : SelectMode.select; - return Column(children: [ - ConnectionStatusHeader(), - Expanded( - child: StreamBuilder( - stream: Matrix.of(context) - .client - .onSync - .stream - .where((s) => s.hasRoomUpdate), - builder: (context, snapshot) { - return FutureBuilder( - future: waitForFirstSync(context), - builder: (BuildContext context, snapshot) { - 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)) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - searchMode - ? Icons.search_outlined - : Icons.maps_ugc_outlined, - size: 80, - color: Colors.grey, - ), - Center( - child: Text( - searchMode - ? L10n.of(context).noRoomsFound - : L10n.of(context).startYourFirstChat, - textAlign: TextAlign.start, - style: TextStyle( - color: Colors.grey, - fontSize: 16, - ), - ), - ), - ], - ); - } - final totalCount = rooms.length; - return ListView.builder( - controller: _scrollController, - itemCount: totalCount + 1, - padding: EdgeInsets.only(bottom: 24), - 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, - onChanged: (_) => setState(() => null), - padding: EdgeInsets.zero, - ), - ) - : ChatListItem( - rooms[i - 1], - selected: - _selectedRoomIds.contains(rooms[i - 1].id), - onTap: selectMode == SelectMode.select && - widget.onCustomAppBar != null - ? () => _toggleSelection(rooms[i - 1].id) - : null, - onLongPress: widget.onCustomAppBar != null - ? () => _toggleSelection(rooms[i - 1].id) - : null, - activeChat: widget.activeChat == rooms[i - 1].id, - ), - ); - } else { - return Center( - child: CircularProgressIndicator(), - ); - } - }, - ); - }), - ), - ]); - } -} diff --git a/lib/views/home_view_parts/contact_list.dart b/lib/views/home_view_parts/contact_list.dart deleted file mode 100644 index 78c28acc..00000000 --- a/lib/views/home_view_parts/contact_list.dart +++ /dev/null @@ -1,154 +0,0 @@ -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'; -import 'package:fluffychat/components/list_items/contact_list_tile.dart'; -import 'package:fluffychat/components/matrix.dart'; -import 'package:fluffychat/utils/fluffy_share.dart'; -import 'package:flutter/material.dart'; -import '../../app_config.dart'; -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; - StreamSubscription _onSync; - 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() { - _onSync?.cancel(); - _onAppBarButtonTapSub?.cancel(); - super.dispose(); - } - - DateTime _lastSetState = DateTime.now(); - Timer _coolDown; - - void _updateView() { - _lastSetState = DateTime.now(); - setState(() => null); - } - - @override - Widget build(BuildContext context) { - _onSync ??= Matrix.of(context).client.onSync.stream.listen((_) { - if (DateTime.now().millisecondsSinceEpoch - - _lastSetState.millisecondsSinceEpoch < - 1000) { - _coolDown?.cancel(); - _coolDown = Timer(Duration(seconds: 1), _updateView); - } else { - _updateView(); - } - }); - 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, - ), - title: Text(L10n.of(context).addNewContact), - onTap: () => - AdaptivePageLayout.of(context).pushNamed('/newprivatechat'), - ), - Divider(height: 1), - Builder(builder: (context) { - final contactList = Matrix.of(context) - .client - .contactList - .where((p) => - p.senderId.toLowerCase().contains(_searchQuery.toLowerCase())) - .toList(); - if (contactList.isEmpty) { - return Column( - children: [ - SizedBox(height: 32), - Icon( - Icons.people_outlined, - size: 80, - color: Colors.grey, - ), - RaisedButton( - elevation: 7, - color: Theme.of(context).primaryColor, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppConfig.borderRadius), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.share_outlined, color: Colors.white), - SizedBox(width: 16), - Text( - L10n.of(context).inviteContact, - style: TextStyle( - color: Colors.white, - fontSize: 16, - ), - ), - ], - ), - onPressed: () => FluffyShare.share( - L10n.of(context).inviteText( - Matrix.of(context).client.userID, - 'https://matrix.to/#/${Matrix.of(context).client.userID}'), - context), - ), - ], - ); - } - 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 deleted file mode 100644 index bf5869b3..00000000 --- a/lib/views/home_view_parts/discover.dart +++ /dev/null @@ -1,281 +0,0 @@ -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/default_app_bar_search_field.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; -import 'package:fluffychat/components/matrix.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import '../../utils/localized_exception_extension.dart'; - -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(); -} - -class _DiscoverState extends State { - Future _publicRoomsResponse; - String _lastServer; - 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( - Duration(milliseconds: 500), - () => setState(() { - _genericSearchTerm = query; - _publicRoomsResponse = null; - }), - ); - } - - 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, - ) == - 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}'); - } - } - - @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(); - } - - @override - Widget build(BuildContext context) { - final server = _genericSearchTerm?.isValidMatrixId ?? false - ? _genericSearchTerm.domain - : widget.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; - }); - 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), - padding: EdgeInsets.zero, - ), - ), - 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, - ), - ], - ), - ), - ), - ), - ); - }), - ], - ); - } -} diff --git a/lib/views/home_view_parts/settings.dart b/lib/views/settings.dart similarity index 54% rename from lib/views/home_view_parts/settings.dart rename to lib/views/settings.dart index 278ad799..50ed5d24 100644 --- a/lib/views/home_view_parts/settings.dart +++ b/lib/views/settings.dart @@ -21,16 +21,13 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:image_picker/image_picker.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../../components/content_banner.dart'; +import '../components/content_banner.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; -import '../../components/matrix.dart'; -import '../../app_config.dart'; -import '../../config/setting_keys.dart'; +import '../components/matrix.dart'; +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(); } @@ -42,21 +39,6 @@ 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( @@ -343,202 +325,220 @@ class _SettingsState extends State { return c; }); } - return ListView( - children: [ - ContentBanner( - profile?.avatarUrl, - height: 200, - opacity: 1, - defaultIcon: Icons.account_circle_outlined, - loading: profile == null, - onEdit: () => setAvatarAction(context), - ), - ListTile( - title: Text( - L10n.of(context).notifications, - style: TextStyle( - color: Theme.of(context).accentColor, - fontWeight: FontWeight.bold, + return Scaffold( + body: NestedScrollView( + headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) => + [ + SliverAppBar( + elevation: Theme.of(context).appBarTheme.elevation, + leading: BackButton(), + expandedHeight: 300.0, + floating: true, + pinned: true, + title: Text(L10n.of(context).settings, + style: TextStyle( + color: Theme.of(context) + .appBarTheme + .textTheme + .headline6 + .color)), + backgroundColor: Theme.of(context).appBarTheme.color, + flexibleSpace: FlexibleSpaceBar( + background: ContentBanner(profile?.avatarUrl, + onEdit: () => setAvatarAction(context)), ), ), - ), - ListTile( - trailing: Icon(Icons.notifications_outlined), - title: Text(L10n.of(context).notifications), - onTap: () => AdaptivePageLayout.of(context) - .pushNamed('/settings/notifications'), - ), - Divider(thickness: 1), - ListTile( - title: Text( - L10n.of(context).chat, - style: TextStyle( - color: Theme.of(context).accentColor, - fontWeight: FontWeight.bold, - ), - ), - ), - ListTile( - title: Text(L10n.of(context).changeTheme), - onTap: () => - AdaptivePageLayout.of(context).pushNamed('/settings/style'), - trailing: Icon(Icons.style_outlined), - ), - SettingsSwitchListTile( - title: L10n.of(context).renderRichContent, - onChanged: (b) => AppConfig.renderHtml = b, - storeKey: SettingKeys.renderHtml, - defaultValue: AppConfig.renderHtml, - ), - SettingsSwitchListTile( - title: L10n.of(context).hideRedactedEvents, - onChanged: (b) => AppConfig.hideRedactedEvents = b, - storeKey: SettingKeys.hideRedactedEvents, - defaultValue: AppConfig.hideRedactedEvents, - ), - SettingsSwitchListTile( - title: L10n.of(context).hideUnknownEvents, - onChanged: (b) => AppConfig.hideUnknownEvents = b, - storeKey: SettingKeys.hideUnknownEvents, - defaultValue: AppConfig.hideUnknownEvents, - ), - ListTile( - title: Text(L10n.of(context).emoteSettings), - onTap: () => - AdaptivePageLayout.of(context).pushNamed('/settings/emotes'), - trailing: Icon(Icons.insert_emoticon_outlined), - ), - ListTile( - title: Text(L10n.of(context).archive), - onTap: () => AdaptivePageLayout.of(context).pushNamed('/archive'), - trailing: Icon(Icons.archive_outlined), - ), - Divider(thickness: 1), - ListTile( - title: Text( - L10n.of(context).account, - style: TextStyle( - color: Theme.of(context).accentColor, - fontWeight: FontWeight.bold, - ), - ), - ), - ListTile( - trailing: Icon(Icons.edit_outlined), - title: Text(L10n.of(context).editDisplayname), - subtitle: Text(profile?.displayname ?? client.userID.localpart), - onTap: () => setDisplaynameAction(context), - ), - ListTile( - trailing: Icon(Icons.phone_outlined), - title: Text(L10n.of(context).editJitsiInstance), - subtitle: Text(AppConfig.jitsiInstance), - onTap: () => setJitsiInstanceAction(context), - ), - ListTile( - trailing: Icon(Icons.devices_other_outlined), - title: Text(L10n.of(context).devices), - onTap: () => - AdaptivePageLayout.of(context).pushNamed('/settings/devices'), - ), - ListTile( - trailing: Icon(Icons.block_outlined), - title: Text(L10n.of(context).ignoredUsers), - onTap: () => - AdaptivePageLayout.of(context).pushNamed('/settings/ignore'), - ), - SentrySwitchListTile(), - Divider(thickness: 1), - ListTile( - trailing: Icon(Icons.security_outlined), - title: Text( - L10n.of(context).changePassword, - ), - onTap: () => _changePasswordAccountAction(context), - ), - ListTile( - trailing: Icon(Icons.email_outlined), - title: Text(L10n.of(context).passwordRecovery), - onTap: () => - AdaptivePageLayout.of(context).pushNamed('/settings/3pid'), - ), - ListTile( - trailing: Icon(Icons.exit_to_app_outlined), - title: Text(L10n.of(context).logout), - onTap: () => logoutAction(context), - ), - ListTile( - trailing: Icon(Icons.delete_forever_outlined), - title: Text( - L10n.of(context).deleteAccount, - style: TextStyle(color: Colors.red), - ), - onTap: () => _deleteAccountAction(context), - ), - if (client.encryption != null) ...{ - Divider(thickness: 1), - ListTile( - title: Text( - L10n.of(context).security, - style: TextStyle( - color: Theme.of(context).accentColor, - fontWeight: FontWeight.bold, + ], + body: ListView( + children: [ + ListTile( + title: Text( + L10n.of(context).notifications, + style: TextStyle( + color: Theme.of(context).accentColor, + fontWeight: FontWeight.bold, + ), ), ), - ), - if (PlatformInfos.isMobile) ListTile( - trailing: Icon(Icons.lock_outlined), - title: Text(L10n.of(context).appLock), - onTap: () => _setAppLockAction(context), + trailing: Icon(Icons.notifications_outlined), + title: Text(L10n.of(context).notifications), + onTap: () => AdaptivePageLayout.of(context) + .pushNamed('/settings/notifications'), ), - ListTile( - title: Text(L10n.of(context).yourPublicKey), - onTap: () => showOkAlertDialog( - context: context, - title: L10n.of(context).yourPublicKey, - message: client.fingerprintKey.beautified, + Divider(thickness: 1), + ListTile( + title: Text( + L10n.of(context).chat, + style: TextStyle( + color: Theme.of(context).accentColor, + fontWeight: FontWeight.bold, + ), + ), ), - trailing: Icon(Icons.vpn_key_outlined), - ), - ListTile( - title: Text(L10n.of(context).cachedKeys), - trailing: Icon(Icons.wb_cloudy_outlined), - subtitle: Text( - '${client.encryption.keyManager.enabled ? L10n.of(context).onlineKeyBackupEnabled : L10n.of(context).onlineKeyBackupDisabled}\n${client.encryption.crossSigning.enabled ? L10n.of(context).crossSigningEnabled : L10n.of(context).crossSigningDisabled}'), - onTap: () => BootstrapDialog( - l10n: L10n.of(context), - client: Matrix.of(context).client, - ).show(context), - ), - }, - Divider(thickness: 1), - ListTile( - title: Text( - L10n.of(context).about, - style: TextStyle( - color: Theme.of(context).accentColor, - fontWeight: FontWeight.bold, + ListTile( + title: Text(L10n.of(context).changeTheme), + onTap: () => + AdaptivePageLayout.of(context).pushNamed('/settings/style'), + trailing: Icon(Icons.style_outlined), ), - ), - onTap: () => AdaptivePageLayout.of(context).pushNamed('/logs'), + SettingsSwitchListTile( + title: L10n.of(context).renderRichContent, + onChanged: (b) => AppConfig.renderHtml = b, + storeKey: SettingKeys.renderHtml, + defaultValue: AppConfig.renderHtml, + ), + SettingsSwitchListTile( + title: L10n.of(context).hideRedactedEvents, + onChanged: (b) => AppConfig.hideRedactedEvents = b, + storeKey: SettingKeys.hideRedactedEvents, + defaultValue: AppConfig.hideRedactedEvents, + ), + SettingsSwitchListTile( + title: L10n.of(context).hideUnknownEvents, + onChanged: (b) => AppConfig.hideUnknownEvents = b, + storeKey: SettingKeys.hideUnknownEvents, + defaultValue: AppConfig.hideUnknownEvents, + ), + ListTile( + title: Text(L10n.of(context).emoteSettings), + onTap: () => + AdaptivePageLayout.of(context).pushNamed('/settings/emotes'), + trailing: Icon(Icons.insert_emoticon_outlined), + ), + ListTile( + title: Text(L10n.of(context).archive), + onTap: () => AdaptivePageLayout.of(context).pushNamed('/archive'), + trailing: Icon(Icons.archive_outlined), + ), + Divider(thickness: 1), + ListTile( + title: Text( + L10n.of(context).account, + style: TextStyle( + color: Theme.of(context).accentColor, + fontWeight: FontWeight.bold, + ), + ), + ), + ListTile( + trailing: Icon(Icons.edit_outlined), + title: Text(L10n.of(context).editDisplayname), + subtitle: Text(profile?.displayname ?? client.userID.localpart), + onTap: () => setDisplaynameAction(context), + ), + ListTile( + trailing: Icon(Icons.phone_outlined), + title: Text(L10n.of(context).editJitsiInstance), + subtitle: Text(AppConfig.jitsiInstance), + onTap: () => setJitsiInstanceAction(context), + ), + ListTile( + trailing: Icon(Icons.devices_other_outlined), + title: Text(L10n.of(context).devices), + onTap: () => + AdaptivePageLayout.of(context).pushNamed('/settings/devices'), + ), + ListTile( + trailing: Icon(Icons.block_outlined), + title: Text(L10n.of(context).ignoredUsers), + onTap: () => + AdaptivePageLayout.of(context).pushNamed('/settings/ignore'), + ), + SentrySwitchListTile(), + Divider(thickness: 1), + ListTile( + trailing: Icon(Icons.security_outlined), + title: Text( + L10n.of(context).changePassword, + ), + onTap: () => _changePasswordAccountAction(context), + ), + ListTile( + trailing: Icon(Icons.email_outlined), + title: Text(L10n.of(context).passwordRecovery), + onTap: () => + AdaptivePageLayout.of(context).pushNamed('/settings/3pid'), + ), + ListTile( + trailing: Icon(Icons.exit_to_app_outlined), + title: Text(L10n.of(context).logout), + onTap: () => logoutAction(context), + ), + ListTile( + trailing: Icon(Icons.delete_forever_outlined), + title: Text( + L10n.of(context).deleteAccount, + style: TextStyle(color: Colors.red), + ), + onTap: () => _deleteAccountAction(context), + ), + if (client.encryption != null) ...{ + Divider(thickness: 1), + ListTile( + title: Text( + L10n.of(context).security, + style: TextStyle( + color: Theme.of(context).accentColor, + fontWeight: FontWeight.bold, + ), + ), + ), + if (PlatformInfos.isMobile) + ListTile( + trailing: Icon(Icons.lock_outlined), + title: Text(L10n.of(context).appLock), + onTap: () => _setAppLockAction(context), + ), + ListTile( + title: Text(L10n.of(context).yourPublicKey), + onTap: () => showOkAlertDialog( + context: context, + title: L10n.of(context).yourPublicKey, + message: client.fingerprintKey.beautified, + ), + trailing: Icon(Icons.vpn_key_outlined), + ), + ListTile( + title: Text(L10n.of(context).cachedKeys), + trailing: Icon(Icons.wb_cloudy_outlined), + subtitle: Text( + '${client.encryption.keyManager.enabled ? L10n.of(context).onlineKeyBackupEnabled : L10n.of(context).onlineKeyBackupDisabled}\n${client.encryption.crossSigning.enabled ? L10n.of(context).crossSigningEnabled : L10n.of(context).crossSigningDisabled}'), + onTap: () => BootstrapDialog( + l10n: L10n.of(context), + client: Matrix.of(context).client, + ).show(context), + ), + }, + Divider(thickness: 1), + ListTile( + title: Text( + L10n.of(context).about, + style: TextStyle( + color: Theme.of(context).accentColor, + fontWeight: FontWeight.bold, + ), + ), + onTap: () => AdaptivePageLayout.of(context).pushNamed('/logs'), + ), + ListTile( + trailing: Icon(Icons.help_outlined), + title: Text(L10n.of(context).help), + onTap: () => launch(AppConfig.supportUrl), + ), + ListTile( + trailing: Icon(Icons.privacy_tip_outlined), + title: Text(L10n.of(context).privacy), + onTap: () => launch(AppConfig.privacyUrl), + ), + ListTile( + trailing: Icon(Icons.link_outlined), + title: Text(L10n.of(context).about), + onTap: () => PlatformInfos.showDialog(context), + ), + ], ), - ListTile( - trailing: Icon(Icons.help_outlined), - title: Text(L10n.of(context).help), - onTap: () => launch(AppConfig.supportUrl), - ), - ListTile( - trailing: Icon(Icons.privacy_tip_outlined), - title: Text(L10n.of(context).privacy), - onTap: () => launch(AppConfig.privacyUrl), - ), - ListTile( - trailing: Icon(Icons.link_outlined), - title: Text(L10n.of(context).about), - onTap: () => PlatformInfos.showDialog(context), - ), - ], + ), ); } } diff --git a/lib/views/share_view.dart b/lib/views/share_view.dart deleted file mode 100644 index 9656a5de..00000000 --- a/lib/views/share_view.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'dart:async'; - -import 'package:adaptive_page_layout/adaptive_page_layout.dart'; -import 'package:fluffychat/components/matrix.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; - -import 'home_view_parts/chat_list.dart'; - -class ShareView extends StatelessWidget { - final StreamController _onAppBarButtonTap = - StreamController.broadcast(); - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - leading: IconButton( - icon: Icon(Icons.close_outlined), - onPressed: () { - Matrix.of(context).shareContent = null; - AdaptivePageLayout.of(context).pop(); - }, - ), - title: Text(L10n.of(context).share), - actions: [ - IconButton( - icon: Icon(Icons.search_outlined), - onPressed: () => _onAppBarButtonTap.add(1), - ), - ], - ), - body: ChatList(onAppBarButtonTap: _onAppBarButtonTap.stream), - ); - } -}