From 14f4192b6ff58b5f4eb690e885575a0508327486 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 12 Jun 2024 09:58:12 -0400 Subject: [PATCH 1/3] 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 2/3] 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 3/3] 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) {