refactor: move all messaging sending logic into the chore controller, reduce full rebuilds of the chat view
This commit is contained in:
parent
e021130bda
commit
f681ffa71f
24 changed files with 1182 additions and 1215 deletions
|
|
@ -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<ShareItem>? shareItems;
|
||||
|
|
@ -181,6 +189,7 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
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<ChatPageWithRoom>
|
|||
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<ChatPageWithRoom>
|
|||
|
||||
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<ChatPageWithRoom>
|
|||
// Pangea#
|
||||
_tryLoadTimeline();
|
||||
if (kIsWeb) {
|
||||
// #Pangea
|
||||
onFocusSub?.cancel();
|
||||
// Pangea#
|
||||
onFocusSub = html.window.onFocus.listen((_) => setReadMarker());
|
||||
}
|
||||
}
|
||||
|
|
@ -602,7 +605,10 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
void updateView() {
|
||||
if (!mounted) return;
|
||||
setReadMarker();
|
||||
setState(() {});
|
||||
// #Pangea
|
||||
// setState(() {});
|
||||
if (mounted) timelineUpdateNotifier.notify();
|
||||
// Pangea#
|
||||
}
|
||||
|
||||
Future<void>? loadTimelineFuture;
|
||||
|
|
@ -611,14 +617,6 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
|
||||
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<ChatPageWithRoom>
|
|||
@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<ChatPageWithRoom>
|
|||
// 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<ChatPageWithRoom>
|
|||
|
||||
// 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<ChatPageWithRoom>
|
|||
pangeaEditingEvent = 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.
|
||||
String? sendFakeMessage() {
|
||||
Future<String?> 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<void> 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<void> send({
|
||||
required String message,
|
||||
PangeaRepresentation? originalSent,
|
||||
PangeaRepresentation? originalWritten,
|
||||
PangeaMessageTokens? tokensSent,
|
||||
PangeaMessageTokens? tokensWritten,
|
||||
ChoreoRecordModel? choreo,
|
||||
String? tempEventId,
|
||||
}) async {
|
||||
Future<void> 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<ChatPageWithRoom>
|
|||
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<ChatPageWithRoom>
|
|||
// 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<ChatPageWithRoom>
|
|||
}
|
||||
},
|
||||
).catchError((err, s) {
|
||||
clearFakeEvent(tempEventId);
|
||||
if (err is EventTooLarge) {
|
||||
showAdaptiveDialog(
|
||||
context: context,
|
||||
|
|
@ -1036,20 +1023,20 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
},
|
||||
);
|
||||
});
|
||||
// #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<ChatPageWithRoom>
|
|||
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<ChatPageWithRoom>
|
|||
// 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<ChatPageWithRoom>
|
|||
|
||||
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<ChatPageWithRoom>
|
|||
),
|
||||
);
|
||||
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<ChatPageWithRoom>
|
|||
);
|
||||
}
|
||||
// #Pangea
|
||||
// setState(() {
|
||||
// showEmojiPicker = false;
|
||||
// selectedEvents.clear();
|
||||
// });
|
||||
clearSelectedEvents();
|
||||
// Pangea#
|
||||
setState(() {
|
||||
showEmojiPicker = false;
|
||||
selectedEvents.clear();
|
||||
});
|
||||
}
|
||||
|
||||
List<Client?> get currentRoomBundle {
|
||||
|
|
@ -1522,17 +1509,20 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
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<ChatPageWithRoom>
|
|||
// });
|
||||
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<ChatPageWithRoom>
|
|||
});
|
||||
}
|
||||
|
||||
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<ChatPageWithRoom>
|
|||
);
|
||||
}
|
||||
|
||||
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<String, int> thisEventsKeyMap) {
|
||||
// this method is called very often. As such, it has to be optimized for speed.
|
||||
|
|
@ -1757,17 +1750,13 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
// void onInputBarSubmitted(String _) {
|
||||
Future<void> 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<ChatPageWithRoom>
|
|||
..removeWhere((oldEvent) => oldEvent == eventId);
|
||||
// #Pangea
|
||||
if (scrollToEventIdMarker == eventId) {
|
||||
setState(() {
|
||||
scrollToEventIdMarker = null;
|
||||
});
|
||||
scrollToEventIdMarker = null;
|
||||
}
|
||||
// Pangea#
|
||||
showFutureLoadingDialog(
|
||||
|
|
@ -1854,31 +1841,38 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
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<ChatPageWithRoom>
|
|||
}
|
||||
}
|
||||
|
||||
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<ChatPageWithRoom>
|
|||
|
||||
final StreamController<void> 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<ChatPageWithRoom>
|
|||
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<ChatPageWithRoom>
|
|||
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<ChatPageWithRoom>
|
|||
l1 != activityLang;
|
||||
}
|
||||
|
||||
void onSelectMatch(PangeaMatchState? match) {
|
||||
if (match != null) {
|
||||
if (match.updatedMatch.isITStart) {
|
||||
choreographer.openIT(match);
|
||||
} else {
|
||||
OverlayUtil.showIGCMatch(
|
||||
match,
|
||||
choreographer,
|
||||
context,
|
||||
);
|
||||
}
|
||||
Future<void> 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<void> 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<ChatPageWithRoom>
|
|||
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<ChatPageWithRoom>
|
|||
|
||||
final ScrollController carouselController = ScrollController();
|
||||
|
||||
ActivityRoleModel? highlightedRole;
|
||||
ValueNotifier<ActivityRoleModel?> highlightedRole = ValueNotifier(null);
|
||||
void highlightRole(ActivityRoleModel role) {
|
||||
if (mounted) setState(() => highlightedRole = role);
|
||||
if (mounted) highlightedRole.value = role;
|
||||
}
|
||||
|
||||
bool showInstructions = false;
|
||||
ValueNotifier<bool> showInstructions = ValueNotifier(false);
|
||||
void toggleShowInstructions() {
|
||||
if (mounted) setState(() => showInstructions = !showInstructions);
|
||||
if (mounted) {
|
||||
showInstructions.value = !showInstructions.value;
|
||||
}
|
||||
}
|
||||
|
||||
bool showActivityDropdown = false;
|
||||
ValueNotifier<bool> showActivityDropdown = ValueNotifier(false);
|
||||
void toggleShowDropdown() async {
|
||||
setState(() => showActivityDropdown = !showActivityDropdown);
|
||||
if (mounted) {
|
||||
showActivityDropdown.value = !showActivityDropdown.value;
|
||||
}
|
||||
}
|
||||
|
||||
bool hasRainedConfetti = false;
|
||||
ValueNotifier<bool> hasRainedConfetti = ValueNotifier(false);
|
||||
void setHasRainedConfetti(bool show) {
|
||||
if (mounted) {
|
||||
setState(() => hasRainedConfetti = show);
|
||||
hasRainedConfetti.value = show;
|
||||
}
|
||||
}
|
||||
// Pangea#
|
||||
|
|
|
|||
|
|
@ -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<Profile>(
|
||||
future: controller.sendingClient.fetchOwnProfile(),
|
||||
builder: (context, snapshot) => PopupMenuButton<String>(
|
||||
useRootNavigator: true,
|
||||
onSelected: (mxid) => _popupMenuButtonSelected(mxid, context),
|
||||
itemBuilder: (BuildContext context) => clients
|
||||
.map(
|
||||
(client) => PopupMenuItem<String>(
|
||||
value: client!.userID,
|
||||
child: FutureBuilder<Profile>(
|
||||
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<Profile>(
|
||||
// future: controller.sendingClient.fetchOwnProfile(),
|
||||
// builder: (context, snapshot) => PopupMenuButton<String>(
|
||||
// useRootNavigator: true,
|
||||
// onSelected: (mxid) => _popupMenuButtonSelected(mxid, context),
|
||||
// itemBuilder: (BuildContext context) => clients
|
||||
// .map(
|
||||
// (client) => PopupMenuItem<String>(
|
||||
// value: client!.userID,
|
||||
// child: FutureBuilder<Profile>(
|
||||
// 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#
|
||||
|
|
|
|||
|
|
@ -353,12 +353,19 @@ class ChatView extends StatelessWidget {
|
|||
children: <Widget>[
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<String>? 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<Map<String, String?>>(
|
||||
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<Map<String, String?>>(
|
||||
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<String, String?> 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<String, String?> 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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,140 +134,145 @@ class ActivityStatsMenuState extends State<ActivityStatsMenu> {
|
|||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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: <Widget>[
|
||||
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: <Widget>[
|
||||
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<double> primaryAnimation,
|
||||
Animation<double> 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<double> primaryAnimation,
|
||||
Animation<double> 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<void> 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<void> _startWritingAssistance({
|
||||
Future<void> requestWritingAssistance() =>
|
||||
_getWritingAssistance(manual: true);
|
||||
|
||||
Future<void> _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<void> 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<void> _sendWithIGC() async {
|
||||
if (chatController.sendController.text.trim().isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final message = chatController.sendController.text;
|
||||
final fakeEventId = chatController.sendFakeMessage();
|
||||
|
||||
Future<PangeaMessageContentModel> 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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<StartIGCButton> createState() => StartIGCButtonState();
|
||||
State<StartIGCButton> createState() => _StartIGCButtonState();
|
||||
}
|
||||
|
||||
class StartIGCButtonState extends State<StartIGCButton>
|
||||
with SingleTickerProviderStateMixin {
|
||||
AssistanceStateEnum get assistanceState =>
|
||||
widget.controller.choreographer.assistanceState;
|
||||
AnimationController? _controller;
|
||||
class _StartIGCButtonState extends State<StartIGCButton>
|
||||
with TickerProviderStateMixin {
|
||||
AnimationController? _spinController;
|
||||
late Animation<double> _rotation;
|
||||
|
||||
AnimationController? _colorController;
|
||||
late Animation<Color?> _iconColor;
|
||||
late Animation<Color?> _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<void> _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<StartIGCButton>
|
|||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ class ITController {
|
|||
final ValueNotifier<ITStepModel?> _currentITStep = ValueNotifier(null);
|
||||
final ValueNotifier<bool> _open = ValueNotifier(false);
|
||||
final ValueNotifier<bool> _editing = ValueNotifier(false);
|
||||
bool _dismissed = false;
|
||||
|
||||
ITController(this.onError);
|
||||
|
||||
|
|
@ -32,7 +31,6 @@ class ITController {
|
|||
ValueNotifier<bool> get editing => _editing;
|
||||
ValueNotifier<ITStepModel?> get currentITStep => _currentITStep;
|
||||
ValueNotifier<String?> 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<ITStepModel>();
|
||||
_queue.add(completer);
|
||||
final resp = await _safeRequest(currentText);
|
||||
|
|
|
|||
21
lib/pangea/choreographer/pangea_message_content_model.dart
Normal file
21
lib/pangea/choreographer/pangea_message_content_model.dart
Normal file
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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<TransparentBackdrop>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _opacityTween;
|
||||
late Animation<double> _blurTween;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration:
|
||||
const Duration(milliseconds: AppConfig.overlayAnimationDuration),
|
||||
vsync: this,
|
||||
);
|
||||
_opacityTween = Tween<double>(begin: 0.0, end: 0.8).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
),
|
||||
);
|
||||
_blurTween = Tween<double>(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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -158,11 +158,11 @@ extension EventsRoomExtension on Room {
|
|||
return content;
|
||||
}
|
||||
|
||||
String sendFakeMessage({
|
||||
Future<String> 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(
|
||||
|
|
|
|||
|
|
@ -51,6 +51,10 @@ class LoadParticipantsBuilderState extends State<LoadParticipantsBuilder> {
|
|||
}
|
||||
|
||||
Future<void> _loadParticipants() async {
|
||||
if (widget.room == null || widget.room!.participantListComplete) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setState(() {
|
||||
loading = true;
|
||||
|
|
|
|||
|
|
@ -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 = <String>{};
|
||||
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<String, Object?>('m.relates_to')
|
||||
?.tryGet<String>('key'),
|
||||
)
|
||||
.whereType<String>(),
|
||||
);
|
||||
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<String, Object?>('m.relates_to')
|
||||
?.tryGet<String>('key'),
|
||||
)
|
||||
.whereType<String>(),
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -747,7 +747,6 @@ class MoreButton extends StatelessWidget {
|
|||
break;
|
||||
case MessageActions.info:
|
||||
controller.showEventInfo();
|
||||
controller.clearSelectedEvents();
|
||||
break;
|
||||
case MessageActions.deleteOnError:
|
||||
controller.deleteErrorEventsAction();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue