Merge branch 'main' into analytics-rooms-data
This commit is contained in:
commit
7163076390
10 changed files with 186 additions and 67 deletions
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -1300,9 +1300,18 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
}
|
||||
// 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(
|
||||
|
|
|
|||
|
|
@ -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<Settings> {
|
|||
// Pangea#
|
||||
|
||||
super.initState();
|
||||
// #Pangea
|
||||
profileUpdated = true;
|
||||
profileFuture = null;
|
||||
// Pangea#
|
||||
}
|
||||
|
||||
void checkBootstrap() async {
|
||||
|
|
|
|||
|
|
@ -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<IGCTextData> igcCompleter = Completer();
|
||||
final Map<int, _SpanDetailsCacheItem> _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<void> 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;
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ class StartIGCButtonState extends State<StartIGCButton>
|
|||
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<StartIGCButton>
|
|||
|
||||
@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<StartIGCButton>
|
|||
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<StartIGCButton>
|
|||
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<StartIGCButton>
|
|||
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<StartIGCButton>
|
|||
}
|
||||
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -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}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String, dynamic> 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<String, dynamic> toJson() => {
|
||||
'value': value,
|
||||
'type': type.name,
|
||||
'selected': selected,
|
||||
'feedback': feedback,
|
||||
'timestamp': timestamp?.toIso8601String(),
|
||||
};
|
||||
|
||||
String feedbackToDisplay(BuildContext context) {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ class ToolbarDisplayController {
|
|||
}
|
||||
|
||||
void showToolbar(BuildContext context, {MessageMode? mode}) {
|
||||
bool toolbarUp = true;
|
||||
if (highlighted) return;
|
||||
if (controller.selectMode) {
|
||||
controller.clearSelectedEvents();
|
||||
|
|
@ -76,8 +77,22 @@ 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;
|
||||
}
|
||||
|
||||
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;
|
||||
|
|
@ -88,18 +103,9 @@ class ToolbarDisplayController {
|
|||
? CrossAxisAlignment.end
|
||||
: CrossAxisAlignment.start,
|
||||
children: [
|
||||
toolbar!,
|
||||
toolbarUp ? toolbar! : overlayMessage,
|
||||
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 : toolbar!,
|
||||
],
|
||||
);
|
||||
} catch (err) {
|
||||
|
|
@ -113,11 +119,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),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ class SpanCardState extends State<SpanCard> {
|
|||
// debugger(when: kDebugMode);
|
||||
super.initState();
|
||||
getSpanDetails();
|
||||
fetchSelected();
|
||||
}
|
||||
|
||||
//get selected choice
|
||||
|
|
@ -67,6 +68,23 @@ class SpanCardState extends State<SpanCard> {
|
|||
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 {
|
||||
try {
|
||||
if (widget.scm.pangeaMatch?.isITStart ?? false) return;
|
||||
|
|
@ -110,6 +128,16 @@ class WordMatchContent extends StatelessWidget {
|
|||
|
||||
Future<void> 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!,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue