From f03fa4f36e02c30b36ce6a024f73927c6a28345f Mon Sep 17 00:00:00 2001 From: Kelrap Date: Fri, 7 Jun 2024 16:07:05 -0400 Subject: [PATCH 1/9] Save timestamp of selected choices --- lib/pangea/models/span_data.dart | 8 +++++-- lib/pangea/widgets/igc/span_card.dart | 31 ++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/lib/pangea/models/span_data.dart b/lib/pangea/models/span_data.dart index 186e2834e..d420e1ffd 100644 --- a/lib/pangea/models/span_data.dart +++ b/lib/pangea/models/span_data.dart @@ -4,9 +4,8 @@ // SpanChoice of text in message from options // Call to server for additional/followup info -import 'package:flutter/material.dart'; - import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; import '../enum/span_choice_type.dart'; import '../enum/span_data_type.dart'; @@ -105,6 +104,7 @@ class SpanChoice { required this.type, this.feedback, this.selected = false, + this.timestamp, }); factory SpanChoice.fromJson(Map json) { return SpanChoice( @@ -117,6 +117,8 @@ class SpanChoice { : SpanChoiceType.bestCorrection, feedback: json['feedback'], selected: json['selected'] ?? false, + timestamp: + json['timestamp'] != null ? DateTime.parse(json['timestamp']) : null, ); } @@ -124,12 +126,14 @@ class SpanChoice { SpanChoiceType type; bool selected; String? feedback; + DateTime? timestamp; Map toJson() => { 'value': value, 'type': type.name, 'selected': selected, 'feedback': feedback, + 'timestamp': timestamp?.toIso8601String(), }; String feedbackToDisplay(BuildContext context) { diff --git a/lib/pangea/widgets/igc/span_card.dart b/lib/pangea/widgets/igc/span_card.dart index fd44383a0..ad8ea2a72 100644 --- a/lib/pangea/widgets/igc/span_card.dart +++ b/lib/pangea/widgets/igc/span_card.dart @@ -55,18 +55,36 @@ class SpanCardState extends State { // debugger(when: kDebugMode); super.initState(); getSpanDetails(); + // fetchSelected(); } //get selected choice SpanChoice? get selectedChoice { if (selectedChoiceIndex == null || widget.scm.pangeaMatch?.match.choices == null || - widget.scm.pangeaMatch!.match.choices!.length >= selectedChoiceIndex!) { + widget.scm.pangeaMatch!.match.choices!.length <= selectedChoiceIndex!) { return null; } return widget.scm.pangeaMatch?.match.choices?[selectedChoiceIndex!]; } + void fetchSelected() { + if (widget.scm.pangeaMatch?.match.choices == null) { + return; + } + if (selectedChoiceIndex == null) { + DateTime? mostRecent; + for (int i = 0; i < widget.scm.pangeaMatch!.match.choices!.length; i++) { + final choice = widget.scm.pangeaMatch?.match.choices![i]; + if (choice!.timestamp != null && + (mostRecent == null || choice.timestamp!.isAfter(mostRecent))) { + mostRecent = choice.timestamp; + selectedChoiceIndex = i; + } + } + } + } + Future getSpanDetails() async { try { if (widget.scm.pangeaMatch?.isITStart ?? false) return; @@ -110,6 +128,16 @@ class WordMatchContent extends StatelessWidget { Future onChoiceSelect(int index) async { controller.selectedChoiceIndex = index; + controller + .widget + .scm + .choreographer + .igc + .igcTextData + ?.matches[controller.widget.scm.matchIndex] + .match + .choices?[index] + .timestamp = DateTime.now(); controller .widget .scm @@ -152,6 +180,7 @@ class WordMatchContent extends StatelessWidget { offset: controller.widget.scm.pangeaMatch?.match.offset, ); } + final MatchCopy matchCopy = MatchCopy( context, controller.widget.scm.pangeaMatch!, From ddb42091234e6b5f47c12896a853654984cdeee2 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Mon, 10 Jun 2024 08:57:09 -0400 Subject: [PATCH 2/9] Uncommented a line --- lib/pangea/widgets/igc/span_card.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pangea/widgets/igc/span_card.dart b/lib/pangea/widgets/igc/span_card.dart index ad8ea2a72..09fdba4dd 100644 --- a/lib/pangea/widgets/igc/span_card.dart +++ b/lib/pangea/widgets/igc/span_card.dart @@ -55,7 +55,7 @@ class SpanCardState extends State { // debugger(when: kDebugMode); super.initState(); getSpanDetails(); - // fetchSelected(); + fetchSelected(); } //get selected choice From 429d4a97eca5c74815a7331870caacb1153bf5cb Mon Sep 17 00:00:00 2001 From: Kelrap Date: Mon, 10 Jun 2024 13:24:58 -0400 Subject: [PATCH 3/9] Check for eventId match when selecting/deselecting --- lib/pages/chat/chat.dart | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 4a204c404..73a0447d5 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -1327,9 +1327,18 @@ class ChatController extends State } // Pangea# if (!event.redacted) { - if (selectedEvents.contains(event)) { + // #Pangea + // If previous selectedEvent has same eventId, delete previous selectedEvent + final matches = + selectedEvents.where((e) => e.eventId == event.eventId).toList(); + if (matches.isNotEmpty) { + // if (selectedEvents.contains(event)) { + // Pangea# setState( - () => selectedEvents.remove(event), + // #Pangea + () => selectedEvents.remove(matches.first), + // () => selectedEvents.remove(event), + // Pangea# ); } else { setState( From 96da73c2e9b85e4c5406a8e9bd5d4832eb3a6292 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Mon, 10 Jun 2024 15:46:13 -0400 Subject: [PATCH 4/9] Edited displayname shows when return to settings --- lib/pages/settings/settings.dart | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index 0e32fe478..55b39fa3d 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -1,18 +1,17 @@ import 'dart:async'; -import 'package:flutter/material.dart'; - import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart'; import 'package:file_picker/file_picker.dart'; +import 'package:fluffychat/pangea/utils/logout.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/app_lock.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:image_picker/image_picker.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/pangea/utils/logout.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/widgets/app_lock.dart'; import '../../widgets/matrix.dart'; import 'settings_view.dart'; @@ -171,6 +170,10 @@ class SettingsController extends State { // Pangea# super.initState(); + // #Pangea + profileUpdated = true; + profileFuture = null; + // Pangea# } void checkBootstrap() async { From 64084fd32704091bce4fc20ddf64ebbb6186d50a Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 11 Jun 2024 16:31:25 -0400 Subject: [PATCH 5/9] small tweak to start IGC button --- assets/l10n/intl_en.arb | 2 +- .../widgets/start_igc_button.dart | 35 ++++++++++++------- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index ef3042700..a7fb07449 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -3951,7 +3951,7 @@ "autoIGCToolName": "Run Language Assistance Automatically", "autoIGCToolDescription": "Automatically run language assistance after typing messages", "runGrammarCorrection": "Run grammar correction", - "grammarCorrectionFailed": "Grammar correction failed", + "grammarCorrectionFailed": "Issues to address", "grammarCorrectionComplete": "Grammar correction complete", "leaveRoomDescription": "The chat will be moved to the archive. Other users will be able to see that you have left the chat.", "archiveSpaceDescription": "All chats within this space will be moved to the archive for yourself and other non-admin users.", diff --git a/lib/pangea/choreographer/widgets/start_igc_button.dart b/lib/pangea/choreographer/widgets/start_igc_button.dart index 183ac690d..5f786306f 100644 --- a/lib/pangea/choreographer/widgets/start_igc_button.dart +++ b/lib/pangea/choreographer/widgets/start_igc_button.dart @@ -33,7 +33,7 @@ class StartIGCButtonState extends State void initState() { _controller = AnimationController( vsync: this, - duration: const Duration(seconds: 1), + duration: const Duration(seconds: 2), ); choreoListener = widget.controller.choreographer.stateListener.stream .listen(updateSpinnerState); @@ -54,14 +54,15 @@ class StartIGCButtonState extends State @override Widget build(BuildContext context) { - if (widget.controller.choreographer.isAutoIGCEnabled) { + if (widget.controller.choreographer.isAutoIGCEnabled || + widget.controller.choreographer.choreoMode == ChoreoMode.it) { return const SizedBox.shrink(); } final Widget icon = Icon( Icons.autorenew_rounded, size: 46, - color: assistanceState.stateColor, + color: assistanceState.stateColor(context), ); return SizedBox( @@ -71,15 +72,23 @@ class StartIGCButtonState extends State tooltip: assistanceState.tooltip( L10n.of(context)!, ), - backgroundColor: Colors.white, + backgroundColor: Theme.of(context).scaffoldBackgroundColor, disabledElevation: 0, shape: const CircleBorder(), onPressed: () { if (assistanceState != AssistanceState.complete) { - widget.controller.choreographer.getLanguageHelp( + widget.controller.choreographer + .getLanguageHelp( false, true, - ); + ) + .then((_) { + if (widget.controller.choreographer.igc.igcTextData != null && + widget.controller.choreographer.igc.igcTextData!.matches + .isNotEmpty) { + widget.controller.choreographer.igc.showFirstMatch(context); + } + }); } }, child: Stack( @@ -95,9 +104,9 @@ class StartIGCButtonState extends State Container( width: 26, height: 26, - decoration: const BoxDecoration( + decoration: BoxDecoration( shape: BoxShape.circle, - color: Colors.white, + color: Theme.of(context).scaffoldBackgroundColor, ), ), Container( @@ -105,13 +114,13 @@ class StartIGCButtonState extends State height: 20, decoration: BoxDecoration( shape: BoxShape.circle, - color: assistanceState.stateColor, + color: assistanceState.stateColor(context), ), ), - const Icon( + Icon( size: 16, Icons.check, - color: Colors.white, + color: Theme.of(context).scaffoldBackgroundColor, ), ], ), @@ -121,12 +130,12 @@ class StartIGCButtonState extends State } extension AssistanceStateExtension on AssistanceState { - Color get stateColor { + Color stateColor(context) { switch (this) { case AssistanceState.noMessage: case AssistanceState.notFetched: case AssistanceState.fetching: - return AppConfig.primaryColor; + return Theme.of(context).colorScheme.primary; case AssistanceState.fetched: return PangeaColors.igcError; case AssistanceState.complete: From 14f4192b6ff58b5f4eb690e885575a0508327486 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 12 Jun 2024 09:58:12 -0400 Subject: [PATCH 6/9] added caching for span details api responses, also pre-calling of span details endpoint for each match after fetching IGC data --- .../controllers/igc_controller.dart | 73 ++++++++++++++++--- lib/pangea/repo/span_data_repo.dart | 18 +++++ 2 files changed, 80 insertions(+), 11 deletions(-) diff --git a/lib/pangea/choreographer/controllers/igc_controller.dart b/lib/pangea/choreographer/controllers/igc_controller.dart index 646543f22..b0c8dd462 100644 --- a/lib/pangea/choreographer/controllers/igc_controller.dart +++ b/lib/pangea/choreographer/controllers/igc_controller.dart @@ -19,14 +19,37 @@ import '../../repo/tokens_repo.dart'; import '../../utils/error_handler.dart'; import '../../utils/overlay.dart'; +class _SpanDetailsCacheItem { + SpanDetailsRepoReqAndRes data; + + _SpanDetailsCacheItem({required this.data}); +} + class IgcController { Choreographer choreographer; IGCTextData? igcTextData; Object? igcError; Completer igcCompleter = Completer(); + final Map _cache = {}; + Timer? _cacheClearTimer; - IgcController(this.choreographer); + IgcController(this.choreographer) { + _initializeCacheClearing(); + } + + void _initializeCacheClearing() { + const duration = Duration(minutes: 2); + _cacheClearTimer = Timer.periodic(duration, (Timer t) => _clearCache()); + } + + void _clearCache() { + _cache.clear(); + } + + void dispose() { + _cacheClearTimer?.cancel(); + } Future getIGCTextData({required bool tokensOnly}) async { try { @@ -80,6 +103,14 @@ class IgcController { igcTextData = igcTextDataResponse; + // After fetching igc data, pre-call span details for each match optimistically. + // This will make the loading of span details faster for the user + if (igcTextData?.matches.isNotEmpty ?? false) { + for (int i = 0; i < igcTextData!.matches.length; i++) { + getSpanDetails(i); + } + } + debugPrint("igc text ${igcTextData.toString()}"); } catch (err, stack) { debugger(when: kDebugMode); @@ -99,18 +130,38 @@ class IgcController { debugger(when: kDebugMode); return; } - final SpanData span = igcTextData!.matches[matchIndex].match; - final SpanDetailsRepoReqAndRes response = await SpanDataRepo.getSpanDetails( - await choreographer.accessToken, - request: SpanDetailsRepoReqAndRes( - userL1: choreographer.l1LangCode!, - userL2: choreographer.l2LangCode!, - enableIGC: choreographer.igcEnabled, - enableIT: choreographer.itEnabled, - span: span, - ), + /// Retrieves the span data from the `igcTextData` matches at the specified `matchIndex`. + /// Creates a `SpanDetailsRepoReqAndRes` object with the retrieved span data and other parameters. + /// Generates a cache key based on the created `SpanDetailsRepoReqAndRes` object. + final SpanData span = igcTextData!.matches[matchIndex].match; + final req = SpanDetailsRepoReqAndRes( + userL1: choreographer.l1LangCode!, + userL2: choreographer.l2LangCode!, + enableIGC: choreographer.igcEnabled, + enableIT: choreographer.itEnabled, + span: span, ); + final int cacheKey = req.hashCode; + + /// Retrieves the [SpanDetailsRepoReqAndRes] response from the cache if it exists, + /// otherwise makes an API call to get the response and stores it in the cache. + SpanDetailsRepoReqAndRes response; + if (_cache.containsKey(cacheKey)) { + response = _cache[cacheKey]!.data; + } else { + response = await SpanDataRepo.getSpanDetails( + await choreographer.accessToken, + request: SpanDetailsRepoReqAndRes( + userL1: choreographer.l1LangCode!, + userL2: choreographer.l2LangCode!, + enableIGC: choreographer.igcEnabled, + enableIT: choreographer.itEnabled, + span: span, + ), + ); + _cache[cacheKey] = _SpanDetailsCacheItem(data: response); + } try { igcTextData!.matches[matchIndex].match = response.span; diff --git a/lib/pangea/repo/span_data_repo.dart b/lib/pangea/repo/span_data_repo.dart index c6025af6c..e253bb1d0 100644 --- a/lib/pangea/repo/span_data_repo.dart +++ b/lib/pangea/repo/span_data_repo.dart @@ -72,6 +72,24 @@ class SpanDetailsRepoReqAndRes { enableIGC: json['enable_igc'] as bool, span: SpanData.fromJson(json['span']), ); + + /// Overrides the equality operator to compare two [SpanDetailsRepoReqAndRes] objects. + /// Returns true if the objects are identical or have the same property + /// values (based on the results of the toJson function), false otherwise. + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! SpanDetailsRepoReqAndRes) return false; + + return toJson().toString() == other.toJson().toString(); + } + + /// Overrides the hashCode getter to generate a hash code for the [SpanDetailsRepoReqAndRes] object. + /// Used as keys in response cache in igc_controller. + @override + int get hashCode { + return toJson().toString().hashCode; + } } final spanDataRepomockSpan = SpanData( From d6b9273605973725415899fa1582bf2066fb3356 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Wed, 12 Jun 2024 11:25:13 -0400 Subject: [PATCH 7/9] Toolbar shows below message if high on screen --- lib/pangea/widgets/chat/message_toolbar.dart | 55 ++++++++++++++------ 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index 142a27227..fa45b6d10 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -59,6 +59,7 @@ class ToolbarDisplayController { } void showToolbar(BuildContext context, {MessageMode? mode}) { + bool toolbarUp = true; if (highlighted) return; if (controller.selectMode) { controller.clearSelectedEvents(); @@ -76,6 +77,9 @@ class ToolbarDisplayController { if (targetRenderBox != null) { final Size transformTargetSize = (targetRenderBox as RenderBox).size; messageWidth = transformTargetSize.width; + final Offset targetOffset = (targetRenderBox).localToGlobal(Offset.zero); + final double screenHeight = MediaQuery.of(context).size.height; + toolbarUp = targetOffset.dy >= screenHeight / 2; } WidgetsBinding.instance.addPostFrameCallback((timeStamp) { @@ -88,18 +92,31 @@ class ToolbarDisplayController { ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ - toolbar!, + toolbarUp + ? toolbar! + : OverlayMessage( + pangeaMessageEvent.event, + timeline: pangeaMessageEvent.timeline, + immersionMode: immersionMode, + ownMessage: pangeaMessageEvent.ownMessage, + toolbarController: this, + width: messageWidth, + nextEvent: nextEvent, + previousEvent: previousEvent, + ), const SizedBox(height: 6), - OverlayMessage( - pangeaMessageEvent.event, - timeline: pangeaMessageEvent.timeline, - immersionMode: immersionMode, - ownMessage: pangeaMessageEvent.ownMessage, - toolbarController: this, - width: messageWidth, - nextEvent: nextEvent, - previousEvent: previousEvent, - ), + toolbarUp + ? OverlayMessage( + pangeaMessageEvent.event, + timeline: pangeaMessageEvent.timeline, + immersionMode: immersionMode, + ownMessage: pangeaMessageEvent.ownMessage, + toolbarController: this, + width: messageWidth, + nextEvent: nextEvent, + previousEvent: previousEvent, + ) + : toolbar!, ], ); } catch (err) { @@ -113,11 +130,19 @@ class ToolbarDisplayController { child: overlayEntry, transformTargetId: targetId, targetAnchor: pangeaMessageEvent.ownMessage - ? Alignment.bottomRight - : Alignment.bottomLeft, + ? toolbarUp + ? Alignment.bottomRight + : Alignment.topRight + : toolbarUp + ? Alignment.bottomLeft + : Alignment.topLeft, followerAnchor: pangeaMessageEvent.ownMessage - ? Alignment.bottomRight - : Alignment.bottomLeft, + ? toolbarUp + ? Alignment.bottomRight + : Alignment.topRight + : toolbarUp + ? Alignment.bottomLeft + : Alignment.topLeft, backgroundColor: const Color.fromRGBO(0, 0, 0, 1).withAlpha(100), ); From 2fbe7e7960f58730f9f4ccca95c76bd18a175f6c Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 12 Jun 2024 13:40:39 -0400 Subject: [PATCH 8/9] moved overlay message into widget variable to reduce repetition --- lib/pangea/widgets/chat/message_toolbar.dart | 37 +++++++------------- 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index fa45b6d10..ba508906b 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -82,6 +82,17 @@ class ToolbarDisplayController { toolbarUp = targetOffset.dy >= screenHeight / 2; } + final Widget overlayMessage = OverlayMessage( + pangeaMessageEvent.event, + timeline: pangeaMessageEvent.timeline, + immersionMode: immersionMode, + ownMessage: pangeaMessageEvent.ownMessage, + toolbarController: this, + width: messageWidth, + nextEvent: nextEvent, + previousEvent: previousEvent, + ); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { Widget overlayEntry; if (toolbar == null) return; @@ -92,31 +103,9 @@ class ToolbarDisplayController { ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ - toolbarUp - ? toolbar! - : OverlayMessage( - pangeaMessageEvent.event, - timeline: pangeaMessageEvent.timeline, - immersionMode: immersionMode, - ownMessage: pangeaMessageEvent.ownMessage, - toolbarController: this, - width: messageWidth, - nextEvent: nextEvent, - previousEvent: previousEvent, - ), + toolbarUp ? toolbar! : overlayMessage, const SizedBox(height: 6), - toolbarUp - ? OverlayMessage( - pangeaMessageEvent.event, - timeline: pangeaMessageEvent.timeline, - immersionMode: immersionMode, - ownMessage: pangeaMessageEvent.ownMessage, - toolbarController: this, - width: messageWidth, - nextEvent: nextEvent, - previousEvent: previousEvent, - ) - : toolbar!, + toolbarUp ? overlayMessage : toolbar!, ], ); } catch (err) { From e3281b46d6d8885a9383b3acb8836a1bc72ff2da Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 12 Jun 2024 14:39:38 -0400 Subject: [PATCH 9/9] remove setting of bot power on invite --- lib/pangea/controllers/pangea_controller.dart | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/lib/pangea/controllers/pangea_controller.dart b/lib/pangea/controllers/pangea_controller.dart index ad2a27145..68cfc59fc 100644 --- a/lib/pangea/controllers/pangea_controller.dart +++ b/lib/pangea/controllers/pangea_controller.dart @@ -248,29 +248,11 @@ class PangeaController { if (!userIds.contains(BotName.byEnvironment)) { try { await space.invite(BotName.byEnvironment); - await space.postLoad(); - await space.setPower( - BotName.byEnvironment, - ClassDefaultValues.powerLevelOfAdmin, - ); } catch (err) { ErrorHandler.logError( e: "Failed to invite pangea bot to space ${space.id}", ); } - } else if (space.getPowerLevelByUserId(BotName.byEnvironment) < - ClassDefaultValues.powerLevelOfAdmin) { - try { - await space.postLoad(); - await space.setPower( - BotName.byEnvironment, - ClassDefaultValues.powerLevelOfAdmin, - ); - } catch (err) { - ErrorHandler.logError( - e: "Failed to reset power level for pangea bot in space ${space.id}", - ); - } } } }