diff --git a/CODE_STYLE.md b/CODE_STYLE.md index 90fd86ff..5e672832 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 +### 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 -- `/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 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 a5565fcb..c19b5d9a 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 00cd7005..00000000 --- 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 03bc0425..ce887dcb 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 becf30c4..bdb4ef20 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 7796c98d..c12a5e87 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), - ), + void clearSelectedEvents() => () => setState(() => selectedEvents.clear()); + + 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(); + } + + void onEventActionPopupMenuSelected(selected) { + switch (selected) { + case 'copy': + copyEventsAction(); + break; + case 'redact': + redactEventsAction(); + break; + case 'report': + reportEventAction(); + break; + } + } + + 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}'); + } + } + + 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), ); } - matrix.client.activeRoomId = widget.id; - - if (room.membership == Membership.invite) { - showFutureLoadingDialog(context: context, future: () => room.join()); - } - - 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(), - ); - } - - 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, - ); - } - - // 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; - } - - 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: _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'); - - 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(), - ], - ), - ), - ], - ), - ); } -} -class _EditContent extends StatelessWidget { - final Event event; + 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; + } - _EditContent(this.event); + void onInputBarSubmitted(String text) { + send(); + FocusScope.of(context).requestFocus(inputFocus); + } + + void onAddPopupMenuButtonSelected(String choice) { + if (choice == 'file') { + sendFileAction(); + } else if (choice == 'image') { + sendImageAction(); + } + if (choice == 'camera') { + openCameraAction(); + } + if (choice == 'voice') { + voiceMessageAction(); + } + } + + 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); + } + } + + 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 6982dd9b..ec739176 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'; -import '../utils/url_launcher.dart'; +class ChatDetails extends StatefulWidget { + final String roomId; -class ChatDetailsView extends StatelessWidget { - final ChatDetailsController controller; - - const ChatDetailsView(this.controller, {Key key}) : super(key: key); + const ChatDetails(this.roomId); @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), - ), + 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); - 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, - ), - ), - ), - ), - ); - }); + 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 1fd265c9..6f77aa50 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 5e1035ac..fbbc9122 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 741dc6dc..1dfd3a31 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 10b40a73..c5578c79 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 b0afa532..c582fbb6 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 773b6091..e120ec21 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 e904a977..a5f9f543 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 28bc651e..a0384e38 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 2a99c5d1..4440aaef 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 eaa0f1b2..a1b73cf0 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 3df67182..c994e9e9 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 fafff851..d741b721 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 00000000..2baf5cf4 --- /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 eff25e08..d666dbc0 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 ebe750fe..c2a1ce66 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 b448c09e..65d84c11 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 00000000..6b00ae61 --- /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 3b606d20..dec6b633 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 a19b788b..5c41fa4d 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 3b67ce57..3951dd24 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 0b4bc562..c36fd68e 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 00806c4b..35bb82f6 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 cadbef40..8aaa733d 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 2b57b80a..b9ff44d1 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 710567c7..550ac7e9 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 983944f5..22bef217 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 5509af66..7e50c25f 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 6b82f7e2..6bcade3c 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 a5022764..271972a7 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 f79d0b56..c7384962 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 b94a9dcc..7c3432b8 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 d62a3672..5fc35924 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 f4d62370..6d1ae3d5 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 202eac48..d52cfab2 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 d13dcb31..6d141ad0 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 4d552cdf..75bf36c3 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 db426f10..1013c4b1 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';