From cd9b4ee46bd45cf6e9d0eb1bbb3aedd36b9a8ee5 Mon Sep 17 00:00:00 2001 From: Christian Pauly Date: Thu, 15 Apr 2021 13:03:14 +0200 Subject: [PATCH] refactor: Folder structure and MVC chat ui --- CODE_STYLE.md | 39 +- lib/config/routes.dart | 44 +- lib/controllers/chat_details_controller.dart | 223 ---- lib/utils/event_extension.dart | 2 +- .../archive.dart} | 4 +- lib/views/chat.dart | 1065 +++-------------- lib/views/chat_details.dart | 558 ++++----- .../chat_encryption_settings.dart} | 6 +- .../chat_list.dart} | 6 +- .../chat_permissions_settings.dart} | 4 +- .../device_settings.dart} | 6 +- .../homeserver_picker.dart} | 4 +- .../image_viewer.dart} | 4 +- .../invitation_selection.dart} | 4 +- .../new_group.dart} | 4 +- .../new_private_chat.dart} | 4 +- .../sign_up.dart} | 4 +- .../sign_up_password.dart} | 4 +- .../{archive_view.dart => ui/archive_ui.dart} | 6 +- lib/views/ui/chat_details_ui.dart | 369 ++++++ .../chat_encryption_settings_ui.dart} | 9 +- .../chat_list_ui.dart} | 8 +- .../chat_permissions_settings_ui.dart} | 7 +- lib/views/ui/chat_ui.dart | 747 ++++++++++++ .../device_settings_ui.dart} | 8 +- .../empty_page_ui.dart} | 0 .../homeserver_picker_ui.dart} | 9 +- .../image_viewer_ui.dart} | 6 +- .../invitation_selection_ui.dart} | 9 +- lib/views/{login.dart => ui/login_ui.dart} | 2 +- .../new_group_ui.dart} | 9 +- .../new_private_chat_ui.dart} | 6 +- .../{search_view.dart => ui/search_ui.dart} | 2 +- .../settings_3pid_ui.dart} | 0 .../settings_emotes_ui.dart} | 2 +- .../settings_ignore_list_ui.dart} | 2 +- .../settings_multiple_emotes_ui.dart} | 0 .../settings_notifications_ui.dart} | 4 +- .../settings_style_ui.dart} | 4 +- .../{settings.dart => ui/settings_ui.dart} | 8 +- .../sign_up_password_ui.dart} | 9 +- .../{sign_up_view.dart => ui/sign_up_ui.dart} | 9 +- lib/views/widgets/image_bubble.dart | 2 +- lib/views/widgets/matrix.dart | 2 +- test/homeserver_picker_test.dart | 2 +- test/sign_up_password_test.dart | 2 +- test/sign_up_test.dart | 2 +- 47 files changed, 1648 insertions(+), 1582 deletions(-) delete mode 100644 lib/controllers/chat_details_controller.dart rename lib/{controllers/archive_controller.dart => views/archive.dart} (83%) rename lib/{controllers/chat_encryption_settings_controller.dart => views/chat_encryption_settings.dart} (89%) rename lib/{controllers/chat_list_controller.dart => views/chat_list.dart} (97%) rename lib/{controllers/chat_permissions_settings_controller.dart => views/chat_permissions_settings.dart} (95%) rename lib/{controllers/device_settings_controller.dart => views/device_settings.dart} (96%) rename lib/{controllers/homeserver_picker_controller.dart => views/homeserver_picker.dart} (97%) rename lib/{controllers/image_viewer_controller.dart => views/image_viewer.dart} (91%) rename lib/{controllers/invitation_selection_controller.dart => views/invitation_selection.dart} (96%) rename lib/{controllers/new_group_controller.dart => views/new_group.dart} (92%) rename lib/{controllers/new_private_chat_controller.dart => views/new_private_chat.dart} (96%) rename lib/{controllers/sign_up_controller.dart => views/sign_up.dart} (94%) rename lib/{controllers/sign_up_password_controller.dart => views/sign_up_password.dart} (97%) rename lib/views/{archive_view.dart => ui/archive_ui.dart} (87%) create mode 100644 lib/views/ui/chat_details_ui.dart rename lib/views/{chat_encryption_settings_view.dart => ui/chat_encryption_settings_ui.dart} (97%) rename lib/views/{chat_list_view.dart => ui/chat_list_ui.dart} (98%) rename lib/views/{chat_permissions_settings_view.dart => ui/chat_permissions_settings_ui.dart} (95%) create mode 100644 lib/views/ui/chat_ui.dart rename lib/views/{device_settings_view.dart => ui/device_settings_ui.dart} (93%) rename lib/views/{empty_page.dart => ui/empty_page_ui.dart} (100%) rename lib/views/{homeserver_picker_view.dart => ui/homeserver_picker_ui.dart} (94%) rename lib/views/{image_viewer_view.dart => ui/image_viewer_ui.dart} (90%) rename lib/views/{invitation_selection_view.dart => ui/invitation_selection_ui.dart} (93%) rename lib/views/{login.dart => ui/login_ui.dart} (99%) rename lib/views/{new_group_view.dart => ui/new_group_ui.dart} (89%) rename lib/views/{new_private_chat_view.dart => ui/new_private_chat_ui.dart} (96%) rename lib/views/{search_view.dart => ui/search_ui.dart} (99%) rename lib/views/{settings_3pid.dart => ui/settings_3pid_ui.dart} (100%) rename lib/views/{settings_emotes.dart => ui/settings_emotes_ui.dart} (99%) rename lib/views/{settings_ignore_list.dart => ui/settings_ignore_list_ui.dart} (99%) rename lib/views/{settings_multiple_emotes.dart => ui/settings_multiple_emotes_ui.dart} (100%) rename lib/views/{settings_notifications.dart => ui/settings_notifications_ui.dart} (98%) rename lib/views/{settings_style.dart => ui/settings_style_ui.dart} (98%) rename lib/views/{settings.dart => ui/settings_ui.dart} (99%) rename lib/views/{sign_up_password_view.dart => ui/sign_up_password_ui.dart} (91%) rename lib/views/{sign_up_view.dart => ui/sign_up_ui.dart} (95%) diff --git a/CODE_STYLE.md b/CODE_STYLE.md index 90fd86ff1..5e6728324 100644 --- a/CODE_STYLE.md +++ b/CODE_STYLE.md @@ -2,14 +2,37 @@ FluffyChat tries to be as minimal as possible even in the code style. We try to keep the code clean, simple and easy to read. The source code of the app is under `/lib` with the main entry point `/lib/main.dart`. -### Directory Structure - -- `/lib/config/` Constants, styles and other configurations -- `/lib/controllers/` Controller classes regarding the MVC separation -- `/lib/l10n/` Localization files wi -- `/lib/utils/` Helper functions and extensions -- `/lib/views/` View classes and widgets -- `/lib/views/widgets/` Reusable Flutter widgets +### Directory Structure: + + +- /lib + - /config + - app_config.dart + - ...Constants, styles and other configurations + - /l10n + - intl_en.arb + - ...Localization files + - /models + - app_model.dart + - ...Data models used in the app + - /utils + - handy_function.dart + - ...Helper functions and extensions + - /views + - /ui + - home_ui.dart + - details_ui.dart + - /widgets + - /dialogs + - /ui + - /list_items + - /ui + - /ui + - home_view.dart + - details_view.dart + - ...The views and widgets of the app separated in Controllers and Views + - main.dart + Most of the business model is in the Famedly Matrix Dart SDK. We try to not keep a model inside of the source code but extend it under `/utils`. diff --git a/lib/config/routes.dart b/lib/config/routes.dart index a5565fcb9..c19b5d9a8 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -1,31 +1,31 @@ import 'package:adaptive_page_layout/adaptive_page_layout.dart'; import 'package:famedlysdk/famedlysdk.dart'; -import 'package:fluffychat/controllers/archive_controller.dart'; -import 'package:fluffychat/controllers/homeserver_picker_controller.dart'; -import 'package:fluffychat/controllers/invitation_selection_controller.dart'; -import 'package:fluffychat/controllers/sign_up_controller.dart'; -import 'package:fluffychat/controllers/sign_up_password_controller.dart'; +import 'package:fluffychat/views/archive.dart'; +import 'package:fluffychat/views/homeserver_picker.dart'; +import 'package:fluffychat/views/invitation_selection.dart'; +import 'package:fluffychat/views/sign_up.dart'; +import 'package:fluffychat/views/sign_up_password.dart'; import 'package:fluffychat/views/widgets/matrix.dart'; import 'package:fluffychat/views/chat.dart'; -import 'package:fluffychat/controllers/chat_details_controller.dart'; -import 'package:fluffychat/controllers/chat_encryption_settings_controller.dart'; -import 'package:fluffychat/controllers/chat_list_controller.dart'; -import 'package:fluffychat/controllers/chat_permissions_settings_controller.dart'; -import 'package:fluffychat/views/empty_page.dart'; +import 'package:fluffychat/views/chat_details.dart'; +import 'package:fluffychat/views/chat_encryption_settings.dart'; +import 'package:fluffychat/views/chat_list.dart'; +import 'package:fluffychat/views/chat_permissions_settings.dart'; +import 'package:fluffychat/views/ui/empty_page_ui.dart'; import 'package:fluffychat/views/widgets/loading_view.dart'; import 'package:fluffychat/views/widgets/log_view.dart'; -import 'package:fluffychat/views/login.dart'; -import 'package:fluffychat/controllers/new_group_controller.dart'; -import 'package:fluffychat/controllers/new_private_chat_controller.dart'; -import 'package:fluffychat/views/search_view.dart'; -import 'package:fluffychat/views/settings.dart'; -import 'package:fluffychat/views/settings_3pid.dart'; -import 'package:fluffychat/controllers/device_settings_controller.dart'; -import 'package:fluffychat/views/settings_emotes.dart'; -import 'package:fluffychat/views/settings_ignore_list.dart'; -import 'package:fluffychat/views/settings_multiple_emotes.dart'; -import 'package:fluffychat/views/settings_notifications.dart'; -import 'package:fluffychat/views/settings_style.dart'; +import 'package:fluffychat/views/ui/login_ui.dart'; +import 'package:fluffychat/views/new_group.dart'; +import 'package:fluffychat/views/new_private_chat.dart'; +import 'package:fluffychat/views/ui/search_ui.dart'; +import 'package:fluffychat/views/ui/settings_ui.dart'; +import 'package:fluffychat/views/ui/settings_3pid_ui.dart'; +import 'package:fluffychat/views/device_settings.dart'; +import 'package:fluffychat/views/ui/settings_emotes_ui.dart'; +import 'package:fluffychat/views/ui/settings_ignore_list_ui.dart'; +import 'package:fluffychat/views/ui/settings_multiple_emotes_ui.dart'; +import 'package:fluffychat/views/ui/settings_notifications_ui.dart'; +import 'package:fluffychat/views/ui/settings_style_ui.dart'; import 'package:flutter/material.dart'; class FluffyRoutes { diff --git a/lib/controllers/chat_details_controller.dart b/lib/controllers/chat_details_controller.dart deleted file mode 100644 index 00cd7005c..000000000 --- a/lib/controllers/chat_details_controller.dart +++ /dev/null @@ -1,223 +0,0 @@ -import 'package:adaptive_dialog/adaptive_dialog.dart'; -import 'package:adaptive_page_layout/adaptive_page_layout.dart'; - -import 'package:famedlysdk/famedlysdk.dart'; - -import 'package:file_picker_cross/file_picker_cross.dart'; -import 'package:fluffychat/views/chat_details.dart'; -import 'package:fluffychat/views/widgets/matrix.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; -import 'package:fluffychat/utils/matrix_locals.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:image_picker/image_picker.dart'; - -class ChatDetails extends StatefulWidget { - final String roomId; - - const ChatDetails(this.roomId); - - @override - ChatDetailsController createState() => ChatDetailsController(); -} - -class ChatDetailsController extends State { - List members; - - @override - void initState() { - super.initState(); - members ??= - Matrix.of(context).client.getRoomById(widget.roomId).getParticipants(); - } - - void setDisplaynameAction() async { - final room = Matrix.of(context).client.getRoomById(widget.roomId); - final input = await showTextInputDialog( - context: context, - title: L10n.of(context).changeTheNameOfTheGroup, - okLabel: L10n.of(context).ok, - cancelLabel: L10n.of(context).cancel, - useRootNavigator: false, - textFields: [ - DialogTextField( - initialText: room.getLocalizedDisplayname( - MatrixLocals( - L10n.of(context), - ), - ), - ) - ], - ); - if (input == null) return; - final success = await showFutureLoadingDialog( - context: context, - future: () => room.setName(input.single), - ); - if (success.error == null) { - AdaptivePageLayout.of(context).showSnackBar( - SnackBar(content: Text(L10n.of(context).displaynameHasBeenChanged))); - } - } - - void setCanonicalAliasAction(context) async { - final input = await showTextInputDialog( - context: context, - title: L10n.of(context).setInvitationLink, - okLabel: L10n.of(context).ok, - cancelLabel: L10n.of(context).cancel, - useRootNavigator: false, - textFields: [ - DialogTextField( - hintText: '#localpart:domain', - initialText: L10n.of(context).alias.toLowerCase(), - ) - ], - ); - if (input == null) return; - final room = Matrix.of(context).client.getRoomById(widget.roomId); - final domain = room.client.userID.domain; - final canonicalAlias = '%23' + input.single + '%3A' + domain; - final aliasEvent = room.getState('m.room.aliases', domain); - final aliases = - aliasEvent != null ? aliasEvent.content['aliases'] ?? [] : []; - if (aliases.indexWhere((s) => s == canonicalAlias) == -1) { - final newAliases = List.from(aliases); - newAliases.add(canonicalAlias); - final response = await showFutureLoadingDialog( - context: context, - future: () => room.client.requestRoomAliasInformation(canonicalAlias), - ); - if (response.error != null) { - final success = await showFutureLoadingDialog( - context: context, - future: () => room.client.createRoomAlias(canonicalAlias, room.id), - ); - if (success.error != null) return; - } - } - await showFutureLoadingDialog( - context: context, - future: () => room.client.sendState(room.id, 'm.room.canonical_alias', { - 'alias': input.single, - }), - ); - } - - void setTopicAction() async { - final room = Matrix.of(context).client.getRoomById(widget.roomId); - final input = await showTextInputDialog( - context: context, - title: L10n.of(context).setGroupDescription, - okLabel: L10n.of(context).ok, - cancelLabel: L10n.of(context).cancel, - useRootNavigator: false, - textFields: [ - DialogTextField( - hintText: L10n.of(context).setGroupDescription, - initialText: room.topic, - minLines: 1, - maxLines: 4, - ) - ], - ); - if (input == null) return; - final success = await showFutureLoadingDialog( - context: context, - future: () => room.setDescription(input.single), - ); - if (success.error == null) { - AdaptivePageLayout.of(context).showSnackBar(SnackBar( - content: Text(L10n.of(context).groupDescriptionHasBeenChanged))); - } - } - - void setGuestAccessAction(GuestAccess guestAccess) => showFutureLoadingDialog( - context: context, - future: () => Matrix.of(context) - .client - .getRoomById(widget.roomId) - .setGuestAccess(guestAccess), - ); - - void setHistoryVisibilityAction(HistoryVisibility historyVisibility) => - showFutureLoadingDialog( - context: context, - future: () => Matrix.of(context) - .client - .getRoomById(widget.roomId) - .setHistoryVisibility(historyVisibility), - ); - - void setJoinRulesAction(JoinRules joinRule) => showFutureLoadingDialog( - context: context, - future: () => Matrix.of(context) - .client - .getRoomById(widget.roomId) - .setJoinRules(joinRule), - ); - - void goToEmoteSettings() async { - final room = Matrix.of(context).client.getRoomById(widget.roomId); - // okay, we need to test if there are any emote state events other than the default one - // if so, we need to be directed to a selection screen for which pack we want to look at - // otherwise, we just open the normal one. - if ((room.states['im.ponies.room_emotes'] ?? {}) - .keys - .any((String s) => s.isNotEmpty)) { - await AdaptivePageLayout.of(context) - .pushNamed('/rooms/${room.id}/emotes'); - } else { - await AdaptivePageLayout.of(context) - .pushNamed('/settings/emotes', arguments: {'room': room}); - } - } - - void setAvatarAction() async { - MatrixFile file; - if (PlatformInfos.isMobile) { - final result = await ImagePicker().getImage( - source: ImageSource.gallery, - imageQuality: 50, - maxWidth: 1600, - maxHeight: 1600); - if (result == null) return; - file = MatrixFile( - bytes: await result.readAsBytes(), - name: result.path, - ); - } else { - final result = await FilePickerCross.importFromStorage( - type: FileTypeCross.image, - ); - if (result == null) return; - file = MatrixFile( - bytes: result.toUint8List(), - name: result.fileName, - ); - } - final room = Matrix.of(context).client.getRoomById(widget.roomId); - - final success = await showFutureLoadingDialog( - context: context, - future: () => room.setAvatar(file), - ); - if (success.error == null) { - AdaptivePageLayout.of(context).showSnackBar( - SnackBar(content: Text(L10n.of(context).avatarHasBeenChanged))); - } - } - - void requestMoreMembersAction() async { - final room = Matrix.of(context).client.getRoomById(widget.roomId); - final participants = await showFutureLoadingDialog( - context: context, future: () => room.requestParticipants()); - if (participants.error == null) { - setState(() => members = participants.result); - } - } - - @override - Widget build(BuildContext context) => ChatDetailsView(this); -} diff --git a/lib/utils/event_extension.dart b/lib/utils/event_extension.dart index 03bc04255..ce887dcb7 100644 --- a/lib/utils/event_extension.dart +++ b/lib/utils/event_extension.dart @@ -4,7 +4,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'matrix_file_extension.dart'; -import '../controllers/image_viewer_controller.dart'; +import '../views/image_viewer.dart'; extension LocalizedBody on Event { void openFile(BuildContext context, {bool downloadOnly = false}) async { diff --git a/lib/controllers/archive_controller.dart b/lib/views/archive.dart similarity index 83% rename from lib/controllers/archive_controller.dart rename to lib/views/archive.dart index becf30c4e..bdb4ef20e 100644 --- a/lib/controllers/archive_controller.dart +++ b/lib/views/archive.dart @@ -1,5 +1,5 @@ import 'package:famedlysdk/famedlysdk.dart'; -import 'package:fluffychat/views/archive_view.dart'; +import 'package:fluffychat/views/ui/archive_ui.dart'; import 'package:fluffychat/views/widgets/matrix.dart'; import 'package:flutter/material.dart'; @@ -19,5 +19,5 @@ class ArchiveController extends State { void forgetAction(int i) => setState(() => archive.removeAt(i)); @override - Widget build(BuildContext context) => ArchiveView(this); + Widget build(BuildContext context) => ArchiveUI(this); } diff --git a/lib/views/chat.dart b/lib/views/chat.dart index 7796c98d3..c12a5e87d 100644 --- a/lib/views/chat.dart +++ b/lib/views/chat.dart @@ -1,7 +1,5 @@ import 'dart:async'; import 'dart:io'; -import 'dart:math'; -import 'dart:ui'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:adaptive_page_layout/adaptive_page_layout.dart'; @@ -9,38 +7,25 @@ import 'package:emoji_picker/emoji_picker.dart'; import 'package:famedlysdk/famedlysdk.dart'; import 'package:file_picker_cross/file_picker_cross.dart'; import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/views/widgets/avatar.dart'; -import 'package:fluffychat/views/widgets/chat_settings_popup_menu.dart'; -import 'package:fluffychat/views/widgets/connection_status_header.dart'; +import 'package:fluffychat/views/ui/chat_ui.dart'; import 'package:fluffychat/views/widgets/dialogs/recording_dialog.dart'; -import 'package:fluffychat/views/widgets/unread_badge_back_button.dart'; -import 'package:fluffychat/config/themes.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; -import 'package:fluffychat/views/widgets/encryption_button.dart'; -import 'package:fluffychat/views/widgets/list_items/message.dart'; import 'package:fluffychat/views/widgets/matrix.dart'; -import 'package:fluffychat/views/widgets/reply_content.dart'; -import 'package:fluffychat/views/widgets/user_bottom_sheet.dart'; -import 'package:fluffychat/config/app_emojis.dart'; import 'package:fluffychat/utils/matrix_locals.dart'; import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/utils/room_status_extension.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:image_picker/image_picker.dart'; -import 'package:pedantic/pedantic.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; -import 'package:swipe_to_action/swipe_to_action.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../views/widgets/dialogs/send_file_dialog.dart'; -import '../views/widgets/input_bar.dart'; +import 'widgets/dialogs/send_file_dialog.dart'; import '../utils/filtered_timeline_extension.dart'; import '../utils/matrix_file_extension.dart'; @@ -52,17 +37,17 @@ class Chat extends StatefulWidget { : super(key: key ?? Key('chatroom-$id')); @override - _ChatState createState() => _ChatState(); + ChatController createState() => ChatController(); } -class _ChatState extends State { +class ChatController extends State { Room room; Timeline timeline; MatrixState matrix; - final AutoScrollController _scrollController = AutoScrollController(); + final AutoScrollController scrollController = AutoScrollController(); FocusNode inputFocus = FocusNode(); @@ -74,7 +59,7 @@ class _ChatState extends State { List filteredEvents; - final Set _unfolded = {}; + final Set unfolded = {}; Event replyEvent; @@ -90,9 +75,9 @@ class _ChatState extends State { String pendingText = ''; - bool get _canLoadMore => timeline.events.last.type != EventTypes.RoomCreate; + bool get canLoadMore => timeline.events.last.type != EventTypes.RoomCreate; - void startCallAction(BuildContext context) async { + void startCallAction() async { final url = '${AppConfig.jitsiInstance}${Uri.encodeComponent(Matrix.of(context).client.generateUniqueTransactionId())}'; @@ -107,7 +92,7 @@ class _ChatState extends State { } void requestHistory() async { - if (_canLoadMore) { + if (canLoadMore) { try { await timeline.requestHistory(historyCount: _loadHistoryCount); } catch (err) { @@ -121,17 +106,16 @@ class _ChatState extends State { if (!mounted) { return; } - if (_scrollController.position.pixels == - _scrollController.position.maxScrollExtent && + if (scrollController.position.pixels == + scrollController.position.maxScrollExtent && timeline.events.isNotEmpty && timeline.events[timeline.events.length - 1].type != EventTypes.RoomCreate) { requestHistory(); } - if (_scrollController.position.pixels > 0 && - showScrollDownButton == false) { + if (scrollController.position.pixels > 0 && showScrollDownButton == false) { setState(() => showScrollDownButton = true); - } else if (_scrollController.position.pixels == 0 && + } else if (scrollController.position.pixels == 0 && showScrollDownButton == true) { setState(() => showScrollDownButton = false); } @@ -139,7 +123,7 @@ class _ChatState extends State { @override void initState() { - _scrollController.addListener(_updateScrollController); + scrollController.addListener(_updateScrollController); super.initState(); } @@ -147,33 +131,45 @@ class _ChatState extends State { if (!mounted) return; setState( () { - filteredEvents = timeline.getFilteredEvents(unfolded: _unfolded); + filteredEvents = timeline.getFilteredEvents(unfolded: unfolded); }, ); } - void _unfold(String eventId) { + void unfold(String eventId) { var i = filteredEvents.indexWhere((e) => e.eventId == eventId); setState(() { while (i < filteredEvents.length - 1 && filteredEvents[i].isState) { - _unfolded.add(filteredEvents[i].eventId); + unfolded.add(filteredEvents[i].eventId); i++; } - filteredEvents = timeline.getFilteredEvents(unfolded: _unfolded); + filteredEvents = timeline.getFilteredEvents(unfolded: unfolded); }); } - Future getTimeline(BuildContext context) async { + Future getTimeline() async { if (timeline == null) { timeline = await room.getTimeline(onUpdate: updateView); if (timeline.events.isNotEmpty) { - unawaited(room.setUnread(false).catchError((err) { + // ignore: unawaited_futures + room.setUnread(false).catchError((err) { if (err is MatrixException && err.errcode == 'M_FORBIDDEN') { // ignore if the user is not in the room (still joining) return; } throw err; - })); + }); + } + if (room.notificationCount != null && + room.notificationCount > 0 && + timeline != null && + timeline.events.isNotEmpty && + Matrix.of(context).webHasFocus) { + // ignore: unawaited_futures + room.sendReadMarker( + timeline.events.first.eventId, + readReceiptLocationEventId: timeline.events.first.eventId, + ); } // when the scroll controller is attached we want to scroll to an event id, if specified @@ -182,7 +178,7 @@ class _ChatState extends State { SchedulerBinding.instance.addPostFrameCallback((_) async { if (mounted) { if (widget.scrollToEventId != null) { - _scrollToEventId(widget.scrollToEventId, context: context); + scrollToEventId(widget.scrollToEventId); } _updateScrollController(); } @@ -216,7 +212,7 @@ class _ChatState extends State { }); } - void sendFileAction(BuildContext context) async { + void sendFileAction() async { final result = await FilePickerCross.importFromStorage(type: FileTypeCross.any); if (result == null) return; @@ -233,7 +229,7 @@ class _ChatState extends State { ); } - void sendImageAction(BuildContext context) async { + void sendImageAction() async { final result = await FilePickerCross.importFromStorage(type: FileTypeCross.image); if (result == null) return; @@ -250,7 +246,7 @@ class _ChatState extends State { ); } - void openCameraAction(BuildContext context) async { + void openCameraAction() async { final file = await ImagePicker().getImage(source: ImageSource.camera); if (file == null) return; final bytes = await file.readAsBytes(); @@ -267,7 +263,7 @@ class _ChatState extends State { ); } - void voiceMessageAction(BuildContext context) async { + void voiceMessageAction() async { if (await Permission.microphone.isGranted != true) { final status = await Permission.microphone.request(); if (status != PermissionStatus.granted) return; @@ -290,7 +286,7 @@ class _ChatState extends State { ); } - String _getSelectedEventString(BuildContext context) { + String _getSelectedEventString() { var copyString = ''; if (selectedEvents.length == 1) { return selectedEvents.first @@ -306,12 +302,12 @@ class _ChatState extends State { return copyString; } - void copyEventsAction(BuildContext context) { - Clipboard.setData(ClipboardData(text: _getSelectedEventString(context))); + void copyEventsAction() { + Clipboard.setData(ClipboardData(text: _getSelectedEventString())); setState(() => selectedEvents.clear()); } - void reportEventAction(BuildContext context) async { + void reportEventAction() async { final event = selectedEvents.single; final score = await showConfirmationDialog( context: context, @@ -355,7 +351,7 @@ class _ChatState extends State { SnackBar(content: Text(L10n.of(context).contentHasBeenReported))); } - void redactEventsAction(BuildContext context) async { + void redactEventsAction() async { final confirmed = await showOkCancelAlertDialog( context: context, title: L10n.of(context).messageWillBeRemovedWarning, @@ -380,20 +376,20 @@ class _ChatState extends State { return true; } - void forwardEventsAction(BuildContext context) async { + void forwardEventsAction() async { if (selectedEvents.length == 1) { Matrix.of(context).shareContent = selectedEvents.first.content; } else { Matrix.of(context).shareContent = { 'msgtype': 'm.text', - 'body': _getSelectedEventString(context), + 'body': _getSelectedEventString(), }; } setState(() => selectedEvents.clear()); AdaptivePageLayout.of(context).popUntilIsFirst(); } - void sendAgainAction(Timeline timeline) { + void sendAgainAction() { final event = selectedEvents.first; if (event.status == -1) { event.sendAgain(); @@ -415,7 +411,7 @@ class _ChatState extends State { inputFocus.requestFocus(); } - void _scrollToEventId(String eventId, {BuildContext context}) async { + void scrollToEventId(String eventId) async { var eventIndex = filteredEvents.indexWhere((e) => e.eventId == eventId); if (eventIndex == -1) { // event id not found...maybe we can fetch it? @@ -437,7 +433,7 @@ class _ChatState extends State { } // okay, we know that the event *is* in the room while (eventIndex == -1) { - if (!_canLoadMore) { + if (!canLoadMore) { // we can't load any more events but still haven't found ours yet...better stop here return; } @@ -462,13 +458,14 @@ class _ChatState extends State { if (!mounted) { return; } - await _scrollController.scrollToIndex(eventIndex, + await scrollController.scrollToIndex(eventIndex, preferPosition: AutoScrollPosition.middle); _updateScrollController(); } - void _pickEmojiAction( - BuildContext context, Iterable allReactionEvents) async { + void scrollDown() => scrollController.jumpTo(0); + + void pickEmojiAction(Iterable allReactionEvents) async { final emoji = await showModalBottomSheet( context: context, backgroundColor: Colors.transparent, @@ -495,10 +492,10 @@ class _ChatState extends State { // make sure we don't send the same emoji twice if (allReactionEvents .any((e) => e.content['m.relates_to']['key'] == emoji.emoji)) return; - return _sendEmojiAction(context, emoji.emoji); + return sendEmojiAction(emoji.emoji); } - void _sendEmojiAction(BuildContext context, String emoji) async { + void sendEmojiAction(String emoji) async { await showFutureLoadingDialog( context: context, future: () => room.sendReaction( @@ -509,835 +506,151 @@ class _ChatState extends State { setState(() => selectedEvents.clear()); } - @override - Widget build(BuildContext context) { - matrix = Matrix.of(context); - final client = matrix.client; - room ??= client.getRoomById(widget.id); - if (room == null) { - return Scaffold( - appBar: AppBar( - title: Text(L10n.of(context).oopsSomethingWentWrong), - ), - body: Center( - child: Text(L10n.of(context).youAreNoLongerParticipatingInThisChat), - ), - ); - } - matrix.client.activeRoomId = widget.id; + void clearSelectedEvents() => () => setState(() => selectedEvents.clear()); - if (room.membership == Membership.invite) { - showFutureLoadingDialog(context: context, future: () => room.join()); - } + void editSelectedEventAction() { + setState(() { + pendingText = sendController.text; + editEvent = selectedEvents.first; + inputText = sendController.text = editEvent + .getDisplayEvent(timeline) + .getLocalizedBody(MatrixLocals(L10n.of(context)), + withSenderNamePrefix: false, hideReply: true); + selectedEvents.clear(); + }); + inputFocus.requestFocus(); + } - return Scaffold( - appBar: AppBar( - leading: selectMode - ? IconButton( - icon: Icon(Icons.close), - onPressed: () => setState(() => selectedEvents.clear()), - tooltip: L10n.of(context).close, - ) - : AdaptivePageLayout.of(context).columnMode(context) - ? null - : UnreadBadgeBackButton(roomId: widget.id), - titleSpacing: - AdaptivePageLayout.of(context).columnMode(context) ? null : 0, - title: selectedEvents.isEmpty - ? StreamBuilder( - stream: room.onUpdate.stream, - builder: (context, snapshot) => ListTile( - leading: Avatar(room.avatar, room.displayname), - contentPadding: EdgeInsets.zero, - onTap: room.isDirectChat - ? () => showModalBottomSheet( - context: context, - builder: (c) => UserBottomSheet( - user: room.getUserByMXIDSync( - room.directChatMatrixID), - onMention: () => sendController.text += - '${room.directChatMatrixID} ', - ), - ) - : () => (!AdaptivePageLayout.of(context) - .columnMode(context) || - AdaptivePageLayout.of(context) - .viewDataStack - .length < - 3) - ? AdaptivePageLayout.of(context) - .pushNamed('/rooms/${room.id}/details') - : null, - title: Text( - room.getLocalizedDisplayname( - MatrixLocals(L10n.of(context))), - maxLines: 1), - subtitle: room.getLocalizedTypingText(context).isEmpty - ? StreamBuilder( - stream: Matrix.of(context) - .client - .onPresence - .stream - .where((p) => - p.senderId == room.directChatMatrixID), - builder: (context, snapshot) => Text( - room.getLocalizedStatus(context), - maxLines: 1, - //overflow: TextOverflow.ellipsis, - )) - : Row( - children: [ - Icon(Icons.edit_outlined, - color: Theme.of(context).accentColor, - size: 13), - SizedBox(width: 4), - Expanded( - child: Text( - room.getLocalizedTypingText(context), - maxLines: 1, - style: TextStyle( - color: Theme.of(context).accentColor, - fontStyle: FontStyle.italic, - ), - ), - ), - ], - ), - )) - : Text(L10n.of(context) - .numberSelected(selectedEvents.length.toString())), - actions: selectMode - ? [ - if (selectedEvents.length == 1 && - selectedEvents.first.status > 0 && - selectedEvents.first.senderId == client.userID) - IconButton( - icon: Icon(Icons.edit_outlined), - tooltip: L10n.of(context).edit, - onPressed: () { - setState(() { - pendingText = sendController.text; - editEvent = selectedEvents.first; - inputText = sendController.text = editEvent - .getDisplayEvent(timeline) - .getLocalizedBody(MatrixLocals(L10n.of(context)), - withSenderNamePrefix: false, hideReply: true); - selectedEvents.clear(); - }); - inputFocus.requestFocus(); - }, - ), - PopupMenuButton( - onSelected: (selected) { - switch (selected) { - case 'copy': - copyEventsAction(context); - break; - case 'redact': - redactEventsAction(context); - break; - case 'report': - reportEventAction(context); - break; - } - }, - itemBuilder: (_) => [ - PopupMenuItem( - value: 'copy', - child: Text(L10n.of(context).copy), - ), - if (canRedactSelectedEvents) - PopupMenuItem( - value: 'redact', - child: Text( - L10n.of(context).redactMessage, - style: TextStyle(color: Colors.orange), - ), - ), - if (selectedEvents.length == 1) - PopupMenuItem( - value: 'report', - child: Text( - L10n.of(context).reportMessage, - style: TextStyle(color: Colors.red), - ), - ), - ], - ), - ] - : [ - if (room.canSendDefaultStates) - IconButton( - tooltip: L10n.of(context).videoCall, - icon: Icon(Icons.video_call_outlined), - onPressed: () => startCallAction(context), - ), - ChatSettingsPopupMenu(room, !room.isDirectChat), - ], - ), - floatingActionButton: showScrollDownButton - ? Padding( - padding: const EdgeInsets.only(bottom: 56.0), - child: FloatingActionButton( - onPressed: () => _scrollController.jumpTo(0), - foregroundColor: Theme.of(context).textTheme.bodyText2.color, - backgroundColor: Theme.of(context).scaffoldBackgroundColor, - mini: true, - child: Icon(Icons.arrow_downward_outlined, - color: Theme.of(context).primaryColor), - ), - ) - : null, - body: Stack( - children: [ - if (Matrix.of(context).wallpaper != null) - Image.file( - Matrix.of(context).wallpaper, - width: double.infinity, - height: double.infinity, - fit: BoxFit.cover, - ), - SafeArea( - child: Column( - children: [ - ConnectionStatusHeader(), - if (room.getState(EventTypes.RoomTombstone) != null) - Container( - height: 72, - child: Material( - color: Theme.of(context).secondaryHeaderColor, - child: ListTile( - leading: CircleAvatar( - foregroundColor: Theme.of(context).accentColor, - backgroundColor: Theme.of(context).backgroundColor, - child: Icon(Icons.upgrade_outlined), - ), - title: Text( - room - .getState(EventTypes.RoomTombstone) - .parsedTombstoneContent - .body, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - subtitle: Text(L10n.of(context).goToTheNewRoom), - onTap: () async { - if (OkCancelResult.ok != - await showOkCancelAlertDialog( - context: context, - title: L10n.of(context).goToTheNewRoom, - message: room - .getState(EventTypes.RoomTombstone) - .parsedTombstoneContent - .body, - okLabel: L10n.of(context).ok, - cancelLabel: L10n.of(context).cancel, - )) { - return; - } - final result = await showFutureLoadingDialog( - context: context, - future: () => room.client.joinRoom(room - .getState(EventTypes.RoomTombstone) - .parsedTombstoneContent - .replacementRoom), - ); - await showFutureLoadingDialog( - context: context, - future: () => room.leave(), - ); - if (result.error == null) { - await AdaptivePageLayout.of(context) - .pushNamedAndRemoveUntilIsFirst( - '/rooms/${result.result}'); - } - }, - ), - ), - ), - Expanded( - child: FutureBuilder( - future: getTimeline(context), - builder: (BuildContext context, snapshot) { - if (!snapshot.hasData) { - return Center( - child: CircularProgressIndicator(), - ); - } + void onEventActionPopupMenuSelected(selected) { + switch (selected) { + case 'copy': + copyEventsAction(); + break; + case 'redact': + redactEventsAction(); + break; + case 'report': + reportEventAction(); + break; + } + } - if (room.notificationCount != null && - room.notificationCount > 0 && - timeline != null && - timeline.events.isNotEmpty && - Matrix.of(context).webHasFocus) { - room.sendReadMarker( - timeline.events.first.eventId, - readReceiptLocationEventId: - timeline.events.first.eventId, - ); - } + void goToNewRoomAction() async { + if (OkCancelResult.ok != + await showOkCancelAlertDialog( + context: context, + title: L10n.of(context).goToTheNewRoom, + message: room + .getState(EventTypes.RoomTombstone) + .parsedTombstoneContent + .body, + okLabel: L10n.of(context).ok, + cancelLabel: L10n.of(context).cancel, + )) { + return; + } + final result = await showFutureLoadingDialog( + context: context, + future: () => room.client.joinRoom(room + .getState(EventTypes.RoomTombstone) + .parsedTombstoneContent + .replacementRoom), + ); + await showFutureLoadingDialog( + context: context, + future: room.leave, + ); + if (result.error == null) { + await AdaptivePageLayout.of(context) + .pushNamedAndRemoveUntilIsFirst('/rooms/${result.result}'); + } + } - // create a map of eventId --> index to greatly improve performance of - // ListView's findChildIndexCallback - final thisEventsKeyMap = {}; - for (var i = 0; i < filteredEvents.length; i++) { - thisEventsKeyMap[filteredEvents[i].eventId] = i; - } + void onSelectMessage(Event event) { + if (!event.redacted) { + if (selectedEvents.contains(event)) { + setState( + () => selectedEvents.remove(event), + ); + } else { + setState( + () => selectedEvents.add(event), + ); + } + selectedEvents.sort( + (a, b) => a.originServerTs.compareTo(b.originServerTs), + ); + } + } - final horizontalPadding = max( - 0, - (MediaQuery.of(context).size.width - - FluffyThemes.columnWidth * - (AdaptivePageLayout.of(context) - .currentViewData - .rightView != - null - ? 4.5 - : 3.5)) / - 2) - .toDouble(); + int findChildIndexCallback(Key key, Map thisEventsKeyMap) { + // this method is called very often. As such, it has to be optimized for speed. + if (!(key is ValueKey)) { + return null; + } + final eventId = (key as ValueKey).value; + if (!(eventId is String)) { + return null; + } + // first fetch the last index the event was at + final index = thisEventsKeyMap[eventId]; + if (index == null) { + return null; + } + // we need to +1 as 0 is the typing thing at the bottom + return index + 1; + } - return ListView.custom( - padding: EdgeInsets.only( - top: 16, - left: horizontalPadding, - right: horizontalPadding, - ), - reverse: true, - controller: _scrollController, - childrenDelegate: SliverChildBuilderDelegate( - (BuildContext context, int i) { - return i == filteredEvents.length + 1 - ? timeline.isRequestingHistory - ? Container( - height: 50, - alignment: Alignment.center, - padding: EdgeInsets.all(8), - child: CircularProgressIndicator(), - ) - : _canLoadMore - ? TextButton( - onPressed: requestHistory, - child: Text( - L10n.of(context).loadMore, - style: TextStyle( - color: Theme.of(context) - .primaryColor, - fontWeight: FontWeight.bold, - decoration: - TextDecoration.underline, - ), - ), - ) - : Container() - : i == 0 - ? StreamBuilder( - stream: room.onUpdate.stream, - builder: (_, __) { - final seenByText = - room.getLocalizedSeenByText( - context, - timeline, - filteredEvents, - _unfolded, - ); - return AnimatedContainer( - height: seenByText.isEmpty ? 0 : 24, - duration: seenByText.isEmpty - ? Duration(milliseconds: 0) - : Duration(milliseconds: 300), - alignment: - filteredEvents.first.senderId == - client.userID - ? Alignment.topRight - : Alignment.topLeft, - padding: EdgeInsets.only( - left: 8, - right: 8, - bottom: 8, - ), - child: Container( - padding: EdgeInsets.symmetric( - horizontal: 4), - decoration: BoxDecoration( - color: Theme.of(context) - .scaffoldBackgroundColor - .withOpacity(0.8), - borderRadius: - BorderRadius.circular(4), - ), - child: Text( - seenByText, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: Theme.of(context) - .accentColor, - ), - ), - ), - ); - }, - ) - : AutoScrollTag( - key: ValueKey( - filteredEvents[i - 1].eventId), - index: i - 1, - controller: _scrollController, - child: Swipeable( - key: ValueKey( - filteredEvents[i - 1].eventId), - background: Padding( - padding: EdgeInsets.symmetric( - horizontal: 12.0), - child: Center( - child: Icon(Icons.reply_outlined), - ), - ), - direction: SwipeDirection.endToStart, - onSwipe: (direction) => replyAction( - replyTo: filteredEvents[i - 1]), - child: Message(filteredEvents[i - 1], - onAvatarTab: (Event event) => - showModalBottomSheet( - context: context, - builder: (c) => - UserBottomSheet( - user: event.sender, - onMention: () => - sendController.text += - '${event.senderId} ', - ), - ), - unfold: _unfold, - onSelect: (Event event) { - if (!event.redacted) { - if (selectedEvents - .contains(event)) { - setState( - () => selectedEvents - .remove(event), - ); - } else { - setState( - () => selectedEvents - .add(event), - ); - } - selectedEvents.sort( - (a, b) => a.originServerTs - .compareTo( - b.originServerTs), - ); - } - }, - scrollToEventId: - (String eventId) => - _scrollToEventId(eventId, - context: context), - longPressSelect: - selectedEvents.isEmpty, - selected: selectedEvents.contains( - filteredEvents[i - 1]), - timeline: timeline, - nextEvent: i >= 2 - ? filteredEvents[i - 2] - : null), - ), - ); - }, - childCount: filteredEvents.length + 2, - findChildIndexCallback: (Key key) { - // this method is called very often. As such, it has to be optimized for speed. - if (!(key is ValueKey)) { - return null; - } - final eventId = (key as ValueKey).value; - if (!(eventId is String)) { - return null; - } - // first fetch the last index the event was at - final index = thisEventsKeyMap[eventId]; - if (index == null) { - return null; - } - // we need to +1 as 0 is the typing thing at the bottom - return index + 1; - }, - ), - ); - }, - ), - ), - AnimatedContainer( - duration: Duration(milliseconds: 300), - height: (editEvent == null && - replyEvent == null && - room.canSendDefaultMessages && - selectedEvents.length == 1) - ? 56 - : 0, - child: Material( - color: Theme.of(context).secondaryHeaderColor, - child: Builder(builder: (context) { - if (!(editEvent == null && - replyEvent == null && - selectedEvents.length == 1)) { - return Container(); - } - final emojis = List.from(AppEmojis.emojis); - final allReactionEvents = selectedEvents.first - .aggregatedEvents( - timeline, RelationshipTypes.Reaction) - ?.where((event) => - event.senderId == event.room.client.userID && - event.type == 'm.reaction'); + void onInputBarSubmitted(String text) { + send(); + FocusScope.of(context).requestFocus(inputFocus); + } - allReactionEvents.forEach((event) { - try { - emojis.remove(event.content['m.relates_to']['key']); - } catch (_) {} - }); - return ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: emojis.length + 1, - itemBuilder: (c, i) => i == emojis.length - ? InkWell( - borderRadius: BorderRadius.circular(8), - onTap: () => _pickEmojiAction( - context, allReactionEvents), - child: Container( - width: 56, - height: 56, - alignment: Alignment.center, - child: Icon(Icons.add_outlined), - ), - ) - : InkWell( - borderRadius: BorderRadius.circular(8), - onTap: () => - _sendEmojiAction(context, emojis[i]), - child: Container( - width: 56, - height: 56, - alignment: Alignment.center, - child: Text( - emojis[i], - style: TextStyle(fontSize: 30), - ), - ), - ), - ); - }), - ), - ), - AnimatedContainer( - duration: Duration(milliseconds: 300), - height: editEvent != null || replyEvent != null ? 56 : 0, - child: Material( - color: Theme.of(context).secondaryHeaderColor, - child: Row( - children: [ - IconButton( - tooltip: L10n.of(context).close, - icon: Icon(Icons.close), - onPressed: () => setState(() { - if (editEvent != null) { - inputText = sendController.text = pendingText; - pendingText = ''; - } - replyEvent = null; - editEvent = null; - }), - ), - Expanded( - child: replyEvent != null - ? ReplyContent(replyEvent, timeline: timeline) - : _EditContent( - editEvent?.getDisplayEvent(timeline)), - ), - ], - ), - ), - ), - Divider( - height: 1, - thickness: 1, - ), - room.canSendDefaultMessages && - room.membership == Membership.join - ? Container( - decoration: BoxDecoration( - color: Theme.of(context).backgroundColor, - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: selectMode - ? [ - Container( - height: 56, - child: TextButton( - onPressed: () => - forwardEventsAction(context), - child: Row( - children: [ - Icon(Icons - .keyboard_arrow_left_outlined), - Text(L10n.of(context).forward), - ], - ), - ), - ), - selectedEvents.length == 1 - ? selectedEvents.first - .getDisplayEvent(timeline) - .status > - 0 - ? Container( - height: 56, - child: TextButton( - onPressed: () => replyAction(), - child: Row( - children: [ - Text( - L10n.of(context).reply), - Icon(Icons - .keyboard_arrow_right), - ], - ), - ), - ) - : Container( - height: 56, - child: TextButton( - onPressed: () => - sendAgainAction(timeline), - child: Row( - children: [ - Text(L10n.of(context) - .tryToSendAgain), - SizedBox(width: 4), - Icon(Icons.send_outlined, - size: 16), - ], - ), - ), - ) - : Container(), - ] - : [ - if (inputText.isEmpty) - Container( - height: 56, - alignment: Alignment.center, - child: PopupMenuButton( - icon: Icon(Icons.add_outlined), - onSelected: (String choice) async { - if (choice == 'file') { - sendFileAction(context); - } else if (choice == 'image') { - sendImageAction(context); - } - if (choice == 'camera') { - openCameraAction(context); - } - if (choice == 'voice') { - voiceMessageAction(context); - } - }, - itemBuilder: (BuildContext context) => - >[ - PopupMenuItem( - value: 'file', - child: ListTile( - leading: CircleAvatar( - backgroundColor: Colors.green, - foregroundColor: Colors.white, - child: Icon( - Icons.attachment_outlined), - ), - title: Text( - L10n.of(context).sendFile), - contentPadding: EdgeInsets.all(0), - ), - ), - PopupMenuItem( - value: 'image', - child: ListTile( - leading: CircleAvatar( - backgroundColor: Colors.blue, - foregroundColor: Colors.white, - child: - Icon(Icons.image_outlined), - ), - title: Text( - L10n.of(context).sendImage), - contentPadding: EdgeInsets.all(0), - ), - ), - if (PlatformInfos.isMobile) - PopupMenuItem( - value: 'camera', - child: ListTile( - leading: CircleAvatar( - backgroundColor: - Colors.purple, - foregroundColor: Colors.white, - child: Icon(Icons - .camera_alt_outlined), - ), - title: Text(L10n.of(context) - .openCamera), - contentPadding: - EdgeInsets.all(0), - ), - ), - if (PlatformInfos.isMobile) - PopupMenuItem( - value: 'voice', - child: ListTile( - leading: CircleAvatar( - backgroundColor: Colors.red, - foregroundColor: Colors.white, - child: Icon( - Icons.mic_none_outlined), - ), - title: Text(L10n.of(context) - .voiceMessage), - contentPadding: - EdgeInsets.all(0), - ), - ), - ], - ), - ), - Container( - height: 56, - alignment: Alignment.center, - child: EncryptionButton(room), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 4.0), - child: InputBar( - room: room, - minLines: 1, - maxLines: kIsWeb ? 1 : 8, - autofocus: !PlatformInfos.isMobile, - keyboardType: !PlatformInfos.isMobile - ? TextInputType.text - : TextInputType.multiline, - onSubmitted: (String text) { - send(); - FocusScope.of(context) - .requestFocus(inputFocus); - }, - focusNode: inputFocus, - controller: sendController, - decoration: InputDecoration( - hintText: - L10n.of(context).writeAMessage, - hintMaxLines: 1, - border: InputBorder.none, - enabledBorder: InputBorder.none, - filled: false, - ), - onChanged: (String text) { - typingCoolDown?.cancel(); - typingCoolDown = - Timer(Duration(seconds: 2), () { - typingCoolDown = null; - currentlyTyping = false; - room.sendTypingNotification(false); - }); - typingTimeout ??= - Timer(Duration(seconds: 30), () { - typingTimeout = null; - currentlyTyping = false; - }); - if (!currentlyTyping) { - currentlyTyping = true; - room.sendTypingNotification(true, - timeout: Duration(seconds: 30) - .inMilliseconds); - } - // Workaround for a current desktop bug - if (!PlatformInfos.isBetaDesktop) { - setState(() => inputText = text); - } - }, - ), - ), - ), - if (PlatformInfos.isMobile && - inputText.isEmpty) - Container( - height: 56, - alignment: Alignment.center, - child: IconButton( - tooltip: L10n.of(context).voiceMessage, - icon: Icon(Icons.mic_none_outlined), - onPressed: () => - voiceMessageAction(context), - ), - ), - if (!PlatformInfos.isMobile || - inputText.isNotEmpty) - Container( - height: 56, - alignment: Alignment.center, - child: IconButton( - icon: Icon(Icons.send_outlined), - onPressed: () => send(), - tooltip: L10n.of(context).send, - ), - ), - ], - ), - ) - : Container(), - ], - ), - ), - ], - ), - ); + void onAddPopupMenuButtonSelected(String choice) { + if (choice == 'file') { + sendFileAction(); + } else if (choice == 'image') { + sendImageAction(); + } + if (choice == 'camera') { + openCameraAction(); + } + if (choice == 'voice') { + voiceMessageAction(); + } } -} -class _EditContent extends StatelessWidget { - final Event event; + void onInputBarChanged(String text) { + typingCoolDown?.cancel(); + typingCoolDown = Timer(Duration(seconds: 2), () { + typingCoolDown = null; + currentlyTyping = false; + room.sendTypingNotification(false); + }); + typingTimeout ??= Timer(Duration(seconds: 30), () { + typingTimeout = null; + currentlyTyping = false; + }); + if (!currentlyTyping) { + currentlyTyping = true; + room.sendTypingNotification(true, + timeout: Duration(seconds: 30).inMilliseconds); + } + // Workaround for a current desktop bug + if (!PlatformInfos.isBetaDesktop) { + setState(() => inputText = text); + } + } - _EditContent(this.event); + void cancelReplyEventAction() => setState(() { + if (editEvent != null) { + inputText = sendController.text = pendingText; + pendingText = ''; + } + replyEvent = null; + editEvent = null; + }); @override - Widget build(BuildContext context) { - if (event == null) { - return Container(); - } - return Row( - children: [ - Icon( - Icons.edit, - color: Theme.of(context).primaryColor, - ), - Container(width: 15.0), - Text( - event?.getLocalizedBody( - MatrixLocals(L10n.of(context)), - withSenderNamePrefix: false, - hideReply: true, - ) ?? - '', - overflow: TextOverflow.ellipsis, - maxLines: 1, - style: TextStyle( - color: Theme.of(context).textTheme.bodyText2.color, - ), - ), - ], - ); - } + Widget build(BuildContext context) => ChatUI(this); } diff --git a/lib/views/chat_details.dart b/lib/views/chat_details.dart index 6982dd9b4..ec739176b 100644 --- a/lib/views/chat_details.dart +++ b/lib/views/chat_details.dart @@ -1,369 +1,223 @@ +import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:adaptive_page_layout/adaptive_page_layout.dart'; -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/controllers/chat_details_controller.dart'; -import 'package:fluffychat/views/widgets/avatar.dart'; -import 'package:fluffychat/views/widgets/matrix.dart'; -import 'package:fluffychat/utils/fluffy_share.dart'; import 'package:famedlysdk/famedlysdk.dart'; -import 'package:fluffychat/views/widgets/chat_settings_popup_menu.dart'; -import 'package:fluffychat/views/widgets/content_banner.dart'; -import 'package:fluffychat/views/widgets/max_width_body.dart'; -import 'package:fluffychat/views/widgets/list_items/participant_list_item.dart'; +import 'package:file_picker_cross/file_picker_cross.dart'; +import 'package:fluffychat/views/ui/chat_details_ui.dart'; +import 'package:fluffychat/views/widgets/matrix.dart'; +import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:fluffychat/utils/matrix_locals.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:matrix_link_text/link_text.dart'; +import 'package:image_picker/image_picker.dart'; + +class ChatDetails extends StatefulWidget { + final String roomId; -import '../utils/url_launcher.dart'; + const ChatDetails(this.roomId); -class ChatDetailsView extends StatelessWidget { - final ChatDetailsController controller; + @override + ChatDetailsController createState() => ChatDetailsController(); +} - const ChatDetailsView(this.controller, {Key key}) : super(key: key); +class ChatDetailsController extends State { + List members; @override - Widget build(BuildContext context) { - final room = - Matrix.of(context).client.getRoomById(controller.widget.roomId); - if (room == null) { - return Scaffold( - appBar: AppBar( - leading: BackButton(), - title: Text(L10n.of(context).oopsSomethingWentWrong), - ), - body: Center( - child: Text(L10n.of(context).youAreNoLongerParticipatingInThisChat), - ), + void initState() { + super.initState(); + members ??= + Matrix.of(context).client.getRoomById(widget.roomId).getParticipants(); + } + + void setDisplaynameAction() async { + final room = Matrix.of(context).client.getRoomById(widget.roomId); + final input = await showTextInputDialog( + context: context, + title: L10n.of(context).changeTheNameOfTheGroup, + okLabel: L10n.of(context).ok, + cancelLabel: L10n.of(context).cancel, + useRootNavigator: false, + textFields: [ + DialogTextField( + initialText: room.getLocalizedDisplayname( + MatrixLocals( + L10n.of(context), + ), + ), + ) + ], + ); + if (input == null) return; + final success = await showFutureLoadingDialog( + context: context, + future: () => room.setName(input.single), + ); + if (success.error == null) { + AdaptivePageLayout.of(context).showSnackBar( + SnackBar(content: Text(L10n.of(context).displaynameHasBeenChanged))); + } + } + + void setCanonicalAliasAction(context) async { + final input = await showTextInputDialog( + context: context, + title: L10n.of(context).setInvitationLink, + okLabel: L10n.of(context).ok, + cancelLabel: L10n.of(context).cancel, + useRootNavigator: false, + textFields: [ + DialogTextField( + hintText: '#localpart:domain', + initialText: L10n.of(context).alias.toLowerCase(), + ) + ], + ); + if (input == null) return; + final room = Matrix.of(context).client.getRoomById(widget.roomId); + final domain = room.client.userID.domain; + final canonicalAlias = '%23' + input.single + '%3A' + domain; + final aliasEvent = room.getState('m.room.aliases', domain); + final aliases = + aliasEvent != null ? aliasEvent.content['aliases'] ?? [] : []; + if (aliases.indexWhere((s) => s == canonicalAlias) == -1) { + final newAliases = List.from(aliases); + newAliases.add(canonicalAlias); + final response = await showFutureLoadingDialog( + context: context, + future: () => room.client.requestRoomAliasInformation(canonicalAlias), ); + if (response.error != null) { + final success = await showFutureLoadingDialog( + context: context, + future: () => room.client.createRoomAlias(canonicalAlias, room.id), + ); + if (success.error != null) return; + } } + await showFutureLoadingDialog( + context: context, + future: () => room.client.sendState(room.id, 'm.room.canonical_alias', { + 'alias': input.single, + }), + ); + } - controller.members.removeWhere((u) => u.membership == Membership.leave); - final actualMembersCount = - room.mInvitedMemberCount + room.mJoinedMemberCount; - final canRequestMoreMembers = - controller.members.length < actualMembersCount; - return StreamBuilder( - stream: room.onUpdate.stream, - builder: (context, snapshot) { - 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, - actions: [ - if (room.canonicalAlias?.isNotEmpty ?? false) - IconButton( - tooltip: L10n.of(context).share, - icon: Icon(Icons.share_outlined), - onPressed: () => FluffyShare.share( - AppConfig.inviteLinkPrefix + room.canonicalAlias, - context), - ), - ChatSettingsPopupMenu(room, false) - ], - title: Text( - room.getLocalizedDisplayname( - MatrixLocals(L10n.of(context))), - style: TextStyle( - color: Theme.of(context) - .appBarTheme - .textTheme - .headline6 - .color)), - backgroundColor: Theme.of(context).appBarTheme.color, - flexibleSpace: FlexibleSpaceBar( - background: ContentBanner(room.avatar, - onEdit: room.canSendEvent('m.room.avatar') - ? controller.setAvatarAction - : null), - ), - ), - ], - body: MaxWidthBody( - child: ListView.builder( - itemCount: controller.members.length + - 1 + - (canRequestMoreMembers ? 1 : 0), - itemBuilder: (BuildContext context, int i) => i == 0 - ? Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ListTile( - leading: room.canSendEvent('m.room.topic') - ? CircleAvatar( - backgroundColor: Theme.of(context) - .scaffoldBackgroundColor, - foregroundColor: Colors.grey, - radius: Avatar.defaultSize / 2, - child: Icon(Icons.edit_outlined), - ) - : null, - title: Text( - '${L10n.of(context).groupDescription}:', - style: TextStyle( - color: Theme.of(context).accentColor, - fontWeight: FontWeight.bold)), - subtitle: LinkText( - text: room.topic?.isEmpty ?? true - ? L10n.of(context).addGroupDescription - : room.topic, - linkStyle: TextStyle(color: Colors.blueAccent), - textStyle: TextStyle( - fontSize: 14, - color: Theme.of(context) - .textTheme - .bodyText2 - .color, - ), - onLinkTap: (url) => - UrlLauncher(context, url).launchUrl(), - ), - onTap: room.canSendEvent('m.room.topic') - ? controller.setTopicAction - : null, - ), - Divider(thickness: 1), - ListTile( - title: Text( - L10n.of(context).settings, - style: TextStyle( - color: Theme.of(context).accentColor, - fontWeight: FontWeight.bold, - ), - ), - ), - if (room.canSendEvent('m.room.name')) - ListTile( - leading: CircleAvatar( - backgroundColor: - Theme.of(context).scaffoldBackgroundColor, - foregroundColor: Colors.grey, - child: Icon(Icons.people_outlined), - ), - title: Text( - L10n.of(context).changeTheNameOfTheGroup), - subtitle: Text(room.getLocalizedDisplayname( - MatrixLocals(L10n.of(context)))), - onTap: controller.setDisplaynameAction, - ), - if (room.canSendEvent('m.room.canonical_alias') && - room.joinRules == JoinRules.public) - ListTile( - leading: CircleAvatar( - backgroundColor: - Theme.of(context).scaffoldBackgroundColor, - foregroundColor: Colors.grey, - child: Icon(Icons.link_outlined), - ), - onTap: () => - controller.setCanonicalAliasAction(context), - title: Text(L10n.of(context).setInvitationLink), - subtitle: Text( - (room.canonicalAlias?.isNotEmpty ?? false) - ? room.canonicalAlias - : L10n.of(context).none), - ), - ListTile( - leading: CircleAvatar( - backgroundColor: - Theme.of(context).scaffoldBackgroundColor, - foregroundColor: Colors.grey, - child: Icon(Icons.insert_emoticon_outlined), - ), - title: Text(L10n.of(context).emoteSettings), - subtitle: Text(L10n.of(context).setCustomEmotes), - onTap: controller.goToEmoteSettings, - ), - PopupMenuButton( - onSelected: controller.setJoinRulesAction, - itemBuilder: (BuildContext context) => - >[ - if (room.canChangeJoinRules) - PopupMenuItem( - value: JoinRules.public, - child: Text(JoinRules.public - .getLocalizedString( - MatrixLocals(L10n.of(context)))), - ), - if (room.canChangeJoinRules) - PopupMenuItem( - value: JoinRules.invite, - child: Text(JoinRules.invite - .getLocalizedString( - MatrixLocals(L10n.of(context)))), - ), - ], - child: ListTile( - leading: CircleAvatar( - backgroundColor: Theme.of(context) - .scaffoldBackgroundColor, - foregroundColor: Colors.grey, - child: Icon(Icons.public_outlined)), - title: Text(L10n.of(context) - .whoIsAllowedToJoinThisGroup), - subtitle: Text( - room.joinRules.getLocalizedString( - MatrixLocals(L10n.of(context))), - ), - ), - ), - PopupMenuButton( - onSelected: controller.setHistoryVisibilityAction, - itemBuilder: (BuildContext context) => - >[ - if (room.canChangeHistoryVisibility) - PopupMenuItem( - value: HistoryVisibility.invited, - child: Text(HistoryVisibility.invited - .getLocalizedString( - MatrixLocals(L10n.of(context)))), - ), - if (room.canChangeHistoryVisibility) - PopupMenuItem( - value: HistoryVisibility.joined, - child: Text(HistoryVisibility.joined - .getLocalizedString( - MatrixLocals(L10n.of(context)))), - ), - if (room.canChangeHistoryVisibility) - PopupMenuItem( - value: HistoryVisibility.shared, - child: Text(HistoryVisibility.shared - .getLocalizedString( - MatrixLocals(L10n.of(context)))), - ), - if (room.canChangeHistoryVisibility) - PopupMenuItem( - value: HistoryVisibility.world_readable, - child: Text(HistoryVisibility.world_readable - .getLocalizedString( - MatrixLocals(L10n.of(context)))), - ), - ], - child: ListTile( - leading: CircleAvatar( - backgroundColor: - Theme.of(context).scaffoldBackgroundColor, - foregroundColor: Colors.grey, - child: Icon(Icons.visibility_outlined), - ), - title: Text(L10n.of(context) - .visibilityOfTheChatHistory), - subtitle: Text( - room.historyVisibility.getLocalizedString( - MatrixLocals(L10n.of(context))), - ), - ), - ), - if (room.joinRules == JoinRules.public) - PopupMenuButton( - onSelected: controller.setGuestAccessAction, - itemBuilder: (BuildContext context) => - >[ - if (room.canChangeGuestAccess) - PopupMenuItem( - value: GuestAccess.can_join, - child: Text( - GuestAccess.can_join.getLocalizedString( - MatrixLocals(L10n.of(context))), - ), - ), - if (room.canChangeGuestAccess) - PopupMenuItem( - value: GuestAccess.forbidden, - child: Text( - GuestAccess.forbidden - .getLocalizedString( - MatrixLocals(L10n.of(context))), - ), - ), - ], - child: ListTile( - leading: CircleAvatar( - backgroundColor: Theme.of(context) - .scaffoldBackgroundColor, - foregroundColor: Colors.grey, - child: Icon(Icons.info_outline), - ), - title: Text( - L10n.of(context).areGuestsAllowedToJoin), - subtitle: Text( - room.guestAccess.getLocalizedString( - MatrixLocals(L10n.of(context))), - ), - ), - ), - ListTile( - title: Text(L10n.of(context).editChatPermissions), - subtitle: Text( - L10n.of(context).whoCanPerformWhichAction), - leading: CircleAvatar( - backgroundColor: - Theme.of(context).scaffoldBackgroundColor, - foregroundColor: Colors.grey, - child: Icon(Icons.edit_attributes_outlined), - ), - onTap: () => AdaptivePageLayout.of(context) - .pushNamed('/rooms/${room.id}/permissions'), - ), - Divider(thickness: 1), - ListTile( - title: Text( - actualMembersCount > 1 - ? L10n.of(context).countParticipants( - actualMembersCount.toString()) - : L10n.of(context).emptyChat, - style: TextStyle( - color: Theme.of(context).accentColor, - fontWeight: FontWeight.bold, - ), - ), - ), - room.canInvite - ? ListTile( - title: Text(L10n.of(context).inviteContact), - leading: CircleAvatar( - backgroundColor: - Theme.of(context).primaryColor, - foregroundColor: Colors.white, - radius: Avatar.defaultSize / 2, - child: Icon(Icons.add_outlined), - ), - onTap: () => AdaptivePageLayout.of(context) - .pushNamed('/rooms/${room.id}/invite'), - ) - : Container(), - ], - ) - : i < controller.members.length + 1 - ? ParticipantListItem(controller.members[i - 1]) - : ListTile( - title: Text(L10n.of(context) - .loadCountMoreParticipants( - (actualMembersCount - - controller.members.length) - .toString())), - leading: CircleAvatar( - backgroundColor: - Theme.of(context).scaffoldBackgroundColor, - child: Icon( - Icons.refresh, - color: Colors.grey, - ), - ), - onTap: controller.requestMoreMembersAction, - ), - ), - ), - ), - ); - }); + void setTopicAction() async { + final room = Matrix.of(context).client.getRoomById(widget.roomId); + final input = await showTextInputDialog( + context: context, + title: L10n.of(context).setGroupDescription, + okLabel: L10n.of(context).ok, + cancelLabel: L10n.of(context).cancel, + useRootNavigator: false, + textFields: [ + DialogTextField( + hintText: L10n.of(context).setGroupDescription, + initialText: room.topic, + minLines: 1, + maxLines: 4, + ) + ], + ); + if (input == null) return; + final success = await showFutureLoadingDialog( + context: context, + future: () => room.setDescription(input.single), + ); + if (success.error == null) { + AdaptivePageLayout.of(context).showSnackBar(SnackBar( + content: Text(L10n.of(context).groupDescriptionHasBeenChanged))); + } + } + + void setGuestAccessAction(GuestAccess guestAccess) => showFutureLoadingDialog( + context: context, + future: () => Matrix.of(context) + .client + .getRoomById(widget.roomId) + .setGuestAccess(guestAccess), + ); + + void setHistoryVisibilityAction(HistoryVisibility historyVisibility) => + showFutureLoadingDialog( + context: context, + future: () => Matrix.of(context) + .client + .getRoomById(widget.roomId) + .setHistoryVisibility(historyVisibility), + ); + + void setJoinRulesAction(JoinRules joinRule) => showFutureLoadingDialog( + context: context, + future: () => Matrix.of(context) + .client + .getRoomById(widget.roomId) + .setJoinRules(joinRule), + ); + + void goToEmoteSettings() async { + final room = Matrix.of(context).client.getRoomById(widget.roomId); + // okay, we need to test if there are any emote state events other than the default one + // if so, we need to be directed to a selection screen for which pack we want to look at + // otherwise, we just open the normal one. + if ((room.states['im.ponies.room_emotes'] ?? {}) + .keys + .any((String s) => s.isNotEmpty)) { + await AdaptivePageLayout.of(context) + .pushNamed('/rooms/${room.id}/emotes'); + } else { + await AdaptivePageLayout.of(context) + .pushNamed('/settings/emotes', arguments: {'room': room}); + } + } + + void setAvatarAction() async { + MatrixFile file; + if (PlatformInfos.isMobile) { + final result = await ImagePicker().getImage( + source: ImageSource.gallery, + imageQuality: 50, + maxWidth: 1600, + maxHeight: 1600); + if (result == null) return; + file = MatrixFile( + bytes: await result.readAsBytes(), + name: result.path, + ); + } else { + final result = await FilePickerCross.importFromStorage( + type: FileTypeCross.image, + ); + if (result == null) return; + file = MatrixFile( + bytes: result.toUint8List(), + name: result.fileName, + ); + } + final room = Matrix.of(context).client.getRoomById(widget.roomId); + + final success = await showFutureLoadingDialog( + context: context, + future: () => room.setAvatar(file), + ); + if (success.error == null) { + AdaptivePageLayout.of(context).showSnackBar( + SnackBar(content: Text(L10n.of(context).avatarHasBeenChanged))); + } } + + void requestMoreMembersAction() async { + final room = Matrix.of(context).client.getRoomById(widget.roomId); + final participants = await showFutureLoadingDialog( + context: context, future: () => room.requestParticipants()); + if (participants.error == null) { + setState(() => members = participants.result); + } + } + + @override + Widget build(BuildContext context) => ChatDetailsUI(this); } diff --git a/lib/controllers/chat_encryption_settings_controller.dart b/lib/views/chat_encryption_settings.dart similarity index 89% rename from lib/controllers/chat_encryption_settings_controller.dart rename to lib/views/chat_encryption_settings.dart index 1fd265c9e..6f77aa500 100644 --- a/lib/controllers/chat_encryption_settings_controller.dart +++ b/lib/views/chat_encryption_settings.dart @@ -1,9 +1,9 @@ import 'package:famedlysdk/encryption.dart'; import 'package:famedlysdk/famedlysdk.dart'; -import 'package:fluffychat/views/chat_encryption_settings_view.dart'; +import 'package:fluffychat/views/ui/chat_encryption_settings_ui.dart'; import 'package:fluffychat/views/widgets/matrix.dart'; import 'package:flutter/material.dart'; -import '../views/widgets/dialogs/key_verification_dialog.dart'; +import 'widgets/dialogs/key_verification_dialog.dart'; class ChatEncryptionSettings extends StatefulWidget { final String id; @@ -61,5 +61,5 @@ class ChatEncryptionSettingsController extends State { } @override - Widget build(BuildContext context) => ChatEncryptionSettingsView(this); + Widget build(BuildContext context) => ChatEncryptionSettingsUI(this); } diff --git a/lib/controllers/chat_list_controller.dart b/lib/views/chat_list.dart similarity index 97% rename from lib/controllers/chat_list_controller.dart rename to lib/views/chat_list.dart index 5e1035acb..fbbc91220 100644 --- a/lib/controllers/chat_list_controller.dart +++ b/lib/views/chat_list.dart @@ -5,7 +5,7 @@ import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:adaptive_page_layout/adaptive_page_layout.dart'; import 'package:famedlysdk/famedlysdk.dart'; import 'package:fluffychat/utils/fluffy_share.dart'; -import 'package:fluffychat/views/chat_list_view.dart'; +import 'package:fluffychat/views/ui/chat_list_ui.dart'; import 'package:flutter/cupertino.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/utils/platform_infos.dart'; @@ -13,7 +13,7 @@ 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 '../views/widgets/matrix.dart'; +import 'widgets/matrix.dart'; import '../utils/matrix_file_extension.dart'; import '../utils/url_launcher.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -224,7 +224,7 @@ class ChatListController extends State { } @override - Widget build(BuildContext context) => ChatListView(this); + Widget build(BuildContext context) => ChatListUI(this); } enum ChatListPopupMenuItemActions { diff --git a/lib/controllers/chat_permissions_settings_controller.dart b/lib/views/chat_permissions_settings.dart similarity index 95% rename from lib/controllers/chat_permissions_settings_controller.dart rename to lib/views/chat_permissions_settings.dart index 741dc6dcd..1dfd3a316 100644 --- a/lib/controllers/chat_permissions_settings_controller.dart +++ b/lib/views/chat_permissions_settings.dart @@ -2,7 +2,7 @@ import 'dart:developer'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:adaptive_page_layout/adaptive_page_layout.dart'; -import 'package:fluffychat/views/chat_permissions_settings_view.dart'; +import 'package:fluffychat/views/ui/chat_permissions_settings_ui.dart'; import 'package:fluffychat/views/widgets/dialogs/permission_slider_dialog.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:fluffychat/views/widgets/matrix.dart'; @@ -92,5 +92,5 @@ class ChatPermissionsSettingsController extends State { } @override - Widget build(BuildContext context) => ChatPermissionsSettingsView(this); + Widget build(BuildContext context) => ChatPermissionsSettingsUI(this); } diff --git a/lib/controllers/device_settings_controller.dart b/lib/views/device_settings.dart similarity index 96% rename from lib/controllers/device_settings_controller.dart rename to lib/views/device_settings.dart index 10b40a73c..c5578c79b 100644 --- a/lib/controllers/device_settings_controller.dart +++ b/lib/views/device_settings.dart @@ -1,13 +1,13 @@ import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:famedlysdk/encryption/utils/key_verification.dart'; import 'package:famedlysdk/famedlysdk.dart'; -import 'package:fluffychat/views/device_settings_view.dart'; +import 'package:fluffychat/views/ui/device_settings_ui.dart'; import 'package:fluffychat/views/widgets/dialogs/key_verification_dialog.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import '../views/widgets/matrix.dart'; +import 'widgets/matrix.dart'; class DevicesSettings extends StatefulWidget { @override @@ -136,5 +136,5 @@ class DevicesSettingsController extends State { ..sort((a, b) => b.lastSeenTs.compareTo(a.lastSeenTs)); @override - Widget build(BuildContext context) => DevicesSettingsView(this); + Widget build(BuildContext context) => DevicesSettingsUI(this); } diff --git a/lib/controllers/homeserver_picker_controller.dart b/lib/views/homeserver_picker.dart similarity index 97% rename from lib/controllers/homeserver_picker_controller.dart rename to lib/views/homeserver_picker.dart index b0afa5329..c582fbb66 100644 --- a/lib/controllers/homeserver_picker_controller.dart +++ b/lib/views/homeserver_picker.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:adaptive_page_layout/adaptive_page_layout.dart'; import 'package:famedlysdk/famedlysdk.dart'; -import 'package:fluffychat/views/homeserver_picker_view.dart'; +import 'package:fluffychat/views/ui/homeserver_picker_ui.dart'; import 'package:fluffychat/views/widgets/matrix.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/setting_keys.dart'; @@ -130,5 +130,5 @@ class HomeserverPickerController extends State { } @override - Widget build(BuildContext context) => HomeserverPickerView(this); + Widget build(BuildContext context) => HomeserverPickerUI(this); } diff --git a/lib/controllers/image_viewer_controller.dart b/lib/views/image_viewer.dart similarity index 91% rename from lib/controllers/image_viewer_controller.dart rename to lib/views/image_viewer.dart index 773b60910..e120ec21e 100644 --- a/lib/controllers/image_viewer_controller.dart +++ b/lib/views/image_viewer.dart @@ -1,7 +1,7 @@ import 'package:adaptive_page_layout/adaptive_page_layout.dart'; import 'package:famedlysdk/famedlysdk.dart'; import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/views/image_viewer_view.dart'; +import 'package:fluffychat/views/ui/image_viewer_ui.dart'; import 'package:fluffychat/views/widgets/matrix.dart'; import 'package:flutter/material.dart'; @@ -38,5 +38,5 @@ class ImageViewerController extends State { } @override - Widget build(BuildContext context) => ImageViewerView(this); + Widget build(BuildContext context) => ImageViewerUI(this); } diff --git a/lib/controllers/invitation_selection_controller.dart b/lib/views/invitation_selection.dart similarity index 96% rename from lib/controllers/invitation_selection_controller.dart rename to lib/views/invitation_selection.dart index e904a977e..a5f9f5437 100644 --- a/lib/controllers/invitation_selection_controller.dart +++ b/lib/views/invitation_selection.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:adaptive_page_layout/adaptive_page_layout.dart'; import 'package:famedlysdk/famedlysdk.dart'; -import 'package:fluffychat/views/invitation_selection_view.dart'; +import 'package:fluffychat/views/ui/invitation_selection_ui.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:fluffychat/views/widgets/matrix.dart'; import 'package:flutter/material.dart'; @@ -116,5 +116,5 @@ class InvitationSelectionController extends State { } @override - Widget build(BuildContext context) => InvitationSelectionView(this); + Widget build(BuildContext context) => InvitationSelectionUI(this); } diff --git a/lib/controllers/new_group_controller.dart b/lib/views/new_group.dart similarity index 92% rename from lib/controllers/new_group_controller.dart rename to lib/views/new_group.dart index 28bc651ef..a0384e388 100644 --- a/lib/controllers/new_group_controller.dart +++ b/lib/views/new_group.dart @@ -1,6 +1,6 @@ import 'package:adaptive_page_layout/adaptive_page_layout.dart'; import 'package:famedlysdk/famedlysdk.dart' as sdk; -import 'package:fluffychat/views/new_group_view.dart'; +import 'package:fluffychat/views/ui/new_group_ui.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:fluffychat/views/widgets/matrix.dart'; import 'package:flutter/material.dart'; @@ -40,5 +40,5 @@ class NewGroupController extends State { } @override - Widget build(BuildContext context) => NewGroupView(this); + Widget build(BuildContext context) => NewGroupUI(this); } diff --git a/lib/controllers/new_private_chat_controller.dart b/lib/views/new_private_chat.dart similarity index 96% rename from lib/controllers/new_private_chat_controller.dart rename to lib/views/new_private_chat.dart index 2a99c5d1f..4440aaefc 100644 --- a/lib/controllers/new_private_chat_controller.dart +++ b/lib/views/new_private_chat.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:adaptive_page_layout/adaptive_page_layout.dart'; import 'package:famedlysdk/famedlysdk.dart'; import 'package:fluffychat/utils/fluffy_share.dart'; -import 'package:fluffychat/views/new_private_chat_view.dart'; +import 'package:fluffychat/views/ui/new_private_chat_ui.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:fluffychat/views/widgets/matrix.dart'; import 'package:flutter/material.dart'; @@ -112,5 +112,5 @@ class NewPrivateChatController extends State { ); @override - Widget build(BuildContext context) => NewPrivateChatView(this); + Widget build(BuildContext context) => NewPrivateChatUI(this); } diff --git a/lib/controllers/sign_up_controller.dart b/lib/views/sign_up.dart similarity index 94% rename from lib/controllers/sign_up_controller.dart rename to lib/views/sign_up.dart index eaa0f1b2a..a1b73cf07 100644 --- a/lib/controllers/sign_up_controller.dart +++ b/lib/views/sign_up.dart @@ -1,7 +1,7 @@ import 'package:adaptive_page_layout/adaptive_page_layout.dart'; import 'package:famedlysdk/famedlysdk.dart'; import 'package:file_picker_cross/file_picker_cross.dart'; -import 'package:fluffychat/views/sign_up_view.dart'; +import 'package:fluffychat/views/ui/sign_up_ui.dart'; import 'package:fluffychat/views/widgets/matrix.dart'; import 'package:flutter/cupertino.dart'; @@ -67,5 +67,5 @@ class SignUpController extends State { } @override - Widget build(BuildContext context) => SignUpView(this); + Widget build(BuildContext context) => SignUpUI(this); } diff --git a/lib/controllers/sign_up_password_controller.dart b/lib/views/sign_up_password.dart similarity index 97% rename from lib/controllers/sign_up_password_controller.dart rename to lib/views/sign_up_password.dart index 3df67182f..c994e9e9f 100644 --- a/lib/controllers/sign_up_password_controller.dart +++ b/lib/views/sign_up_password.dart @@ -2,7 +2,7 @@ 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/sign_up_password_view.dart'; +import 'package:fluffychat/views/ui/sign_up_password_ui.dart'; import 'package:fluffychat/views/widgets/matrix.dart'; import 'package:flutter/material.dart'; @@ -127,5 +127,5 @@ class SignUpPasswordController extends State { } @override - Widget build(BuildContext context) => SignUpPasswordView(this); + Widget build(BuildContext context) => SignUpPasswordUI(this); } diff --git a/lib/views/archive_view.dart b/lib/views/ui/archive_ui.dart similarity index 87% rename from lib/views/archive_view.dart rename to lib/views/ui/archive_ui.dart index fafff851d..d741b721c 100644 --- a/lib/views/archive_view.dart +++ b/lib/views/ui/archive_ui.dart @@ -1,13 +1,13 @@ import 'package:famedlysdk/famedlysdk.dart'; -import 'package:fluffychat/controllers/archive_controller.dart'; +import 'package:fluffychat/views/archive.dart'; import 'package:fluffychat/views/widgets/list_items/chat_list_item.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -class ArchiveView extends StatelessWidget { +class ArchiveUI extends StatelessWidget { final ArchiveController controller; - const ArchiveView(this.controller, {Key key}) : super(key: key); + const ArchiveUI(this.controller, {Key key}) : super(key: key); @override Widget build(BuildContext context) { diff --git a/lib/views/ui/chat_details_ui.dart b/lib/views/ui/chat_details_ui.dart new file mode 100644 index 000000000..2baf5cf44 --- /dev/null +++ b/lib/views/ui/chat_details_ui.dart @@ -0,0 +1,369 @@ +import 'package:adaptive_page_layout/adaptive_page_layout.dart'; +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/views/chat_details.dart'; +import 'package:fluffychat/views/widgets/avatar.dart'; +import 'package:fluffychat/views/widgets/matrix.dart'; +import 'package:fluffychat/utils/fluffy_share.dart'; + +import 'package:famedlysdk/famedlysdk.dart'; + +import 'package:fluffychat/views/widgets/chat_settings_popup_menu.dart'; +import 'package:fluffychat/views/widgets/content_banner.dart'; +import 'package:fluffychat/views/widgets/max_width_body.dart'; +import 'package:fluffychat/views/widgets/list_items/participant_list_item.dart'; +import 'package:fluffychat/utils/matrix_locals.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix_link_text/link_text.dart'; + +import '../../utils/url_launcher.dart'; + +class ChatDetailsUI extends StatelessWidget { + final ChatDetailsController controller; + + const ChatDetailsUI(this.controller, {Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final room = + Matrix.of(context).client.getRoomById(controller.widget.roomId); + if (room == null) { + return Scaffold( + appBar: AppBar( + leading: BackButton(), + title: Text(L10n.of(context).oopsSomethingWentWrong), + ), + body: Center( + child: Text(L10n.of(context).youAreNoLongerParticipatingInThisChat), + ), + ); + } + + controller.members.removeWhere((u) => u.membership == Membership.leave); + final actualMembersCount = + room.mInvitedMemberCount + room.mJoinedMemberCount; + final canRequestMoreMembers = + controller.members.length < actualMembersCount; + return StreamBuilder( + stream: room.onUpdate.stream, + builder: (context, snapshot) { + 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, + actions: [ + if (room.canonicalAlias?.isNotEmpty ?? false) + IconButton( + tooltip: L10n.of(context).share, + icon: Icon(Icons.share_outlined), + onPressed: () => FluffyShare.share( + AppConfig.inviteLinkPrefix + room.canonicalAlias, + context), + ), + ChatSettingsPopupMenu(room, false) + ], + title: Text( + room.getLocalizedDisplayname( + MatrixLocals(L10n.of(context))), + style: TextStyle( + color: Theme.of(context) + .appBarTheme + .textTheme + .headline6 + .color)), + backgroundColor: Theme.of(context).appBarTheme.color, + flexibleSpace: FlexibleSpaceBar( + background: ContentBanner(room.avatar, + onEdit: room.canSendEvent('m.room.avatar') + ? controller.setAvatarAction + : null), + ), + ), + ], + body: MaxWidthBody( + child: ListView.builder( + itemCount: controller.members.length + + 1 + + (canRequestMoreMembers ? 1 : 0), + itemBuilder: (BuildContext context, int i) => i == 0 + ? Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ListTile( + leading: room.canSendEvent('m.room.topic') + ? CircleAvatar( + backgroundColor: Theme.of(context) + .scaffoldBackgroundColor, + foregroundColor: Colors.grey, + radius: Avatar.defaultSize / 2, + child: Icon(Icons.edit_outlined), + ) + : null, + title: Text( + '${L10n.of(context).groupDescription}:', + style: TextStyle( + color: Theme.of(context).accentColor, + fontWeight: FontWeight.bold)), + subtitle: LinkText( + text: room.topic?.isEmpty ?? true + ? L10n.of(context).addGroupDescription + : room.topic, + linkStyle: TextStyle(color: Colors.blueAccent), + textStyle: TextStyle( + fontSize: 14, + color: Theme.of(context) + .textTheme + .bodyText2 + .color, + ), + onLinkTap: (url) => + UrlLauncher(context, url).launchUrl(), + ), + onTap: room.canSendEvent('m.room.topic') + ? controller.setTopicAction + : null, + ), + Divider(thickness: 1), + ListTile( + title: Text( + L10n.of(context).settings, + style: TextStyle( + color: Theme.of(context).accentColor, + fontWeight: FontWeight.bold, + ), + ), + ), + if (room.canSendEvent('m.room.name')) + ListTile( + leading: CircleAvatar( + backgroundColor: + Theme.of(context).scaffoldBackgroundColor, + foregroundColor: Colors.grey, + child: Icon(Icons.people_outlined), + ), + title: Text( + L10n.of(context).changeTheNameOfTheGroup), + subtitle: Text(room.getLocalizedDisplayname( + MatrixLocals(L10n.of(context)))), + onTap: controller.setDisplaynameAction, + ), + if (room.canSendEvent('m.room.canonical_alias') && + room.joinRules == JoinRules.public) + ListTile( + leading: CircleAvatar( + backgroundColor: + Theme.of(context).scaffoldBackgroundColor, + foregroundColor: Colors.grey, + child: Icon(Icons.link_outlined), + ), + onTap: () => + controller.setCanonicalAliasAction(context), + title: Text(L10n.of(context).setInvitationLink), + subtitle: Text( + (room.canonicalAlias?.isNotEmpty ?? false) + ? room.canonicalAlias + : L10n.of(context).none), + ), + ListTile( + leading: CircleAvatar( + backgroundColor: + Theme.of(context).scaffoldBackgroundColor, + foregroundColor: Colors.grey, + child: Icon(Icons.insert_emoticon_outlined), + ), + title: Text(L10n.of(context).emoteSettings), + subtitle: Text(L10n.of(context).setCustomEmotes), + onTap: controller.goToEmoteSettings, + ), + PopupMenuButton( + onSelected: controller.setJoinRulesAction, + itemBuilder: (BuildContext context) => + >[ + if (room.canChangeJoinRules) + PopupMenuItem( + value: JoinRules.public, + child: Text(JoinRules.public + .getLocalizedString( + MatrixLocals(L10n.of(context)))), + ), + if (room.canChangeJoinRules) + PopupMenuItem( + value: JoinRules.invite, + child: Text(JoinRules.invite + .getLocalizedString( + MatrixLocals(L10n.of(context)))), + ), + ], + child: ListTile( + leading: CircleAvatar( + backgroundColor: Theme.of(context) + .scaffoldBackgroundColor, + foregroundColor: Colors.grey, + child: Icon(Icons.public_outlined)), + title: Text(L10n.of(context) + .whoIsAllowedToJoinThisGroup), + subtitle: Text( + room.joinRules.getLocalizedString( + MatrixLocals(L10n.of(context))), + ), + ), + ), + PopupMenuButton( + onSelected: controller.setHistoryVisibilityAction, + itemBuilder: (BuildContext context) => + >[ + if (room.canChangeHistoryVisibility) + PopupMenuItem( + value: HistoryVisibility.invited, + child: Text(HistoryVisibility.invited + .getLocalizedString( + MatrixLocals(L10n.of(context)))), + ), + if (room.canChangeHistoryVisibility) + PopupMenuItem( + value: HistoryVisibility.joined, + child: Text(HistoryVisibility.joined + .getLocalizedString( + MatrixLocals(L10n.of(context)))), + ), + if (room.canChangeHistoryVisibility) + PopupMenuItem( + value: HistoryVisibility.shared, + child: Text(HistoryVisibility.shared + .getLocalizedString( + MatrixLocals(L10n.of(context)))), + ), + if (room.canChangeHistoryVisibility) + PopupMenuItem( + value: HistoryVisibility.world_readable, + child: Text(HistoryVisibility.world_readable + .getLocalizedString( + MatrixLocals(L10n.of(context)))), + ), + ], + child: ListTile( + leading: CircleAvatar( + backgroundColor: + Theme.of(context).scaffoldBackgroundColor, + foregroundColor: Colors.grey, + child: Icon(Icons.visibility_outlined), + ), + title: Text(L10n.of(context) + .visibilityOfTheChatHistory), + subtitle: Text( + room.historyVisibility.getLocalizedString( + MatrixLocals(L10n.of(context))), + ), + ), + ), + if (room.joinRules == JoinRules.public) + PopupMenuButton( + onSelected: controller.setGuestAccessAction, + itemBuilder: (BuildContext context) => + >[ + if (room.canChangeGuestAccess) + PopupMenuItem( + value: GuestAccess.can_join, + child: Text( + GuestAccess.can_join.getLocalizedString( + MatrixLocals(L10n.of(context))), + ), + ), + if (room.canChangeGuestAccess) + PopupMenuItem( + value: GuestAccess.forbidden, + child: Text( + GuestAccess.forbidden + .getLocalizedString( + MatrixLocals(L10n.of(context))), + ), + ), + ], + child: ListTile( + leading: CircleAvatar( + backgroundColor: Theme.of(context) + .scaffoldBackgroundColor, + foregroundColor: Colors.grey, + child: Icon(Icons.info_outline), + ), + title: Text( + L10n.of(context).areGuestsAllowedToJoin), + subtitle: Text( + room.guestAccess.getLocalizedString( + MatrixLocals(L10n.of(context))), + ), + ), + ), + ListTile( + title: Text(L10n.of(context).editChatPermissions), + subtitle: Text( + L10n.of(context).whoCanPerformWhichAction), + leading: CircleAvatar( + backgroundColor: + Theme.of(context).scaffoldBackgroundColor, + foregroundColor: Colors.grey, + child: Icon(Icons.edit_attributes_outlined), + ), + onTap: () => AdaptivePageLayout.of(context) + .pushNamed('/rooms/${room.id}/permissions'), + ), + Divider(thickness: 1), + ListTile( + title: Text( + actualMembersCount > 1 + ? L10n.of(context).countParticipants( + actualMembersCount.toString()) + : L10n.of(context).emptyChat, + style: TextStyle( + color: Theme.of(context).accentColor, + fontWeight: FontWeight.bold, + ), + ), + ), + room.canInvite + ? ListTile( + title: Text(L10n.of(context).inviteContact), + leading: CircleAvatar( + backgroundColor: + Theme.of(context).primaryColor, + foregroundColor: Colors.white, + radius: Avatar.defaultSize / 2, + child: Icon(Icons.add_outlined), + ), + onTap: () => AdaptivePageLayout.of(context) + .pushNamed('/rooms/${room.id}/invite'), + ) + : Container(), + ], + ) + : i < controller.members.length + 1 + ? ParticipantListItem(controller.members[i - 1]) + : ListTile( + title: Text(L10n.of(context) + .loadCountMoreParticipants( + (actualMembersCount - + controller.members.length) + .toString())), + leading: CircleAvatar( + backgroundColor: + Theme.of(context).scaffoldBackgroundColor, + child: Icon( + Icons.refresh, + color: Colors.grey, + ), + ), + onTap: controller.requestMoreMembersAction, + ), + ), + ), + ), + ); + }); + } +} diff --git a/lib/views/chat_encryption_settings_view.dart b/lib/views/ui/chat_encryption_settings_ui.dart similarity index 97% rename from lib/views/chat_encryption_settings_view.dart rename to lib/views/ui/chat_encryption_settings_ui.dart index eff25e08e..d666dbc09 100644 --- a/lib/views/chat_encryption_settings_view.dart +++ b/lib/views/ui/chat_encryption_settings_ui.dart @@ -1,17 +1,16 @@ import 'package:famedlysdk/famedlysdk.dart'; -import 'package:fluffychat/controllers/chat_encryption_settings_controller.dart'; +import 'package:fluffychat/views/chat_encryption_settings.dart'; import 'package:fluffychat/views/widgets/avatar.dart'; import 'package:fluffychat/views/widgets/matrix.dart'; import 'package:fluffychat/views/widgets/max_width_body.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import '../utils/device_extension.dart'; +import '../../utils/device_extension.dart'; -class ChatEncryptionSettingsView extends StatelessWidget { +class ChatEncryptionSettingsUI extends StatelessWidget { final ChatEncryptionSettingsController controller; - const ChatEncryptionSettingsView(this.controller, {Key key}) - : super(key: key); + const ChatEncryptionSettingsUI(this.controller, {Key key}) : super(key: key); @override Widget build(BuildContext context) { diff --git a/lib/views/chat_list_view.dart b/lib/views/ui/chat_list_ui.dart similarity index 98% rename from lib/views/chat_list_view.dart rename to lib/views/ui/chat_list_ui.dart index ebe750fee..c2a1ce662 100644 --- a/lib/views/chat_list_view.dart +++ b/lib/views/ui/chat_list_ui.dart @@ -1,19 +1,19 @@ import 'package:adaptive_page_layout/adaptive_page_layout.dart'; import 'package:famedlysdk/famedlysdk.dart'; -import 'package:fluffychat/controllers/chat_list_controller.dart'; +import 'package:fluffychat/views/chat_list.dart'; import 'package:fluffychat/views/widgets/connection_status_header.dart'; import 'package:fluffychat/views/widgets/list_items/chat_list_item.dart'; import 'package:flutter/cupertino.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'widgets/matrix.dart'; +import '../widgets/matrix.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -class ChatListView extends StatelessWidget { +class ChatListUI extends StatelessWidget { final ChatListController controller; - const ChatListView(this.controller, {Key key}) : super(key: key); + const ChatListUI(this.controller, {Key key}) : super(key: key); @override Widget build(BuildContext context) { diff --git a/lib/views/chat_permissions_settings_view.dart b/lib/views/ui/chat_permissions_settings_ui.dart similarity index 95% rename from lib/views/chat_permissions_settings_view.dart rename to lib/views/ui/chat_permissions_settings_ui.dart index b448c09ea..65d84c11b 100644 --- a/lib/views/chat_permissions_settings_view.dart +++ b/lib/views/ui/chat_permissions_settings_ui.dart @@ -1,4 +1,4 @@ -import 'package:fluffychat/controllers/chat_permissions_settings_controller.dart'; +import 'package:fluffychat/views/chat_permissions_settings.dart'; import 'package:fluffychat/views/widgets/list_items/permission_list_tile.dart'; import 'package:fluffychat/views/widgets/max_width_body.dart'; import 'package:fluffychat/views/widgets/matrix.dart'; @@ -7,11 +7,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:famedlysdk/famedlysdk.dart'; -class ChatPermissionsSettingsView extends StatelessWidget { +class ChatPermissionsSettingsUI extends StatelessWidget { final ChatPermissionsSettingsController controller; - const ChatPermissionsSettingsView(this.controller, {Key key}) - : super(key: key); + const ChatPermissionsSettingsUI(this.controller, {Key key}) : super(key: key); @override Widget build(BuildContext context) { diff --git a/lib/views/ui/chat_ui.dart b/lib/views/ui/chat_ui.dart new file mode 100644 index 000000000..6b00ae612 --- /dev/null +++ b/lib/views/ui/chat_ui.dart @@ -0,0 +1,747 @@ +import 'dart:math'; +import 'dart:ui'; + +import 'package:adaptive_page_layout/adaptive_page_layout.dart'; +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:fluffychat/views/chat.dart'; +import 'package:fluffychat/views/widgets/avatar.dart'; +import 'package:fluffychat/views/widgets/chat_settings_popup_menu.dart'; +import 'package:fluffychat/views/widgets/connection_status_header.dart'; +import 'package:fluffychat/views/widgets/input_bar.dart'; +import 'package:fluffychat/views/widgets/unread_badge_back_button.dart'; +import 'package:fluffychat/config/themes.dart'; + +import 'package:future_loading_dialog/future_loading_dialog.dart'; +import 'package:fluffychat/views/widgets/encryption_button.dart'; +import 'package:fluffychat/views/widgets/list_items/message.dart'; +import 'package:fluffychat/views/widgets/matrix.dart'; +import 'package:fluffychat/views/widgets/reply_content.dart'; +import 'package:fluffychat/views/widgets/user_bottom_sheet.dart'; +import 'package:fluffychat/config/app_emojis.dart'; +import 'package:fluffychat/utils/matrix_locals.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/utils/room_status_extension.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:scroll_to_index/scroll_to_index.dart'; +import 'package:swipe_to_action/swipe_to_action.dart'; + +class ChatUI extends StatelessWidget { + final ChatController controller; + + const ChatUI(this.controller, {Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + controller.matrix = Matrix.of(context); + final client = controller.matrix.client; + controller.room ??= client.getRoomById(controller.widget.id); + if (controller.room == null) { + return Scaffold( + appBar: AppBar( + title: Text(L10n.of(context).oopsSomethingWentWrong), + ), + body: Center( + child: Text(L10n.of(context).youAreNoLongerParticipatingInThisChat), + ), + ); + } + controller.matrix.client.activeRoomId = controller.widget.id; + + if (controller.room.membership == Membership.invite) { + showFutureLoadingDialog( + context: context, future: () => controller.room.join()); + } + + return Scaffold( + appBar: AppBar( + leading: controller.selectMode + ? IconButton( + icon: Icon(Icons.close), + onPressed: controller.clearSelectedEvents, + tooltip: L10n.of(context).close, + ) + : AdaptivePageLayout.of(context).columnMode(context) + ? null + : UnreadBadgeBackButton(roomId: controller.widget.id), + titleSpacing: + AdaptivePageLayout.of(context).columnMode(context) ? null : 0, + title: controller.selectedEvents.isEmpty + ? StreamBuilder( + stream: controller.room.onUpdate.stream, + builder: (context, snapshot) => ListTile( + leading: Avatar( + controller.room.avatar, controller.room.displayname), + contentPadding: EdgeInsets.zero, + onTap: controller.room.isDirectChat + ? () => showModalBottomSheet( + context: context, + builder: (c) => UserBottomSheet( + user: controller.room.getUserByMXIDSync( + controller.room.directChatMatrixID), + onMention: () => controller + .sendController.text += + '${controller.room.directChatMatrixID} ', + ), + ) + : () => (!AdaptivePageLayout.of(context) + .columnMode(context) || + AdaptivePageLayout.of(context) + .viewDataStack + .length < + 3) + ? AdaptivePageLayout.of(context).pushNamed( + '/rooms/${controller.room.id}/details') + : null, + title: Text( + controller.room.getLocalizedDisplayname( + MatrixLocals(L10n.of(context))), + maxLines: 1), + subtitle: controller.room + .getLocalizedTypingText(context) + .isEmpty + ? StreamBuilder( + stream: Matrix.of(context) + .client + .onPresence + .stream + .where((p) => + p.senderId == + controller.room.directChatMatrixID), + builder: (context, snapshot) => Text( + controller.room.getLocalizedStatus(context), + maxLines: 1, + //overflow: TextOverflow.ellipsis, + )) + : Row( + children: [ + Icon(Icons.edit_outlined, + color: Theme.of(context).accentColor, + size: 13), + SizedBox(width: 4), + Expanded( + child: Text( + controller.room + .getLocalizedTypingText(context), + maxLines: 1, + style: TextStyle( + color: Theme.of(context).accentColor, + fontStyle: FontStyle.italic, + ), + ), + ), + ], + ), + )) + : Text(L10n.of(context) + .numberSelected(controller.selectedEvents.length.toString())), + actions: controller.selectMode + ? [ + if (controller.selectedEvents.length == 1 && + controller.selectedEvents.first.status > 0 && + controller.selectedEvents.first.senderId == client.userID) + IconButton( + icon: Icon(Icons.edit_outlined), + tooltip: L10n.of(context).edit, + onPressed: controller.editSelectedEventAction, + ), + PopupMenuButton( + onSelected: controller.onEventActionPopupMenuSelected, + itemBuilder: (_) => [ + PopupMenuItem( + value: 'copy', + child: Text(L10n.of(context).copy), + ), + if (controller.canRedactSelectedEvents) + PopupMenuItem( + value: 'redact', + child: Text( + L10n.of(context).redactMessage, + style: TextStyle(color: Colors.orange), + ), + ), + if (controller.selectedEvents.length == 1) + PopupMenuItem( + value: 'report', + child: Text( + L10n.of(context).reportMessage, + style: TextStyle(color: Colors.red), + ), + ), + ], + ), + ] + : [ + if (controller.room.canSendDefaultStates) + IconButton( + tooltip: L10n.of(context).videoCall, + icon: Icon(Icons.video_call_outlined), + onPressed: controller.startCallAction, + ), + ChatSettingsPopupMenu( + controller.room, !controller.room.isDirectChat), + ], + ), + floatingActionButton: controller.showScrollDownButton + ? Padding( + padding: const EdgeInsets.only(bottom: 56.0), + child: FloatingActionButton( + onPressed: controller.scrollDown, + foregroundColor: Theme.of(context).textTheme.bodyText2.color, + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + mini: true, + child: Icon(Icons.arrow_downward_outlined, + color: Theme.of(context).primaryColor), + ), + ) + : null, + body: Stack( + children: [ + if (Matrix.of(context).wallpaper != null) + Image.file( + Matrix.of(context).wallpaper, + width: double.infinity, + height: double.infinity, + fit: BoxFit.cover, + ), + SafeArea( + child: Column( + children: [ + ConnectionStatusHeader(), + if (controller.room.getState(EventTypes.RoomTombstone) != null) + Container( + height: 72, + child: Material( + color: Theme.of(context).secondaryHeaderColor, + child: ListTile( + leading: CircleAvatar( + foregroundColor: Theme.of(context).accentColor, + backgroundColor: Theme.of(context).backgroundColor, + child: Icon(Icons.upgrade_outlined), + ), + title: Text( + controller.room + .getState(EventTypes.RoomTombstone) + .parsedTombstoneContent + .body, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: Text(L10n.of(context).goToTheNewRoom), + onTap: controller.goToNewRoomAction, + ), + ), + ), + Expanded( + child: FutureBuilder( + future: controller.getTimeline(), + builder: (BuildContext context, snapshot) { + if (!snapshot.hasData) { + return Center( + child: CircularProgressIndicator(), + ); + } + + // create a map of eventId --> index to greatly improve performance of + // ListView's findChildIndexCallback + final thisEventsKeyMap = {}; + for (var i = 0; + i < controller.filteredEvents.length; + i++) { + thisEventsKeyMap[controller.filteredEvents[i].eventId] = + i; + } + + final horizontalPadding = max( + 0, + (MediaQuery.of(context).size.width - + FluffyThemes.columnWidth * + (AdaptivePageLayout.of(context) + .currentViewData + .rightView != + null + ? 4.5 + : 3.5)) / + 2) + .toDouble(); + + return ListView.custom( + padding: EdgeInsets.only( + top: 16, + left: horizontalPadding, + right: horizontalPadding, + ), + reverse: true, + controller: controller.scrollController, + childrenDelegate: SliverChildBuilderDelegate( + (BuildContext context, int i) { + return i == controller.filteredEvents.length + 1 + ? controller.timeline.isRequestingHistory + ? Container( + height: 50, + alignment: Alignment.center, + padding: EdgeInsets.all(8), + child: CircularProgressIndicator(), + ) + : controller.canLoadMore + ? TextButton( + onPressed: + controller.requestHistory, + child: Text( + L10n.of(context).loadMore, + style: TextStyle( + color: Theme.of(context) + .primaryColor, + fontWeight: FontWeight.bold, + decoration: + TextDecoration.underline, + ), + ), + ) + : Container() + : i == 0 + ? StreamBuilder( + stream: controller.room.onUpdate.stream, + builder: (_, __) { + final seenByText = controller.room + .getLocalizedSeenByText( + context, + controller.timeline, + controller.filteredEvents, + controller.unfolded, + ); + return AnimatedContainer( + height: seenByText.isEmpty ? 0 : 24, + duration: seenByText.isEmpty + ? Duration(milliseconds: 0) + : Duration(milliseconds: 300), + alignment: controller.filteredEvents + .first.senderId == + client.userID + ? Alignment.topRight + : Alignment.topLeft, + padding: EdgeInsets.only( + left: 8, + right: 8, + bottom: 8, + ), + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 4), + decoration: BoxDecoration( + color: Theme.of(context) + .scaffoldBackgroundColor + .withOpacity(0.8), + borderRadius: + BorderRadius.circular(4), + ), + child: Text( + seenByText, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Theme.of(context) + .accentColor, + ), + ), + ), + ); + }, + ) + : AutoScrollTag( + key: ValueKey(controller + .filteredEvents[i - 1].eventId), + index: i - 1, + controller: controller.scrollController, + child: Swipeable( + key: ValueKey(controller + .filteredEvents[i - 1].eventId), + background: Padding( + padding: EdgeInsets.symmetric( + horizontal: 12.0), + child: Center( + child: Icon(Icons.reply_outlined), + ), + ), + direction: SwipeDirection.endToStart, + onSwipe: (direction) => + controller.replyAction( + replyTo: controller + .filteredEvents[i - 1]), + child: Message( + controller.filteredEvents[i - 1], + onAvatarTab: (Event event) => + showModalBottomSheet( + context: context, + builder: (c) => + UserBottomSheet( + user: event.sender, + onMention: () => controller + .sendController + .text += + '${event.senderId} ', + ), + ), + unfold: controller.unfold, + onSelect: + controller.onSelectMessage, + scrollToEventId: + (String eventId) => controller + .scrollToEventId(eventId), + longPressSelect: controller + .selectedEvents.isEmpty, + selected: controller + .selectedEvents + .contains(controller + .filteredEvents[i - 1]), + timeline: controller.timeline, + nextEvent: i >= 2 + ? controller + .filteredEvents[i - 2] + : null), + ), + ); + }, + childCount: controller.filteredEvents.length + 2, + findChildIndexCallback: (key) => controller + .findChildIndexCallback(key, thisEventsKeyMap), + ), + ); + }, + ), + ), + AnimatedContainer( + duration: Duration(milliseconds: 300), + height: (controller.editEvent == null && + controller.replyEvent == null && + controller.room.canSendDefaultMessages && + controller.selectedEvents.length == 1) + ? 56 + : 0, + child: Material( + color: Theme.of(context).secondaryHeaderColor, + child: Builder(builder: (context) { + if (!(controller.editEvent == null && + controller.replyEvent == null && + controller.selectedEvents.length == 1)) { + return Container(); + } + final emojis = List.from(AppEmojis.emojis); + final allReactionEvents = controller.selectedEvents.first + .aggregatedEvents( + controller.timeline, RelationshipTypes.Reaction) + ?.where((event) => + event.senderId == event.room.client.userID && + event.type == 'm.reaction'); + + allReactionEvents.forEach((event) { + try { + emojis.remove(event.content['m.relates_to']['key']); + } catch (_) {} + }); + return ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: emojis.length + 1, + itemBuilder: (c, i) => i == emojis.length + ? InkWell( + borderRadius: BorderRadius.circular(8), + onTap: () => controller + .pickEmojiAction(allReactionEvents), + child: Container( + width: 56, + height: 56, + alignment: Alignment.center, + child: Icon(Icons.add_outlined), + ), + ) + : InkWell( + borderRadius: BorderRadius.circular(8), + onTap: () => + controller.sendEmojiAction(emojis[i]), + child: Container( + width: 56, + height: 56, + alignment: Alignment.center, + child: Text( + emojis[i], + style: TextStyle(fontSize: 30), + ), + ), + ), + ); + }), + ), + ), + AnimatedContainer( + duration: Duration(milliseconds: 300), + height: controller.editEvent != null || + controller.replyEvent != null + ? 56 + : 0, + child: Material( + color: Theme.of(context).secondaryHeaderColor, + child: Row( + children: [ + IconButton( + tooltip: L10n.of(context).close, + icon: Icon(Icons.close), + onPressed: controller.cancelReplyEventAction, + ), + Expanded( + child: controller.replyEvent != null + ? ReplyContent(controller.replyEvent, + timeline: controller.timeline) + : _EditContent(controller.editEvent + ?.getDisplayEvent(controller.timeline)), + ), + ], + ), + ), + ), + Divider( + height: 1, + thickness: 1, + ), + controller.room.canSendDefaultMessages && + controller.room.membership == Membership.join + ? Container( + decoration: BoxDecoration( + color: Theme.of(context).backgroundColor, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: controller.selectMode + ? [ + Container( + height: 56, + child: TextButton( + onPressed: controller.forwardEventsAction, + child: Row( + children: [ + Icon(Icons + .keyboard_arrow_left_outlined), + Text(L10n.of(context).forward), + ], + ), + ), + ), + controller.selectedEvents.length == 1 + ? controller.selectedEvents.first + .getDisplayEvent( + controller.timeline) + .status > + 0 + ? Container( + height: 56, + child: TextButton( + onPressed: + controller.replyAction, + child: Row( + children: [ + Text( + L10n.of(context).reply), + Icon(Icons + .keyboard_arrow_right), + ], + ), + ), + ) + : Container( + height: 56, + child: TextButton( + onPressed: + controller.sendAgainAction, + child: Row( + children: [ + Text(L10n.of(context) + .tryToSendAgain), + SizedBox(width: 4), + Icon(Icons.send_outlined, + size: 16), + ], + ), + ), + ) + : Container(), + ] + : [ + if (controller.inputText.isEmpty) + Container( + height: 56, + alignment: Alignment.center, + child: PopupMenuButton( + icon: Icon(Icons.add_outlined), + onSelected: controller + .onAddPopupMenuButtonSelected, + itemBuilder: (BuildContext context) => + >[ + PopupMenuItem( + value: 'file', + child: ListTile( + leading: CircleAvatar( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + child: Icon( + Icons.attachment_outlined), + ), + title: Text( + L10n.of(context).sendFile), + contentPadding: EdgeInsets.all(0), + ), + ), + PopupMenuItem( + value: 'image', + child: ListTile( + leading: CircleAvatar( + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + child: + Icon(Icons.image_outlined), + ), + title: Text( + L10n.of(context).sendImage), + contentPadding: EdgeInsets.all(0), + ), + ), + if (PlatformInfos.isMobile) + PopupMenuItem( + value: 'camera', + child: ListTile( + leading: CircleAvatar( + backgroundColor: + Colors.purple, + foregroundColor: Colors.white, + child: Icon(Icons + .camera_alt_outlined), + ), + title: Text(L10n.of(context) + .openCamera), + contentPadding: + EdgeInsets.all(0), + ), + ), + if (PlatformInfos.isMobile) + PopupMenuItem( + value: 'voice', + child: ListTile( + leading: CircleAvatar( + backgroundColor: Colors.red, + foregroundColor: Colors.white, + child: Icon( + Icons.mic_none_outlined), + ), + title: Text(L10n.of(context) + .voiceMessage), + contentPadding: + EdgeInsets.all(0), + ), + ), + ], + ), + ), + Container( + height: 56, + alignment: Alignment.center, + child: EncryptionButton(controller.room), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 4.0), + child: InputBar( + room: controller.room, + minLines: 1, + maxLines: kIsWeb ? 1 : 8, + autofocus: !PlatformInfos.isMobile, + keyboardType: !PlatformInfos.isMobile + ? TextInputType.text + : TextInputType.multiline, + onSubmitted: + controller.onInputBarSubmitted, + focusNode: controller.inputFocus, + controller: controller.sendController, + decoration: InputDecoration( + hintText: + L10n.of(context).writeAMessage, + hintMaxLines: 1, + border: InputBorder.none, + enabledBorder: InputBorder.none, + filled: false, + ), + onChanged: controller.onInputBarChanged, + ), + ), + ), + if (PlatformInfos.isMobile && + controller.inputText.isEmpty) + Container( + height: 56, + alignment: Alignment.center, + child: IconButton( + tooltip: L10n.of(context).voiceMessage, + icon: Icon(Icons.mic_none_outlined), + onPressed: + controller.voiceMessageAction, + ), + ), + if (!PlatformInfos.isMobile || + controller.inputText.isNotEmpty) + Container( + height: 56, + alignment: Alignment.center, + child: IconButton( + icon: Icon(Icons.send_outlined), + onPressed: controller.send, + tooltip: L10n.of(context).send, + ), + ), + ], + ), + ) + : Container(), + ], + ), + ), + ], + ), + ); + } +} + +class _EditContent extends StatelessWidget { + final Event event; + + _EditContent(this.event); + + @override + Widget build(BuildContext context) { + if (event == null) { + return Container(); + } + return Row( + children: [ + Icon( + Icons.edit, + color: Theme.of(context).primaryColor, + ), + Container(width: 15.0), + Text( + event?.getLocalizedBody( + MatrixLocals(L10n.of(context)), + withSenderNamePrefix: false, + hideReply: true, + ) ?? + '', + overflow: TextOverflow.ellipsis, + maxLines: 1, + style: TextStyle( + color: Theme.of(context).textTheme.bodyText2.color, + ), + ), + ], + ); + } +} diff --git a/lib/views/device_settings_view.dart b/lib/views/ui/device_settings_ui.dart similarity index 93% rename from lib/views/device_settings_view.dart rename to lib/views/ui/device_settings_ui.dart index 3b606d20a..dec6b633d 100644 --- a/lib/views/device_settings_view.dart +++ b/lib/views/ui/device_settings_ui.dart @@ -1,14 +1,14 @@ -import 'package:fluffychat/controllers/device_settings_controller.dart'; +import 'package:fluffychat/views/device_settings.dart'; import 'package:fluffychat/views/widgets/max_width_body.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'widgets/list_items/user_device_list_item.dart'; +import '../widgets/list_items/user_device_list_item.dart'; -class DevicesSettingsView extends StatelessWidget { +class DevicesSettingsUI extends StatelessWidget { final DevicesSettingsController controller; - const DevicesSettingsView(this.controller, {Key key}) : super(key: key); + const DevicesSettingsUI(this.controller, {Key key}) : super(key: key); @override Widget build(BuildContext context) { diff --git a/lib/views/empty_page.dart b/lib/views/ui/empty_page_ui.dart similarity index 100% rename from lib/views/empty_page.dart rename to lib/views/ui/empty_page_ui.dart diff --git a/lib/views/homeserver_picker_view.dart b/lib/views/ui/homeserver_picker_ui.dart similarity index 94% rename from lib/views/homeserver_picker_view.dart rename to lib/views/ui/homeserver_picker_ui.dart index a19b788ba..5c41fa4dd 100644 --- a/lib/views/homeserver_picker_view.dart +++ b/lib/views/ui/homeserver_picker_ui.dart @@ -1,4 +1,4 @@ -import '../controllers/homeserver_picker_controller.dart'; +import '../homeserver_picker.dart'; import 'package:fluffychat/views/widgets/default_app_bar_search_field.dart'; import 'package:fluffychat/views/widgets/fluffy_banner.dart'; import 'package:fluffychat/config/app_config.dart'; @@ -10,13 +10,10 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; -class HomeserverPickerView extends StatelessWidget { +class HomeserverPickerUI extends StatelessWidget { final HomeserverPickerController controller; - const HomeserverPickerView( - this.controller, { - Key key, - }) : super(key: key); + const HomeserverPickerUI(this.controller, {Key key}) : super(key: key); @override Widget build(BuildContext context) { diff --git a/lib/views/image_viewer_view.dart b/lib/views/ui/image_viewer_ui.dart similarity index 90% rename from lib/views/image_viewer_view.dart rename to lib/views/ui/image_viewer_ui.dart index 3b67ce570..3951dd242 100644 --- a/lib/views/image_viewer_view.dart +++ b/lib/views/ui/image_viewer_ui.dart @@ -1,12 +1,12 @@ -import '../controllers/image_viewer_controller.dart'; +import '../image_viewer.dart'; import 'package:fluffychat/views/widgets/image_bubble.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -class ImageViewerView extends StatelessWidget { +class ImageViewerUI extends StatelessWidget { final ImageViewerController controller; - const ImageViewerView(this.controller, {Key key}) : super(key: key); + const ImageViewerUI(this.controller, {Key key}) : super(key: key); @override Widget build(BuildContext context) { diff --git a/lib/views/invitation_selection_view.dart b/lib/views/ui/invitation_selection_ui.dart similarity index 93% rename from lib/views/invitation_selection_view.dart rename to lib/views/ui/invitation_selection_ui.dart index 0b4bc5622..c36fd68e8 100644 --- a/lib/views/invitation_selection_view.dart +++ b/lib/views/ui/invitation_selection_ui.dart @@ -1,4 +1,4 @@ -import 'package:fluffychat/controllers/invitation_selection_controller.dart'; +import 'package:fluffychat/views/invitation_selection.dart'; import 'package:fluffychat/views/widgets/default_app_bar_search_field.dart'; import 'package:famedlysdk/famedlysdk.dart'; @@ -8,13 +8,10 @@ import 'package:fluffychat/views/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -class InvitationSelectionView extends StatelessWidget { +class InvitationSelectionUI extends StatelessWidget { final InvitationSelectionController controller; - const InvitationSelectionView( - this.controller, { - Key key, - }) : super(key: key); + const InvitationSelectionUI(this.controller, {Key key}) : super(key: key); @override Widget build(BuildContext context) { diff --git a/lib/views/login.dart b/lib/views/ui/login_ui.dart similarity index 99% rename from lib/views/login.dart rename to lib/views/ui/login_ui.dart index 00806c4b9..35bb82f61 100644 --- a/lib/views/login.dart +++ b/lib/views/ui/login_ui.dart @@ -9,7 +9,7 @@ import 'package:fluffychat/views/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import '../utils/platform_infos.dart'; +import '../../utils/platform_infos.dart'; import 'package:email_validator/email_validator.dart'; class Login extends StatefulWidget { diff --git a/lib/views/new_group_view.dart b/lib/views/ui/new_group_ui.dart similarity index 89% rename from lib/views/new_group_view.dart rename to lib/views/ui/new_group_ui.dart index cadbef40b..8aaa733d0 100644 --- a/lib/views/new_group_view.dart +++ b/lib/views/ui/new_group_ui.dart @@ -1,15 +1,12 @@ -import 'package:fluffychat/controllers/new_group_controller.dart'; +import 'package:fluffychat/views/new_group.dart'; import 'package:fluffychat/views/widgets/max_width_body.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -class NewGroupView extends StatelessWidget { +class NewGroupUI extends StatelessWidget { final NewGroupController controller; - const NewGroupView( - this.controller, { - Key key, - }) : super(key: key); + const NewGroupUI(this.controller, {Key key}) : super(key: key); @override Widget build(BuildContext context) { diff --git a/lib/views/new_private_chat_view.dart b/lib/views/ui/new_private_chat_ui.dart similarity index 96% rename from lib/views/new_private_chat_view.dart rename to lib/views/ui/new_private_chat_ui.dart index 2b57b80a7..b9ff44d1d 100644 --- a/lib/views/new_private_chat_view.dart +++ b/lib/views/ui/new_private_chat_ui.dart @@ -1,5 +1,5 @@ import 'package:adaptive_page_layout/adaptive_page_layout.dart'; -import 'package:fluffychat/controllers/new_private_chat_controller.dart'; +import 'package:fluffychat/views/new_private_chat.dart'; import 'package:fluffychat/views/widgets/avatar.dart'; import 'package:fluffychat/views/widgets/contacts_list.dart'; import 'package:fluffychat/views/widgets/max_width_body.dart'; @@ -8,10 +8,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:famedlysdk/famedlysdk.dart'; -class NewPrivateChatView extends StatelessWidget { +class NewPrivateChatUI extends StatelessWidget { final NewPrivateChatController controller; - const NewPrivateChatView(this.controller, {Key key}) : super(key: key); + const NewPrivateChatUI(this.controller, {Key key}) : super(key: key); @override Widget build(BuildContext context) { diff --git a/lib/views/search_view.dart b/lib/views/ui/search_ui.dart similarity index 99% rename from lib/views/search_view.dart rename to lib/views/ui/search_ui.dart index 710567c7e..550ac7e91 100644 --- a/lib/views/search_view.dart +++ b/lib/views/ui/search_ui.dart @@ -11,7 +11,7 @@ import 'package:fluffychat/views/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; -import '../utils/localized_exception_extension.dart'; +import '../../utils/localized_exception_extension.dart'; class SearchView extends StatefulWidget { final String alias; diff --git a/lib/views/settings_3pid.dart b/lib/views/ui/settings_3pid_ui.dart similarity index 100% rename from lib/views/settings_3pid.dart rename to lib/views/ui/settings_3pid_ui.dart diff --git a/lib/views/settings_emotes.dart b/lib/views/ui/settings_emotes_ui.dart similarity index 99% rename from lib/views/settings_emotes.dart rename to lib/views/ui/settings_emotes_ui.dart index 983944f53..22bef2172 100644 --- a/lib/views/settings_emotes.dart +++ b/lib/views/ui/settings_emotes_ui.dart @@ -13,7 +13,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:image_picker/image_picker.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; -import '../views/widgets/matrix.dart'; +import '../widgets/matrix.dart'; class EmotesSettings extends StatefulWidget { final Room room; diff --git a/lib/views/settings_ignore_list.dart b/lib/views/ui/settings_ignore_list_ui.dart similarity index 99% rename from lib/views/settings_ignore_list.dart rename to lib/views/ui/settings_ignore_list_ui.dart index 5509af666..7e50c25f3 100644 --- a/lib/views/settings_ignore_list.dart +++ b/lib/views/ui/settings_ignore_list_ui.dart @@ -5,7 +5,7 @@ import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import '../views/widgets/matrix.dart'; +import '../widgets/matrix.dart'; class SettingsIgnoreList extends StatefulWidget { final String initialUserId; diff --git a/lib/views/settings_multiple_emotes.dart b/lib/views/ui/settings_multiple_emotes_ui.dart similarity index 100% rename from lib/views/settings_multiple_emotes.dart rename to lib/views/ui/settings_multiple_emotes_ui.dart diff --git a/lib/views/settings_notifications.dart b/lib/views/ui/settings_notifications_ui.dart similarity index 98% rename from lib/views/settings_notifications.dart rename to lib/views/ui/settings_notifications_ui.dart index 6b82f7e20..6bcade3c2 100644 --- a/lib/views/settings_notifications.dart +++ b/lib/views/ui/settings_notifications_ui.dart @@ -9,9 +9,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:open_noti_settings/open_noti_settings.dart'; -import '../utils/localized_exception_extension.dart'; +import '../../utils/localized_exception_extension.dart'; -import '../views/widgets/matrix.dart'; +import '../widgets/matrix.dart'; class NotificationSettingsItem { final PushRuleKind type; diff --git a/lib/views/settings_style.dart b/lib/views/ui/settings_style_ui.dart similarity index 98% rename from lib/views/settings_style.dart rename to lib/views/ui/settings_style_ui.dart index a50227648..271972a7e 100644 --- a/lib/views/settings_style.dart +++ b/lib/views/ui/settings_style_ui.dart @@ -7,8 +7,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:image_picker/image_picker.dart'; -import '../config/app_config.dart'; -import '../views/widgets/matrix.dart'; +import '../../config/app_config.dart'; +import '../widgets/matrix.dart'; class SettingsStyle extends StatefulWidget { @override diff --git a/lib/views/settings.dart b/lib/views/ui/settings_ui.dart similarity index 99% rename from lib/views/settings.dart rename to lib/views/ui/settings_ui.dart index f79d0b56e..c73849629 100644 --- a/lib/views/settings.dart +++ b/lib/views/ui/settings_ui.dart @@ -21,11 +21,11 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:image_picker/image_picker.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../views/widgets/content_banner.dart'; +import '../widgets/content_banner.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; -import '../views/widgets/matrix.dart'; -import '../config/app_config.dart'; -import '../config/setting_keys.dart'; +import '../widgets/matrix.dart'; +import '../../config/app_config.dart'; +import '../../config/setting_keys.dart'; class Settings extends StatefulWidget { @override diff --git a/lib/views/sign_up_password_view.dart b/lib/views/ui/sign_up_password_ui.dart similarity index 91% rename from lib/views/sign_up_password_view.dart rename to lib/views/ui/sign_up_password_ui.dart index b94a9dccf..7c3432b8b 100644 --- a/lib/views/sign_up_password_view.dart +++ b/lib/views/ui/sign_up_password_ui.dart @@ -1,16 +1,13 @@ -import 'package:fluffychat/controllers/sign_up_password_controller.dart'; +import 'package:fluffychat/views/sign_up_password.dart'; import 'package:fluffychat/views/widgets/one_page_card.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -class SignUpPasswordView extends StatelessWidget { +class SignUpPasswordUI extends StatelessWidget { final SignUpPasswordController controller; - const SignUpPasswordView( - this.controller, { - Key key, - }) : super(key: key); + const SignUpPasswordUI(this.controller, {Key key}) : super(key: key); @override Widget build(BuildContext context) { diff --git a/lib/views/sign_up_view.dart b/lib/views/ui/sign_up_ui.dart similarity index 95% rename from lib/views/sign_up_view.dart rename to lib/views/ui/sign_up_ui.dart index d62a36724..5fc359240 100644 --- a/lib/views/sign_up_view.dart +++ b/lib/views/ui/sign_up_ui.dart @@ -1,5 +1,5 @@ import 'package:adaptive_page_layout/adaptive_page_layout.dart'; -import 'package:fluffychat/controllers/sign_up_controller.dart'; +import 'package:fluffychat/views/sign_up.dart'; import 'package:fluffychat/views/widgets/fluffy_banner.dart'; import 'package:fluffychat/views/widgets/matrix.dart'; @@ -8,13 +8,10 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -class SignUpView extends StatelessWidget { +class SignUpUI extends StatelessWidget { final SignUpController controller; - const SignUpView( - this.controller, { - Key key, - }) : super(key: key); + const SignUpUI(this.controller, {Key key}) : super(key: key); @override Widget build(BuildContext context) { diff --git a/lib/views/widgets/image_bubble.dart b/lib/views/widgets/image_bubble.dart index f4d623702..6d1ae3d52 100644 --- a/lib/views/widgets/image_bubble.dart +++ b/lib/views/widgets/image_bubble.dart @@ -1,5 +1,5 @@ import 'package:famedlysdk/famedlysdk.dart'; -import 'package:fluffychat/controllers/image_viewer_controller.dart'; +import 'package:fluffychat/views/image_viewer.dart'; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_blurhash/flutter_blurhash.dart'; diff --git a/lib/views/widgets/matrix.dart b/lib/views/widgets/matrix.dart index 202eac48a..d52cfab2a 100644 --- a/lib/views/widgets/matrix.dart +++ b/lib/views/widgets/matrix.dart @@ -19,7 +19,7 @@ import 'package:provider/provider.dart'; import 'package:universal_html/prefer_universal/html.dart' as html; import 'package:http/http.dart' as http; import 'package:url_launcher/url_launcher.dart'; -/*import 'package:fluffychat/views/chat.dart'; +/*import 'package:fluffychat/views/chat_ui.dart'; import 'package:fluffychat/app_config.dart'; import 'package:dbus/dbus.dart'; import 'package:desktop_notifications/desktop_notifications.dart';*/ diff --git a/test/homeserver_picker_test.dart b/test/homeserver_picker_test.dart index d13dcb31f..6d141ad03 100644 --- a/test/homeserver_picker_test.dart +++ b/test/homeserver_picker_test.dart @@ -1,4 +1,4 @@ -import 'package:fluffychat/controllers/homeserver_picker_controller.dart'; +import 'package:fluffychat/views/homeserver_picker.dart'; import 'package:fluffychat/main.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/test/sign_up_password_test.dart b/test/sign_up_password_test.dart index 4d552cdf0..75bf36c33 100644 --- a/test/sign_up_password_test.dart +++ b/test/sign_up_password_test.dart @@ -1,4 +1,4 @@ -import 'package:fluffychat/controllers/sign_up_password_controller.dart'; +import 'package:fluffychat/views/sign_up_password.dart'; import 'package:fluffychat/main.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/test/sign_up_test.dart b/test/sign_up_test.dart index db426f102..1013c4b18 100644 --- a/test/sign_up_test.dart +++ b/test/sign_up_test.dart @@ -1,4 +1,4 @@ -import 'package:fluffychat/controllers/sign_up_controller.dart'; +import 'package:fluffychat/views/sign_up.dart'; import 'package:fluffychat/main.dart'; import 'package:flutter_test/flutter_test.dart';