refactor: improvements to fake message display, allow users to send more than one fake message at a time (#2925)

This commit is contained in:
ggurdin 2025-06-04 14:01:19 -04:00 committed by GitHub
parent 583479873d
commit 0c4597226f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 102 additions and 57 deletions

View file

@ -3,6 +3,7 @@
import 'dart:async';
import 'dart:developer';
import 'dart:io';
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@ -39,6 +40,7 @@ import 'package:fluffychat/pangea/choreographer/enums/edit_type.dart';
import 'package:fluffychat/pangea/choreographer/models/choreo_record.dart';
import 'package:fluffychat/pangea/choreographer/widgets/igc/message_analytics_feedback.dart';
import 'package:fluffychat/pangea/choreographer/widgets/igc/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';
@ -515,7 +517,7 @@ class ChatController extends State<ChatPageWithRoom>
// #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) {
if (_fakeEventIDs.isNotEmpty) {
animateInEventIndex = null;
return;
}
@ -687,7 +689,6 @@ class ChatController extends State<ChatPageWithRoom>
MatrixState.pAnyState.closeAllOverlays(force: true);
showToolbarStream.close();
stopMediaStream.close();
hideTextController.dispose();
_levelSubscription?.cancel();
_analyticsSubscription?.cancel();
_router.routeInformationProvider.removeListener(_onRouteChanged);
@ -720,10 +721,6 @@ class ChatController extends State<ChatPageWithRoom>
// 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) {
@ -758,26 +755,47 @@ class ChatController extends State<ChatPageWithRoom>
pangeaEditingEvent = null;
}
String? _fakeEventID;
bool get obscureText => _fakeEventID != null;
final List<String> _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.
void sendFakeMessage() {
String? sendFakeMessage() {
if (sendController.text.trim().isEmpty) return null;
final eventID = room.sendFakeMessage(
text: sendController.text,
inReplyTo: replyEvent,
editEventId: editEvent?.eventId,
);
setState(() => _fakeEventID = eventID);
sendController.setSystemText("", EditType.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() {
if (_fakeEventID == null) return;
timeline?.events.removeWhere((e) => e.eventId == _fakeEventID);
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(() {
_fakeEventID = null;
_fakeEventIDs.remove(eventId);
});
}
@ -786,20 +804,26 @@ class ChatController extends State<ChatPageWithRoom>
// but for choero, the tx id is generated before the message send.
// Also, adding PangeaMessageData
Future<void> send({
required String message,
PangeaRepresentation? originalSent,
PangeaRepresentation? originalWritten,
PangeaMessageTokens? tokensSent,
PangeaMessageTokens? tokensWritten,
ChoreoRecord? choreo,
String? tempEventId,
}) async {
if (message.trim().isEmpty) return;
// if (sendController.text.trim().isEmpty) return;
// Pangea#
if (sendController.text.trim().isEmpty) return;
_storeInputTimeoutTimer?.cancel();
final prefs = await SharedPreferences.getInstance();
prefs.remove('draft_$roomId');
var parseCommands = true;
final commandMatch = RegExp(r'^\/(\w+)').firstMatch(sendController.text);
// #Pangea
// final commandMatch = RegExp(r'^\/(\w+)').firstMatch(sendController.text);
final commandMatch = RegExp(r'^\/(\w+)').firstMatch(message);
// Pangea#
if (commandMatch != null &&
!sendingClient.commands.keys.contains(commandMatch[1]!.toLowerCase())) {
final l10n = L10n.of(context);
@ -810,7 +834,13 @@ class ChatController extends State<ChatPageWithRoom>
okLabel: l10n.sendAsText,
cancelLabel: l10n.cancel,
);
if (dialogResult == OkCancelResult.cancel) return;
// #Pangea
// if (dialogResult == OkCancelResult.cancel) return;
if (dialogResult == OkCancelResult.cancel) {
clearFakeEvent(tempEventId);
return;
}
// Pangea#
parseCommands = false;
}
@ -822,15 +852,20 @@ class ChatController extends State<ChatPageWithRoom>
// editEventId: editEvent?.eventId,
// 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.onTimelineEvent.stream.first.then((_) => clearFakeEvent());
// If the message and the sendController text don't match, it's possible
// that there was a delay in tokenization before send, and the user started
// typing a new message. We don't want to erase that, so only reset the input
// bar text if the message is the same as the sendController text.
if (message == sendController.text) {
sendController.setSystemText("", EditType.other);
}
final previousEdit = editEvent;
room
.pangeaSendTextEvent(
sendController.text,
message,
inReplyTo: replyEvent,
editEventId: editEvent?.eventId,
parseCommands: parseCommands,
@ -839,6 +874,7 @@ class ChatController extends State<ChatPageWithRoom>
tokensSent: tokensSent,
tokensWritten: tokensWritten,
choreo: choreo,
tempEventId: tempEventId,
)
.then(
(String? msgEventId) async {
@ -915,7 +951,7 @@ class ChatController extends State<ChatPageWithRoom>
s: StackTrace.current,
data: {
'roomId': roomId,
'text': sendController.text,
'text': message,
'inReplyTo': replyEvent?.eventId,
'editEventId': editEvent?.eventId,
},
@ -924,7 +960,7 @@ class ChatController extends State<ChatPageWithRoom>
}
},
).catchError((err, s) {
clearFakeEvent();
clearFakeEvent(tempEventId);
if (err is EventTooLarge) {
showAdaptiveDialog(
context: context,
@ -937,22 +973,21 @@ class ChatController extends State<ChatPageWithRoom>
s: s,
data: {
'roomId': roomId,
'text': sendController.text,
'text': message,
'inReplyTo': replyEvent?.eventId,
'editEventId': editEvent?.eventId,
},
);
});
// sendController.value = TextEditingValue(
// text: pendingText,
// selection: const TextSelection.collapsed(offset: 0),
// );
// Pangea#
sendController.value = TextEditingValue(
text: pendingText,
selection: const TextSelection.collapsed(offset: 0),
);
setState(() {
// #Pangea
// sendController.text = pendingText;
sendController.setSystemText(pendingText, EditType.other);
// Pangea#
_inputTextIsEmpty = pendingText.isEmpty;
replyEvent = null;

View file

@ -320,7 +320,12 @@ class ChatInputRow extends StatelessWidget {
)
: FloatingActionButton.small(
tooltip: L10n.of(context).send,
onPressed: controller.send,
// #Pangea
// onPressed: controller.send,
onPressed: () => controller.send(
message: controller.sendController.text,
),
// Pangea#
elevation: 0,
heroTag: null,
shape: RoundedRectangleBorder(

View file

@ -429,16 +429,7 @@ class InputBar extends StatelessWidget {
direction: VerticalDirection.up,
hideOnEmpty: true,
hideOnLoading: true,
// #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#
controller: controller,
focusNode: focusNode,
hideOnSelect: false,
debounceDuration: const Duration(milliseconds: 50),
@ -447,14 +438,10 @@ class InputBar extends StatelessWidget {
builder: (context, _, focusNode) {
final textField = TextField(
enableSuggestions: enableAutocorrect,
readOnly: controller != null &&
(controller!.choreographer.isRunningIT ||
controller!.choreographer.chatController.obscureText),
readOnly:
controller != null && (controller!.choreographer.isRunningIT),
autocorrect: enableAutocorrect,
controller:
(controller?.choreographer.chatController.obscureText) ?? false
? controller?.choreographer.chatController.hideTextController
: controller,
controller: controller,
focusNode: focusNode,
contextMenuBuilder: (c, e) => markdownContextBuilder(
c,

View file

@ -74,7 +74,10 @@ class ChatInputBarState extends State<ChatInputBar> {
),
child: Column(
children: [
ReplyDisplay(widget.controller),
// #Pangea
if (!widget.controller.obscureText)
// Pangea#
ReplyDisplay(widget.controller),
PangeaChatInputRow(
controller: widget.controller,
),

View file

@ -114,7 +114,9 @@ class Choreographer {
maxWidth: 325,
transformTargetId: inputTransformTargetKey,
)
: chatController.send();
: chatController.send(
message: chatController.sendController.text,
);
return;
}
@ -135,7 +137,12 @@ class Choreographer {
return;
}
chatController.sendFakeMessage();
if (chatController.sendController.text.trim().isEmpty) {
return;
}
final message = chatController.sendController.text;
final fakeEventId = chatController.sendFakeMessage();
final PangeaRepresentation? originalWritten =
choreoRecord.includedIT && itController.sourceText != null
? PangeaRepresentation(
@ -156,7 +163,7 @@ class Choreographer {
repEventId: null,
room: chatController.room,
req: TokensRequestModel(
fullText: currentText,
fullText: message,
senderL1: l1LangCode!,
senderL2: l2LangCode!,
),
@ -167,7 +174,7 @@ class Choreographer {
originalSent = PangeaRepresentation(
langCode: res?.detections.firstOrNull?.langCode ??
LanguageKeys.unknownLanguage,
text: currentText,
text: message,
originalSent: true,
originalWritten: originalWritten == null,
);
@ -183,7 +190,7 @@ class Choreographer {
e: e,
s: s,
data: {
"currentText": currentText,
"currentText": message,
"l1LangCode": l1LangCode,
"l2LangCode": l2LangCode,
"choreoRecord": choreoRecord.toJson(),
@ -191,9 +198,11 @@ class Choreographer {
);
} finally {
chatController.send(
message: message,
originalSent: originalSent,
tokensSent: tokensSent,
choreo: choreoRecord,
tempEventId: fakeEventId,
);
clear();
}
@ -558,8 +567,6 @@ class Choreographer {
choreoRecord = ChoreoRecord.newRecord;
itController.clear();
igc.dispose();
//@ggurdin - why is this commented out?
// errorService.clear();
_resetDebounceTimer();
}

View file

@ -90,6 +90,7 @@ 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";

View file

@ -97,7 +97,10 @@ class MessageDataController extends BaseController {
repEventId: repEventId,
req: req,
room: room,
);
).catchError((e, s) {
_tokensCache.remove(req.hashCode);
return Future<TokensResponseModel>.error(e, s);
});
/////// translation ////////

View file

@ -202,6 +202,7 @@ extension EventsRoomExtension on Room {
PangeaMessageTokens? tokensWritten,
ChoreoRecord? choreo,
String? messageTag,
String? tempEventId,
}) {
// if (parseCommands) {
// return client.parseAndRunCommand(this, message,
@ -233,6 +234,9 @@ extension EventsRoomExtension on Room {
if (messageTag != null) {
event[ModelKey.messageTags] = messageTag;
}
if (tempEventId != null) {
event[ModelKey.tempEventId] = tempEventId;
}
if (parseMarkdown) {
final html = markdown(