merge conflict

pull/1183/head
ggurdin 1 year ago
commit c8aac0b450

@ -3128,7 +3128,7 @@
"maybeLater": "Maybe Later",
"mainMenu": "Main Menu",
"toggleImmersionMode": "Immersion Mode",
"toggleImmersionModeDesc": "When enabled, all messages are displayed in your target language and you can click the message to access definitions and translations.",
"toggleImmersionModeDesc": "When enabled, all messages are displayed in your target language. This setting is most useful in language exchanges.",
"itToggleDescription": "This language learning tool will identify words in your base language and help you translate them to your target language. Though rare, the AI can make translation errors.",
"igcToggleDescription": "This language learning tool will identify common spelling, grammar and punctuation errors in your message and suggest corrections. Though rare, the AI can make correction errors.",
"sendOnEnterDescription": "Turn this off to be able to add line spaces in messages. When the toggle is off on the browser app, you can press Shift + Enter to start a new line. When the toggle is off on mobile apps, just Enter will start a new line.",
@ -3609,7 +3609,7 @@
"zmCountryDisplayName": "Zambia",
"zwCountryDisplayName": "Zimbabwe",
"pay": "Pay",
"allPrivateChats": "All private chats in space (including with Pangea Bot)",
"allPrivateChats": "Direct chats",
"unknownPrivateChat": "Unknown private chat",
"copyClassCodeDesc": "Students who are already in the app can 'Join class or exchange' via the main menu.",
"addToClass": "Add exchange to class",
@ -3681,24 +3681,8 @@
"lockSpace": "Lock Space",
"lockChat": "Lock Chat",
"archiveSpace": "Archive Space",
"suggestTo": "Suggest to {spaceName}",
"@suggestTo": {
"placeholders": {
"spaceName": {}
}
},
"suggestChatDesc": "Suggested chats will appear in the chat list for {spaceName}",
"@suggestToDesc": {
"placeholders": {
"spaceName": {}
}
},
"suggestExchangeDesc": "Suggested exchanges will appear in the chat list for {spaceName}",
"@suggestToExchangeDesc": {
"placeholders": {
"spaceName": {}
}
},
"suggestToChat": "Suggest this chat",
"suggestToChatDesc": "Suggested chats will appear in chat lists",
"acceptSelection": "Accept Correction",
"acceptSelectionAnyway": "Use this anyway",
"makingActivity": "Making activity",
@ -3724,7 +3708,7 @@
},
"noTeachersFound": "No teachers found to report to",
"pleaseEnterANumber": "Please enter a number greater than 0",
"archiveRoomDescription": "The chat will be moved to the archive. Other users will be able to see that you have left the chat.",
"archiveRoomDescription": "The chat will be moved to the archive for yourself and other non-admin users.",
"roomUpgradeDescription": "The chat will then be recreated with the new room version. All participants will be notified that they need to switch to the new chat. You can find out more about room versions at https://spec.matrix.org/latest/rooms/",
"removeDevicesDescription": "You will be logged out of this device and will no longer be able to receive messages.",
"banUserDescription": "The user will be banned from the chat and will not be able to enter the chat again until they are unbanned.",
@ -3963,5 +3947,17 @@
"studentAnalyticsNotAvailable": "Student data not currently available",
"roomDataMissing": "Some data may be missing from rooms in which you are not a member.",
"updatePhoneOS": "You may need to update your device's OS version.",
"wordsPerMinute": "Words per minute"
"wordsPerMinute": "Words per minute",
"autoIGCToolName": "Run Language Assistance Automatically",
"autoIGCToolDescription": "Automatically run language assistance after typing messages",
"runGrammarCorrection": "Run grammar correction",
"grammarCorrectionFailed": "Grammar correction failed",
"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.",
"leaveSpaceDescription": "All chats within this space will be moved to the archive. Other users will be able to see that you have left the space.",
"onlyAdminDescription": "Since there are no other admins, all other participants will also be removed.",
"tooltipInstructionsTitle": "Not sure what that does?",
"tooltipInstructionsMobileBody": "Press and hold items to view tooltips.",
"tooltipInstructionsBrowserBody": "Hover over items to view tooltips."
}

@ -3281,7 +3281,7 @@
"generateVocabulary": "Generar vocabulario basado en el título y la descripción",
"generatePrompts": "Generar preguntas basado en el título y la descripción",
"toggleImmersionMode": "Modo de inmersión",
"toggleImmersionModeDesc": "Cuando está habilitado, todos los mensajes se muestran en su idioma de destino y puede hacer clic en el mensaje para acceder a definiciones y traducciones.",
"toggleImmersionModeDesc": "Cuando está habilitado, todos los mensajes se muestran en su idioma de destino. Esta configuración es más útil en intercambios de idiomas.",
"subscribe": "Subscríbase",
"getAccess": "Activar herramientas",
"subscriptionDesc": "¡Enviar y recibir mensajes es gratis! Suscríbase para aceder a la traducción interactiva, la revisión gramatical y el análisis de aprendizaje.",
@ -3698,7 +3698,7 @@
"@optionalRedactReason": {},
"dehydrate": "Exportar sesión y borrar dispositivo",
"@dehydrate": {},
"archiveRoomDescription": "",
"archiveRoomDescription": "El chat se moverá al archivo para ti y para otros usuarios que no sean administradores",
"@archiveRoomDescription": {},
"pleaseEnterRecoveryKeyDescription": "Para desbloquear sus mensajes antiguos, ingrese su clave de recuperación que se generó en una sesión anterior. Su clave de recuperación NO es su contraseña.",
"@pleaseEnterRecoveryKeyDescription": {},
@ -4274,7 +4274,7 @@
"zwCountryDisplayName": "Zimbabue",
"downloadXLSXFile": "Descargar archivo Excel",
"unknownPrivateChat": "Chat Privado Desconocido",
"allPrivateChats": "Todos los chats privados (incluso con bots) en clase",
"allPrivateChats": "Chats privado",
"chatHasBeenAddedToThisSpace": "Se ha añadido el chat a este espacio",
"classes": "Clases",
"spaceIsPublic": "El espacio es público",

@ -227,6 +227,17 @@ class ChatController extends State<ChatPageWithRoom>
context.go('/rooms');
}
// #Pangea
void archiveChat() async {
final success = await showFutureLoadingDialog(
context: context,
future: room.archive,
);
if (success.error != null) return;
context.go('/rooms');
}
// Pangea#
EmojiPickerType emojiPickerType = EmojiPickerType.keyboard;
// #Pangea

