diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 6143b3feb..745277ab6 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -1,3 +1,5 @@ +// ignore_for_file: depend_on_referenced_packages, implementation_imports + import 'dart:async'; import 'dart:core'; import 'dart:developer'; @@ -386,6 +388,14 @@ 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 (_fakeEventID != null) { + animateInEventIndex = null; + return; + } + // Pangea# animateInEventIndex = i; } @@ -552,6 +562,7 @@ class ChatController extends State clearSelectedEvents(); MatrixState.pAnyState.closeOverlay(); showToolbarStream.close(); + hideTextController.dispose(); //Pangea# super.dispose(); } @@ -559,6 +570,10 @@ class ChatController extends State // #Pangea // TextEditingController sendController = TextEditingController(); PangeaTextController get sendController => choreographer.textController; + + /// used to obscure text in text field after sending fake message without + /// changing the actual text in the sendController + final TextEditingController hideTextController = TextEditingController(); // #Pangea void setSendingClient(Client c) { @@ -593,6 +608,29 @@ class ChatController extends State pangeaEditingEvent = null; } + String? _fakeEventID; + bool get obscureText => _fakeEventID != null; + + /// 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. + void sendFakeMessage() { + final eventID = room.sendFakeMessage( + text: sendController.text, + inReplyTo: replyEvent, + editEventId: editEvent?.eventId, + ); + setState(() => _fakeEventID = eventID); + } + + void clearFakeEvent() { + if (_fakeEventID == null) return; + timeline?.events.removeWhere((e) => e.eventId == _fakeEventID); + setState(() { + _fakeEventID = null; + }); + } + // 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. @@ -635,6 +673,11 @@ class ChatController extends State // parseCommands: parseCommands, // ); final previousEdit = editEvent; + + // wait for the next event to come through before clearing any fake event, + // to make the replacement look smooth + room.client.onEvent.stream.first.then((_) => clearFakeEvent()); + room .pangeaSendTextEvent( sendController.text, diff --git a/lib/pages/chat/input_bar.dart b/lib/pages/chat/input_bar.dart index 6c264bb72..6dd92ff59 100644 --- a/lib/pages/chat/input_bar.dart +++ b/lib/pages/chat/input_bar.dart @@ -467,7 +467,16 @@ class InputBar extends StatelessWidget { direction: VerticalDirection.up, hideOnEmpty: true, hideOnLoading: true, - controller: controller, + // #Pangea + // if should obscure text (to make it looks that a message has been sent after sending fake message), + // use hideTextController + + // controller: controller, + controller: + (controller?.choreographer.chatController.obscureText) ?? false + ? controller?.choreographer.chatController.hideTextController + : controller, + // Pangea# focusNode: focusNode, hideOnSelect: false, debounceDuration: const Duration(milliseconds: 50), @@ -480,8 +489,13 @@ class InputBar extends StatelessWidget { readOnly: controller != null && controller!.choreographer.isRunningIT, autocorrect: false, + // controller: controller, + controller: (controller + ?.choreographer.chatController.obscureText) ?? + false + ? controller?.choreographer.chatController.hideTextController + : controller, // Pangea# - controller: controller, focusNode: focusNode, contextMenuBuilder: (c, e) => markdownContextBuilder( c, diff --git a/lib/pangea/choreographer/controllers/choreographer.dart b/lib/pangea/choreographer/controllers/choreographer.dart index 5a36bd6cb..b2ff1bcaf 100644 --- a/lib/pangea/choreographer/controllers/choreographer.dart +++ b/lib/pangea/choreographer/controllers/choreographer.dart @@ -158,6 +158,7 @@ class Choreographer { "choreoRecord": choreoRecord.toJson(), }, ); + await igc.getIGCTextData(onlyTokensAndLanguageDetection: true); } diff --git a/lib/pangea/choreographer/controllers/igc_controller.dart b/lib/pangea/choreographer/controllers/igc_controller.dart index 3f33763cf..462500d6e 100644 --- a/lib/pangea/choreographer/controllers/igc_controller.dart +++ b/lib/pangea/choreographer/controllers/igc_controller.dart @@ -77,6 +77,12 @@ class IgcController { try { if (choreographer.currentText.isEmpty) return clear(); + // if tokenizing on message send, tokenization might take a while + // so add a fake event to the timeline to visually indicate that the message is being sent + if (onlyTokensAndLanguageDetection) { + choreographer.chatController.sendFakeMessage(); + } + debugPrint('getIGCTextData called with ${choreographer.currentText}'); debugPrint( 'getIGCTextData called with tokensOnly = $onlyTokensAndLanguageDetection', diff --git a/lib/pangea/extensions/pangea_room_extension.dart b/lib/pangea/extensions/pangea_room_extension.dart index c7329b8a1..ad519b677 100644 --- a/lib/pangea/extensions/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension.dart @@ -172,6 +172,17 @@ extension PangeaRoom on Room { messageTag: messageTag, ); + String sendFakeMessage({ + required String text, + Event? inReplyTo, + String? editEventId, + }) => + _sendFakeMessage( + text: text, + inReplyTo: inReplyTo, + editEventId: editEventId, + ); + // room_information Future get numNonAdmins async => await _numNonAdmins; diff --git a/lib/pangea/extensions/room_events_extension.dart b/lib/pangea/extensions/room_events_extension.dart index aa2cf886a..a3021d623 100644 --- a/lib/pangea/extensions/room_events_extension.dart +++ b/lib/pangea/extensions/room_events_extension.dart @@ -221,6 +221,94 @@ extension EventsRoomExtension on Room { } } + String _sendFakeMessage({ + required String text, + Event? inReplyTo, + String? editEventId, + }) { + final content = { + 'msgtype': MessageTypes.Text, + 'body': text, + }; + + final html = markdown( + content['body'], + getEmotePacks: () => getImagePacksFlat(ImagePackUsage.emoticon), + getMention: getMention, + ); + // if the decoded html is the same as the body, there is no need in sending a formatted message + if (HtmlUnescape().convert(html.replaceAll(RegExp(r'
\n?'), '\n')) != + content['body']) { + content['format'] = 'org.matrix.custom.html'; + content['formatted_body'] = html; + } + + // Create new transaction id + final messageID = client.generateUniqueTransactionId(); + + if (inReplyTo != null) { + var replyText = '<${inReplyTo.senderId}> ${inReplyTo.body}'; + replyText = replyText.split('\n').map((line) => '> $line').join('\n'); + content['format'] = 'org.matrix.custom.html'; + // be sure that we strip any previous reply fallbacks + final replyHtml = (inReplyTo.formattedText.isNotEmpty + ? inReplyTo.formattedText + : htmlEscape.convert(inReplyTo.body).replaceAll('\n', '
')) + .replaceAll( + RegExp( + r'.*', + caseSensitive: false, + multiLine: false, + dotAll: true, + ), + '', + ); + final repliedHtml = content.tryGet('formatted_body') ?? + htmlEscape + .convert(content.tryGet('body') ?? '') + .replaceAll('\n', '
'); + content['formatted_body'] = + '
In reply to ${inReplyTo.senderId}
$replyHtml
$repliedHtml'; + // We escape all @room-mentions here to prevent accidental room pings when an admin + // replies to a message containing that! + content['body'] = + '${replyText.replaceAll('@room', '@\u200broom')}\n\n${content.tryGet('body') ?? ''}'; + content['m.relates_to'] = { + 'm.in_reply_to': { + 'event_id': inReplyTo.eventId, + }, + }; + } + + if (editEventId != null) { + final newContent = content.copy(); + content['m.new_content'] = newContent; + content['m.relates_to'] = { + 'event_id': editEventId, + 'rel_type': RelationshipTypes.edit, + }; + if (content['body'] is String) { + content['body'] = '* ${content['body']}'; + } + if (content['formatted_body'] is String) { + content['formatted_body'] = '* ${content['formatted_body']}'; + } + } + + final Event event = Event( + content: content, + type: EventTypes.Message, + senderId: client.userID!, + eventId: messageID, + room: this, + originServerTs: DateTime.now(), + status: EventStatus.sending, + ); + + timeline?.events.insert(0, event); + return messageID; + } + Future _pangeaSendTextEvent( String message, { String? txid,