From f681ffa71fa93fa0af4e34cb55af329cd1234f73 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 10 Nov 2025 13:56:12 -0500 Subject: [PATCH] refactor: move all messaging sending logic into the chore controller, reduce full rebuilds of the chat view --- lib/pages/chat/chat.dart | 613 +++++++++--------- lib/pages/chat/chat_input_row.dart | 141 ++-- lib/pages/chat/chat_view.dart | 28 +- lib/pages/chat/events/message.dart | 22 +- lib/pages/chat/input_bar.dart | 302 +++++---- .../activity_stats_menu.dart | 263 ++++---- .../activity_user_summaries_widget.dart | 66 +- lib/pangea/chat/widgets/chat_input_bar.dart | 2 +- .../chat/widgets/pangea_chat_input_row.dart | 222 +++---- .../choreographer/assistance_state_enum.dart | 19 +- lib/pangea/choreographer/choreographer.dart | 147 ++--- .../choreographer_send_button.dart | 14 +- .../choreographer_state_extension.dart | 5 +- .../choreographer/igc/start_igc_button.dart | 209 +++--- .../choreographer/it/it_controller.dart | 11 +- .../pangea_message_content_model.dart | 21 + lib/pangea/common/constants/model_keys.dart | 1 - lib/pangea/common/utils/overlay.dart | 79 ++- .../common/widgets/transparent_backdrop.dart | 115 +--- .../extensions/room_events_extension.dart | 30 +- .../spaces/utils/load_participants_util.dart | 4 + .../widgets/message_selection_positioner.dart | 48 +- .../toolbar/widgets/select_mode_buttons.dart | 1 - .../widgets/word_zoom/word_zoom_widget.dart | 34 +- 24 files changed, 1182 insertions(+), 1215 deletions(-) create mode 100644 lib/pangea/choreographer/pangea_message_content_model.dart diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index b0f34935e..6d9129908 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -1,10 +1,23 @@ import 'dart:async'; -import 'dart:developer'; import 'dart:io'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + import 'package:collection/collection.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; +import 'package:go_router/go_router.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:just_audio/just_audio.dart'; +import 'package:matrix/matrix.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:scroll_to_index/scroll_to_index.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:universal_html/html.dart' as html; + import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/config/themes.dart'; @@ -26,14 +39,15 @@ import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart'; import 'package:fluffychat/pangea/bot/utils/bot_name.dart'; import 'package:fluffychat/pangea/chat/utils/unlocked_morphs_snackbar.dart'; import 'package:fluffychat/pangea/chat/widgets/event_too_large_dialog.dart'; +import 'package:fluffychat/pangea/choreographer/assistance_state_enum.dart'; import 'package:fluffychat/pangea/choreographer/choregrapher_user_settings_extension.dart'; import 'package:fluffychat/pangea/choreographer/choreo_record_model.dart'; import 'package:fluffychat/pangea/choreographer/choreographer.dart'; +import 'package:fluffychat/pangea/choreographer/choreographer_state_extension.dart'; import 'package:fluffychat/pangea/choreographer/choreographer_ui_extension.dart'; import 'package:fluffychat/pangea/choreographer/igc/pangea_match_state_model.dart'; import 'package:fluffychat/pangea/choreographer/text_editing/edit_type_enum.dart'; import 'package:fluffychat/pangea/choreographer/text_editing/pangea_text_controller.dart'; -import 'package:fluffychat/pangea/common/constants/model_keys.dart'; import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/common/utils/firebase_analytics.dart'; @@ -71,24 +85,18 @@ import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart' import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/share_scaffold_dialog.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:go_router/go_router.dart'; -import 'package:image_picker/image_picker.dart'; -import 'package:just_audio/just_audio.dart'; -import 'package:matrix/matrix.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:scroll_to_index/scroll_to_index.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:universal_html/html.dart' as html; - -import '../../utils/account_bundles.dart'; import '../../utils/localized_exception_extension.dart'; import 'send_file_dialog.dart'; import 'send_location_dialog.dart'; +// #Pangea +class _TimelineUpdateNotifier extends ChangeNotifier { + void notify() { + notifyListeners(); + } +} +// Pangea# + class ChatPage extends StatelessWidget { final String roomId; final List? shareItems; @@ -181,6 +189,7 @@ class ChatController extends State StreamSubscription? _levelSubscription; StreamSubscription? _analyticsSubscription; StreamSubscription? _botAudioSubscription; + final timelineUpdateNotifier = _TimelineUpdateNotifier(); // Pangea# Room get room => sendingClient.getRoomById(roomId) ?? widget.room; @@ -438,12 +447,6 @@ class ChatController extends State if (!mounted) return; Future.delayed(const Duration(seconds: 1), () async { if (!mounted) return; - debugPrint( - "chat.dart l1 ${pangeaController.languageController.userL1?.langCode}", - ); - debugPrint( - "chat.dart l2 ${pangeaController.languageController.userL2?.langCode}", - ); if (mounted) { pangeaController.languageController.showDialogOnEmptyLanguage( context, @@ -519,9 +522,6 @@ class ChatController extends State final event = Event.fromMatrixEvent(botAudioEvent, room); final audioFile = await event.getPangeaAudioFile(); - debugPrint( - "audiofile: ${audioFile?.mimeType} ${audioFile?.bytes.length}", - ); if (audioFile == null) return; if (!kIsWeb) { @@ -545,6 +545,9 @@ class ChatController extends State // Pangea# _tryLoadTimeline(); if (kIsWeb) { + // #Pangea + onFocusSub?.cancel(); + // Pangea# onFocusSub = html.window.onFocus.listen((_) => setReadMarker()); } } @@ -602,7 +605,10 @@ class ChatController extends State void updateView() { if (!mounted) return; setReadMarker(); - setState(() {}); + // #Pangea + // setState(() {}); + if (mounted) timelineUpdateNotifier.notify(); + // Pangea# } Future? loadTimelineFuture; @@ -611,14 +617,6 @@ class ChatController extends State void onInsert(int i) { // setState will be called by updateView() anyway - // #Pangea - // If fake event was sent, don't animate in the next event. - // It makes the replacement of the fake event jumpy. - if (_fakeEventIDs.isNotEmpty) { - animateInEventIndex = null; - return; - } - // Pangea# animateInEventIndex = i; } @@ -686,6 +684,7 @@ class ChatController extends State @override void didChangeAppLifecycleState(AppLifecycleState state) { // #Pangea + super.didChangeAppLifecycleState(state); // On iOS, if the toolbar is open and the app is closed, then the user goes // back to do more toolbar activities, the toolbar buttons / selection don't // update properly. So, when the user closes the app, close the toolbar overlay. @@ -785,6 +784,17 @@ class ChatController extends State // Pangea# onFocusSub?.cancel(); //#Pangea + WidgetsBinding.instance.removeObserver(this); + _storeInputTimeoutTimer?.cancel(); + _displayChatDetailsColumn.dispose(); + timelineUpdateNotifier.dispose(); + highlightedRole.dispose(); + showInstructions.dispose(); + showActivityDropdown.dispose(); + hasRainedConfetti.dispose(); + typingCoolDown?.cancel(); + typingTimeout?.cancel(); + scrollController.removeListener(_updateScrollController); choreographer.dispose(); MatrixState.pAnyState.closeAllOverlays(force: true); showToolbarStream.close(); @@ -826,33 +836,37 @@ class ChatController extends State // TextEditingController sendController = TextEditingController(); PangeaTextController get sendController => choreographer.textController; + // Pangea# + // #Pangea + // void setSendingClient(Client c) { + // // first cancel typing with the old sending client + // if (currentlyTyping) { + // // no need to have the setting typing to false be blocking + // typingCoolDown?.cancel(); + // typingCoolDown = null; + // room.setTyping(false); + // currentlyTyping = false; + // } + // // then cancel the old timeline + // // fixes bug with read reciepts and quick switching + // loadTimelineFuture = _getTimeline(eventContextId: room.fullyRead).onError( + // ErrorReporter( + // context, + // 'Unable to load timeline after changing sending Client', + // ).onErrorCallback, + // ); - void setSendingClient(Client c) { - // first cancel typing with the old sending client - if (currentlyTyping) { - // no need to have the setting typing to false be blocking - typingCoolDown?.cancel(); - typingCoolDown = null; - room.setTyping(false); - currentlyTyping = false; - } - // then cancel the old timeline - // fixes bug with read reciepts and quick switching - loadTimelineFuture = _getTimeline(eventContextId: room.fullyRead).onError( - ErrorReporter( - context, - 'Unable to load timeline after changing sending Client', - ).onErrorCallback, - ); + // // then set the new sending client + // setState(() => sendingClient = c); + // } - // then set the new sending client - setState(() => sendingClient = c); - } - - void setActiveClient(Client c) => setState(() { - Matrix.of(context).setActiveClient(c); - }); + // void setActiveClient(Client c) { + // setState(() { + // Matrix.of(context).setActiveClient(c); + // }); + // } + // Pangea# // #Pangea Event? pangeaEditingEvent; @@ -860,64 +874,38 @@ class ChatController extends State pangeaEditingEvent = null; } - final List _fakeEventIDs = []; - bool get obscureText => _fakeEventIDs.isNotEmpty; - /// Add a fake event to the timeline to visually indicate that a message is being sent. /// Used when tokenizing after message send, specifically because tokenization for some /// languages takes some time. - String? sendFakeMessage() { + Future sendFakeMessage(Event? edit, Event? reply) async { if (sendController.text.trim().isEmpty) return null; - - final eventID = room.sendFakeMessage( - text: sendController.text, - inReplyTo: replyEvent, - editEventId: editEvent?.eventId, - ); + final message = sendController.text; inputFocus.unfocus(); sendController.setSystemText("", EditTypeEnum.other); - setState(() => _fakeEventIDs.add(eventID)); - // wait for the next event to come through before clearing any fake event, - // to make the replacement look smooth - room.client.onTimelineEvent.stream - .firstWhere((event) => event.content[ModelKey.tempEventId] == eventID) - .then( - (_) => clearFakeEvent(eventID), - ); - - return eventID; - } - - void clearFakeEvent(String? eventId) { - if (eventId == null) return; - - final inTimeline = timeline != null && - timeline!.events.any( - (e) => e.eventId == eventId, - ); - - if (!inTimeline) return; - timeline?.events.removeWhere((e) => e.eventId == eventId); - - setState(() { - _fakeEventIDs.remove(eventId); - }); + return room.sendFakeMessage( + text: message, + inReplyTo: reply, + editEventId: edit?.eventId, + ); } // Future send() async { // Original send function gets the tx id within the matrix lib, // but for choero, the tx id is generated before the message send. // Also, adding PangeaMessageData - Future send({ - required String message, - PangeaRepresentation? originalSent, - PangeaRepresentation? originalWritten, - PangeaMessageTokens? tokensSent, - PangeaMessageTokens? tokensWritten, - ChoreoRecordModel? choreo, - String? tempEventId, - }) async { + Future send() async { + final message = sendController.text; + final edit = editEvent; + final reply = replyEvent; + editEvent = null; + replyEvent = null; + pendingText = ''; + + final tempEventId = await sendFakeMessage(edit, reply); + final content = await choreographer.getMessageContent(message); + choreographer.clear(); + if (message.trim().isEmpty) return; // if (sendController.text.trim().isEmpty) return; // Pangea# @@ -967,15 +955,15 @@ class ChatController extends State room .pangeaSendTextEvent( message, - inReplyTo: replyEvent, - editEventId: editEvent?.eventId, + inReplyTo: reply, + editEventId: edit?.eventId, parseCommands: parseCommands, - originalSent: originalSent, - originalWritten: originalWritten, - tokensSent: tokensSent, - tokensWritten: tokensWritten, - choreo: choreo, - tempEventId: tempEventId, + originalSent: content.originalSent, + originalWritten: content.originalWritten, + tokensSent: content.tokensSent, + tokensWritten: content.tokensWritten, + choreo: content.choreo, + txid: tempEventId, ) .then( (String? msgEventId) async { @@ -985,9 +973,9 @@ class ChatController extends State // stream sends the data for newly sent messages. _sendMessageAnalytics( msgEventId, - originalSent: originalSent, - tokensSent: tokensSent, - choreo: choreo, + originalSent: content.originalSent, + tokensSent: content.tokensSent, + choreo: content.choreo, ); if (previousEdit != null) { @@ -1017,7 +1005,6 @@ class ChatController extends State } }, ).catchError((err, s) { - clearFakeEvent(tempEventId); if (err is EventTooLarge) { showAdaptiveDialog( context: context, @@ -1036,20 +1023,20 @@ class ChatController extends State }, ); }); + // #Pangea // sendController.value = TextEditingValue( // text: pendingText, // selection: const TextSelection.collapsed(offset: 0), // ); - setState(() { - // #Pangea - // sendController.text = pendingText; - // Pangea# - _inputTextIsEmpty = pendingText.isEmpty; - replyEvent = null; - editEvent = null; - pendingText = ''; - }); + // setState(() { + // sendController.text = pendingText; + // _inputTextIsEmpty = pendingText.isEmpty; + // replyEvent = null; + // editEvent = null; + // pendingText = ''; + // }); + // Pangea# } void sendFileAction({FileSelectorType type = FileSelectorType.any}) async { @@ -1159,10 +1146,18 @@ class ChatController extends State name: result.fileName ?? audioFile.path, ); + // #Pangea + final reply = replyEvent; + replyEvent = null; + // Pangea# + await room .sendFileEvent( file, - inReplyTo: replyEvent, + // #Pangea + // inReplyTo: replyEvent, + inReplyTo: reply, + // Pangea# extraContent: { 'info': { ...file.info, @@ -1214,24 +1209,13 @@ class ChatController extends State // setState(() { // replyEvent = null; // }); - if (mounted) setState(() => replyEvent = null); // Pangea# } void hideEmojiPicker() { - // #Pangea - clearSelectedEvents(); - // Pangea# setState(() => showEmojiPicker = false); } - // #Pangea - void hideOverlayEmojiPicker() { - MatrixState.pAnyState.closeOverlay(); - setState(() => showEmojiPicker = false); - } - // Pangea - void emojiPickerAction() { if (showEmojiPicker) { inputFocus.requestFocus(); @@ -1275,13 +1259,13 @@ class ChatController extends State void copyEventsAction() { Clipboard.setData(ClipboardData(text: _getSelectedEventString())); - setState(() { - showEmojiPicker = false; - // #Pangea - // selectedEvents.clear(); - clearSelectedEvents(); - // Pangea# - }); + // #Pangea + // setState(() { + // showEmojiPicker = false; + // selectedEvents.clear(); + // }); + clearSelectedEvents(); + // Pangea# } void reportEventAction() async { @@ -1325,10 +1309,13 @@ class ChatController extends State ), ); if (result.error != null) return; - setState(() { - showEmojiPicker = false; - selectedEvents.clear(); - }); + // #Pangea + // setState(() { + // showEmojiPicker = false; + // selectedEvents.clear(); + // }); + clearSelectedEvents(); + // Pangea# ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(L10n.of(context).contentHasBeenReported)), ); @@ -1419,12 +1406,12 @@ class ChatController extends State ); } // #Pangea + // setState(() { + // showEmojiPicker = false; + // selectedEvents.clear(); + // }); clearSelectedEvents(); // Pangea# - setState(() { - showEmojiPicker = false; - selectedEvents.clear(); - }); } List get currentRoomBundle { @@ -1522,17 +1509,20 @@ class ChatController extends State for (final e in allEditEvents) { e.sendAgain(); } - setState(() => selectedEvents.clear()); + // #Pangea + // setState(() => selectedEvents.clear()); + // Pangea# } void replyAction({Event? replyTo}) { - setState(() { - replyEvent = replyTo ?? selectedEvents.first; - selectedEvents.clear(); - }); // #Pangea + replyEvent = replyTo ?? selectedEvents.first; clearSelectedEvents(); - // Pangea + // setState(() { + // replyEvent = replyTo ?? selectedEvents.first; + // selectedEvents.clear(); + // }); + // Pangea# inputFocus.requestFocus(); } @@ -1637,8 +1627,9 @@ class ChatController extends State // }); void clearSelectedEvents() { if (!mounted) return; + if (!_isToolbarOpen && selectedEvents.isEmpty) return; + MatrixState.pAnyState.closeAllOverlays(); setState(() { - MatrixState.pAnyState.closeAllOverlays(); selectedEvents.clear(); showEmojiPicker = false; }); @@ -1651,32 +1642,45 @@ class ChatController extends State }); } - void clearSingleSelectedEvent() { - if (selectedEvents.length <= 1) { - clearSelectedEvents(); - } - } + // #Pangea + // void clearSingleSelectedEvent() { + // if (selectedEvents.length <= 1) { + // clearSelectedEvents(); + // } + // } + // Pangea# void editSelectedEventAction() { - final client = currentRoomBundle.firstWhere( - (cl) => selectedEvents.first.senderId == cl!.userID, - orElse: () => null, - ); - if (client == null) { - return; - } - setSendingClient(client); - setState(() { - pendingText = sendController.text; - editEvent = selectedEvents.first; - sendController.text = - editEvent!.getDisplayEvent(timeline!).calcLocalizedBodyFallback( - MatrixLocals(L10n.of(context)), - withSenderNamePrefix: false, - hideReply: true, - ); - selectedEvents.clear(); - }); + // #Pangea + // final client = currentRoomBundle.firstWhere( + // (cl) => selectedEvents.first.senderId == cl!.userID, + // orElse: () => null, + // ); + // if (client == null) { + // return; + // } + // setSendingClient(client); + // setState(() { + // pendingText = sendController.text; + // editEvent = selectedEvents.first; + // sendController.text = + // editEvent!.getDisplayEvent(timeline!).calcLocalizedBodyFallback( + // MatrixLocals(L10n.of(context)), + // withSenderNamePrefix: false, + // hideReply: true, + // ); + // selectedEvents.clear(); + // }); + pendingText = sendController.text; + editEvent = selectedEvents.first; + sendController.text = + editEvent!.getDisplayEvent(timeline!).calcLocalizedBodyFallback( + MatrixLocals(L10n.of(context)), + withSenderNamePrefix: false, + hideReply: true, + ); + clearSelectedEvents(); + // Pangea# inputFocus.requestFocus(); } @@ -1705,35 +1709,24 @@ class ChatController extends State ); } - void onSelectMessage(Event event) { - // #Pangea - if (choreographer.itController.open.value) { - return; - } - // Pangea# - if (!event.redacted) { - // #Pangea - // if (selectedEvents.contains(event)) { - // setState( - // () => selectedEvents.remove(event), - // ); - // } - - // If delete first selected event with the selected eventID - final matches = selectedEvents.where((e) => e.eventId == event.eventId); - if (matches.isNotEmpty) { - setState(() => selectedEvents.remove(matches.first)); - // Pangea# - } else { - setState( - () => selectedEvents.add(event), - ); - } - selectedEvents.sort( - (a, b) => a.originServerTs.compareTo(b.originServerTs), - ); - } - } + // #Pangea + // 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), + // ); + // } + // } + // Pangea# int? findChildIndexCallback(Key key, Map thisEventsKeyMap) { // this method is called very often. As such, it has to be optimized for speed. @@ -1757,17 +1750,13 @@ class ChatController extends State // void onInputBarSubmitted(String _) { Future onInputBarSubmitted() async { // send(); - try { - await choreographer.send(); - } on ShowPaywallException { + if (MatrixState.pangeaController.subscriptionController.shouldShowPaywall) { PaywallCard.show(context, choreographer.inputTransformTargetKey); return; - } on OpenMatchesException { - onSelectMatch(choreographer.igcController.firstOpenMatch); - return; } + await onRequestWritingAssistance(autosend: true); + // FocusScope.of(context).requestFocus(inputFocus); // Pangea# - FocusScope.of(context).requestFocus(inputFocus); } void onAddPopupMenuButtonSelected(String choice) { @@ -1809,9 +1798,7 @@ class ChatController extends State ..removeWhere((oldEvent) => oldEvent == eventId); // #Pangea if (scrollToEventIdMarker == eventId) { - setState(() { - scrollToEventIdMarker = null; - }); + scrollToEventIdMarker = null; } // Pangea# showFutureLoadingDialog( @@ -1854,31 +1841,38 @@ class ChatController extends State static const Duration _storeInputTimeout = Duration(milliseconds: 500); void onInputBarChanged(String text) { - if (_inputTextIsEmpty != text.isEmpty) { - setState(() { - _inputTextIsEmpty = text.isEmpty; - }); - } + // #Pangea + // if (_inputTextIsEmpty != text.isEmpty) { + // setState(() { + // _inputTextIsEmpty = text.isEmpty; + // }); + // } + // if (_inputTextIsEmpty.value != text.isEmpty) { + // _inputTextIsEmpty.value = text.isEmpty; + // } + // Pangea# _storeInputTimeoutTimer?.cancel(); _storeInputTimeoutTimer = Timer(_storeInputTimeout, () async { final prefs = await SharedPreferences.getInstance(); await prefs.setString('draft_$roomId', text); }); - if (text.endsWith(' ') && Matrix.of(context).hasComplexBundles) { - final clients = currentRoomBundle; - for (final client in clients) { - final prefix = client!.sendPrefix; - if ((prefix.isNotEmpty) && - text.toLowerCase() == '${prefix.toLowerCase()} ') { - setSendingClient(client); - setState(() { - sendController.clear(); - }); - return; - } - } - } + // #Pangea + // if (text.endsWith(' ') && Matrix.of(context).hasComplexBundles) { + // final clients = currentRoomBundle; + // for (final client in clients) { + // final prefix = client!.sendPrefix; + // if ((prefix.isNotEmpty) && + // text.toLowerCase() == '${prefix.toLowerCase()} ') { + // setSendingClient(client); + // setState(() { + // sendController.clear(); + // }); + // return; + // } + // } + // } + // Pangea# if (AppConfig.sendTypingNotifications) { typingCoolDown?.cancel(); typingCoolDown = Timer(const Duration(seconds: 2), () { @@ -1900,7 +1894,9 @@ class ChatController extends State } } - bool _inputTextIsEmpty = true; + // #Pangea + // bool _inputTextIsEmpty = true; + // Pangea# bool get isArchived => {Membership.leave, Membership.ban}.contains(room.membership); @@ -1985,6 +1981,9 @@ class ChatController extends State final StreamController stopMediaStream = StreamController.broadcast(); + bool get _isToolbarOpen => + MatrixState.pAnyState.isOverlayOpen(RegExp(r'^message_toolbar_overlay$')); + void showToolbar( Event event, { PangeaMessageEvent? pangeaMessageEvent, @@ -2012,31 +2011,14 @@ class ChatController extends State return; } - Widget? overlayEntry; - try { - overlayEntry = MessageSelectionOverlay( - chatController: this, - event: event, - timeline: timeline!, - initialSelectedToken: selectedToken, - nextEvent: nextEvent, - prevEvent: prevEvent, - ); - } catch (err) { - debugger(when: kDebugMode); - ErrorHandler.logError( - e: err, - s: StackTrace.current, - data: { - 'roomId': roomId, - 'event': event.toJson(), - 'selectedToken': selectedToken?.toJson(), - 'nextEvent': nextEvent?.toJson(), - 'prevEvent': prevEvent?.toJson(), - }, - ); - return; - } + final overlayEntry = MessageSelectionOverlay( + chatController: this, + event: event, + timeline: timeline!, + initialSelectedToken: selectedToken, + nextEvent: nextEvent, + prevEvent: prevEvent, + ); // you've clicked a message so lets turn this off InstructionsEnum.clickMessage.setToggledOff(true); @@ -2045,29 +2027,36 @@ class ChatController extends State if (!kIsWeb) { HapticFeedback.mediumImpact(); } - stopMediaStream.add(null); - Future.delayed( - Duration(milliseconds: buttonEventID == event.eventId ? 200 : 0), () { - if (_router.state.path != ':roomid') { - // The user has navigated away from the chat, - // so we don't want to show the overlay. - return; - } - + if (buttonEventID == event.eventId) { + Future.delayed(const Duration(milliseconds: 200), () { + if (_router.state.path != ':roomid') { + // The user has navigated away from the chat, + // so we don't want to show the overlay. + return; + } + OverlayUtil.showOverlay( + context: context, + child: overlayEntry, + position: OverlayPositionEnum.centered, + onDismiss: clearSelectedEvents, + blurBackground: true, + backgroundColor: Colors.black, + overlayKey: "message_toolbar_overlay", + ); + }); + } else { OverlayUtil.showOverlay( context: context, - child: overlayEntry!, + child: overlayEntry, position: OverlayPositionEnum.centered, onDismiss: clearSelectedEvents, blurBackground: true, backgroundColor: Colors.black, + overlayKey: "message_toolbar_overlay", ); - - // select the message - onSelectMessage(event); - }); + } } bool get displayChatDetailsColumn { @@ -2196,23 +2185,36 @@ class ChatController extends State l1 != activityLang; } - void onSelectMatch(PangeaMatchState? match) { - if (match != null) { - if (match.updatedMatch.isITStart) { - choreographer.openIT(match); - } else { - OverlayUtil.showIGCMatch( - match, - choreographer, - context, - ); - } + Future onRequestWritingAssistance({bool autosend = false}) async { + if (shouldShowLanguageMismatchPopup) { + return showLanguageMismatchPopup(); + } + + await choreographer.requestWritingAssistance(); + if (choreographer.assistanceState == AssistanceStateEnum.fetched) { + onSelectMatch(choreographer.igcController.firstOpenMatch); + } else if (autosend) { + await send(); } else { inputFocus.requestFocus(); } } - Future showLanguageMismatchPopup() async { + void onSelectMatch(PangeaMatchState? match) { + if (match != null) { + match.updatedMatch.isITStart + ? choreographer.openIT(match) + : OverlayUtil.showIGCMatch( + match, + choreographer, + context, + ); + return; + } + inputFocus.requestFocus(); + } + + void showLanguageMismatchPopup() { if (!shouldShowLanguageMismatchPopup) { return; } @@ -2231,10 +2233,9 @@ class ChatController extends State waitForDataInSync: true, ); - WidgetsBinding.instance.addPostFrameCallback((_) async { - await choreographer.requestLanguageAssistance(); - onSelectMatch(choreographer.igcController.firstOpenMatch); - }); + WidgetsBinding.instance.addPostFrameCallback( + (_) => onRequestWritingAssistance(autosend: true), + ); }, ), maxHeight: 325, @@ -2310,25 +2311,29 @@ class ChatController extends State final ScrollController carouselController = ScrollController(); - ActivityRoleModel? highlightedRole; + ValueNotifier highlightedRole = ValueNotifier(null); void highlightRole(ActivityRoleModel role) { - if (mounted) setState(() => highlightedRole = role); + if (mounted) highlightedRole.value = role; } - bool showInstructions = false; + ValueNotifier showInstructions = ValueNotifier(false); void toggleShowInstructions() { - if (mounted) setState(() => showInstructions = !showInstructions); + if (mounted) { + showInstructions.value = !showInstructions.value; + } } - bool showActivityDropdown = false; + ValueNotifier showActivityDropdown = ValueNotifier(false); void toggleShowDropdown() async { - setState(() => showActivityDropdown = !showActivityDropdown); + if (mounted) { + showActivityDropdown.value = !showActivityDropdown.value; + } } - bool hasRainedConfetti = false; + ValueNotifier hasRainedConfetti = ValueNotifier(false); void setHasRainedConfetti(bool show) { if (mounted) { - setState(() => hasRainedConfetti = show); + hasRainedConfetti.value = show; } } // Pangea# diff --git a/lib/pages/chat/chat_input_row.dart b/lib/pages/chat/chat_input_row.dart index 10dd2ca23..1bf8f4d98 100644 --- a/lib/pages/chat/chat_input_row.dart +++ b/lib/pages/chat/chat_input_row.dart @@ -7,8 +7,6 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/utils/other_party_can_receive.dart'; import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/widgets/avatar.dart'; -import 'package:fluffychat/widgets/matrix.dart'; import '../../config/themes.dart'; import 'chat.dart'; import 'input_bar.dart'; @@ -256,15 +254,17 @@ class ChatInputRow extends StatelessWidget { onPressed: controller.emojiPickerAction, ), ), - if (Matrix.of(context).isMultiAccount && - Matrix.of(context).hasComplexBundles && - Matrix.of(context).currentBundle!.length > 1) - Container( - width: height, - height: height, - alignment: Alignment.center, - child: _ChatAccountPicker(controller), - ), + // #Pangea + // if (Matrix.of(context).isMultiAccount && + // Matrix.of(context).hasComplexBundles && + // Matrix.of(context).currentBundle!.length > 1) + // Container( + // width: height, + // height: height, + // alignment: Alignment.center, + // child: _ChatAccountPicker(controller), + // ), + // Pangea# Expanded( child: Padding( padding: const EdgeInsets.symmetric(vertical: 0.0), @@ -300,7 +300,7 @@ class ChatInputRow extends StatelessWidget { ), onChanged: controller.onInputBarChanged, // #Pangea - hintText: "", + choreographer: controller.choreographer, // Pangea# ), ), @@ -325,12 +325,7 @@ class ChatInputRow extends StatelessWidget { ) : FloatingActionButton.small( tooltip: L10n.of(context).send, - // #Pangea - // onPressed: controller.send, - onPressed: () => controller.send( - message: controller.sendController.text, - ), - // Pangea# + onPressed: controller.send, elevation: 0, heroTag: null, shape: RoundedRectangleBorder( @@ -346,60 +341,62 @@ class ChatInputRow extends StatelessWidget { } } -class _ChatAccountPicker extends StatelessWidget { - final ChatController controller; +// #Pangea +// class _ChatAccountPicker extends StatelessWidget { +// final ChatController controller; - const _ChatAccountPicker(this.controller); +// const _ChatAccountPicker(this.controller); - void _popupMenuButtonSelected(String mxid, BuildContext context) { - final client = Matrix.of(context) - .currentBundle! - .firstWhere((cl) => cl!.userID == mxid, orElse: () => null); - if (client == null) { - Logs().w('Attempted to switch to a non-existing client $mxid'); - return; - } - controller.setSendingClient(client); - } +// void _popupMenuButtonSelected(String mxid, BuildContext context) { +// final client = Matrix.of(context) +// .currentBundle! +// .firstWhere((cl) => cl!.userID == mxid, orElse: () => null); +// if (client == null) { +// Logs().w('Attempted to switch to a non-existing client $mxid'); +// return; +// } +// controller.setSendingClient(client); +// } - @override - Widget build(BuildContext context) { - final clients = controller.currentRoomBundle; - return Padding( - padding: const EdgeInsets.all(8.0), - child: FutureBuilder( - future: controller.sendingClient.fetchOwnProfile(), - builder: (context, snapshot) => PopupMenuButton( - useRootNavigator: true, - onSelected: (mxid) => _popupMenuButtonSelected(mxid, context), - itemBuilder: (BuildContext context) => clients - .map( - (client) => PopupMenuItem( - value: client!.userID, - child: FutureBuilder( - future: client.fetchOwnProfile(), - builder: (context, snapshot) => ListTile( - leading: Avatar( - mxContent: snapshot.data?.avatarUrl, - name: snapshot.data?.displayName ?? - client.userID!.localpart, - size: 20, - ), - title: Text(snapshot.data?.displayName ?? client.userID!), - contentPadding: const EdgeInsets.all(0), - ), - ), - ), - ) - .toList(), - child: Avatar( - mxContent: snapshot.data?.avatarUrl, - name: snapshot.data?.displayName ?? - Matrix.of(context).client.userID!.localpart, - size: 20, - ), - ), - ), - ); - } -} +// @override +// Widget build(BuildContext context) { +// final clients = controller.currentRoomBundle; +// return Padding( +// padding: const EdgeInsets.all(8.0), +// child: FutureBuilder( +// future: controller.sendingClient.fetchOwnProfile(), +// builder: (context, snapshot) => PopupMenuButton( +// useRootNavigator: true, +// onSelected: (mxid) => _popupMenuButtonSelected(mxid, context), +// itemBuilder: (BuildContext context) => clients +// .map( +// (client) => PopupMenuItem( +// value: client!.userID, +// child: FutureBuilder( +// future: client.fetchOwnProfile(), +// builder: (context, snapshot) => ListTile( +// leading: Avatar( +// mxContent: snapshot.data?.avatarUrl, +// name: snapshot.data?.displayName ?? +// client.userID!.localpart, +// size: 20, +// ), +// title: Text(snapshot.data?.displayName ?? client.userID!), +// contentPadding: const EdgeInsets.all(0), +// ), +// ), +// ), +// ) +// .toList(), +// child: Avatar( +// mxContent: snapshot.data?.avatarUrl, +// name: snapshot.data?.displayName ?? +// Matrix.of(context).client.userID!.localpart, +// size: 20, +// ), +// ), +// ), +// ); +// } +// } +// Pangea# diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 71d4d8ac8..e08bad4f4 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -353,12 +353,19 @@ class ChatView extends StatelessWidget { children: [ Expanded( child: GestureDetector( - onTap: controller.clearSingleSelectedEvent, // #Pangea + // onTap: controller.clearSingleSelectedEvent, // child: ChatEventList(controller: controller), child: Stack( children: [ - ChatEventList(controller: controller), + ListenableBuilder( + listenable: controller.timelineUpdateNotifier, + builder: (context, _) { + return ChatEventList( + controller: controller, + ); + }, + ), ChatViewBackground( controller.choreographer.itController.open, ), @@ -477,11 +484,18 @@ class ChatView extends StatelessWidget { ), // #Pangea ActivityStatsMenu(controller), - if (controller.room.activitySummary?.summary != null && - controller.hasRainedConfetti == false) - StarRainWidget( - showBlast: true, - onFinished: () => controller.setHasRainedConfetti(true), + if (controller.room.activitySummary?.summary != null) + ValueListenableBuilder( + valueListenable: controller.hasRainedConfetti, + builder: (context, hasRained, __) { + return hasRained + ? const SizedBox() + : StarRainWidget( + showBlast: true, + onFinished: () => + controller.setHasRainedConfetti(true), + ); + }, ), // if (controller.dragging) // Container( diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index e358ab039..d4ea83981 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -137,14 +137,20 @@ class Message extends StatelessWidget { // #Pangea if (event.type == PangeaEventTypes.activityPlan && event.room.activityPlan != null) { - return ActivitySummary( - activity: event.room.activityPlan!, - room: event.room, - showInstructions: controller.showInstructions, - toggleInstructions: controller.toggleShowInstructions, - getParticipantOpacity: (role) => - role == null || role.isFinished ? 0.5 : 1.0, - isParticipantSelected: (id) => controller.room.ownRoleState?.id == id, + return ValueListenableBuilder( + valueListenable: controller.showInstructions, + builder: (context, show, __) { + return ActivitySummary( + activity: event.room.activityPlan!, + room: event.room, + showInstructions: show, + toggleInstructions: controller.toggleShowInstructions, + getParticipantOpacity: (role) => + role == null || role.isFinished ? 0.5 : 1.0, + isParticipantSelected: (id) => + controller.room.ownRoleState?.id == id, + ); + }, ); } diff --git a/lib/pages/chat/input_bar.dart b/lib/pages/chat/input_bar.dart index 19a9e57e5..5316ff887 100644 --- a/lib/pages/chat/input_bar.dart +++ b/lib/pages/chat/input_bar.dart @@ -8,9 +8,12 @@ import 'package:matrix/matrix.dart'; import 'package:slugify/slugify.dart'; import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pangea/choreographer/choregrapher_user_settings_extension.dart'; +import 'package:fluffychat/pangea/choreographer/choreographer.dart'; import 'package:fluffychat/pangea/choreographer/choreographer_state_extension.dart'; import 'package:fluffychat/pangea/choreographer/choreographer_ui_extension.dart'; import 'package:fluffychat/pangea/choreographer/text_editing/pangea_text_controller.dart'; +import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart'; import 'package:fluffychat/pangea/subscription/controllers/subscription_controller.dart'; import 'package:fluffychat/pangea/subscription/widgets/paywall_card.dart'; import 'package:fluffychat/pangea/toolbar/utils/shrinkable_text.dart'; @@ -32,7 +35,7 @@ class InputBar extends StatelessWidget { // #Pangea // final TextEditingController? controller; final PangeaTextController? controller; - final String hintText; + final Choreographer choreographer; // Pangea# final InputDecoration? decoration; final ValueChanged? onChanged; @@ -54,7 +57,7 @@ class InputBar extends StatelessWidget { this.textInputAction, this.readOnly = false, // #Pangea - required this.hintText, + required this.choreographer, // Pangea# super.key, }); @@ -426,6 +429,22 @@ class InputBar extends StatelessWidget { } // #Pangea + String hintText(BuildContext context) { + if (choreographer.itController.open.value) { + return L10n.of(context).buildTranslation; + } + + return choreographer.l1Lang != null && + choreographer.l2Lang != null && + choreographer.l1Lang!.langCode != LanguageKeys.unknownLanguage && + choreographer.l2Lang!.langCode != LanguageKeys.unknownLanguage + ? L10n.of(context).writeAMessageLangCodes( + choreographer.l1Lang!.displayName, + choreographer.l2Lang!.displayName, + ) + : L10n.of(context).writeAMessage; + } + void onInputTap(BuildContext context) { if (controller == null || controller!.text.isEmpty) return; final choreographer = controller!.choreographer; @@ -458,148 +477,153 @@ class InputBar extends StatelessWidget { @override Widget build(BuildContext context) { // #Pangea - final enableAutocorrect = MatrixState - .pangeaController.userController.profile.toolSettings.enableAutocorrect; - // Pangea# - return TypeAheadField>( - direction: VerticalDirection.up, - hideOnEmpty: true, - hideOnLoading: true, - controller: controller, - focusNode: focusNode, - hideOnSelect: false, - debounceDuration: const Duration(milliseconds: 50), - // show suggestions after 50ms idle time (default is 300) - // #Pangea - builder: (context, _, focusNode) { - final textField = TextField( - enableSuggestions: enableAutocorrect, - readOnly: - controller != null && (controller!.choreographer.isRunningIT), - autocorrect: enableAutocorrect, + return ListenableBuilder( + listenable: choreographer.textController, + builder: (context, _) { + final enableAutocorrect = MatrixState.pangeaController.userController + .profile.toolSettings.enableAutocorrect; + // Pangea# + return TypeAheadField>( + direction: VerticalDirection.up, + hideOnEmpty: true, + hideOnLoading: true, controller: controller, focusNode: focusNode, - contextMenuBuilder: (c, e) => markdownContextBuilder( - c, - e, - _, - ), - contentInsertionConfiguration: ContentInsertionConfiguration( - onContentInserted: (KeyboardInsertedContent content) { - final data = content.data; - if (data == null) return; - - final file = MatrixFile( - mimeType: content.mimeType, - bytes: data, - name: content.uri.split('/').last, - ); - room.sendFileEvent( - file, - shrinkImageMaxDimension: 1600, - ); - }, - ), - minLines: minLines, - maxLines: maxLines, - keyboardType: keyboardType!, - textInputAction: textInputAction, - autofocus: autofocus!, - inputFormatters: [ - //LengthLimitingTextInputFormatter((maxPDUSize / 3).floor()), - //setting max character count to 1000 - //after max, nothing else can be typed - LengthLimitingTextInputFormatter(1000), - ], - onSubmitted: (text) { - // fix for library for now - // it sets the types for the callback incorrectly - onSubmitted!(text); - }, - style: controller?.exceededMaxLength ?? false - ? const TextStyle(color: Colors.red) - : null, - onTap: () => onInputTap(context), - decoration: decoration!, - onChanged: (text) { - // fix for the library for now - // it sets the types for the callback incorrectly - onChanged!(text); - }, - textCapitalization: TextCapitalization.sentences, - ); - // fix for issue with typing not working sometimes on Firefox and Safari - return Stack( - alignment: Alignment.centerLeft, - children: [ - if (controller != null && controller!.text.isEmpty) - Padding( - padding: const EdgeInsets.only(left: 8.0), - child: ShrinkableText( - text: hintText, - maxWidth: double.infinity, - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: Theme.of(context).disabledColor, - ), - ), + hideOnSelect: false, + debounceDuration: const Duration(milliseconds: 50), + // show suggestions after 50ms idle time (default is 300) + // #Pangea + builder: (context, _, focusNode) { + final textField = TextField( + enableSuggestions: enableAutocorrect, + readOnly: controller!.choreographer.isRunningIT, + autocorrect: enableAutocorrect, + controller: controller, + focusNode: focusNode, + contextMenuBuilder: (c, e) => markdownContextBuilder( + c, + e, + _, ), - kIsWeb ? SelectionArea(child: textField) : textField, - ], + contentInsertionConfiguration: ContentInsertionConfiguration( + onContentInserted: (KeyboardInsertedContent content) { + final data = content.data; + if (data == null) return; + + final file = MatrixFile( + mimeType: content.mimeType, + bytes: data, + name: content.uri.split('/').last, + ); + room.sendFileEvent( + file, + shrinkImageMaxDimension: 1600, + ); + }, + ), + minLines: minLines, + maxLines: maxLines, + keyboardType: keyboardType!, + textInputAction: textInputAction, + autofocus: autofocus!, + inputFormatters: [ + //LengthLimitingTextInputFormatter((maxPDUSize / 3).floor()), + //setting max character count to 1000 + //after max, nothing else can be typed + LengthLimitingTextInputFormatter(1000), + ], + onSubmitted: (text) { + // fix for library for now + // it sets the types for the callback incorrectly + onSubmitted!(text); + }, + style: controller?.exceededMaxLength ?? false + ? const TextStyle(color: Colors.red) + : null, + onTap: () => onInputTap(context), + decoration: decoration!, + onChanged: (text) { + // fix for the library for now + // it sets the types for the callback incorrectly + onChanged!(text); + }, + textCapitalization: TextCapitalization.sentences, + ); + + return Stack( + alignment: Alignment.centerLeft, + children: [ + if (controller != null && controller!.text.isEmpty) + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: ShrinkableText( + text: hintText(context), + maxWidth: double.infinity, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Theme.of(context).disabledColor, + ), + ), + ), + kIsWeb ? SelectionArea(child: textField) : textField, + ], + ); + }, + // builder: (context, controller, focusNode) => TextField( + // controller: controller, + // focusNode: focusNode, + // readOnly: readOnly, + // contextMenuBuilder: (c, e) => markdownContextBuilder(c, e, controller), + // contentInsertionConfiguration: ContentInsertionConfiguration( + // onContentInserted: (KeyboardInsertedContent content) { + // final data = content.data; + // if (data == null) return; + + // final file = MatrixFile( + // mimeType: content.mimeType, + // bytes: data, + // name: content.uri.split('/').last, + // ); + // room.sendFileEvent( + // file, + // shrinkImageMaxDimension: 1600, + // ); + // }, + // ), + // minLines: minLines, + // maxLines: maxLines, + // keyboardType: keyboardType!, + // textInputAction: textInputAction, + // autofocus: autofocus!, + // inputFormatters: [ + // LengthLimitingTextInputFormatter((maxPDUSize / 3).floor()), + // ], + // onSubmitted: (text) { + // // fix for library for now + // // it sets the types for the callback incorrectly + // onSubmitted!(text); + // }, + // decoration: decoration!, + // onChanged: (text) { + // // fix for the library for now + // // it sets the types for the callback incorrectly + // onChanged!(text); + // }, + // textCapitalization: TextCapitalization.sentences, + // ), + // Pangea# + suggestionsCallback: getSuggestions, + itemBuilder: (c, s) => + buildSuggestion(c, s, Matrix.of(context).client), + onSelected: (Map suggestion) => + insertSuggestion(context, suggestion), + errorBuilder: (BuildContext context, Object? error) => + const SizedBox.shrink(), + loadingBuilder: (BuildContext context) => const SizedBox.shrink(), + // fix loading briefly flickering a dark box + emptyBuilder: (BuildContext context) => const SizedBox + .shrink(), // fix loading briefly showing no suggestions ); }, - // builder: (context, controller, focusNode) => TextField( - // controller: controller, - // focusNode: focusNode, - // readOnly: readOnly, - // contextMenuBuilder: (c, e) => markdownContextBuilder(c, e, controller), - // contentInsertionConfiguration: ContentInsertionConfiguration( - // onContentInserted: (KeyboardInsertedContent content) { - // final data = content.data; - // if (data == null) return; - - // final file = MatrixFile( - // mimeType: content.mimeType, - // bytes: data, - // name: content.uri.split('/').last, - // ); - // room.sendFileEvent( - // file, - // shrinkImageMaxDimension: 1600, - // ); - // }, - // ), - // minLines: minLines, - // maxLines: maxLines, - // keyboardType: keyboardType!, - // textInputAction: textInputAction, - // autofocus: autofocus!, - // inputFormatters: [ - // LengthLimitingTextInputFormatter((maxPDUSize / 3).floor()), - // ], - // onSubmitted: (text) { - // // fix for library for now - // // it sets the types for the callback incorrectly - // onSubmitted!(text); - // }, - // decoration: decoration!, - // onChanged: (text) { - // // fix for the library for now - // // it sets the types for the callback incorrectly - // onChanged!(text); - // }, - // textCapitalization: TextCapitalization.sentences, - // ), - // Pangea# - suggestionsCallback: getSuggestions, - itemBuilder: (c, s) => buildSuggestion(c, s, Matrix.of(context).client), - onSelected: (Map suggestion) => - insertSuggestion(context, suggestion), - errorBuilder: (BuildContext context, Object? error) => - const SizedBox.shrink(), - loadingBuilder: (BuildContext context) => const SizedBox.shrink(), - // fix loading briefly flickering a dark box - emptyBuilder: (BuildContext context) => - const SizedBox.shrink(), // fix loading briefly showing no suggestions ); } } diff --git a/lib/pangea/activity_sessions/activity_session_chat/activity_stats_menu.dart b/lib/pangea/activity_sessions/activity_session_chat/activity_stats_menu.dart index f36da856a..78658354a 100644 --- a/lib/pangea/activity_sessions/activity_session_chat/activity_stats_menu.dart +++ b/lib/pangea/activity_sessions/activity_session_chat/activity_stats_menu.dart @@ -134,140 +134,145 @@ class ActivityStatsMenuState extends State { shouldShowEndForAll = false; } - return Positioned( - top: 0, - left: 0, - right: 0, - bottom: widget.controller.showActivityDropdown ? 0 : null, - child: Column( - children: [ - ClipRect( - child: AnimatedAlign( - duration: FluffyThemes.animationDuration, - curve: Curves.easeInOut, - heightFactor: widget.controller.showActivityDropdown ? 1.0 : 0.0, - alignment: Alignment.topCenter, - child: GestureDetector( - onPanUpdate: (details) { - if (details.delta.dy < -2) { - widget.controller.toggleShowDropdown(); - } - }, - child: Container( - width: double.infinity, - decoration: BoxDecoration( - color: theme.colorScheme.surface, - ), - padding: const EdgeInsets.all(12.0), - child: Column( - spacing: 12.0, - mainAxisSize: MainAxisSize.min, - children: [ - Column( - spacing: 8.0, - mainAxisSize: MainAxisSize.min, - children: [ - ActivitySessionDetailsRow( - icon: Symbols.radar, - iconSize: 16.0, - child: Text( - room.activityPlan!.learningObjective, - style: const TextStyle(fontSize: 12.0), - ), - ), - ActivitySessionDetailsRow( - icon: Symbols.dictionary, - iconSize: 16.0, - child: Wrap( - spacing: 4.0, - runSpacing: 4.0, - children: [ - ...room.activityPlan!.vocab.map( - (v) => VocabTile( - vocab: v, - langCode: - room.activityPlan!.req.targetLanguage, - isUsed: (_usedVocab ?? {}) - .contains(v.lemma.toLowerCase()), - ), - ), - ], - ), - ), - ], - ), - if (!userComplete) ...[ - Text( - L10n.of(context).activityDropdownDesc, - textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.w600, - ), - ), - if (shouldShowEndForAll) - ElevatedButton( - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 8.0, - ), - side: BorderSide( - color: theme.colorScheme.secondaryContainer, - width: 2, - ), - foregroundColor: theme.colorScheme.primary, - backgroundColor: theme.colorScheme.surface, - ), - onPressed: () => _finishActivity(forAll: true), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - L10n.of(context).endForAll, - style: TextStyle( - fontSize: isColumnMode ? 16.0 : 12.0, - ), - ), - ], - ), - ), - if (shouldShowImDone) - ElevatedButton( - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 8.0, - ), - ), - onPressed: _finishActivity, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - L10n.of(context).endActivity, - style: TextStyle( - fontSize: isColumnMode ? 16.0 : 12.0, - ), - ), - ], - ), - ), - ], - ], + return ValueListenableBuilder( + valueListenable: widget.controller.showActivityDropdown, + builder: (context, showDropdown, child) { + return Positioned( + top: 0, + left: 0, + right: 0, + bottom: showDropdown ? 0 : null, + child: Column( + children: [ + ClipRect( + child: AnimatedAlign( + duration: FluffyThemes.animationDuration, + curve: Curves.easeInOut, + heightFactor: showDropdown ? 1.0 : 0.0, + alignment: Alignment.topCenter, + child: GestureDetector( + onPanUpdate: (details) { + if (details.delta.dy < -2) { + widget.controller.toggleShowDropdown(); + } + }, + child: child, ), ), ), - ), + if (showDropdown) + Expanded( + child: GestureDetector( + onTap: widget.controller.toggleShowDropdown, + child: Container(color: Colors.black.withAlpha(100)), + ), + ), + ], ), - if (widget.controller.showActivityDropdown) - Expanded( - child: GestureDetector( - onTap: widget.controller.toggleShowDropdown, - child: Container(color: Colors.black.withAlpha(100)), - ), + ); + }, + child: Container( + width: double.infinity, + decoration: BoxDecoration( + color: theme.colorScheme.surface, + ), + padding: const EdgeInsets.all(12.0), + child: Column( + spacing: 12.0, + mainAxisSize: MainAxisSize.min, + children: [ + Column( + spacing: 8.0, + mainAxisSize: MainAxisSize.min, + children: [ + ActivitySessionDetailsRow( + icon: Symbols.radar, + iconSize: 16.0, + child: Text( + room.activityPlan!.learningObjective, + style: const TextStyle(fontSize: 12.0), + ), + ), + ActivitySessionDetailsRow( + icon: Symbols.dictionary, + iconSize: 16.0, + child: Wrap( + spacing: 4.0, + runSpacing: 4.0, + children: [ + ...room.activityPlan!.vocab.map( + (v) => VocabTile( + vocab: v, + langCode: room.activityPlan!.req.targetLanguage, + isUsed: (_usedVocab ?? {}) + .contains(v.lemma.toLowerCase()), + ), + ), + ], + ), + ), + ], ), - ], + if (!userComplete) ...[ + Text( + L10n.of(context).activityDropdownDesc, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.w600, + ), + ), + if (shouldShowEndForAll) + ElevatedButton( + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 8.0, + ), + side: BorderSide( + color: theme.colorScheme.secondaryContainer, + width: 2, + ), + foregroundColor: theme.colorScheme.primary, + backgroundColor: theme.colorScheme.surface, + ), + onPressed: () => _finishActivity(forAll: true), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + L10n.of(context).endForAll, + style: TextStyle( + fontSize: isColumnMode ? 16.0 : 12.0, + ), + ), + ], + ), + ), + if (shouldShowImDone) + ElevatedButton( + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 8.0, + ), + ), + onPressed: _finishActivity, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + L10n.of(context).endActivity, + style: TextStyle( + fontSize: isColumnMode ? 16.0 : 12.0, + ), + ), + ], + ), + ), + ], + ], + ), ), ); } diff --git a/lib/pangea/activity_sessions/activity_user_summaries_widget.dart b/lib/pangea/activity_sessions/activity_user_summaries_widget.dart index 3a4e69884..30266e263 100644 --- a/lib/pangea/activity_sessions/activity_user_summaries_widget.dart +++ b/lib/pangea/activity_sessions/activity_user_summaries_widget.dart @@ -50,27 +50,6 @@ class ActivityUserSummaries extends StatelessWidget { summary: summary, controller: controller, ), - // Row( - // mainAxisSize: MainAxisSize.min, - // children: userSummaries.map((p) { - // final user = room.getParticipants().firstWhereOrNull( - // (u) => u.id == p.participantId, - // ); - // final userRole = assignedRoles.values.firstWhere( - // (role) => role.userId == p.participantId, - // ); - // final userRoleInfo = availableRoles[userRole.id]!; - // return ActivityParticipantIndicator( - // availableRole: userRoleInfo, - // assignedRole: userRole, - // avatarUrl: - // userRoleInfo.avatarUrl ?? user?.avatarUrl?.toString(), - // borderRadius: BorderRadius.circular(4), - // selected: controller.highlightedRole?.id == userRole.id, - // onTap: () => controller.highlightRole(userRole), - // ); - // }).toList(), - // ), ], ), ); @@ -225,28 +204,33 @@ class ButtonControlledCarouselView extends StatelessWidget { ), ), const SizedBox(height: 12), - Row( - mainAxisSize: MainAxisSize.min, - children: userSummaries.mapIndexed((i, p) { - final user = room.getParticipants().firstWhereOrNull( - (u) => u.id == p.participantId, + ValueListenableBuilder( + valueListenable: controller.highlightedRole, + builder: (context, highlightedRole, __) { + return Row( + mainAxisSize: MainAxisSize.min, + children: userSummaries.mapIndexed((i, p) { + final user = room.getParticipants().firstWhereOrNull( + (u) => u.id == p.participantId, + ); + final userRole = assignedRoles.values.firstWhere( + (role) => role.userId == p.participantId, ); - final userRole = assignedRoles.values.firstWhere( - (role) => role.userId == p.participantId, + final userRoleInfo = availableRoles[userRole.id]!; + return ActivityParticipantIndicator( + name: userRoleInfo.name, + userId: p.participantId, + user: user, + borderRadius: BorderRadius.circular(4), + selected: highlightedRole?.id == userRole.id, + onTap: () { + controller.highlightRole(userRole); + controller.carouselController.jumpTo(i * 250.0); + }, + ); + }).toList(), ); - final userRoleInfo = availableRoles[userRole.id]!; - return ActivityParticipantIndicator( - name: userRoleInfo.name, - userId: p.participantId, - user: user, - borderRadius: BorderRadius.circular(4), - selected: controller.highlightedRole?.id == userRole.id, - onTap: () { - controller.highlightRole(userRole); - controller.carouselController.jumpTo(i * 250.0); - }, - ); - }).toList(), + }, ), ], ); diff --git a/lib/pangea/chat/widgets/chat_input_bar.dart b/lib/pangea/chat/widgets/chat_input_bar.dart index 72250651c..c691f01d0 100644 --- a/lib/pangea/chat/widgets/chat_input_bar.dart +++ b/lib/pangea/chat/widgets/chat_input_bar.dart @@ -27,7 +27,7 @@ class ChatInputBar extends StatelessWidget { hide: controller.choreographer.itController.open, ), ITBar(choreographer: controller.choreographer), - if (!controller.obscureText) ReplyDisplay(controller), + ReplyDisplay(controller), PangeaChatInputRow( controller: controller, ), diff --git a/lib/pangea/chat/widgets/pangea_chat_input_row.dart b/lib/pangea/chat/widgets/pangea_chat_input_row.dart index 80602bc28..3155a969e 100644 --- a/lib/pangea/chat/widgets/pangea_chat_input_row.dart +++ b/lib/pangea/chat/widgets/pangea_chat_input_row.dart @@ -8,9 +8,9 @@ import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/input_bar.dart'; import 'package:fluffychat/pangea/choreographer/choreographer_send_button.dart'; +import 'package:fluffychat/pangea/choreographer/choreographer_state_extension.dart'; import 'package:fluffychat/pangea/choreographer/choreographer_ui_extension.dart'; import 'package:fluffychat/pangea/choreographer/igc/start_igc_button.dart'; -import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart'; import 'package:fluffychat/pangea/learning_settings/models/language_model.dart'; import 'package:fluffychat/utils/platform_infos.dart'; @@ -27,55 +27,39 @@ class PangeaChatInputRow extends StatelessWidget { LanguageModel? get activel2 => controller.pangeaController.languageController.activeL2Model(); - String hintText(BuildContext context) { - if (controller.choreographer.itController.open.value) { - return L10n.of(context).buildTranslation; - } - return activel1 != null && - activel2 != null && - activel1!.langCode != LanguageKeys.unknownLanguage && - activel2!.langCode != LanguageKeys.unknownLanguage - ? L10n.of(context).writeAMessageLangCodes( - activel1!.displayName, - activel2!.displayName, - ) - : L10n.of(context).writeAMessage; - } - @override Widget build(BuildContext context) { final theme = Theme.of(context); const height = 48.0; + final state = controller.choreographer.assistanceState; - if (controller.selectMode) { - return const SizedBox(height: height); - } - - return ListenableBuilder( - listenable: controller.choreographer, - builder: (context, _) { - return Column( - children: [ - CompositedTransformTarget( - link: controller.choreographer.inputLayerLinkAndKey.link, - child: Container( - decoration: const BoxDecoration( - borderRadius: BorderRadius.all( - Radius.circular(8.0), - ), - ), - child: Row( - key: controller.choreographer.inputLayerLinkAndKey.key, - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const SizedBox(width: 4), - AnimatedContainer( + return Column( + children: [ + CompositedTransformTarget( + link: controller.choreographer.inputLayerLinkAndKey.link, + child: Container( + decoration: const BoxDecoration( + borderRadius: BorderRadius.all( + Radius.circular(8.0), + ), + ), + child: Row( + key: controller.choreographer.inputLayerLinkAndKey.key, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const SizedBox(width: 4), + ValueListenableBuilder( + valueListenable: controller.sendController, + builder: (context, text, __) { + return AnimatedContainer( duration: FluffyThemes.animationDuration, curve: FluffyThemes.animationCurve, height: height, - width: - controller.sendController.text.isEmpty ? height : 0, + width: text.text.isEmpty && + !controller.choreographer.itController.open.value + ? height + : 0, alignment: Alignment.center, clipBehavior: Clip.hardEdge, decoration: const BoxDecoration(), @@ -150,82 +134,90 @@ class PangeaChatInputRow extends StatelessWidget { ), ], ), - ), - if (FluffyThemes.isColumnMode(context)) - Container( - height: height, - width: height, - alignment: Alignment.center, - child: IconButton( - tooltip: L10n.of(context).emojis, - icon: PageTransitionSwitcher( - transitionBuilder: ( - Widget child, - Animation primaryAnimation, - Animation secondaryAnimation, - ) { - return SharedAxisTransition( - animation: primaryAnimation, - secondaryAnimation: secondaryAnimation, - transitionType: SharedAxisTransitionType.scaled, - fillColor: Colors.transparent, - child: child, - ); - }, - child: Icon( - controller.showEmojiPicker - ? Icons.keyboard - : Icons.add_reaction_outlined, - key: ValueKey(controller.showEmojiPicker), - ), - ), - onPressed: controller.emojiPickerAction, + ); + }, + ), + if (FluffyThemes.isColumnMode(context)) + Container( + height: height, + width: height, + alignment: Alignment.center, + child: IconButton( + tooltip: L10n.of(context).emojis, + icon: PageTransitionSwitcher( + transitionBuilder: ( + Widget child, + Animation primaryAnimation, + Animation secondaryAnimation, + ) { + return SharedAxisTransition( + animation: primaryAnimation, + secondaryAnimation: secondaryAnimation, + transitionType: SharedAxisTransitionType.scaled, + fillColor: Colors.transparent, + child: child, + ); + }, + child: Icon( + controller.showEmojiPicker + ? Icons.keyboard + : Icons.add_reaction_outlined, + key: ValueKey(controller.showEmojiPicker), ), ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 0.0), - child: InputBar( - room: controller.room, - minLines: 1, - maxLines: 8, - autofocus: !PlatformInfos.isMobile, - keyboardType: TextInputType.multiline, - textInputAction: AppConfig.sendOnEnter == true && - PlatformInfos.isMobile - ? TextInputAction.send - : null, - onSubmitted: (_) => controller.onInputBarSubmitted(), - onSubmitImage: controller.sendImageFromClipBoard, - focusNode: controller.inputFocus, - controller: controller.sendController, - decoration: const InputDecoration( - contentPadding: EdgeInsets.only( - left: 6.0, - right: 6.0, - bottom: 6.0, - top: 3.0, - ), - disabledBorder: InputBorder.none, - hintMaxLines: 1, - border: InputBorder.none, - enabledBorder: InputBorder.none, - filled: false, - ), - onChanged: controller.onInputBarChanged, - hintText: hintText(context), + onPressed: controller.emojiPickerAction, + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 0.0), + child: InputBar( + room: controller.room, + minLines: 1, + maxLines: 8, + autofocus: !PlatformInfos.isMobile, + keyboardType: TextInputType.multiline, + textInputAction: AppConfig.sendOnEnter == true && + PlatformInfos.isMobile + ? TextInputAction.send + : null, + onSubmitted: (_) => controller.onInputBarSubmitted(), + onSubmitImage: controller.sendImageFromClipBoard, + focusNode: controller.inputFocus, + controller: controller.sendController, + decoration: const InputDecoration( + contentPadding: EdgeInsets.only( + left: 6.0, + right: 6.0, + bottom: 6.0, + top: 3.0, ), + disabledBorder: InputBorder.none, + hintMaxLines: 1, + border: InputBorder.none, + enabledBorder: InputBorder.none, + filled: false, ), + onChanged: controller.onInputBarChanged, + choreographer: controller.choreographer, ), - StartIGCButton( - controller: controller, - ), - Container( + ), + ), + StartIGCButton( + controller: controller, + initialState: state, + initialForegroundColor: state.stateColor(context), + initialBackgroundColor: state.backgroundColor(context), + ), + ValueListenableBuilder( + valueListenable: controller.sendController, + builder: (context, text, __) { + return Container( height: height, width: height, alignment: Alignment.center, child: PlatformInfos.platformCanRecord && - controller.sendController.text.isEmpty && + text.text.isEmpty && !controller.choreographer.itController.open.value ? FloatingActionButton.small( tooltip: L10n.of(context).voiceMessage, @@ -242,14 +234,14 @@ class PangeaChatInputRow extends StatelessWidget { : ChoreographerSendButton( controller: controller, ), - ), - ], + ); + }, ), - ), + ], ), - ], - ); - }, + ), + ), + ], ); } } diff --git a/lib/pangea/choreographer/assistance_state_enum.dart b/lib/pangea/choreographer/assistance_state_enum.dart index b5a0d6412..1680bb972 100644 --- a/lib/pangea/choreographer/assistance_state_enum.dart +++ b/lib/pangea/choreographer/assistance_state_enum.dart @@ -13,10 +13,8 @@ enum AssistanceStateEnum { fetching, fetched, complete, - error, -} + error; -extension AssistanceStateExtension on AssistanceStateEnum { Color stateColor(context) { switch (this) { case AssistanceStateEnum.noSub: @@ -46,4 +44,19 @@ extension AssistanceStateExtension on AssistanceStateEnum { return AppConfig.success; } } + + bool get allowsFeedback => switch (this) { + AssistanceStateEnum.notFetched => true, + _ => false, + }; + + Color backgroundColor(BuildContext context) => switch (this) { + AssistanceStateEnum.noSub || + AssistanceStateEnum.noMessage || + AssistanceStateEnum.fetched || + AssistanceStateEnum.complete || + AssistanceStateEnum.error => + Theme.of(context).colorScheme.surfaceContainerHighest, + _ => Theme.of(context).colorScheme.primaryContainer, + }; } diff --git a/lib/pangea/choreographer/choreographer.dart b/lib/pangea/choreographer/choreographer.dart index 06235ce36..7d0e488d4 100644 --- a/lib/pangea/choreographer/choreographer.dart +++ b/lib/pangea/choreographer/choreographer.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'package:flutter/material.dart'; + import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pangea/choreographer/assistance_state_enum.dart'; import 'package:fluffychat/pangea/choreographer/choregrapher_user_settings_extension.dart'; @@ -10,6 +12,7 @@ import 'package:fluffychat/pangea/choreographer/choreographer_state_extension.da import 'package:fluffychat/pangea/choreographer/igc/igc_controller.dart'; import 'package:fluffychat/pangea/choreographer/igc/pangea_match_state_model.dart'; import 'package:fluffychat/pangea/choreographer/igc/pangea_match_status_enum.dart'; +import 'package:fluffychat/pangea/choreographer/pangea_message_content_model.dart'; import 'package:fluffychat/pangea/choreographer/text_editing/edit_type_enum.dart'; import 'package:fluffychat/pangea/choreographer/text_editing/pangea_text_controller.dart'; import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart'; @@ -22,9 +25,6 @@ import 'package:fluffychat/pangea/learning_settings/constants/language_constants import 'package:fluffychat/pangea/subscription/controllers/subscription_controller.dart'; import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; -import 'package:flutter/material.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; - import '../../widgets/matrix.dart'; import 'choreographer_error_controller.dart'; import 'it/it_controller.dart'; @@ -82,18 +82,15 @@ class Choreographer extends ChangeNotifier { _languageStream = pangeaController.userController.languageStream.stream.listen((update) { clear(); - notifyListeners(); }); _settingsUpdateStream = pangeaController.userController.settingsUpdateStream.stream.listen((_) { notifyListeners(); }); - clear(); } void clear() { - setChoreoMode(ChoreoModeEnum.igc); _lastChecked = null; _timesClicked = 0; _isFetching.value = false; @@ -102,6 +99,7 @@ class Choreographer extends ChangeNotifier { itController.clearSourceText(); igcController.clear(); _resetDebounceTimer(); + setChoreoMode(ChoreoModeEnum.igc); } @override @@ -165,51 +163,45 @@ class Choreographer extends ChangeNotifier { notifyListeners(); } - Future requestLanguageAssistance() => - _startWritingAssistance(manual: true); - /// Handles any changes to the text input void _onChange() { + // listener triggers when edit type changes, even if text didn't + // so prevent unnecessary calls if (_lastChecked != null && _lastChecked == textController.text) { return; } + if (_lastChecked == null || + _lastChecked!.isEmpty || + textController.text.isEmpty) { + notifyListeners(); + } _lastChecked = textController.text; - if (textController.editType == EditTypeEnum.it) { - return; - } - - if (textController.editType == EditTypeEnum.igc || - textController.editType == EditTypeEnum.itDismissed) { - textController.editType = EditTypeEnum.keyboard; - return; - } - - // Close any open IGC overlays - MatrixState.pAnyState.closeOverlay(); if (errorService.isError) return; - - igcController.clear(); if (textController.editType == EditTypeEnum.keyboard) { - itController.clearSourceText(); + if (igcController.hasIGCTextData || + itController.sourceText.value != null) { + igcController.clear(); + itController.clearSourceText(); + notifyListeners(); + } + + _resetDebounceTimer(); + _debounceTimer ??= Timer( + const Duration(milliseconds: ChoreoConstants.msBeforeIGCStart), + () => _getWritingAssistance(), + ); } - - _resetDebounceTimer(); - _debounceTimer ??= Timer( - const Duration(milliseconds: ChoreoConstants.msBeforeIGCStart), - () => _startWritingAssistance(), - ); - - //Note: we don't set the keyboard type on each keyboard stroke so this is how we default to - //a change being from the keyboard unless explicitly set to one of the other - //types when that action happens (e.g. an it/igc choice is selected) textController.editType = EditTypeEnum.keyboard; } - Future _startWritingAssistance({ + Future requestWritingAssistance() => + _getWritingAssistance(manual: true); + + Future _getWritingAssistance({ bool manual = false, }) async { - if (errorService.isError || isRunningIT) return; + if (assistanceState != AssistanceStateEnum.notFetched) return; final SubscriptionStatus canSendStatus = pangeaController.subscriptionController.subscriptionStatus; @@ -228,79 +220,17 @@ class Choreographer extends ChangeNotifier { textController.text, chatController.room.getPreviousMessages(), ); + + // trigger a re-render of the text field to show IGC matches + textController.setSystemText( + textController.text, + EditTypeEnum.igc, + ); _acceptNormalizationMatches(); _stopLoading(); } - Future send([int recurrence = 0]) async { - // if isFetching, already called to getLanguageHelp and hasn't completed yet - // could happen if user clicked send button multiple times in a row - if (_isFetching.value) return; - - if (errorService.isError) { - await _sendWithIGC(); - return; - } - - if (recurrence > 1) { - ErrorHandler.logError( - e: Exception("Choreographer send exceeded max recurrences"), - level: SentryLevel.warning, - data: { - "currentText": chatController.sendController.text, - "l1LangCode": l1LangCode, - "l2LangCode": l2LangCode, - "choreoRecord": _choreoRecord?.toJson(), - }, - ); - await _sendWithIGC(); - return; - } - - if (igcController.canShowFirstMatch) { - throw OpenMatchesException(); - } else if (isRunningIT) { - // If the user is in the middle of IT, don't send the message. - // If they've already clicked the send button once, this will - // not be true, so they can still send it if they want. - return; - } - - final subscriptionStatus = - pangeaController.subscriptionController.subscriptionStatus; - - if (subscriptionStatus != SubscriptionStatus.subscribed) { - if (subscriptionStatus == SubscriptionStatus.shouldShowPaywall) { - throw ShowPaywallException(); - } - chatController.send(message: chatController.sendController.text); - return; - } - - if (chatController.shouldShowLanguageMismatchPopup) { - chatController.showLanguageMismatchPopup(); - return; - } - - if (!igcController.hasIGCTextData && !itController.dismissed) { - await _startWritingAssistance(); - // it's possible for this not to be true, i.e. if IGC has an error - if (igcController.hasIGCTextData) { - await send(recurrence + 1); - } - } else { - await _sendWithIGC(); - } - } - - Future _sendWithIGC() async { - if (chatController.sendController.text.trim().isEmpty) { - return; - } - - final message = chatController.sendController.text; - final fakeEventId = chatController.sendFakeMessage(); - + Future getMessageContent(String message) async { TokensResponseModel? tokensResp; if (l1LangCode != null && l2LangCode != null) { final res = await pangeaController.messageData @@ -320,8 +250,9 @@ class Choreographer extends ChangeNotifier { final hasOriginalWritten = _choreoRecord?.includedIT == true && itController.sourceText.value != null; - chatController.send( + return PangeaMessageContentModel( message: message, + choreo: _choreoRecord, originalSent: PangeaRepresentation( langCode: tokensResp?.detections.firstOrNull?.langCode ?? LanguageKeys.unknownLanguage, @@ -343,11 +274,7 @@ class Choreographer extends ChangeNotifier { detections: tokensResp.detections, ) : null, - choreo: _choreoRecord, - tempEventId: fakeEventId, ); - - clear(); } void openIT(PangeaMatchState itMatch) { diff --git a/lib/pangea/choreographer/choreographer_send_button.dart b/lib/pangea/choreographer/choreographer_send_button.dart index 98755b588..b367ee482 100644 --- a/lib/pangea/choreographer/choreographer_send_button.dart +++ b/lib/pangea/choreographer/choreographer_send_button.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pages/chat/chat.dart'; -import 'package:fluffychat/pangea/choreographer/assistance_state_enum.dart'; import 'package:fluffychat/pangea/choreographer/choreographer_state_extension.dart'; class ChoreographerSendButton extends StatelessWidget { @@ -19,12 +18,9 @@ class ChoreographerSendButton extends StatelessWidget { @override Widget build(BuildContext context) { - return ListenableBuilder( - listenable: Listenable.merge([ - controller.sendController, - controller.choreographer.isFetching, - ]), - builder: (context, _) { + return ValueListenableBuilder( + valueListenable: controller.choreographer.isFetching, + builder: (context, fetching, __) { return Container( height: 56, alignment: Alignment.center, @@ -32,9 +28,7 @@ class ChoreographerSendButton extends StatelessWidget { icon: const Icon(Icons.send_outlined), color: controller.choreographer.assistanceState .sendButtonColor(context), - onPressed: controller.choreographer.isFetching.value - ? null - : () => _onPressed(context), + onPressed: fetching ? null : () => _onPressed(context), tooltip: L10n.of(context).send, ), ); diff --git a/lib/pangea/choreographer/choreographer_state_extension.dart b/lib/pangea/choreographer/choreographer_state_extension.dart index 81dd7e234..55e850cdf 100644 --- a/lib/pangea/choreographer/choreographer_state_extension.dart +++ b/lib/pangea/choreographer/choreographer_state_extension.dart @@ -24,7 +24,10 @@ extension ChoregrapherUserSettingsExtension on Choreographer { } if (isFetching.value) return AssistanceStateEnum.fetching; - if (!igcController.hasIGCTextData) return AssistanceStateEnum.notFetched; + if (!igcController.hasIGCTextData && + itController.sourceText.value == null) { + return AssistanceStateEnum.notFetched; + } return AssistanceStateEnum.complete; } } diff --git a/lib/pangea/choreographer/igc/start_igc_button.dart b/lib/pangea/choreographer/igc/start_igc_button.dart index c106a8377..b3c7e9917 100644 --- a/lib/pangea/choreographer/igc/start_igc_button.dart +++ b/lib/pangea/choreographer/igc/start_igc_button.dart @@ -1,121 +1,143 @@ -import 'dart:async'; -import 'dart:math' as math; +import 'package:flutter/material.dart'; -import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/choreographer/assistance_state_enum.dart'; import 'package:fluffychat/pangea/choreographer/choreographer_state_extension.dart'; import 'package:fluffychat/pangea/learning_settings/pages/settings_learning.dart'; -import 'package:flutter/material.dart'; - import '../../../pages/chat/chat.dart'; class StartIGCButton extends StatefulWidget { + final ChatController controller; + final AssistanceStateEnum initialState; + final Color initialForegroundColor; + final Color initialBackgroundColor; + const StartIGCButton({ super.key, required this.controller, + required this.initialState, + required this.initialForegroundColor, + required this.initialBackgroundColor, }); - final ChatController controller; - @override - State createState() => StartIGCButtonState(); + State createState() => _StartIGCButtonState(); } -class StartIGCButtonState extends State - with SingleTickerProviderStateMixin { - AssistanceStateEnum get assistanceState => - widget.controller.choreographer.assistanceState; - AnimationController? _controller; +class _StartIGCButtonState extends State + with TickerProviderStateMixin { + AnimationController? _spinController; + late Animation _rotation; + + AnimationController? _colorController; + late Animation _iconColor; + late Animation _backgroundColor; AssistanceStateEnum? _prevState; + AssistanceStateEnum get state => + widget.controller.choreographer.assistanceState; + + bool _shouldStop = false; + @override void initState() { - _controller = AnimationController( - vsync: this, - duration: const Duration(seconds: 2), - ); - widget.controller.choreographer.addListener(_updateSpinnerState); super.initState(); + _spinController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 300), + )..addStatusListener((status) { + if (status == AnimationStatus.completed) { + if (_shouldStop) { + _spinController?.stop(); + _spinController?.value = 0; + } else { + _spinController?.forward(from: 0); + } + } + }); + + _rotation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _spinController!, + curve: Curves.linear, + ), + ); + + _colorController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 300), + ); + + _prevState = widget.initialState; + _iconColor = AlwaysStoppedAnimation(widget.initialForegroundColor); + _backgroundColor = AlwaysStoppedAnimation(widget.initialBackgroundColor); + _colorController!.forward(from: 0.0); + + widget.controller.choreographer.addListener(_handleStateChange); } @override void dispose() { - _controller?.dispose(); + widget.controller.choreographer.removeListener(_handleStateChange); + _spinController?.dispose(); + _colorController?.dispose(); super.dispose(); } - void _updateSpinnerState() { - if (_prevState != AssistanceStateEnum.fetching && - assistanceState == AssistanceStateEnum.fetching) { - _controller?.repeat(); - } else if (_prevState == AssistanceStateEnum.fetching && - assistanceState != AssistanceStateEnum.fetching) { - _controller?.reset(); - } - if (mounted) { - setState(() => _prevState = assistanceState); - } - } + void _handleStateChange() { + final prev = _prevState; + final current = state; + _prevState = current; - bool get _enableFeedback { - return ![ - AssistanceStateEnum.fetching, - AssistanceStateEnum.fetched, - AssistanceStateEnum.complete, - AssistanceStateEnum.noMessage, - AssistanceStateEnum.noSub, - AssistanceStateEnum.error, - ].contains(assistanceState); - } + if (!mounted || prev == current) return; + final newIconColor = current.stateColor(context); + final newBgColor = current.backgroundColor(context); + final oldIconColor = _iconColor.value; + final oldBgColor = _backgroundColor.value; - Future _onTap() async { - if (!_enableFeedback) return; - if (widget.controller.shouldShowLanguageMismatchPopup) { - widget.controller.showLanguageMismatchPopup(); - } else { - await widget.controller.choreographer.requestLanguageAssistance(); - final openMatch = - widget.controller.choreographer.igcController.firstOpenMatch; - widget.controller.onSelectMatch(openMatch); - } - } + // Create tweens from current → new colors + _iconColor = ColorTween( + begin: oldIconColor, + end: newIconColor, + ).animate(_colorController!); + _backgroundColor = ColorTween( + begin: oldBgColor, + end: newBgColor, + ).animate(_colorController!); + _colorController!.forward(from: 0.0); - Color get _backgroundColor { - switch (assistanceState) { - case AssistanceStateEnum.noSub: - case AssistanceStateEnum.noMessage: - case AssistanceStateEnum.fetched: - case AssistanceStateEnum.complete: - case AssistanceStateEnum.error: - return Theme.of(context).colorScheme.surfaceContainerHighest; - case AssistanceStateEnum.notFetched: - case AssistanceStateEnum.fetching: - return Theme.of(context).colorScheme.primaryContainer; + if (current == AssistanceStateEnum.fetching) { + _shouldStop = false; + _spinController!.forward(from: 0.0); + } else if (prev == AssistanceStateEnum.fetching) { + _shouldStop = true; } } @override Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: widget.controller.choreographer.textController, - builder: (context, _, __) { - final icon = Icon( - size: 36, - Icons.autorenew_rounded, - color: assistanceState.stateColor(context), - ); + if (_colorController == null || _spinController == null) { + return const SizedBox.shrink(); + } + + return AnimatedBuilder( + animation: Listenable.merge([_colorController!, _spinController!]), + builder: (context, child) { + final enableFeedback = state.allowsFeedback; return Tooltip( - message: _enableFeedback ? L10n.of(context).check : "", + message: enableFeedback ? L10n.of(context).check : "", child: Material( - elevation: _enableFeedback ? 4.0 : 0.0, - borderRadius: BorderRadius.circular(99.0), + elevation: enableFeedback ? 4.0 : 0.0, + shape: const CircleBorder(), + clipBehavior: Clip.antiAlias, shadowColor: Theme.of(context).colorScheme.surface.withAlpha(128), child: InkWell( - enableFeedback: _enableFeedback, - onTap: _enableFeedback ? _onTap : null, + enableFeedback: enableFeedback, customBorder: const CircleBorder(), - onLongPress: _enableFeedback + onTap: enableFeedback + ? widget.controller.onRequestWritingAssistance + : null, + onLongPress: enableFeedback ? () => showDialog( context: context, builder: (c) => const SettingsLearning(), @@ -125,35 +147,40 @@ class StartIGCButtonState extends State child: Stack( alignment: Alignment.center, children: [ - AnimatedContainer( + Container( height: 40.0, width: 40.0, - duration: FluffyThemes.animationDuration, decoration: BoxDecoration( shape: BoxShape.circle, - color: _backgroundColor, + color: _backgroundColor.value, ), ), - _controller != null - ? RotationTransition( - turns: Tween(begin: 0.0, end: math.pi * 2) - .animate(_controller!), - child: icon, - ) - : icon, - AnimatedContainer( + AnimatedBuilder( + animation: _rotation, + builder: (context, child) { + return Transform.rotate( + angle: _rotation.value * 2 * 3.14159, + child: child, + ); + }, + child: Icon( + Icons.autorenew_rounded, + size: 36, + color: _iconColor.value, + ), + ), + Container( width: 20, height: 20, - duration: FluffyThemes.animationDuration, decoration: BoxDecoration( shape: BoxShape.circle, - color: _backgroundColor, + color: _backgroundColor.value, ), ), Icon( size: 16, Icons.check, - color: assistanceState.stateColor(context), + color: _iconColor.value, ), ], ), diff --git a/lib/pangea/choreographer/it/it_controller.dart b/lib/pangea/choreographer/it/it_controller.dart index da55ebb02..5f9e50d64 100644 --- a/lib/pangea/choreographer/it/it_controller.dart +++ b/lib/pangea/choreographer/it/it_controller.dart @@ -24,7 +24,6 @@ class ITController { final ValueNotifier _currentITStep = ValueNotifier(null); final ValueNotifier _open = ValueNotifier(false); final ValueNotifier _editing = ValueNotifier(false); - bool _dismissed = false; ITController(this.onError); @@ -32,7 +31,6 @@ class ITController { ValueNotifier get editing => _editing; ValueNotifier get currentITStep => _currentITStep; ValueNotifier get sourceText => _sourceText; - bool get dismissed => _dismissed; ITRequestModel _request(String textInput) { assert(_sourceText.value != null); @@ -57,12 +55,11 @@ class ITController { ); } - void clear({bool dismissed = false}) { + void clear() { MatrixState.pAnyState.closeOverlay("it_feedback_card"); _open.value = false; _editing.value = false; - _dismissed = dismissed; _queue.clear(); _currentITStep.value = null; _goldRouteTracker = null; @@ -85,7 +82,7 @@ class ITController { continueIT(); } - void closeIT() => clear(dismissed: true); + void closeIT() => clear(); void setEditing(bool value) { _editing.value = value; @@ -193,6 +190,10 @@ class ITController { final goldContinuances = _goldRouteTracker!.continuances; String currentText = goldContinuances[0].text; for (int i = 1; i < goldContinuances.length; i++) { + if (_sourceText.value == null || !_open.value) { + return; + } + final completer = Completer(); _queue.add(completer); final resp = await _safeRequest(currentText); diff --git a/lib/pangea/choreographer/pangea_message_content_model.dart b/lib/pangea/choreographer/pangea_message_content_model.dart new file mode 100644 index 000000000..4f7557b52 --- /dev/null +++ b/lib/pangea/choreographer/pangea_message_content_model.dart @@ -0,0 +1,21 @@ +import 'package:fluffychat/pangea/choreographer/choreo_record_model.dart'; +import 'package:fluffychat/pangea/events/models/representation_content_model.dart'; +import 'package:fluffychat/pangea/events/models/tokens_event_content_model.dart'; + +class PangeaMessageContentModel { + final String message; + final PangeaRepresentation? originalSent; + final PangeaRepresentation? originalWritten; + final PangeaMessageTokens? tokensSent; + final PangeaMessageTokens? tokensWritten; + final ChoreoRecordModel? choreo; + + const PangeaMessageContentModel({ + required this.message, + this.originalSent, + this.originalWritten, + this.tokensSent, + this.tokensWritten, + this.choreo, + }); +} diff --git a/lib/pangea/common/constants/model_keys.dart b/lib/pangea/common/constants/model_keys.dart index ddebf808d..bc6920ab7 100644 --- a/lib/pangea/common/constants/model_keys.dart +++ b/lib/pangea/common/constants/model_keys.dart @@ -90,7 +90,6 @@ class ModelKey { static const String messageTagMorphEdit = "morph_edit"; static const String messageTagLemmaEdit = "lemma_edit"; static const String messageTagActivityPlan = "activity_plan"; - static const String tempEventId = "temporary_event_id"; static const String baseDefinition = "base_definition"; static const String targetDefinition = "target_definition"; diff --git a/lib/pangea/common/utils/overlay.dart b/lib/pangea/common/utils/overlay.dart index 82f8fb38c..66248b5e0 100644 --- a/lib/pangea/common/utils/overlay.dart +++ b/lib/pangea/common/utils/overlay.dart @@ -54,50 +54,45 @@ class OverlayUtil { } final OverlayEntry entry = OverlayEntry( - builder: (context) => AnimatedContainer( - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - child: Stack( - children: [ - if (backDropToDismiss) - IgnorePointer( - ignoring: ignorePointer, - child: TransparentBackdrop( - backgroundColor: backgroundColor, - onDismiss: onDismiss, - blurBackground: blurBackground, - ), + builder: (context) => Stack( + children: [ + if (backDropToDismiss) + IgnorePointer( + ignoring: ignorePointer, + child: TransparentBackdrop( + backgroundColor: backgroundColor, + onDismiss: onDismiss, + blurBackground: blurBackground, ), - Positioned( - top: (position == OverlayPositionEnum.centered || - position == OverlayPositionEnum.top) - ? 0 - : null, - right: (position == OverlayPositionEnum.centered || - position == OverlayPositionEnum.top) - ? 0 - : null, - left: (position == OverlayPositionEnum.centered || - position == OverlayPositionEnum.top) - ? 0 - : null, - bottom: (position == OverlayPositionEnum.centered) ? 0 : null, - child: (position != OverlayPositionEnum.transform) - ? child - : CompositedTransformFollower( - targetAnchor: targetAnchor ?? Alignment.topCenter, - followerAnchor: - followerAnchor ?? Alignment.bottomCenter, - link: MatrixState.pAnyState - .layerLinkAndKey(transformTargetId!) - .link, - showWhenUnlinked: false, - offset: offset ?? Offset.zero, - child: child, - ), ), - ], - ), + Positioned( + top: (position == OverlayPositionEnum.centered || + position == OverlayPositionEnum.top) + ? 0 + : null, + right: (position == OverlayPositionEnum.centered || + position == OverlayPositionEnum.top) + ? 0 + : null, + left: (position == OverlayPositionEnum.centered || + position == OverlayPositionEnum.top) + ? 0 + : null, + bottom: (position == OverlayPositionEnum.centered) ? 0 : null, + child: (position != OverlayPositionEnum.transform) + ? child + : CompositedTransformFollower( + targetAnchor: targetAnchor ?? Alignment.topCenter, + followerAnchor: followerAnchor ?? Alignment.bottomCenter, + link: MatrixState.pAnyState + .layerLinkAndKey(transformTargetId!) + .link, + showWhenUnlinked: false, + offset: offset ?? Offset.zero, + child: child, + ), + ), + ], ), ); diff --git a/lib/pangea/common/widgets/transparent_backdrop.dart b/lib/pangea/common/widgets/transparent_backdrop.dart index 2c9b22a80..7bf26dcde 100644 --- a/lib/pangea/common/widgets/transparent_backdrop.dart +++ b/lib/pangea/common/widgets/transparent_backdrop.dart @@ -2,11 +2,9 @@ import 'dart:ui'; import 'package:flutter/material.dart'; -import 'package:fluffychat/config/app_config.dart'; -import '../../../config/themes.dart'; import '../../../widgets/matrix.dart'; -class TransparentBackdrop extends StatefulWidget { +class TransparentBackdrop extends StatelessWidget { final Color? backgroundColor; final VoidCallback? onDismiss; final bool blurBackground; @@ -18,91 +16,38 @@ class TransparentBackdrop extends StatefulWidget { this.blurBackground = false, }); - @override - TransparentBackdropState createState() => TransparentBackdropState(); -} - -class TransparentBackdropState extends State - with SingleTickerProviderStateMixin { - late AnimationController _controller; - late Animation _opacityTween; - late Animation _blurTween; - - @override - void initState() { - super.initState(); - _controller = AnimationController( - duration: - const Duration(milliseconds: AppConfig.overlayAnimationDuration), - vsync: this, - ); - _opacityTween = Tween(begin: 0.0, end: 0.8).animate( - CurvedAnimation( - parent: _controller, - curve: FluffyThemes.animationCurve, - ), - ); - _blurTween = Tween(begin: 0.0, end: 3.0).animate( - CurvedAnimation( - parent: _controller, - curve: FluffyThemes.animationCurve, - ), - ); - - Future.delayed(const Duration(milliseconds: 100), () { - if (mounted) _controller.forward(); - }); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { - return AnimatedBuilder( - animation: _opacityTween, - builder: (context, _) { - return Material( - borderOnForeground: false, - color: widget.backgroundColor - ?.withAlpha((_opacityTween.value * 255).round()) ?? - Colors.transparent, - clipBehavior: Clip.antiAlias, - child: InkWell( - hoverColor: Colors.transparent, - splashColor: Colors.transparent, - focusColor: Colors.transparent, - highlightColor: Colors.transparent, - onTap: () { - if (widget.onDismiss != null) { - widget.onDismiss!(); - } - MatrixState.pAnyState.closeOverlay(); - }, - child: AnimatedBuilder( - animation: _blurTween, - builder: (context, _) { - return BackdropFilter( - filter: widget.blurBackground - ? ImageFilter.blur( - sigmaX: _blurTween.value, - sigmaY: _blurTween.value, - ) - : ImageFilter.blur(sigmaX: 0, sigmaY: 0), - child: Container( - height: double.infinity, - width: double.infinity, - color: Colors.transparent, - ), - ); - }, - ), + return Material( + borderOnForeground: false, + color: + backgroundColor?.withAlpha((0.8 * 255).round()) ?? Colors.transparent, + clipBehavior: Clip.antiAlias, + child: InkWell( + hoverColor: Colors.transparent, + splashColor: Colors.transparent, + focusColor: Colors.transparent, + highlightColor: Colors.transparent, + onTap: () { + if (onDismiss != null) { + onDismiss!(); + } + MatrixState.pAnyState.closeOverlay(); + }, + child: BackdropFilter( + filter: blurBackground + ? ImageFilter.blur( + sigmaX: 3.0, + sigmaY: 3.0, + ) + : ImageFilter.blur(sigmaX: 0, sigmaY: 0), + child: Container( + height: double.infinity, + width: double.infinity, + color: Colors.transparent, ), - ); - }, + ), + ), ); } } diff --git a/lib/pangea/extensions/room_events_extension.dart b/lib/pangea/extensions/room_events_extension.dart index cae7e6663..fd19ba199 100644 --- a/lib/pangea/extensions/room_events_extension.dart +++ b/lib/pangea/extensions/room_events_extension.dart @@ -158,11 +158,11 @@ extension EventsRoomExtension on Room { return content; } - String sendFakeMessage({ + Future sendFakeMessage({ required String text, Event? inReplyTo, String? editEventId, - }) { + }) async { // Create new transaction id final messageID = client.generateUniqueTransactionId(); @@ -180,9 +180,29 @@ extension EventsRoomExtension on Room { room: this, originServerTs: DateTime.now(), status: EventStatus.sending, + unsigned: { + messageSendingStatusKey: EventStatus.sending.intValue, + 'transaction_id': messageID, + }, ); - timeline?.events.insert(0, event); + final syncUpdate = SyncUpdate( + nextBatch: '', + rooms: RoomsUpdate( + join: { + id: JoinedRoomUpdate( + timeline: TimelineUpdate( + events: [ + event, + ], + ), + ), + }, + ), + ); + await client.database.transaction(() async { + await client.handleSync(syncUpdate); + }); return messageID; } @@ -202,7 +222,6 @@ extension EventsRoomExtension on Room { PangeaMessageTokens? tokensWritten, ChoreoRecordModel? choreo, String? messageTag, - String? tempEventId, }) { // if (parseCommands) { // return client.parseAndRunCommand( @@ -238,9 +257,6 @@ extension EventsRoomExtension on Room { if (messageTag != null) { event[ModelKey.messageTags] = messageTag; } - if (tempEventId != null) { - event[ModelKey.tempEventId] = tempEventId; - } if (parseMarkdown) { final html = markdown( diff --git a/lib/pangea/spaces/utils/load_participants_util.dart b/lib/pangea/spaces/utils/load_participants_util.dart index b5c0fe96c..709f1557b 100644 --- a/lib/pangea/spaces/utils/load_participants_util.dart +++ b/lib/pangea/spaces/utils/load_participants_util.dart @@ -51,6 +51,10 @@ class LoadParticipantsBuilderState extends State { } Future _loadParticipants() async { + if (widget.room == null || widget.room!.participantListComplete) { + return; + } + try { setState(() { loading = true; diff --git a/lib/pangea/toolbar/widgets/message_selection_positioner.dart b/lib/pangea/toolbar/widgets/message_selection_positioner.dart index b22450b96..65cb45e42 100644 --- a/lib/pangea/toolbar/widgets/message_selection_positioner.dart +++ b/lib/pangea/toolbar/widgets/message_selection_positioner.dart @@ -452,31 +452,29 @@ class MessageReactionPicker extends StatelessWidget { @override Widget build(BuildContext context) { - if (chatController.selectedEvents.length != 1) { - return const SizedBox.shrink(); - } - final theme = Theme.of(context); final sentReactions = {}; - final event = chatController.selectedEvents.first; - sentReactions.addAll( - event - .aggregatedEvents( - chatController.timeline!, - RelationshipTypes.reaction, - ) - .where( - (event) => - event.senderId == event.room.client.userID && - event.type == 'm.reaction', - ) - .map( - (event) => event.content - .tryGetMap('m.relates_to') - ?.tryGet('key'), - ) - .whereType(), - ); + final event = chatController.selectedEvents.firstOrNull; + if (event != null) { + sentReactions.addAll( + event + .aggregatedEvents( + chatController.timeline!, + RelationshipTypes.reaction, + ) + .where( + (event) => + event.senderId == event.room.client.userID && + event.type == 'm.reaction', + ) + .map( + (event) => event.content + .tryGetMap('m.relates_to') + ?.tryGet('key'), + ) + .whereType(), + ); + } return Material( elevation: 4, @@ -512,7 +510,7 @@ class MessageReactionPicker extends StatelessWidget { ), onPressed: sentReactions.contains(emoji) ? null - : () => event.room.sendReaction( + : () => event?.room.sendReaction( event.eventId, emoji, ), @@ -583,7 +581,7 @@ class MessageReactionPicker extends StatelessWidget { ); if (emoji == null) return; if (sentReactions.contains(emoji)) return; - await event.room.sendReaction( + await event?.room.sendReaction( event.eventId, emoji, ); diff --git a/lib/pangea/toolbar/widgets/select_mode_buttons.dart b/lib/pangea/toolbar/widgets/select_mode_buttons.dart index 85cdba00c..c6968ef87 100644 --- a/lib/pangea/toolbar/widgets/select_mode_buttons.dart +++ b/lib/pangea/toolbar/widgets/select_mode_buttons.dart @@ -747,7 +747,6 @@ class MoreButton extends StatelessWidget { break; case MessageActions.info: controller.showEventInfo(); - controller.clearSelectedEvents(); break; case MessageActions.deleteOnError: controller.deleteErrorEventsAction(); diff --git a/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart b/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart index 2688389af..a85c591aa 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart @@ -212,27 +212,25 @@ class WordZoomWidget extends StatelessWidget { return Material( type: MaterialType.transparency, - child: SelectionArea( - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).cardColor, - border: Border.all( - color: Theme.of(context).colorScheme.primary, - width: 4.0, - ), - borderRadius: const BorderRadius.all( - Radius.circular(AppConfig.borderRadius), - ), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + border: Border.all( + color: Theme.of(context).colorScheme.primary, + width: 4.0, ), - height: AppConfig.toolbarMaxHeight, - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - content, - ], + borderRadius: const BorderRadius.all( + Radius.circular(AppConfig.borderRadius), ), ), + height: AppConfig.toolbarMaxHeight, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + content, + ], + ), ), ); }