refactor: remove chat controller dependency from choreographer

This commit is contained in:
ggurdin 2025-11-10 15:23:15 -05:00
parent f681ffa71f
commit 941827bb8a
No known key found for this signature in database
GPG key ID: A01CB41737CBB478
12 changed files with 113 additions and 106 deletions

View file

@ -45,7 +45,6 @@ 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/controllers/pangea_controller.dart';
@ -183,7 +182,7 @@ class ChatController extends State<ChatPageWithRoom>
with WidgetsBindingObserver {
// #Pangea
final PangeaController pangeaController = MatrixState.pangeaController;
late Choreographer choreographer = Choreographer(pangeaController, this);
late Choreographer choreographer;
late GoRouter _router;
StreamSubscription? _levelSubscription;
@ -427,6 +426,7 @@ class ChatController extends State<ChatPageWithRoom>
@override
void initState() {
inputFocus = FocusNode(onKeyEvent: _customEnterKeyHandling);
choreographer = Choreographer(inputFocus);
scrollController.addListener(_updateScrollController);
// #Pangea
@ -2192,16 +2192,7 @@ class ChatController extends State<ChatPageWithRoom>
await choreographer.requestWritingAssistance();
if (choreographer.assistanceState == AssistanceStateEnum.fetched) {
onSelectMatch(choreographer.igcController.firstOpenMatch);
} else if (autosend) {
await send();
} else {
inputFocus.requestFocus();
}
}
void onSelectMatch(PangeaMatchState? match) {
if (match != null) {
final match = choreographer.igcController.firstOpenMatch!;
match.updatedMatch.isITStart
? choreographer.openIT(match)
: OverlayUtil.showIGCMatch(
@ -2209,9 +2200,11 @@ class ChatController extends State<ChatPageWithRoom>
choreographer,
context,
);
return;
} else if (autosend) {
await send();
} else {
inputFocus.requestFocus();
}
inputFocus.requestFocus();
}
void showLanguageMismatchPopup() {

View file

@ -13,6 +13,7 @@ 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/common/utils/overlay.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';
@ -461,25 +462,33 @@ class InputBar extends StatelessWidget {
int adjustedOffset = controller!.selection.baseOffset;
final normalizationMatches =
choreographer.igcController.recentAutomaticCorrections;
if (normalizationMatches == null || normalizationMatches.isEmpty) return;
for (final match in normalizationMatches) {
if (match.updatedMatch.match.offset < adjustedOffset &&
match.updatedMatch.match.length > 0) {
adjustedOffset += (match.updatedMatch.match.length - 1);
if (normalizationMatches != null) {
for (final match in normalizationMatches) {
if (match.updatedMatch.match.offset < adjustedOffset &&
match.updatedMatch.match.length > 0) {
adjustedOffset += (match.updatedMatch.match.length - 1);
}
}
}
final match = choreographer.igcController.getMatchByOffset(adjustedOffset);
choreographer.chatController.onSelectMatch(match);
if (match == null) return;
match.updatedMatch.isITStart
? choreographer.openIT(match)
: OverlayUtil.showIGCMatch(
match,
choreographer,
context,
);
}
// Pangea#
@override
Widget build(BuildContext context) {
// #Pangea
return ListenableBuilder(
listenable: choreographer.textController,
builder: (context, _) {
return ValueListenableBuilder(
valueListenable: choreographer.textController,
builder: (context, _, __) {
final enableAutocorrect = MatrixState.pangeaController.userController
.profile.toolSettings.enableAutocorrect;
// Pangea#

View file

@ -19,7 +19,8 @@ class ChatFloatingActionButton extends StatelessWidget {
return ListenableBuilder(
listenable: Listenable.merge(
[
controller.choreographer,
controller.choreographer.errorService,
controller.choreographer.itController.open,
controller.scrollController,
],
),

View file

@ -1,26 +1,26 @@
import 'package:fluffychat/pangea/choreographer/choreographer.dart';
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
import 'package:fluffychat/pangea/spaces/models/space_model.dart';
import 'package:fluffychat/widgets/matrix.dart';
extension ChoregrapherUserSettingsExtension on Choreographer {
LanguageModel? get l2Lang =>
pangeaController.languageController.activeL2Model();
MatrixState.pangeaController.languageController.activeL2Model();
String? get l2LangCode => l2Lang?.langCode;
LanguageModel? get l1Lang =>
pangeaController.languageController.activeL1Model();
MatrixState.pangeaController.languageController.activeL1Model();
String? get l1LangCode => l1Lang?.langCode;
bool get igcEnabled => pangeaController.permissionsController.isToolEnabled(
bool get igcEnabled =>
MatrixState.pangeaController.permissionsController.isToolEnabled(
ToolSetting.interactiveGrammar,
chatController.room,
);
bool get itEnabled => pangeaController.permissionsController.isToolEnabled(
bool get itEnabled =>
MatrixState.pangeaController.permissionsController.isToolEnabled(
ToolSetting.interactiveTranslator,
chatController.room,
);
bool get isAutoIGCEnabled =>
pangeaController.permissionsController.isToolEnabled(
MatrixState.pangeaController.permissionsController.isToolEnabled(
ToolSetting.autoIGC,
chatController.room,
);
}

View file

@ -2,7 +2,6 @@ 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';
import 'package:fluffychat/pangea/choreographer/choreo_constants.dart';
@ -15,12 +14,10 @@ import 'package:fluffychat/pangea/choreographer/igc/pangea_match_status_enum.dar
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';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/events/models/representation_content_model.dart';
import 'package:fluffychat/pangea/events/models/tokens_event_content_model.dart';
import 'package:fluffychat/pangea/events/repo/token_api_models.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart';
import 'package:fluffychat/pangea/subscription/controllers/subscription_controller.dart';
import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart';
@ -29,18 +26,13 @@ import '../../widgets/matrix.dart';
import 'choreographer_error_controller.dart';
import 'it/it_controller.dart';
class OpenMatchesException implements Exception {}
class ShowPaywallException implements Exception {}
class Choreographer extends ChangeNotifier {
final PangeaController pangeaController;
final ChatController chatController;
final FocusNode inputFocus;
late PangeaTextController textController;
late ITController itController;
late IgcController igcController;
late ChoreographerErrorController errorService;
late final PangeaTextController textController;
late final ITController itController;
late final IgcController igcController;
late final ChoreographerErrorController errorService;
ChoreoRecordModel? _choreoRecord;
@ -54,7 +46,9 @@ class Choreographer extends ChangeNotifier {
StreamSubscription? _languageStream;
StreamSubscription? _settingsUpdateStream;
Choreographer(this.pangeaController, this.chatController) {
Choreographer(
this.inputFocus,
) {
_initialize();
}
@ -63,6 +57,12 @@ class Choreographer extends ChangeNotifier {
ChoreoModeEnum get choreoMode => _choreoMode;
String get currentText => textController.text;
ChoreoRecordModel get _record => _choreoRecord ??= ChoreoRecordModel(
originalText: textController.text,
choreoSteps: [],
openMatches: [],
);
void _initialize() {
textController = PangeaTextController(choreographer: this);
textController.addListener(_onChange);
@ -79,13 +79,15 @@ class Choreographer extends ChangeNotifier {
(e) => errorService.setErrorAndLock(ChoreoError(raw: e)),
);
_languageStream =
pangeaController.userController.languageStream.stream.listen((update) {
_languageStream ??= MatrixState
.pangeaController.userController.languageStream.stream
.listen((update) {
clear();
});
_settingsUpdateStream =
pangeaController.userController.settingsUpdateStream.stream.listen((_) {
_settingsUpdateStream ??= MatrixState
.pangeaController.userController.settingsUpdateStream.stream
.listen((_) {
notifyListeners();
});
}
@ -99,12 +101,14 @@ class Choreographer extends ChangeNotifier {
itController.clearSourceText();
igcController.clear();
_resetDebounceTimer();
setChoreoMode(ChoreoModeEnum.igc);
_setChoreoMode(ChoreoModeEnum.igc);
}
@override
void dispose() {
super.dispose();
errorService.removeListener(notifyListeners);
itController.open.removeListener(_onCloseIT);
textController.removeListener(_onChange);
itController.dispose();
errorService.dispose();
textController.dispose();
@ -113,12 +117,10 @@ class Choreographer extends ChangeNotifier {
_debounceTimer?.cancel();
_isFetching.dispose();
TtsController.stop();
super.dispose();
}
void onPaste(value) {
_initChoreoRecord();
_choreoRecord!.pastedStrings.add(value);
}
void onPaste(value) => _record.pastedStrings.add(value);
void onClickSend() {
if (assistanceState == AssistanceStateEnum.fetched) {
@ -132,7 +134,7 @@ class Choreographer extends ChangeNotifier {
}
}
void setChoreoMode(ChoreoModeEnum mode) {
void _setChoreoMode(ChoreoModeEnum mode) {
_choreoMode = mode;
notifyListeners();
}
@ -144,14 +146,6 @@ class Choreographer extends ChangeNotifier {
}
}
void _initChoreoRecord() {
_choreoRecord ??= ChoreoRecordModel(
originalText: textController.text,
choreoSteps: [],
openMatches: [],
);
}
void _startLoading() {
_lastChecked = textController.text;
_isFetching.value = true;
@ -170,6 +164,7 @@ class Choreographer extends ChangeNotifier {
if (_lastChecked != null && _lastChecked == textController.text) {
return;
}
// update assistance state from no message => not fetched and vice versa
if (_lastChecked == null ||
_lastChecked!.isEmpty ||
textController.text.isEmpty) {
@ -203,7 +198,7 @@ class Choreographer extends ChangeNotifier {
}) async {
if (assistanceState != AssistanceStateEnum.notFetched) return;
final SubscriptionStatus canSendStatus =
pangeaController.subscriptionController.subscriptionStatus;
MatrixState.pangeaController.subscriptionController.subscriptionStatus;
if (canSendStatus != SubscriptionStatus.subscribed ||
l2Lang == null ||
@ -214,29 +209,27 @@ class Choreographer extends ChangeNotifier {
}
_resetDebounceTimer();
_initChoreoRecord();
_startLoading();
await igcController.getIGCTextData(
textController.text,
chatController.room.getPreviousMessages(),
[],
);
_acceptNormalizationMatches();
// trigger a re-render of the text field to show IGC matches
textController.setSystemText(
textController.text,
EditTypeEnum.igc,
);
_acceptNormalizationMatches();
_stopLoading();
}
Future<PangeaMessageContentModel> getMessageContent(String message) async {
TokensResponseModel? tokensResp;
if (l1LangCode != null && l2LangCode != null) {
final res = await pangeaController.messageData
final res = await MatrixState.pangeaController.messageData
.getTokens(
repEventId: null,
room: chatController.room,
room: null,
req: TokensRequestModel(
fullText: message,
senderL1: l1LangCode!,
@ -247,12 +240,12 @@ class Choreographer extends ChangeNotifier {
tokensResp = res.isValue ? res.result : null;
}
final hasOriginalWritten = _choreoRecord?.includedIT == true &&
itController.sourceText.value != null;
final hasOriginalWritten =
_record.includedIT && itController.sourceText.value != null;
return PangeaMessageContentModel(
message: message,
choreo: _choreoRecord,
choreo: _record,
originalSent: PangeaRepresentation(
langCode: tokensResp?.detections.firstOrNull?.langCode ??
LanguageKeys.unknownLanguage,
@ -282,17 +275,14 @@ class Choreographer extends ChangeNotifier {
throw Exception("Attempted to open IT with a non-IT start match");
}
chatController.inputFocus.unfocus();
setChoreoMode(ChoreoModeEnum.it);
_setChoreoMode(ChoreoModeEnum.it);
final sourceText = currentText;
textController.setSystemText("", EditTypeEnum.it);
itController.openIT(sourceText);
igcController.clear();
_initChoreoRecord();
itMatch.setStatus(PangeaMatchStatusEnum.accepted);
_choreoRecord!.addRecord(
_record.addRecord(
"",
match: itMatch.updatedMatch,
);
@ -308,7 +298,7 @@ class Choreographer extends ChangeNotifier {
);
}
setChoreoMode(ChoreoModeEnum.igc);
_setChoreoMode(ChoreoModeEnum.igc);
errorService.resetError();
notifyListeners();
}
@ -325,9 +315,8 @@ class Choreographer extends ChangeNotifier {
EditTypeEnum.it,
);
_initChoreoRecord();
_choreoRecord!.addRecord(textController.text, step: step);
chatController.inputFocus.requestFocus();
_record.addRecord(textController.text, step: step);
inputFocus.requestFocus();
notifyListeners();
}
@ -351,20 +340,19 @@ class Choreographer extends ChangeNotifier {
);
if (!updatedMatch.match.isNormalizationError()) {
_initChoreoRecord();
_choreoRecord!.addRecord(
_record.addRecord(
textController.text,
match: updatedMatch,
);
}
MatrixState.pAnyState.closeOverlay();
chatController.inputFocus.requestFocus();
inputFocus.requestFocus();
notifyListeners();
}
void onUndoReplacement(PangeaMatchState match) {
igcController.undoReplacement(match);
_choreoRecord?.choreoSteps.removeWhere(
_record.choreoSteps.removeWhere(
(step) => step.acceptedOrIgnoredMatch == match.updatedMatch,
);
@ -373,21 +361,20 @@ class Choreographer extends ChangeNotifier {
EditTypeEnum.igc,
);
MatrixState.pAnyState.closeOverlay();
chatController.inputFocus.requestFocus();
inputFocus.requestFocus();
notifyListeners();
}
void onIgnoreReplacement({required PangeaMatchState match}) {
final updatedMatch = igcController.ignoreReplacement(match);
if (!updatedMatch.match.isNormalizationError()) {
_initChoreoRecord();
_choreoRecord!.addRecord(
_record.addRecord(
textController.text,
match: updatedMatch,
);
}
MatrixState.pAnyState.closeOverlay();
chatController.inputFocus.requestFocus();
inputFocus.requestFocus();
notifyListeners();
}
@ -395,7 +382,6 @@ class Choreographer extends ChangeNotifier {
final normalizationsMatches = igcController.openNormalizationMatches;
if (normalizationsMatches?.isEmpty ?? true) return;
_initChoreoRecord();
try {
for (final match in normalizationsMatches!) {
match.selectChoice(
@ -412,7 +398,7 @@ class Choreographer extends ChangeNotifier {
igcController.currentText!,
EditTypeEnum.igc,
);
_choreoRecord!.addRecord(
_record.addRecord(
currentText,
match: updatedMatch,
);
@ -425,7 +411,7 @@ class Choreographer extends ChangeNotifier {
"currentText": currentText,
"l1LangCode": l1LangCode,
"l2LangCode": l2LangCode,
"choreoRecord": _choreoRecord?.toJson(),
"choreoRecord": _record.toJson(),
},
);
}

View file

@ -1,6 +1,7 @@
import 'package:fluffychat/pangea/choreographer/assistance_state_enum.dart';
import 'package:fluffychat/pangea/choreographer/choreo_mode_enum.dart';
import 'package:fluffychat/pangea/choreographer/choreographer.dart';
import 'package:fluffychat/widgets/matrix.dart';
extension ChoregrapherUserSettingsExtension on Choreographer {
bool get isRunningIT {
@ -9,7 +10,8 @@ extension ChoregrapherUserSettingsExtension on Choreographer {
}
AssistanceStateEnum get assistanceState {
final isSubscribed = pangeaController.subscriptionController.isSubscribed;
final isSubscribed =
MatrixState.pangeaController.subscriptionController.isSubscribed;
if (isSubscribed == false) return AssistanceStateEnum.noSub;
if (currentText.isEmpty && itController.sourceText.value == null) {
return AssistanceStateEnum.noMessage;

View file

@ -5,8 +5,8 @@ import 'package:fluffychat/widgets/matrix.dart';
extension ChoregrapherUserSettingsExtension on Choreographer {
LayerLinkAndKey get itBarLinkAndKey =>
MatrixState.pAnyState.layerLinkAndKey(itBarTransformTargetKey);
String get itBarTransformTargetKey => 'it_bar${chatController.roomId}';
String get itBarTransformTargetKey => 'it_bar';
LayerLinkAndKey get inputLayerLinkAndKey =>
MatrixState.pAnyState.layerLinkAndKey(inputTransformTargetKey);
String get inputTransformTargetKey => 'input${chatController.roomId}';
String get inputTransformTargetKey => 'input_text_field';
}

View file

@ -12,6 +12,7 @@ import 'package:fluffychat/pangea/choreographer/igc/span_data_model.dart';
import 'package:fluffychat/pangea/choreographer/igc/span_data_type_enum.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/common/utils/feedback_model.dart';
import 'package:fluffychat/pangea/common/utils/overlay.dart';
import 'package:fluffychat/pangea/common/widgets/error_indicator.dart';
import '../../../widgets/matrix.dart';
import '../../common/widgets/choice_array.dart';
@ -156,7 +157,15 @@ class SpanCardState extends State<SpanCard> {
void _showFirstMatch() {
final match = widget.choreographer.igcController.firstOpenMatch;
widget.choreographer.chatController.onSelectMatch(match);
if (match == null) {
MatrixState.pAnyState.closeAllOverlays();
return;
}
OverlayUtil.showIGCMatch(
match,
widget.choreographer,
context,
);
}
@override

View file

@ -24,7 +24,10 @@ import '../../common/widgets/choice_array.dart';
class ITBar extends StatefulWidget {
final Choreographer choreographer;
const ITBar({super.key, required this.choreographer});
const ITBar({
super.key,
required this.choreographer,
});
@override
ITBarState createState() => ITBarState();
@ -193,7 +196,9 @@ class ITBarState extends State<ITBar> with SingleTickerProviderStateMixin {
child: child,
),
child: CompositedTransformTarget(
link: widget.choreographer.itBarLinkAndKey.link,
link: MatrixState.pAnyState
.layerLinkAndKey(widget.choreographer.itBarTransformTargetKey)
.link,
child: Column(
children: [
if (!InstructionsEnum.clickBestOption.isToggledOff) ...[
@ -204,7 +209,9 @@ class ITBarState extends State<ITBar> with SingleTickerProviderStateMixin {
const SizedBox(height: 8.0),
],
Container(
key: widget.choreographer.itBarLinkAndKey.key,
key: MatrixState.pAnyState
.layerLinkAndKey(widget.choreographer.itBarTransformTargetKey)
.key,
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(24),

View file

@ -104,8 +104,8 @@ class PangeaTextController extends TextEditingController {
TextStyle? style,
required bool withComposing,
}) {
final subscription = choreographer
.pangeaController.subscriptionController.subscriptionStatus;
final subscription =
MatrixState.pangeaController.subscriptionController.subscriptionStatus;
if (subscription == SubscriptionStatus.shouldShowPaywall) {
return _buildPaywallSpan(style);

View file

@ -656,7 +656,7 @@ class PangeaMessageEvent {
final bool immersionMode = MatrixState
.pangeaController.permissionsController
.isToolEnabled(ToolSetting.immersionMode, room);
.isToolEnabled(ToolSetting.immersionMode);
final String? originalLangCode = originalSent?.langCode;

View file

@ -56,7 +56,7 @@ class PermissionsController extends BaseController {
}
}
bool isToolEnabled(ToolSetting setting, Room? room) {
bool isToolEnabled(ToolSetting setting) {
// Rules can't be edited; default to true
return userToolSetting(setting);
// if (room?.isSpaceAdmin ?? false) {