@ -9,6 +9,7 @@ import 'package:fluffychat/pages/chat/reactions_picker.dart';
import 'package:fluffychat/pages/chat/reply_display.dart';
import 'package:fluffychat/pangea/choreographer/widgets/has_error_button.dart';
import 'package:fluffychat/pangea/choreographer/widgets/language_permissions_warning_buttons.dart';
import 'package:fluffychat/pangea/choreographer/widgets/start_igc_button.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
import 'package:fluffychat/pangea/pages/class_analytics/measure_able.dart';
import 'package:fluffychat/utils/account_config.dart';
@ -367,6 +368,29 @@ class ChatView extends StatelessWidget {
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [
// #Pangea
if (controller.room.isRoomAdmin)
TextButton.icon(
style: TextButton.styleFrom(
padding:
const EdgeInsets.all(
16,
),
foregroundColor:
Theme.of(context)
.colorScheme
.error,
),
icon: const Icon(
Icons.archive_outlined,
),
onPressed:
controller.archiveChat,
label: Text(
L10n.of(context)!.archive,
),
),
// Pangea#
TextButton.icon(
style: TextButton.styleFrom(
padding: const EdgeInsets.all(
@ -378,7 +402,10 @@ class ChatView extends StatelessWidget {
.error,
),
icon: const Icon(
Icons.archive_outlined,
// #Pangea
// Icons.archive_outlined,
Icons.arrow_forward,
// Pangea#
),
onPressed: controller.leaveChat,
label: Text(
@ -423,8 +450,8 @@ class ChatView extends StatelessWidget {
// #Pangea
// if (controller.dragging)
// Container(
// color: Theme.of(context)
// .scaffoldBackgroundColor
// color: Theme.of(context)
// .scaffoldBackgroundColor
// .withOpacity(0.9),
// alignment: Alignment.center,
// child: const Icon(
@ -432,6 +459,11 @@ class ChatView extends StatelessWidget {
// size: 100,
// ),
// ),
Positioned(
left: 20,
bottom: 75,
child: StartIGCButton(controller: controller),
),
// Pangea#
],
),

@ -199,18 +199,8 @@ class MessageContent extends StatelessWidget {
case MessageTypes.Notice:
case MessageTypes.Emote:
if (AppConfig.renderHtml &&
!event.redacted &&
event.isRichMessage
// #Pangea
&&
!(pangeaMessageEvent?.showRichText(
selected,
isOverlay: isOverlay,
highlighted: toolbarController?.highlighted ?? false,
) ??
false)
// Pangea#
) {
!event.redacted &&
event.isRichMessage) {
var html = event.formattedText;
if (event.messageType == MessageTypes.Emote) {
html = '* $html';
@ -306,18 +296,17 @@ class MessageContent extends StatelessWidget {
decoration: event.redacted ? TextDecoration.lineThrough : null,
height: 1.3,
);
if (pangeaMessageEvent?.showRichText(
selected,
isOverlay: isOverlay,
highlighted: toolbarController?.highlighted ?? false,
) ??
false) {
if (immersionMode && pangeaMessageEvent != null) {
return PangeaRichText(
style: messageTextStyle,
pangeaMessageEvent: pangeaMessageEvent!,
immersionMode: immersionMode,
toolbarController: toolbarController,
);
} else if (pangeaMessageEvent != null) {
toolbarController?.toolbar?.textSelection.setMessageText(
pangeaMessageEvent!.body,
);
}
// Pangea#
return FutureBuilder<String>(

@ -9,7 +9,6 @@ import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_det
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_invitation_buttons.dart';
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_name_button.dart';
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_rules_editor.dart';
import 'package:fluffychat/pangea/utils/archive_space.dart';
import 'package:fluffychat/pangea/utils/lock_room.dart';
import 'package:fluffychat/pangea/widgets/class/add_class_and_invite.dart';
import 'package:fluffychat/pangea/widgets/class/add_space_toggles.dart';
@ -522,52 +521,126 @@ class ChatDetailsView extends StatelessWidget {
),
const Divider(height: 1),
if (!room.isDirectChat)
ListTile(
title: Text(
room.isSpace
? L10n.of(context)!.archiveSpace
: L10n.of(context)!.archive,
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.bold,
if (room.isRoomAdmin)
ListTile(
title: Text(
room.isSpace
? L10n.of(context)!.archiveSpace
: L10n.of(context)!.archive,
style: TextStyle(
color:
Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.bold,
),
),
),
leading: CircleAvatar(
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
foregroundColor: iconColor,
child: const Icon(
Icons.archive_outlined,
leading: CircleAvatar(
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
foregroundColor: iconColor,
child: const Icon(
Icons.archive_outlined,
),
),
onTap: () async {
OkCancelResult confirmed = OkCancelResult.ok;
bool shouldGo = false;
// archiveSpace has its own popup; only show if not space
if (!room.isSpace) {
confirmed = await showOkCancelAlertDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context)!.areYouSure,
okLabel: L10n.of(context)!.ok,
cancelLabel: L10n.of(context)!.cancel,
message: L10n.of(context)!
.archiveRoomDescription,
);
}
if (confirmed == OkCancelResult.ok) {
if (room.isSpace) {
shouldGo = await room.archiveSpace(
context,
Matrix.of(context).client,
);
} else {
final success =
await showFutureLoadingDialog(
context: context,
future: () async {
await room.archive();
},
);
shouldGo = (success.error == null);
}
if (shouldGo) {
context.go('/rooms');
}
}
},
),
onTap: () async {
final confirmed = await showOkCancelAlertDialog(
ListTile(
title: Text(
L10n.of(context)!.leave,
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.bold,
),
),
leading: CircleAvatar(
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
foregroundColor: iconColor,
child: const Icon(
Icons.arrow_forward,
),
),
onTap: () async {
OkCancelResult confirmed = OkCancelResult.ok;
bool shouldGo = false;
// If user is only admin, room will be archived
final bool onlyAdmin = await room.isOnlyAdmin();
// archiveSpace has its own popup; only show if not space
if (!room.isSpace) {
confirmed = await showOkCancelAlertDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context)!.areYouSure,
okLabel: L10n.of(context)!.ok,
cancelLabel: L10n.of(context)!.cancel,
message:
L10n.of(context)!.archiveRoomDescription,
message: onlyAdmin
? L10n.of(context)!.onlyAdminDescription
: L10n.of(context)!.leaveRoomDescription,
);
if (confirmed == OkCancelResult.ok) {
}
if (confirmed == OkCancelResult.ok) {
if (room.isSpace) {
shouldGo = onlyAdmin
? await room.archiveSpace(
context,
Matrix.of(context).client,
onlyAdmin: true,
)
: await room.leaveSpace(
context,
Matrix.of(context).client,
);
} else {
final success = await showFutureLoadingDialog(
context: context,
future: () async {
room.isSpace
? await archiveSpace(
room,
Matrix.of(context).client,
)
onlyAdmin
? await room.archive()
: await room.leave();
},
);
if (success.error == null) {
context.go('/rooms');
}
shouldGo = (success.error == null);
}
},
),
if (shouldGo) {
context.go('/rooms');
}
}
},
),
if (room.isRoomAdmin && !room.isDirectChat)
SwitchListTile.adaptive(
activeColor: AppConfig.activeToggleColor,

@ -515,7 +515,8 @@ class ChatListController extends State<ChatList>
//#Pangea
classStream = pangeaController.classController.stateStream.listen((event) {
if (event["activeSpaceId"] != null && mounted) {
// if (event["activeSpaceId"] != null && mounted) {
if (mounted) {
setActiveSpace(event["activeSpaceId"]);
}
});
@ -679,6 +680,38 @@ class ChatListController extends State<ChatList>
// Pangea#
}
// #Pangea
Future<void> leaveAction() async {
final bool onlyAdmin = await Matrix.of(context)
.client
.getRoomById(selectedRoomIds.first)
?.isOnlyAdmin() ??
false;
final confirmed = await showOkCancelAlertDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context)!.areYouSure,
okLabel: L10n.of(context)!.yes,
cancelLabel: L10n.of(context)!.cancel,
message: onlyAdmin
? L10n.of(context)!.onlyAdminDescription
: L10n.of(context)!.leaveRoomDescription,
) ==
OkCancelResult.ok;
if (!confirmed) return;
final bool leftActiveRoom =
selectedRoomIds.contains(Matrix.of(context).activeRoomId);
await showFutureLoadingDialog(
context: context,
future: () => _leaveSelectedRooms(onlyAdmin),
);
setState(() {});
if (leftActiveRoom) {
context.go('/rooms');
}
}
// Pangea#
void dismissStatusList() async {
final result = await showOkCancelAlertDialog(
title: L10n.of(context)!.hidePresences,
@ -729,17 +762,35 @@ class ChatListController extends State<ChatList>
final roomId = selectedRoomIds.first;
try {
// #Pangea
if (client.getRoomById(roomId)!.isUnread) {
await client.getRoomById(roomId)!.markUnread(false);
}
// await client.getRoomById(roomId)!.leave();
await client.getRoomById(roomId)!.archive();
// Pangea#
await client.getRoomById(roomId)!.leave();
} finally {
toggleSelection(roomId);
}
}
}
// #Pangea
Future<void> _leaveSelectedRooms(bool onlyAdmin) async {
final client = Matrix.of(context).client;
while (selectedRoomIds.isNotEmpty) {
final roomId = selectedRoomIds.first;
try {
final room = client.getRoomById(roomId);
if (!room!.isSpace &&
room.membership == Membership.join &&
room.isUnread) {
await room.markUnread(false);
}
onlyAdmin ? await room.archive() : await room.leave();
} finally {
toggleSelection(roomId);
}
}
}
// Pangea#
Future<void> addToSpace() async {
final selectedSpace = await showConfirmationDialog<String>(
context: context,

@ -1,6 +1,8 @@
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat_list/chat_list.dart';
import 'package:fluffychat/pages/chat_list/client_chooser_button.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
@ -168,14 +170,33 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget {
tooltip: L10n.of(context)!.toggleMuted,
onPressed: controller.toggleMuted,
),
IconButton(
// #Pangea
// icon: const Icon(Icons.delete_outlined),
icon: const Icon(Icons.archive_outlined),
// #Pangea
if (controller.selectedRoomIds.length == 1 &&
!(Matrix.of(context)
.client
.getRoomById(controller.selectedRoomIds.single)
?.isRoomAdmin ??
false))
IconButton(
icon: const Icon(Icons.arrow_forward),
tooltip: L10n.of(context)!.leave,
onPressed: controller.leaveAction,
),
if (controller.selectedRoomIds.length == 1 &&
(Matrix.of(context)
.client
.getRoomById(controller.selectedRoomIds.single)
?.isRoomAdmin ??
false))
// Pangea#
tooltip: L10n.of(context)!.archive,
onPressed: controller.archiveAction,
),
IconButton(
// #Pangea
// icon: const Icon(Icons.delete_outlined),
icon: const Icon(Icons.archive_outlined),
// Pangea#
tooltip: L10n.of(context)!.archive,
onPressed: controller.archiveAction,
),
]
: null,
);

@ -53,14 +53,12 @@ class ChatListItem extends StatelessWidget {
message: L10n.of(context)!.archiveRoomDescription,
);
if (confirmed == OkCancelResult.cancel) return;
// #Pangea
if (room.isUnread) {
await room.markUnread(false);
}
// Pangea#
await showFutureLoadingDialog(
context: context,
future: () => room.leave(),
// #Pangea
// future: () => room.leave(),
future: () => room.archive(),
// Pangea#
);
return;
}

@ -11,7 +11,6 @@ import 'package:fluffychat/pangea/constants/class_default_values.dart';
import 'package:fluffychat/pangea/constants/pangea_room_types.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
import 'package:fluffychat/pangea/extensions/sync_update_extension.dart';
import 'package:fluffychat/pangea/utils/archive_space.dart';
import 'package:fluffychat/pangea/utils/chat_list_handle_space_tap.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/widgets/avatar.dart';
@ -230,7 +229,10 @@ class _SpaceViewState extends State<SpaceView> {
),
message: spaceChild?.topic ?? room?.topic,
actions: [
if (room == null)
// #Pangea
// if (room == null)
if (room == null || room.membership == Membership.leave)
// Pangea#
SheetAction(
key: SpaceChildContextAction.join,
label: L10n.of(context)!.joinRoom,
@ -255,16 +257,21 @@ class _SpaceViewState extends State<SpaceView> {
label: L10n.of(context)!.addToSpace,
icon: Icons.workspaces_outlined,
),
if (room != null && room.isRoomAdmin)
if (room != null &&
room.isRoomAdmin &&
room.membership != Membership.leave)
SheetAction(
key: SpaceChildContextAction.archive,
label: room.isSpace
? L10n.of(context)!.archiveSpace
: L10n.of(context)!.archive,
icon: Icons.architecture_outlined,
isDestructiveAction: true,
),
// Pangea#
if (room != null)
if (room != null && room.membership != Membership.leave)
// if (room != null)
// Pangea#
SheetAction(
key: SpaceChildContextAction.leave,
label: L10n.of(context)!.leave,
@ -283,22 +290,32 @@ class _SpaceViewState extends State<SpaceView> {
_onJoinSpaceChild(spaceChild!);
break;
case SpaceChildContextAction.leave:
await showFutureLoadingDialog(
context: context,
// #Pangea
// future: room!.leave,
future: () async {
if (room!.isUnread) {
await room.markUnread(false);
}
await room.leave();
if (Matrix.of(context).activeRoomId == room.id) {
context.go('/rooms');
}
},
// Pangea#
);
// #Pangea
widget.controller.cancelAction();
if (room == null) return;
if (room.isSpace) {
await room.isOnlyAdmin()
? await room.archiveSpace(
context,
Matrix.of(context).client,
onlyAdmin: true,
)
: await room.leaveSpace(
context,
Matrix.of(context).client,
);
} else {
widget.controller.toggleSelection(room.id);
await widget.controller.leaveAction();
}
_refresh();
break;
// await showFutureLoadingDialog(
// context: context,
// future: room!.leave,
// );
// break;
// Pangea#
case SpaceChildContextAction.removeFromSpace:
await showFutureLoadingDialog(
context: context,
@ -310,20 +327,27 @@ class _SpaceViewState extends State<SpaceView> {
widget.controller.cancelAction();
// #Pangea
if (room == null) return;
// room.isSpace
// ? await showFutureLoadingDialog(
// context: context,
// future: () async {
// await room.archiveSpace(
// Matrix.of(context).client,
// );
// widget.controller.selectedRoomIds.clear();
// },
// )
// : await widget.controller.archiveAction();
if (room.isSpace) {
await room.archiveSpace(
context,
Matrix.of(context).client,
);
} else {
widget.controller.toggleSelection(room.id);
await widget.controller.archiveAction();
}
// Pangea#
widget.controller.toggleSelection(room.id);
room.isSpace
? await showFutureLoadingDialog(
context: context,
future: () async {
await archiveSpace(
room,
Matrix.of(context).client,
);
widget.controller.selectedRoomIds.clear();
},
)
: await widget.controller.archiveAction();
_refresh();
break;
case SpaceChildContextAction.addToSpace:
@ -333,8 +357,10 @@ class _SpaceViewState extends State<SpaceView> {
// Pangea#
widget.controller.toggleSelection(room.id);
await widget.controller.addToSpace();
// #Pangea
setState(() => widget.controller.selectedRoomIds.clear());
// Pangea#
break;
// Pangea#
}
}

@ -46,7 +46,9 @@ void onChatTap(Room room, BuildContext context) async {
}
if (inviteAction == InviteActions.decline) {
// #Pangea
if (room.isUnread) {
if (!room.isSpace &&
room.membership == Membership.join &&
room.isUnread) {
await room.markUnread(false);
}
// Pangea#

@ -159,6 +159,8 @@ class InvitationSelectionController extends State<InvitationSelection> {
future: () async {
if (mode == InvitationSelectionMode.admin) {
await inviteTeacherAction(room, id);
} else {
await room.invite(id);
}
},
// Pangea#

@ -130,9 +130,7 @@ class NewGroupController extends State<NewGroup> {
powerLevelContentOverride:
await ClassChatPowerLevels.powerLevelOverrideForClassChat(
context,
addToSpaceKey.currentState!.parents
.map((suggestionStatus) => suggestionStatus.room)
.toList(),
addToSpaceKey.currentState!.parents,
),
invite: [
if (addConversationBotKey.currentState?.addBot ?? false)

@ -174,9 +174,7 @@ class NewSpaceController extends State<NewSpace> {
powerLevelContentOverride: addToSpaceKey.currentState != null
? await ClassChatPowerLevels.powerLevelOverrideForClassChat(
context,
addToSpaceKey.currentState!.parents
.map((suggestionStatus) => suggestionStatus.room)
.toList(),
addToSpaceKey.currentState!.parents,
)
: null,
// initialState: [

@ -13,6 +13,7 @@ import 'package:fluffychat/pangea/enum/edit_type.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
import 'package:fluffychat/pangea/models/class_model.dart';
import 'package:fluffychat/pangea/models/it_step.dart';
import 'package:fluffychat/pangea/models/language_detection_model.dart';
import 'package:fluffychat/pangea/models/representation_content_model.dart';
import 'package:fluffychat/pangea/models/tokens_event_content_model.dart';
import 'package:fluffychat/pangea/utils/any_state_holder.dart';
@ -51,7 +52,7 @@ class Choreographer {
// last checked by IGC or translation
String? _lastChecked;
ChoreoMode choreoMode = ChoreoMode.igc;
final StreamController stateListener = StreamController();
final StreamController stateListener = StreamController.broadcast();
StreamSubscription? trialStream;
Choreographer(this.pangeaController, this.chatController) {
@ -93,7 +94,7 @@ class Choreographer {
}
}
void _sendWithIGC(BuildContext context) {
Future<void> _sendWithIGC(BuildContext context) async {
if (igc.canSendMessage) {
final PangeaRepresentation? originalWritten =
choreoRecord.includedIT && itController.sourceText != null
@ -105,7 +106,6 @@ class Choreographer {
)
: null;
// PTODO - just put this in original message event
final PangeaRepresentation originalSent = PangeaRepresentation(
langCode: langCodeOfCurrentText ?? LanguageKeys.unknownLanguage,
text: currentText,
@ -115,6 +115,22 @@ class Choreographer {
final ChoreoRecord? applicableChoreo =
isITandIGCEnabled && igc.igcTextData != null ? choreoRecord : null;
// if the message has not been processed to determine its language
// then run it through the language detection endpoint. If the detection
// confidence is high enough, use that language code as the message's language
// to save that pangea representation
if (applicableChoreo == null) {
final resp = await pangeaController.languageDetection.detectLanguage(
currentText,
pangeaController.languageController.userL2?.langCode,
pangeaController.languageController.userL1?.langCode,
);
final LanguageDetection? bestDetection = resp.bestDetection();
if (bestDetection != null) {
originalSent.langCode = bestDetection.langCode;
}
}
final UseType useType = useTypeCalculator(applicableChoreo);
debugPrint("use type in choreographer $useType");
@ -205,14 +221,18 @@ class Choreographer {
textController.editType = EditType.keyboard;
}
Future<void> getLanguageHelp([bool tokensOnly = false]) async {
Future<void> getLanguageHelp([
bool tokensOnly = false,
bool manual = false,
]) async {
try {
if (errorService.isError) return;
final CanSendStatus canSendStatus =
pangeaController.subscriptionController.canSendStatus;
if (canSendStatus != CanSendStatus.subscribed ||
(!igcEnabled && !itEnabled)) {
(!igcEnabled && !itEnabled) ||
(!isAutoIGCEnabled && !manual && choreoMode != ChoreoMode.it)) {
return;
}
@ -525,14 +545,50 @@ class Choreographer {
chatController.room,
);
bool get translationEnabled =>
pangeaController.permissionsController.isToolEnabled(
ToolSetting.translations,
chatController.room,
);
// bool get translationEnabled =>
// pangeaController.permissionsController.isToolEnabled(
// ToolSetting.translations,
// chatController.room,
// );
bool get isITandIGCEnabled =>
pangeaController.permissionsController.isWritingAssistanceEnabled(
chatController.room,
);
bool get isAutoIGCEnabled =>
pangeaController.permissionsController.isToolEnabled(
ToolSetting.autoIGC,
chatController.room,
);
AssistanceState get assistanceState {
if (currentText.isEmpty && itController.sourceText == null) {
return AssistanceState.noMessage;
}
if (igc.igcTextData?.matches.isNotEmpty ?? false) {
return AssistanceState.fetched;
}
if (isFetching) {
return AssistanceState.fetching;
}
if (igc.igcTextData == null) {
return AssistanceState.notFetched;
}
return AssistanceState.complete;
}
}
// assistance state is, user has not typed a message, user has typed a message and IGC has not run,
// IGC is running, IGC has run and there are remaining steps (either IT or IGC), or all steps are done
enum AssistanceState {
noMessage,
notFetched,
fetching,
fetched,
complete,
}

@ -183,7 +183,7 @@ class ITController {
}
}
Future<void>getNextTranslationData() async {
Future<void> getNextTranslationData() async {
try {
if (completedITSteps.length < goldRouteTracker.continuances.length) {
final String currentText = choreographer.currentText;
@ -478,5 +478,5 @@ class CurrentITStep {
// get continuance with highest level
Continuance get best =>
continuances.reduce((a, b) => a.level > b.level ? a : b);
continuances.reduce((a, b) => a.level < b.level ? a : b);
}

@ -17,9 +17,9 @@ class LanguageDisplayToggle extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (!controller.choreographer.translationEnabled) {
return const SizedBox();
}
// if (!controller.choreographer.translationEnabled) {
// return const SizedBox();
// }
return Container(
decoration: BoxDecoration(
shape: BoxShape.circle,

@ -1,8 +1,7 @@
import 'package:fluffychat/pangea/constants/colors.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:fluffychat/pangea/constants/colors.dart';
import '../../../pages/chat/chat.dart';
class ChoreographerSendButton extends StatelessWidget {
@ -16,7 +15,8 @@ class ChoreographerSendButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
// commit for cicd
return controller.choreographer.isFetching
return controller.choreographer.isFetching &&
controller.choreographer.isAutoIGCEnabled
? Container(
height: 56,
width: 56,
@ -28,7 +28,8 @@ class ChoreographerSendButton extends StatelessWidget {
alignment: Alignment.center,
child: IconButton(
icon: const Icon(Icons.send_outlined),
color: controller.choreographer.igc.canSendMessage
color: controller.choreographer.igc.canSendMessage ||
!controller.choreographer.isAutoIGCEnabled
? null
: PangeaColors.igcError,
onPressed: () {

@ -0,0 +1,150 @@
import 'dart:async';
import 'dart:math' as math;
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
import 'package:fluffychat/pangea/constants/colors.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../../../pages/chat/chat.dart';
class StartIGCButton extends StatefulWidget {
const StartIGCButton({
super.key,
required this.controller,
});
final ChatController controller;
@override
State<StartIGCButton> createState() => StartIGCButtonState();
}
class StartIGCButtonState extends State<StartIGCButton>
with SingleTickerProviderStateMixin {
AssistanceState get assistanceState =>
widget.controller.choreographer.assistanceState;
AnimationController? _controller;
StreamSubscription? choreoListener;
AssistanceState? prevState;
@override
void initState() {
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 1),
);
choreoListener = widget.controller.choreographer.stateListener.stream
.listen(updateSpinnerState);
super.initState();
}
void updateSpinnerState(_) {
if (prevState != AssistanceState.fetching &&
assistanceState == AssistanceState.fetching) {
_controller?.repeat();
} else if (prevState == AssistanceState.fetching &&
assistanceState != AssistanceState.fetching) {
_controller?.stop();
_controller?.reverse();
}
setState(() => prevState = assistanceState);
}
@override
Widget build(BuildContext context) {
if (widget.controller.choreographer.isAutoIGCEnabled) {
return const SizedBox.shrink();
}
final Widget icon = Icon(
Icons.autorenew_rounded,
size: 46,
color: assistanceState.stateColor,
);
return SizedBox(
height: 50,
width: 50,
child: FloatingActionButton(
tooltip: assistanceState.tooltip(
L10n.of(context)!,
),
backgroundColor: Colors.white,
disabledElevation: 0,
shape: const CircleBorder(),
onPressed: () {
if (assistanceState != AssistanceState.complete) {
widget.controller.choreographer.getLanguageHelp(
false,
true,
);
}
},
child: Stack(
alignment: Alignment.center,
children: [
_controller != null
? RotationTransition(
turns: Tween(begin: 0.0, end: math.pi * 2)
.animate(_controller!),
child: icon,
)
: icon,
Container(
width: 26,
height: 26,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Colors.white,
),
),
Container(
width: 20,
height: 20,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: assistanceState.stateColor,
),
),
const Icon(
size: 16,
Icons.check,
color: Colors.white,
),
],
),
),
);
}
}
extension AssistanceStateExtension on AssistanceState {
Color get stateColor {
switch (this) {
case AssistanceState.noMessage:
case AssistanceState.notFetched:
case AssistanceState.fetching:
return AppConfig.primaryColor;
case AssistanceState.fetched:
return PangeaColors.igcError;
case AssistanceState.complete:
return AppConfig.success;
}
}
String tooltip(L10n l10n) {
switch (this) {
case AssistanceState.noMessage:
case AssistanceState.notFetched:
return l10n.runGrammarCorrection;
case AssistanceState.fetching:
return "";
case AssistanceState.fetched:
return l10n.grammarCorrectionFailed;
case AssistanceState.complete:
return l10n.grammarCorrectionComplete;
}
}
}

@ -54,6 +54,7 @@ class ModelKey {
static const String offset = "offset";
static const String length = "length";
static const String langCode = 'lang_code';
static const String confidence = 'confidence';
// some old analytics rooms have langCode instead of lang_code in the room creation content
static const String oldLangCode = 'langCode';
static const String wordLang = "word_lang";

@ -27,15 +27,16 @@ class ClassController extends BaseController {
_pangeaController = pangeaController;
}
setActiveSpaceIdInChatListController(String classId) {
setActiveSpaceIdInChatListController(String? classId) {
setState(data: {"activeSpaceId": classId});
}
Future<void> fixClassPowerLevels() async {
try {
final List<Future<void>> classFixes = [];
for (final room in (await _pangeaController
.matrixState.client.classesAndExchangesImTeaching)) {
final teacherSpaces = await _pangeaController
.matrixState.client.classesAndExchangesImTeaching;
for (final room in teacherSpaces) {
classFixes.add(room.setClassPowerLevels());
}
await Future.wait(classFixes);

@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:fluffychat/pangea/config/environment.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/models/language_detection_model.dart';
import 'package:fluffychat/pangea/network/urls.dart';
import 'package:http/http.dart' as http;
@ -48,7 +49,7 @@ class LanguageDetectionRequest {
}
class LanguageDetectionResponse {
List<Map<String, dynamic>> detections;
List<LanguageDetection> detections;
String fullText;
LanguageDetectionResponse({
@ -58,7 +59,11 @@ class LanguageDetectionResponse {
factory LanguageDetectionResponse.fromJson(Map<String, dynamic> json) {
return LanguageDetectionResponse(
detections: List<Map<String, dynamic>>.from(json['detections']),
detections: List<LanguageDetection>.from(
json['detections'].map(
(e) => LanguageDetection.fromJson(e),
),
),
fullText: json['full_text'],
);
}
@ -69,6 +74,20 @@ class LanguageDetectionResponse {
'full_text': fullText,
};
}
LanguageDetection? get _bestDetection {
detections.sort((a, b) => b.confidence.compareTo(a.confidence));
return detections.isNotEmpty ? detections.first : null;
}
final double _confidenceThreshold = 0.95;
LanguageDetection? bestDetection({double? threshold}) {
threshold ??= _confidenceThreshold;
return (_bestDetection?.confidence ?? 0) >= _confidenceThreshold
? _bestDetection!
: null;
}
}
class _LanguageDetectionCacheItem {
@ -103,6 +122,19 @@ class LanguageDetectionController {
_cacheClearTimer?.cancel();
}
Future<LanguageDetectionResponse> detectLanguage(
String fullText,
String? userL2,
String? userL1,
) async {
final LanguageDetectionRequest params = LanguageDetectionRequest(
fullText: fullText,
userL1: userL1,
userL2: userL2,
);
return get(params);
}
Future<LanguageDetectionResponse> get(
LanguageDetectionRequest params,
) async {

@ -8,8 +8,14 @@ class LocalSettings {
_pangeaController = pangeaController;
}
bool userLanguageToolSetting(ToolSetting setting) =>
_pangeaController.pStoreService.read(setting.toString()) ?? true;
bool userLanguageToolSetting(ToolSetting setting) {
final profileSetting =
_pangeaController.pStoreService.read(setting.toString());
if (profileSetting != null) {
return profileSetting;
}
return setting == ToolSetting.immersionMode ? false : true;
}
// bool get userEnableIT =>
// _pangeaController.pStoreService.read(ToolSetting.interactiveTranslator.toString()) ?? true;

@ -131,7 +131,7 @@ class UserController extends BaseController {
final bool? immersionMode =
migratedProfileInfo(MatrixProfile.immersionMode);
final bool? definitions = migratedProfileInfo(MatrixProfile.definitions);
final bool? translations = migratedProfileInfo(MatrixProfile.translations);
// final bool? translations = migratedProfileInfo(MatrixProfile.translations);
final bool? showItInstructions =
migratedProfileInfo(MatrixProfile.showedItInstructions);
final bool? showClickMessage =
@ -147,7 +147,7 @@ class UserController extends BaseController {
interactiveGrammar: interactiveGrammar,
immersionMode: immersionMode,
definitions: definitions,
translations: translations,
// translations: translations,
showedItInstructions: showItInstructions,
showedClickMessage: showClickMessage,
showedBlurMeansTranslate: showBlurMeansTranslate,
@ -228,10 +228,11 @@ class UserController extends BaseController {
bool? interactiveGrammar,
bool? immersionMode,
bool? definitions,
bool? translations,
// bool? translations,
bool? showedItInstructions,
bool? showedClickMessage,
bool? showedBlurMeansTranslate,
bool? showedTooltipInstructions,
String? createdAt,
String? targetLanguage,
String? sourceLanguage,
@ -280,12 +281,12 @@ class UserController extends BaseController {
definitions,
);
}
if (translations != null) {
await _pangeaController.pStoreService.save(
MatrixProfile.translations.title,
translations,
);
}
// if (translations != null) {
// await _pangeaController.pStoreService.save(
// MatrixProfile.translations.title,
// translations,
// );
// }
if (showedItInstructions != null) {
await _pangeaController.pStoreService.save(
MatrixProfile.showedItInstructions.title,
@ -304,6 +305,12 @@ class UserController extends BaseController {
showedBlurMeansTranslate,
);
}
if (showedTooltipInstructions != null) {
await _pangeaController.pStoreService.save(
MatrixProfile.showedTooltipInstructions.title,
showedTooltipInstructions,
);
}
if (createdAt != null) {
await _pangeaController.pStoreService.save(
MatrixProfile.createdAt.title,

@ -1,6 +1,102 @@
part of "pangea_room_extension.dart";
extension EventsRoomExtension on Room {
Future<void> _archive() async {
final students = (await requestParticipants())
.where(
(e) =>
e.id != client.userID &&
e.powerLevel < ClassDefaultValues.powerLevelOfAdmin &&
e.id != BotName.byEnvironment,
)
.toList();
try {
for (final student in students) {
await kick(student.id);
}
if (!isSpace && membership == Membership.join && isUnread) {
await markUnread(false);
}
await leave();
} catch (err, s) {
debugger(when: kDebugMode);
ErrorHandler.logError(e: err, s: s, data: toJson());
}
}
Future<bool> _archiveSpace(
BuildContext context,
Client client, {
bool onlyAdmin = false,
}) async {
final confirmed = await showOkCancelAlertDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context)!.areYouSure,
okLabel: L10n.of(context)!.yes,
cancelLabel: L10n.of(context)!.cancel,
message: onlyAdmin
? L10n.of(context)!.onlyAdminDescription
: L10n.of(context)!.archiveSpaceDescription,
) ==
OkCancelResult.ok;
if (!confirmed) return false;
final success = await showFutureLoadingDialog(
context: context,
future: () async {
final List<Room> children = await getChildRooms();
for (final Room child in children) {
await child.archive();
}
await archive();
},
);
MatrixState.pangeaController.classController
.setActiveSpaceIdInChatListController(
null,
);
return success.error == null;
}
Future<bool> _leaveSpace(BuildContext context, Client client) async {
final confirmed = await showOkCancelAlertDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context)!.areYouSure,
okLabel: L10n.of(context)!.yes,
cancelLabel: L10n.of(context)!.cancel,
message: L10n.of(context)!.leaveSpaceDescription,
) ==
OkCancelResult.ok;
if (!confirmed) return false;
final success = await showFutureLoadingDialog(
context: context,
future: () async {
try {
final List<Room> children = await getChildRooms();
for (final Room child in children) {
if (!child.isSpace &&
child.membership == Membership.join &&
child.isUnread) {
await child.markUnread(false);
}
await child.leave();
}
await leave();
} catch (err, stack) {
debugger(when: kDebugMode);
ErrorHandler.logError(e: err, s: stack, data: powerLevels);
rethrow;
}
},
);
MatrixState.pangeaController.classController
.setActiveSpaceIdInChatListController(
null,
);
return success.error == null;
}
Future<Event?> _sendPangeaEvent({
required Map<String, dynamic> content,
required String parentEventId,

@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:developer';
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:collection/collection.dart';
import 'package:fluffychat/pangea/constants/class_default_values.dart';
import 'package:fluffychat/pangea/constants/model_keys.dart';
@ -15,8 +16,11 @@ import 'package:fluffychat/pangea/models/class_model.dart';
import 'package:fluffychat/pangea/models/tokens_event_content_model.dart';
import 'package:fluffychat/pangea/utils/bot_name.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
// import markdown.dart
import 'package:html_unescape/html_unescape.dart';
import 'package:matrix/matrix.dart';
@ -137,6 +141,18 @@ extension PangeaRoom on Room {
// events
Future<void> archive() async => await _archive();
Future<bool> archiveSpace(
BuildContext context,
Client client, {
bool onlyAdmin = false,
}) async =>
await _archiveSpace(context, client, onlyAdmin: onlyAdmin);
Future<bool> leaveSpace(BuildContext context, Client client) async =>
await _leaveSpace(context, client);
Future<Event?> sendPangeaEvent({
required Map<String, dynamic> content,
required String parentEventId,
@ -229,14 +245,15 @@ extension PangeaRoom on Room {
BotOptionsModel? get botOptions => _botOptions;
Future<bool> suggestedInSpace(Room space) async =>
await _suggestedInSpace(space);
Future<void> setSuggested(bool suggested) async =>
await _setSuggested(suggested);
Future<void> setSuggestedInSpace(bool suggest, Room space) async =>
await _setSuggestedInSpace(suggest, space);
Future<bool> isSuggested() async => await _isSuggested();
// user_permissions
Future<bool> isOnlyAdmin() async => await _isOnlyAdmin();
bool isMadeByUser(String userId) => _isMadeByUser(userId);
bool get isSpaceAdmin => _isSpaceAdmin;

@ -55,7 +55,40 @@ extension RoomSettingsRoomExtension on Room {
);
}
Future<bool> _suggestedInSpace(Room space) async {
Future<bool> _isSuggested() async {
final List<Room> spaceParents = client.rooms
.where(
(room) =>
room.isSpace &&
room.spaceChildren.any(
(sc) => sc.roomId == id,
),
)
.toList();
for (final parent in spaceParents) {
final suggested = await _isSuggestedInSpace(parent);
if (!suggested) return false;
}
return true;
}
Future<void> _setSuggested(bool suggested) async {
final List<Room> spaceParents = client.rooms
.where(
(room) =>
room.isSpace &&
room.spaceChildren.any(
(sc) => sc.roomId == id,
),
)
.toList();
for (final parent in spaceParents) {
await _setSuggestedInSpace(suggested, parent);
}
}
Future<bool> _isSuggestedInSpace(Room space) async {
try {
final Map<String, dynamic> resp =
await client.getRoomStateWithKey(space.id, EventTypes.spaceChild, id);

@ -1,6 +1,32 @@
part of "pangea_room_extension.dart";
extension UserPermissionsRoomExtension on Room {
// If there are no other admins, and at least one non-admin, return true
Future<bool> _isOnlyAdmin() async {
if (!isRoomAdmin) {
return false;
}
final List<User> participants = await requestParticipants();
return ((participants
.where(
(e) =>
e.powerLevel == ClassDefaultValues.powerLevelOfAdmin &&
e.id != BotName.byEnvironment,
)
.toList()
.length) ==
1) &&
(participants
.where(
(e) =>
e.powerLevel < ClassDefaultValues.powerLevelOfAdmin &&
e.id != BotName.byEnvironment,
)
.toList())
.isNotEmpty;
}
bool _isMadeByUser(String userId) =>
getState(EventTypes.RoomCreate)?.senderId == userId;

@ -80,29 +80,6 @@ class PangeaMessageEvent {
return _latestEdit;
}
bool showRichText(
bool selected, {
bool highlighted = false,
bool isOverlay = false,
}) {
if (!_isValidPangeaMessageEvent) {
return false;
}
if ([EventStatus.error, EventStatus.sending].contains(_event.status)) {
return false;
}
if (isOverlay) return true;
// if ownMessage, don't show rich text if not selected or highlighted
// and don't show is the message is not an overlay
if (ownMessage && ((!selected && !highlighted) || !isOverlay)) {
return false;
}
return true;
}
Future<PangeaAudioFile> getMatrixAudioFile(
String langCode,
BuildContext context,

@ -1,13 +1,12 @@
import 'dart:developer';
import 'package:fluffychat/pangea/constants/model_keys.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/constants/model_keys.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import '../constants/class_default_values.dart';
import '../constants/language_keys.dart';
import '../constants/pangea_event_types.dart';
@ -124,6 +123,7 @@ class PangeaRoomRules {
int immersionMode;
int definitions;
int translations;
int autoIGC;
PangeaRoomRules({
this.isPublic = false,
@ -142,6 +142,7 @@ class PangeaRoomRules {
this.immersionMode = ClassDefaultValues.languageToolPermissions,
this.definitions = ClassDefaultValues.languageToolPermissions,
this.translations = ClassDefaultValues.languageToolPermissions,
this.autoIGC = ClassDefaultValues.languageToolPermissions,
});
updatePermission(String key, bool value) {
@ -198,8 +199,11 @@ class PangeaRoomRules {
case ToolSetting.definitions:
definitions = value;
break;
case ToolSetting.translations:
translations = value;
// case ToolSetting.translations:
// translations = value;
// break;
case ToolSetting.autoIGC:
autoIGC = value;
break;
default:
throw Exception('Invalid key for setting permissions - $setting');
@ -235,6 +239,7 @@ class PangeaRoomRules {
json['definitions'] ?? ClassDefaultValues.languageToolPermissions,
translations:
json['translations'] ?? ClassDefaultValues.languageToolPermissions,
autoIGC: json['auto_igc'] ?? ClassDefaultValues.languageToolPermissions,
);
Map<String, dynamic> toJson() {
@ -256,6 +261,7 @@ class PangeaRoomRules {
data['immersion_mode'] = immersionMode;
data['definitions'] = definitions;
data['translations'] = translations;
data['auto_igc'] = autoIGC;
return data;
}
@ -269,8 +275,10 @@ class PangeaRoomRules {
return immersionMode;
case ToolSetting.definitions:
return definitions;
case ToolSetting.translations:
return translations;
// case ToolSetting.translations:
// return translations;
case ToolSetting.autoIGC:
return autoIGC;
default:
throw Exception('Invalid key for setting permissions - $setting');
}
@ -298,7 +306,8 @@ enum ToolSetting {
interactiveGrammar,
immersionMode,
definitions,
translations,
// translations,
autoIGC,
}
extension SettingCopy on ToolSetting {
@ -312,8 +321,10 @@ extension SettingCopy on ToolSetting {
return L10n.of(context)!.toggleImmersionMode;
case ToolSetting.definitions:
return L10n.of(context)!.definitionsToolName;
case ToolSetting.translations:
return L10n.of(context)!.messageTranslationsToolName;
// case ToolSetting.translations:
// return L10n.of(context)!.messageTranslationsToolName;
case ToolSetting.autoIGC:
return L10n.of(context)!.autoIGCToolName;
}
}
@ -328,8 +339,10 @@ extension SettingCopy on ToolSetting {
return L10n.of(context)!.toggleImmersionModeDesc;
case ToolSetting.definitions:
return L10n.of(context)!.definitionsToolDescription;
case ToolSetting.translations:
return L10n.of(context)!.translationsToolDescrption;
// case ToolSetting.translations:
// return L10n.of(context)!.translationsToolDescrption;
case ToolSetting.autoIGC:
return L10n.of(context)!.autoIGCToolDescription;
}
}
}

@ -1,19 +1,23 @@
import 'package:fluffychat/pangea/constants/model_keys.dart';
class LanguageDetection {
String langCode;
double confidence;
LanguageDetection({
required this.langCode,
required this.confidence,
});
factory LanguageDetection.fromJson(Map<String, dynamic> json) {
return LanguageDetection(
langCode: json[_langCodeKey],
langCode: json[ModelKey.langCode],
confidence: json[ModelKey.confidence],
);
}
static const _langCodeKey = "lang_code";
Map<String, dynamic> toJson() => {
_langCodeKey: langCode,
ModelKey.langCode: langCode,
ModelKey.confidence: confidence,
};
}

@ -59,15 +59,17 @@ enum MatrixProfile {
interactiveGrammar,
immersionMode,
definitions,
translations,
// translations,
showedItInstructions,
showedClickMessage,
showedBlurMeansTranslate,
showedTooltipInstructions,
createdAt,
targetLanguage,
sourceLanguage,
country,
publicProfile,
autoIGC,
}
extension MatrixProfileExtension on MatrixProfile {
@ -87,14 +89,18 @@ extension MatrixProfileExtension on MatrixProfile {
return ToolSetting.immersionMode.toString();
case MatrixProfile.definitions:
return ToolSetting.definitions.toString();
case MatrixProfile.translations:
return ToolSetting.translations.toString();
// case MatrixProfile.translations:
// return ToolSetting.translations.toString();
case MatrixProfile.autoIGC:
return ToolSetting.autoIGC.toString();
case MatrixProfile.showedItInstructions:
return InstructionsEnum.itInstructions.toString();
case MatrixProfile.showedClickMessage:
return InstructionsEnum.clickMessage.toString();
case MatrixProfile.showedBlurMeansTranslate:
return InstructionsEnum.blurMeansTranslate.toString();
case MatrixProfile.showedTooltipInstructions:
return InstructionsEnum.tooltipInstructions.toString();
case MatrixProfile.createdAt:
return ModelKey.userCreatedAt;
case MatrixProfile.targetLanguage:

@ -1,13 +1,13 @@
import 'dart:convert';
import 'package:http/http.dart';
import 'package:fluffychat/pangea/config/environment.dart';
import 'package:fluffychat/pangea/models/language_detection_model.dart';
import 'package:fluffychat/pangea/models/lemma.dart';
import 'package:fluffychat/pangea/models/pangea_match_model.dart';
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/repo/span_data_repo.dart';
import 'package:http/http.dart';
import '../constants/model_keys.dart';
import '../models/igc_text_data_model.dart';
import '../network/requests.dart';
@ -39,7 +39,7 @@ class IgcRepo {
await Future.delayed(const Duration(seconds: 2));
final IGCTextData igcTextData = IGCTextData(
detections: [LanguageDetection(langCode: "en")],
detections: [LanguageDetection(langCode: "en", confidence: 0.99)],
tokens: [
PangeaToken(
text: PangeaTokenText(content: "This", offset: 0, length: 4),

@ -1,22 +0,0 @@
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:matrix/matrix.dart';
Future<void> archiveSpace(Room? space, Client client) async {
if (space == null) {
ErrorHandler.logError(
e: 'Tried to archive a space that is null. This should not happen.',
s: StackTrace.current,
);
return;
}
final List<Room> children = await space.getChildRooms();
for (final Room child in children) {
if (child.isUnread) {
await child.markUnread(false);
}
await child.leave();
}
await space.leave();
}

@ -1,3 +1,4 @@
import 'package:collection/collection.dart';
import 'package:fluffychat/pangea/constants/language_keys.dart';
import 'package:fluffychat/pangea/constants/model_keys.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
@ -10,6 +11,17 @@ import 'package:matrix/matrix.dart';
import '../../utils/matrix_sdk_extensions/matrix_locals.dart';
class GetChatListItemSubtitle {
final List<String> hideContentKeys = [
ModelKey.transcription,
];
bool moveBackInTimeline(Event event) =>
hideContentKeys.any(
(key) => event.content.tryGet(key) != null,
) ||
event.type.startsWith("p.") ||
event.type.startsWith("pangea.");
Future<String> getSubtitle(
L10n l10n,
Event? event,
@ -22,23 +34,14 @@ class GetChatListItemSubtitle {
eventContextId = null;
}
final Timeline timeline =
await event.room.getTimeline(eventContextId: eventContextId);
if (event.content.tryGet(ModelKey.transcription) != null) {
int index = timeline.events.indexWhere(
(e) => e.eventId == event!.eventId,
);
while (index < timeline.events.length &&
(timeline.events[index].content.tryGet(ModelKey.transcription) !=
null ||
timeline.events[index].type != EventTypes.Message)) {
index++;
}
final Timeline timeline = await event.room.getTimeline(
eventContextId: eventContextId,
);
if (timeline.events.length > index + 1) {
event = timeline.events[index];
if (moveBackInTimeline(event)) {
event = timeline.events.firstWhereOrNull((e) => !moveBackInTimeline(e));
if (event == null) {
return l10n.emptyChat;
}
}

@ -1,3 +1,4 @@
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
@ -102,6 +103,7 @@ enum InstructionsEnum {
itInstructions,
clickMessage,
blurMeansTranslate,
tooltipInstructions,
}
extension Copy on InstructionsEnum {
@ -113,6 +115,8 @@ extension Copy on InstructionsEnum {
return L10n.of(context)!.clickMessageTitle;
case InstructionsEnum.blurMeansTranslate:
return L10n.of(context)!.blurMeansTranslateTitle;
case InstructionsEnum.tooltipInstructions:
return L10n.of(context)!.tooltipInstructionsTitle;
}
}
@ -124,6 +128,10 @@ extension Copy on InstructionsEnum {
return L10n.of(context)!.clickMessageBody;
case InstructionsEnum.blurMeansTranslate:
return L10n.of(context)!.blurMeansTranslateBody;
case InstructionsEnum.tooltipInstructions:
return PlatformInfos.isMobile
? L10n.of(context)!.tooltipInstructionsMobileBody
: L10n.of(context)!.tooltipInstructionsBrowserBody;
}
}
}

@ -3,6 +3,7 @@ import 'dart:developer';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/models/speech_to_text_models.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/pangea/utils/instructions.dart';
import 'package:fluffychat/pangea/widgets/chat/toolbar_content_loading_indicator.dart';
import 'package:fluffychat/pangea/widgets/common/icon_number_widget.dart';
import 'package:fluffychat/pangea/widgets/igc/card_error_widget.dart';
@ -175,12 +176,24 @@ class MessageSpeechToTextCardState extends State<MessageSpeechToTextCard> {
number:
"${selectedToken?.confidence ?? speechToTextResponse!.transcript.confidence}%",
toolTip: L10n.of(context)!.accuracy,
onPressed: () => MatrixState.pangeaController.instructions.show(
context,
InstructionsEnum.tooltipInstructions,
widget.messageEvent.eventId,
true,
),
),
IconNumberWidget(
icon: Icons.speed,
number:
wordsPerMinuteString != null ? "$wordsPerMinuteString" : "??",
toolTip: L10n.of(context)!.wordsPerMinute,
onPressed: () => MatrixState.pangeaController.instructions.show(
context,
InstructionsEnum.tooltipInstructions,
widget.messageEvent.eventId,
true,
),
),
],
),

@ -33,9 +33,10 @@ class AddToSpaceToggles extends StatefulWidget {
class AddToSpaceState extends State<AddToSpaceToggles> {
late Room? room;
late List<SuggestionStatus> parents;
late List<Room> parents;
late List<Room> possibleParents;
late bool isOpen;
late bool isSuggested;
AddToSpaceState({Key? key});
@ -46,6 +47,9 @@ class AddToSpaceState extends State<AddToSpaceToggles> {
? Matrix.of(context).client.getRoomById(widget.roomId!)
: null;
isSuggested = true;
room?.isSuggested().then((value) => isSuggested = value);
possibleParents = Matrix.of(context)
.client
.rooms
@ -63,8 +67,6 @@ class AddToSpaceState extends State<AddToSpaceToggles> {
(r) =>
r.spaceChildren.any((room) => room.roomId == widget.roomId),
)
.map((r) => SuggestionStatus(false, r))
.cast<SuggestionStatus>()
.toList()
: [];
@ -72,7 +74,7 @@ class AddToSpaceState extends State<AddToSpaceToggles> {
final activeSpace =
Matrix.of(context).client.getRoomById(widget.activeSpaceId!);
if (activeSpace != null && activeSpace.canIAddSpaceChild(null)) {
parents.add(SuggestionStatus(true, activeSpace));
parents.add(activeSpace);
} else {
ErrorHandler.logError(
e: Exception('activeSpaceId ${widget.activeSpaceId} not found'),
@ -84,10 +86,9 @@ class AddToSpaceState extends State<AddToSpaceToggles> {
//if possibleParent in parents, put first
//use sort but use any instead of contains because contains uses == and we want to compare by id
possibleParents.sort((a, b) {
if (parents.any((suggestionStatus) => suggestionStatus.room.id == a.id)) {
if (parents.any((parent) => parent.id == a.id)) {
return -1;
} else if (parents
.any((suggestionStatus) => suggestionStatus.room.id == b.id)) {
} else if (parents.any((parent) => parent.id == b.id)) {
return 1;
} else {
return a.name.compareTo(b.name);
@ -95,35 +96,21 @@ class AddToSpaceState extends State<AddToSpaceToggles> {
});
isOpen = widget.startOpen;
initSuggestedParents();
super.initState();
}
Future<void> initSuggestedParents() async {
if (room != null) {
for (var i = 0; i < parents.length; i++) {
final parent = parents[i];
final bool suggested =
await room?.suggestedInSpace(parent.room) ?? false;
parents[i].suggested = suggested;
}
setState(() {});
}
}
Future<void> _addSingleSpace(String roomToAddId, Room newParent) async {
GoogleAnalytics.addParent(roomToAddId, newParent.classCode);
await newParent.setSpaceChild(
roomToAddId,
suggested: isSuggestedInSpace(newParent),
suggested: isSuggested,
);
await setSuggested(true, newParent);
}
Future<void> addSpaces(String roomToAddId) async {
final List<Future<void>> addFutures = [];
for (final SuggestionStatus newParent in parents) {
addFutures.add(_addSingleSpace(roomToAddId, newParent.room));
for (final Room parent in parents) {
addFutures.add(_addSingleSpace(roomToAddId, parent));
}
await addFutures.wait;
}
@ -148,39 +135,18 @@ class AddToSpaceState extends State<AddToSpaceToggles> {
setState(
() => add
? parents.add(SuggestionStatus(true, possibleParent))
? parents.add(possibleParent)
: parents.removeWhere(
(suggestionStatus) =>
suggestionStatus.room.id == possibleParent.id,
(parent) => parent.id == possibleParent.id,
),
);
}
Future<void> setSuggested(bool suggest, Room possibleParent) async {
if (room != null) {
await showFutureLoadingDialog(
context: context,
future: () => room!.setSuggestedInSpace(suggest, possibleParent),
);
}
for (final SuggestionStatus suggestionStatus in parents) {
if (suggestionStatus.room.id == possibleParent.id) {
suggestionStatus.suggested = suggest;
}
}
setState(() {});
}
bool isSuggestedInSpace(Room parent) =>
parents.firstWhereOrNull((r) => r.room.id == parent.id)?.suggested ??
false;
Widget getAddToSpaceToggleItem(int index) {
final Room possibleParent = possibleParents[index];
final String possibleParentName = possibleParent.getLocalizedDisplayname();
final bool canAdd = possibleParent.canIAddSpaceChild(room);
final bool canAdd = !(!possibleParent.isRoomAdmin &&
widget.mode == AddToClassMode.exchange) &&
possibleParent.canIAddSpaceChild(room);
return Opacity(
opacity: canAdd ? 1 : 0.5,
@ -189,7 +155,7 @@ class AddToSpaceState extends State<AddToSpaceToggles> {
SwitchListTile.adaptive(
title: possibleParent.nameAndRoomTypeIcon(),
activeColor: AppConfig.activeToggleColor,
value: parents.any((r) => r.room.id == possibleParent.id),
value: parents.any((r) => r.id == possibleParent.id),
onChanged: (bool add) => canAdd
? handleAdd(add, possibleParent)
: ScaffoldMessenger.of(context).showSnackBar(
@ -198,53 +164,6 @@ class AddToSpaceState extends State<AddToSpaceToggles> {
),
),
),
AnimatedSize(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
child: parents.any((r) => r.room.id == possibleParent.id)
? SwitchListTile.adaptive(
title: Row(
children: [
const SizedBox(width: 32),
Expanded(
child: Text(
L10n.of(context)!.suggestTo(possibleParentName),
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
subtitle: Row(
children: [
const SizedBox(width: 32),
Expanded(
child: Text(
widget.mode == AddToClassMode.chat
? L10n.of(context)!
.suggestChatDesc(possibleParentName)
: L10n.of(context)!.suggestExchangeDesc(
possibleParentName,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
activeColor: AppConfig.activeToggleColor,
value: isSuggestedInSpace(possibleParent),
onChanged: (bool suggest) => canAdd
? setSuggested(suggest, possibleParent)
: ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.noPermission),
),
),
)
: Container(),
),
Divider(
height: 0.5,
color: Theme.of(context).colorScheme.secondary.withAlpha(25),
@ -254,6 +173,16 @@ class AddToSpaceState extends State<AddToSpaceToggles> {
);
}
Future<void> setSuggested(bool suggested) async {
setState(() => isSuggested = suggested);
if (room != null) {
await showFutureLoadingDialog(
context: context,
future: () async => await room?.setSuggested(suggested),
);
}
}
@override
Widget build(BuildContext context) {
final String title = widget.mode == AddToClassMode.exchange
@ -292,9 +221,28 @@ class AddToSpaceState extends State<AddToSpaceToggles> {
const Divider(height: 1),
possibleParents.isNotEmpty
? Column(
children: possibleParents
.mapIndexed((index, _) => getAddToSpaceToggleItem(index))
.toList(),
children: [
SwitchListTile.adaptive(
title: Text(L10n.of(context)!.suggestToChat),
secondary: Icon(
isSuggested
? Icons.visibility_outlined
: Icons.visibility_off_outlined,
),
subtitle: Text(L10n.of(context)!.suggestToChatDesc),
activeColor: AppConfig.activeToggleColor,
value: isSuggested,
onChanged: (bool add) => setSuggested(add),
),
Divider(
height: 0.5,
color:
Theme.of(context).colorScheme.secondary.withAlpha(25),
),
...possibleParents.mapIndexed(
(index, _) => getAddToSpaceToggleItem(index),
),
],
)
: Center(
child: Padding(
@ -312,10 +260,3 @@ class AddToSpaceState extends State<AddToSpaceToggles> {
);
}
}
class SuggestionStatus {
bool suggested;
final Room room;
SuggestionStatus(this.suggested, this.room);
}

@ -6,6 +6,7 @@ class IconNumberWidget extends StatelessWidget {
final Color? iconColor;
final double? iconSize;
final String? toolTip;
final VoidCallback onPressed;
const IconNumberWidget({
super.key,
@ -14,16 +15,20 @@ class IconNumberWidget extends StatelessWidget {
this.toolTip,
this.iconColor,
this.iconSize,
required this.onPressed,
});
Widget _content(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Icon(
icon,
color: iconColor ?? Theme.of(context).iconTheme.color,
size: iconSize ?? Theme.of(context).iconTheme.size,
IconButton(
icon: Icon(
icon,
color: iconColor ?? Theme.of(context).iconTheme.color,
size: iconSize ?? Theme.of(context).iconTheme.size,
),
onPressed: onPressed,
),
const SizedBox(width: 8),
Text(

@ -61,7 +61,7 @@ class SpanCardState extends State<SpanCard> {
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!];

@ -85,20 +85,31 @@ class ChatSettingsPopupMenuState extends State<ChatSettingsPopupMenu> {
),
// #Pangea
if (!widget.room.isArchived)
// Pangea#
PopupMenuItem<String>(
value: 'leave',
child: Row(
children: [
// #Pangea
// const Icon(Icons.delete_outlined),
const Icon(Icons.arrow_forward),
// Pangea#
const SizedBox(width: 12),
Text(L10n.of(context)!.leave),
],
if (widget.room.isRoomAdmin)
PopupMenuItem<String>(
value: 'archive',
child: Row(
children: [
const Icon(Icons.archive_outlined),
const SizedBox(width: 12),
Text(L10n.of(context)!.archive),
],
),
),
// Pangea#
PopupMenuItem<String>(
value: 'leave',
child: Row(
children: [
// #Pangea
// const Icon(Icons.delete_outlined),
const Icon(Icons.arrow_forward),
// Pangea#
const SizedBox(width: 12),
Text(L10n.of(context)!.leave),
],
),
),
// #Pangea
if (classSettings != null)
PopupMenuItem<String>(
@ -167,7 +178,8 @@ class ChatSettingsPopupMenuState extends State<ChatSettingsPopupMenu> {
PopupMenuButton(
onSelected: (String choice) async {
switch (choice) {
case 'leave':
// #Pangea
case 'archive':
final confirmed = await showOkCancelAlertDialog(
useRootNavigator: false,
context: context,
@ -179,7 +191,31 @@ class ChatSettingsPopupMenuState extends State<ChatSettingsPopupMenu> {
if (confirmed == OkCancelResult.ok) {
final success = await showFutureLoadingDialog(
context: context,
future: () => widget.room.leave(),
future: () => widget.room.archive(),
);
if (success.error == null) {
context.go('/rooms');
}
}
break;
// Pangea#
case 'leave':
final bool onlyAdmin = await widget.room.isOnlyAdmin();
final confirmed = await showOkCancelAlertDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context)!.areYouSure,
okLabel: L10n.of(context)!.ok,
cancelLabel: L10n.of(context)!.cancel,
message: onlyAdmin
? L10n.of(context)!.onlyAdminDescription
: L10n.of(context)!.leaveRoomDescription,
);
if (confirmed == OkCancelResult.ok) {
final success = await showFutureLoadingDialog(
context: context,
future: () =>
onlyAdmin ? widget.room.archive() : widget.room.leave(),
);
if (success.error == null) {
context.go('/rooms');

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save