Merge branch 'main' into analytics-rooms-data

pull/1183/head
ggurdin 1 year ago
commit 7163076390

@ -3951,7 +3951,7 @@
"autoIGCToolName": "Run Language Assistance Automatically", "autoIGCToolName": "Run Language Assistance Automatically",
"autoIGCToolDescription": "Automatically run language assistance after typing messages", "autoIGCToolDescription": "Automatically run language assistance after typing messages",
"runGrammarCorrection": "Run grammar correction", "runGrammarCorrection": "Run grammar correction",
"grammarCorrectionFailed": "Grammar correction failed", "grammarCorrectionFailed": "Issues to address",
"grammarCorrectionComplete": "Grammar correction complete", "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.", "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.", "archiveSpaceDescription": "All chats within this space will be moved to the archive for yourself and other non-admin users.",

@ -1300,9 +1300,18 @@ class ChatController extends State<ChatPageWithRoom>
} }
// Pangea# // Pangea#
if (!event.redacted) { 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( setState(
() => selectedEvents.remove(event), // #Pangea
() => selectedEvents.remove(matches.first),
// () => selectedEvents.remove(event),
// Pangea#
); );
} else { } else {
setState( setState(

@ -1,18 +1,17 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart';
import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:file_picker/file_picker.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:flutter_gen/gen_l10n/l10n.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:matrix/matrix.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 '../../widgets/matrix.dart';
import 'settings_view.dart'; import 'settings_view.dart';
@ -171,6 +170,10 @@ class SettingsController extends State<Settings> {
// Pangea# // Pangea#
super.initState(); super.initState();
// #Pangea
profileUpdated = true;
profileFuture = null;
// Pangea#
} }
void checkBootstrap() async { void checkBootstrap() async {

@ -19,14 +19,37 @@ import '../../repo/tokens_repo.dart';
import '../../utils/error_handler.dart'; import '../../utils/error_handler.dart';
import '../../utils/overlay.dart'; import '../../utils/overlay.dart';
class _SpanDetailsCacheItem {
SpanDetailsRepoReqAndRes data;
_SpanDetailsCacheItem({required this.data});
}
class IgcController { class IgcController {
Choreographer choreographer; Choreographer choreographer;
IGCTextData? igcTextData; IGCTextData? igcTextData;
Object? igcError; Object? igcError;
Completer<IGCTextData> igcCompleter = Completer(); Completer<IGCTextData> igcCompleter = Completer();
final Map<int, _SpanDetailsCacheItem> _cache = {};
Timer? _cacheClearTimer;
IgcController(this.choreographer) {
_initializeCacheClearing();
}
void _initializeCacheClearing() {
const duration = Duration(minutes: 2);
_cacheClearTimer = Timer.periodic(duration, (Timer t) => _clearCache());
}
void _clearCache() {
_cache.clear();
}
IgcController(this.choreographer); void dispose() {
_cacheClearTimer?.cancel();
}
Future<void> getIGCTextData({required bool tokensOnly}) async { Future<void> getIGCTextData({required bool tokensOnly}) async {
try { try {
@ -80,6 +103,14 @@ class IgcController {
igcTextData = igcTextDataResponse; 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()}"); debugPrint("igc text ${igcTextData.toString()}");
} catch (err, stack) { } catch (err, stack) {
debugger(when: kDebugMode); debugger(when: kDebugMode);
@ -99,18 +130,38 @@ class IgcController {
debugger(when: kDebugMode); debugger(when: kDebugMode);
return; return;
} }
final SpanData span = igcTextData!.matches[matchIndex].match;
final SpanDetailsRepoReqAndRes response = await SpanDataRepo.getSpanDetails( /// Retrieves the span data from the `igcTextData` matches at the specified `matchIndex`.
await choreographer.accessToken, /// Creates a `SpanDetailsRepoReqAndRes` object with the retrieved span data and other parameters.
request: SpanDetailsRepoReqAndRes( /// Generates a cache key based on the created `SpanDetailsRepoReqAndRes` object.
userL1: choreographer.l1LangCode!, final SpanData span = igcTextData!.matches[matchIndex].match;
userL2: choreographer.l2LangCode!, final req = SpanDetailsRepoReqAndRes(
enableIGC: choreographer.igcEnabled, userL1: choreographer.l1LangCode!,
enableIT: choreographer.itEnabled, userL2: choreographer.l2LangCode!,
span: span, 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 { try {
igcTextData!.matches[matchIndex].match = response.span; igcTextData!.matches[matchIndex].match = response.span;

@ -33,7 +33,7 @@ class StartIGCButtonState extends State<StartIGCButton>
void initState() { void initState() {
_controller = AnimationController( _controller = AnimationController(
vsync: this, vsync: this,
duration: const Duration(seconds: 1), duration: const Duration(seconds: 2),
); );
choreoListener = widget.controller.choreographer.stateListener.stream choreoListener = widget.controller.choreographer.stateListener.stream
.listen(updateSpinnerState); .listen(updateSpinnerState);
@ -54,14 +54,15 @@ class StartIGCButtonState extends State<StartIGCButton>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (widget.controller.choreographer.isAutoIGCEnabled) { if (widget.controller.choreographer.isAutoIGCEnabled ||
widget.controller.choreographer.choreoMode == ChoreoMode.it) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
final Widget icon = Icon( final Widget icon = Icon(
Icons.autorenew_rounded, Icons.autorenew_rounded,
size: 46, size: 46,
color: assistanceState.stateColor, color: assistanceState.stateColor(context),
); );
return SizedBox( return SizedBox(
@ -71,15 +72,23 @@ class StartIGCButtonState extends State<StartIGCButton>
tooltip: assistanceState.tooltip( tooltip: assistanceState.tooltip(
L10n.of(context)!, L10n.of(context)!,
), ),
backgroundColor: Colors.white, backgroundColor: Theme.of(context).scaffoldBackgroundColor,
disabledElevation: 0, disabledElevation: 0,
shape: const CircleBorder(), shape: const CircleBorder(),
onPressed: () { onPressed: () {
if (assistanceState != AssistanceState.complete) { if (assistanceState != AssistanceState.complete) {
widget.controller.choreographer.getLanguageHelp( widget.controller.choreographer
.getLanguageHelp(
false, false,
true, true,
); )
.then((_) {
if (widget.controller.choreographer.igc.igcTextData != null &&
widget.controller.choreographer.igc.igcTextData!.matches
.isNotEmpty) {
widget.controller.choreographer.igc.showFirstMatch(context);
}
});
} }
}, },
child: Stack( child: Stack(
@ -95,9 +104,9 @@ class StartIGCButtonState extends State<StartIGCButton>
Container( Container(
width: 26, width: 26,
height: 26, height: 26,
decoration: const BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: Colors.white, color: Theme.of(context).scaffoldBackgroundColor,
), ),
), ),
Container( Container(
@ -105,13 +114,13 @@ class StartIGCButtonState extends State<StartIGCButton>
height: 20, height: 20,
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: assistanceState.stateColor, color: assistanceState.stateColor(context),
), ),
), ),
const Icon( Icon(
size: 16, size: 16,
Icons.check, Icons.check,
color: Colors.white, color: Theme.of(context).scaffoldBackgroundColor,
), ),
], ],
), ),
@ -121,12 +130,12 @@ class StartIGCButtonState extends State<StartIGCButton>
} }
extension AssistanceStateExtension on AssistanceState { extension AssistanceStateExtension on AssistanceState {
Color get stateColor { Color stateColor(context) {
switch (this) { switch (this) {
case AssistanceState.noMessage: case AssistanceState.noMessage:
case AssistanceState.notFetched: case AssistanceState.notFetched:
case AssistanceState.fetching: case AssistanceState.fetching:
return AppConfig.primaryColor; return Theme.of(context).colorScheme.primary;
case AssistanceState.fetched: case AssistanceState.fetched:
return PangeaColors.igcError; return PangeaColors.igcError;
case AssistanceState.complete: case AssistanceState.complete:

@ -248,29 +248,11 @@ class PangeaController {
if (!userIds.contains(BotName.byEnvironment)) { if (!userIds.contains(BotName.byEnvironment)) {
try { try {
await space.invite(BotName.byEnvironment); await space.invite(BotName.byEnvironment);
await space.postLoad();
await space.setPower(
BotName.byEnvironment,
ClassDefaultValues.powerLevelOfAdmin,
);
} catch (err) { } catch (err) {
ErrorHandler.logError( ErrorHandler.logError(
e: "Failed to invite pangea bot to space ${space.id}", 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}",
);
}
} }
} }
} }

@ -4,9 +4,8 @@
// SpanChoice of text in message from options // SpanChoice of text in message from options
// Call to server for additional/followup info // Call to server for additional/followup info
import 'package:flutter/material.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import '../enum/span_choice_type.dart'; import '../enum/span_choice_type.dart';
import '../enum/span_data_type.dart'; import '../enum/span_data_type.dart';
@ -105,6 +104,7 @@ class SpanChoice {
required this.type, required this.type,
this.feedback, this.feedback,
this.selected = false, this.selected = false,
this.timestamp,
}); });
factory SpanChoice.fromJson(Map<String, dynamic> json) { factory SpanChoice.fromJson(Map<String, dynamic> json) {
return SpanChoice( return SpanChoice(
@ -117,6 +117,8 @@ class SpanChoice {
: SpanChoiceType.bestCorrection, : SpanChoiceType.bestCorrection,
feedback: json['feedback'], feedback: json['feedback'],
selected: json['selected'] ?? false, selected: json['selected'] ?? false,
timestamp:
json['timestamp'] != null ? DateTime.parse(json['timestamp']) : null,
); );
} }
@ -124,12 +126,14 @@ class SpanChoice {
SpanChoiceType type; SpanChoiceType type;
bool selected; bool selected;
String? feedback; String? feedback;
DateTime? timestamp;
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
'value': value, 'value': value,
'type': type.name, 'type': type.name,
'selected': selected, 'selected': selected,
'feedback': feedback, 'feedback': feedback,
'timestamp': timestamp?.toIso8601String(),
}; };
String feedbackToDisplay(BuildContext context) { String feedbackToDisplay(BuildContext context) {

@ -72,6 +72,24 @@ class SpanDetailsRepoReqAndRes {
enableIGC: json['enable_igc'] as bool, enableIGC: json['enable_igc'] as bool,
span: SpanData.fromJson(json['span']), 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( final spanDataRepomockSpan = SpanData(

@ -59,6 +59,7 @@ class ToolbarDisplayController {
} }
void showToolbar(BuildContext context, {MessageMode? mode}) { void showToolbar(BuildContext context, {MessageMode? mode}) {
bool toolbarUp = true;
if (highlighted) return; if (highlighted) return;
if (controller.selectMode) { if (controller.selectMode) {
controller.clearSelectedEvents(); controller.clearSelectedEvents();
@ -76,8 +77,22 @@ class ToolbarDisplayController {
if (targetRenderBox != null) { if (targetRenderBox != null) {
final Size transformTargetSize = (targetRenderBox as RenderBox).size; final Size transformTargetSize = (targetRenderBox as RenderBox).size;
messageWidth = transformTargetSize.width; messageWidth = transformTargetSize.width;
final Offset targetOffset = (targetRenderBox).localToGlobal(Offset.zero);
final double screenHeight = MediaQuery.of(context).size.height;
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) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
Widget overlayEntry; Widget overlayEntry;
if (toolbar == null) return; if (toolbar == null) return;
@ -88,18 +103,9 @@ class ToolbarDisplayController {
? CrossAxisAlignment.end ? CrossAxisAlignment.end
: CrossAxisAlignment.start, : CrossAxisAlignment.start,
children: [ children: [
toolbar!, toolbarUp ? toolbar! : overlayMessage,
const SizedBox(height: 6), const SizedBox(height: 6),
OverlayMessage( toolbarUp ? overlayMessage : toolbar!,
pangeaMessageEvent.event,
timeline: pangeaMessageEvent.timeline,
immersionMode: immersionMode,
ownMessage: pangeaMessageEvent.ownMessage,
toolbarController: this,
width: messageWidth,
nextEvent: nextEvent,
previousEvent: previousEvent,
),
], ],
); );
} catch (err) { } catch (err) {
@ -113,11 +119,19 @@ class ToolbarDisplayController {
child: overlayEntry, child: overlayEntry,
transformTargetId: targetId, transformTargetId: targetId,
targetAnchor: pangeaMessageEvent.ownMessage targetAnchor: pangeaMessageEvent.ownMessage
? Alignment.bottomRight ? toolbarUp
: Alignment.bottomLeft, ? Alignment.bottomRight
: Alignment.topRight
: toolbarUp
? Alignment.bottomLeft
: Alignment.topLeft,
followerAnchor: pangeaMessageEvent.ownMessage followerAnchor: pangeaMessageEvent.ownMessage
? Alignment.bottomRight ? toolbarUp
: Alignment.bottomLeft, ? Alignment.bottomRight
: Alignment.topRight
: toolbarUp
? Alignment.bottomLeft
: Alignment.topLeft,
backgroundColor: const Color.fromRGBO(0, 0, 0, 1).withAlpha(100), backgroundColor: const Color.fromRGBO(0, 0, 0, 1).withAlpha(100),
); );

@ -55,6 +55,7 @@ class SpanCardState extends State<SpanCard> {
// debugger(when: kDebugMode); // debugger(when: kDebugMode);
super.initState(); super.initState();
getSpanDetails(); getSpanDetails();
fetchSelected();
} }
//get selected choice //get selected choice
@ -67,6 +68,23 @@ class SpanCardState extends State<SpanCard> {
return widget.scm.pangeaMatch?.match.choices?[selectedChoiceIndex!]; 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<void> getSpanDetails() async { Future<void> getSpanDetails() async {
try { try {
if (widget.scm.pangeaMatch?.isITStart ?? false) return; if (widget.scm.pangeaMatch?.isITStart ?? false) return;
@ -110,6 +128,16 @@ class WordMatchContent extends StatelessWidget {
Future<void> onChoiceSelect(int index) async { Future<void> onChoiceSelect(int index) async {
controller.selectedChoiceIndex = index; controller.selectedChoiceIndex = index;
controller
.widget
.scm
.choreographer
.igc
.igcTextData
?.matches[controller.widget.scm.matchIndex]
.match
.choices?[index]
.timestamp = DateTime.now();
controller controller
.widget .widget
.scm .scm
@ -152,6 +180,7 @@ class WordMatchContent extends StatelessWidget {
offset: controller.widget.scm.pangeaMatch?.match.offset, offset: controller.widget.scm.pangeaMatch?.match.offset,
); );
} }
final MatchCopy matchCopy = MatchCopy( final MatchCopy matchCopy = MatchCopy(
context, context,
controller.widget.scm.pangeaMatch!, controller.widget.scm.pangeaMatch!,

Loading…
Cancel
Save