Merge branch 'main' into 512-bump-matrix-sdk

This commit is contained in:
ggurdin 2024-08-07 10:54:44 -04:00 committed by GitHub
commit 500e54bed9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 654 additions and 469 deletions

View file

@ -3879,7 +3879,7 @@
"define": "Define",
"listen": "Listen",
"addConversationBot": "Enable Conversation Bot",
"addConversationBotDesc": "Add a bot to this group chat that will ask questions on a specific topic",
"addConversationBotDesc": "Add a bot to this group chat",
"convoBotSettingsTitle": "Conversation Bot Settings",
"convoBotSettingsDescription": "Edit conversation topic and difficulty",
"enterAConversationTopic": "Enter a conversation topic",
@ -4004,12 +4004,15 @@
"conversationBotCustomZone_customSystemPromptLabel": "System prompt",
"conversationBotCustomZone_customSystemPromptPlaceholder": "Set custom system prompt",
"conversationBotCustomZone_customTriggerReactionEnabledLabel": "Responds on ⏩ reaction",
"botConfig": "Conversation Bot Settings",
"addConversationBotDialogTitleInvite": "Confirm inviting conversation bot",
"addConversationBotButtonInvite": "Invite",
"addConversationBotDialogInviteConfirmation": "Invite",
"addConversationBotButtonTitleRemove": "Confirm removing conversation bot",
"addConversationBotButtonRemove": "Remove",
"addConversationBotDialogRemoveConfirmation": "Remove",
"conversationBotConfigConfirmChange": "Confirm",
"conversationBotStatus": "Bot Status",
"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.",

View file

@ -328,7 +328,6 @@ class ChatController extends State<ChatPageWithRoom>
);
}
await Matrix.of(context).client.roomsLoading;
choreographer.setRoomId(roomId);
});
// Pangea#
_tryLoadTimeline();
@ -486,12 +485,6 @@ class ChatController extends State<ChatPageWithRoom>
final timeline = this.timeline;
if (timeline == null || timeline.events.isEmpty) {
// #Pangea
ErrorHandler.logError(
e: PangeaWarningError("Timeline is null or empty"),
s: StackTrace.current,
);
// Pangea#
return;
}
@ -1114,8 +1107,14 @@ class ChatController extends State<ChatPageWithRoom>
inputFocus.requestFocus();
}
void scrollToEventId(String eventId) async {
final eventIndex = timeline!.events.indexWhere((e) => e.eventId == eventId);
void scrollToEventId(
String eventId, {
bool highlightEvent = true,
}) async {
final eventIndex = timeline!.events
.where((event) => event.isVisibleInGui)
.toList()
.indexWhere((e) => e.eventId == eventId);
if (eventIndex == -1) {
setState(() {
timeline = null;
@ -1131,11 +1130,14 @@ class ChatController extends State<ChatPageWithRoom>
});
return;
}
setState(() {
scrollToEventIdMarker = eventId;
});
if (highlightEvent) {
setState(() {
scrollToEventIdMarker = eventId;
});
}
await scrollController.scrollToIndex(
eventIndex,
eventIndex + 1,
duration: FluffyThemes.animationDuration,
preferPosition: AutoScrollPosition.middle,
);
_updateScrollController();

View file

@ -14,7 +14,6 @@ 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/widgets/chat/chat_floating_action_button.dart';
import 'package:fluffychat/utils/account_config.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/chat_settings_popup_menu.dart';
import 'package:fluffychat/widgets/connection_status_header.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -299,216 +298,232 @@ class ChatView extends StatelessWidget {
),
),
SafeArea(
child: Column(
children: <Widget>[
Expanded(
child: GestureDetector(
onTap: controller.clearSingleSelectedEvent,
child: Builder(
builder: (context) {
if (controller.timeline == null) {
return const Center(
child: CircularProgressIndicator.adaptive(
strokeWidth: 2,
),
);
}
return ChatEventList(
controller: controller,
);
},
),
),
),
if (controller.room.canSendDefaultMessages &&
controller.room.membership == Membership.join)
Container(
margin: EdgeInsets.only(
bottom: bottomSheetPadding,
left: bottomSheetPadding,
right: bottomSheetPadding,
),
constraints: const BoxConstraints(
maxWidth: FluffyThemes.columnWidth * 2.5,
),
alignment: Alignment.center,
child: Material(
clipBehavior: Clip.hardEdge,
color: Theme.of(context)
.colorScheme
// ignore: deprecated_member_use
.surfaceVariant,
borderRadius: const BorderRadius.all(
Radius.circular(24),
),
child: controller.room.isAbandonedDMRoom == true
? Row(
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(
16,
),
foregroundColor: Theme.of(context)
.colorScheme
.error,
),
icon: const Icon(
// #Pangea
// Icons.archive_outlined,
Icons.arrow_forward,
// Pangea#
),
onPressed: controller.leaveChat,
label: Text(
L10n.of(context)!.leave,
),
),
TextButton.icon(
style: TextButton.styleFrom(
padding: const EdgeInsets.all(
16,
),
),
icon: const Icon(
Icons.forum_outlined,
),
onPressed: controller.recreateChat,
label: Text(
L10n.of(context)!.reopenChat,
),
),
],
)
:
// #Pangea
null,
// Column(
// mainAxisSize: MainAxisSize.min,
// children: [
// const ConnectionStatusHeader(),
// ITBar(
// choreographer:
// controller.choreographer,
// ),
// ReactionsPicker(controller),
// ReplyDisplay(controller),
// ChatInputRow(controller),
// ChatEmojiPicker(controller),
// ],
// ),
// Pangea#
),
),
child:
// #Pangea
// Keep messages above minimum input bar height
SizedBox(
height: (PlatformInfos.isMobile ? 30 : 60),
Stack(
children: [
// Pangea#
Column(
children: <Widget>[
Expanded(
child: GestureDetector(
onTap: controller.clearSingleSelectedEvent,
child: Builder(
builder: (context) {
if (controller.timeline == null) {
return const Center(
child:
CircularProgressIndicator.adaptive(
strokeWidth: 2,
),
);
}
return ChatEventList(
controller: controller,
);
},
),
),
),
if (controller.room.canSendDefaultMessages &&
controller.room.membership == Membership.join)
Container(
margin: EdgeInsets.only(
bottom: bottomSheetPadding,
left: bottomSheetPadding,
right: bottomSheetPadding,
),
constraints: const BoxConstraints(
maxWidth: FluffyThemes.columnWidth * 2.5,
),
alignment: Alignment.center,
child: Material(
clipBehavior: Clip.hardEdge,
color: Theme.of(context)
.colorScheme
// ignore: deprecated_member_use
.surfaceVariant,
borderRadius: const BorderRadius.all(
Radius.circular(24),
),
child: controller.room.isAbandonedDMRoom ==
true
? Row(
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(
16,
),
foregroundColor:
Theme.of(context)
.colorScheme
.error,
),
icon: const Icon(
// #Pangea
// Icons.archive_outlined,
Icons.arrow_forward,
// Pangea#
),
onPressed: controller.leaveChat,
label: Text(
L10n.of(context)!.leave,
),
),
TextButton.icon(
style: TextButton.styleFrom(
padding: const EdgeInsets.all(
16,
),
),
icon: const Icon(
Icons.forum_outlined,
),
onPressed:
controller.recreateChat,
label: Text(
L10n.of(context)!.reopenChat,
),
),
],
)
:
// #Pangea
null,
// Column(
// mainAxisSize: MainAxisSize.min,
// children: [
// const ConnectionStatusHeader(),
// ITBar(
// choreographer:
// controller.choreographer,
// ),
// ReactionsPicker(controller),
// ReplyDisplay(controller),
// ChatInputRow(controller),
// ChatEmojiPicker(controller),
// ],
// ),
// Pangea#
),
),
// #Pangea
// Keep messages above minimum input bar height
const SizedBox(
height: 60,
),
// Pangea#
],
),
// #Pangea
Positioned(
left: 0,
right: 0,
bottom: 16,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (!controller.selectMode)
Container(
margin: EdgeInsets.only(
bottom: 10,
left: bottomSheetPadding,
right: bottomSheetPadding,
),
constraints: const BoxConstraints(
maxWidth: FluffyThemes.columnWidth * 2.4,
),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
StartIGCButton(
controller: controller,
),
ChatFloatingActionButton(
controller: controller,
),
],
),
),
Container(
margin: EdgeInsets.only(
bottom: bottomSheetPadding,
left: bottomSheetPadding,
right: bottomSheetPadding,
),
constraints: const BoxConstraints(
maxWidth: FluffyThemes.columnWidth * 2.5,
),
alignment: Alignment.center,
child: Material(
clipBehavior: Clip.hardEdge,
color: Theme.of(context)
.colorScheme
.surfaceContainerHighest,
borderRadius: const BorderRadius.all(
Radius.circular(24),
),
child: Column(
children: [
const ConnectionStatusHeader(),
ITBar(
choreographer: controller.choreographer,
),
ReactionsPicker(controller),
ReplyDisplay(controller),
ChatInputRow(controller),
ChatEmojiPicker(controller),
],
),
),
),
],
),
),
// Pangea#
],
),
// #Pangea
// if (controller.dragging)
// Container(
// color: Theme.of(context)
// .scaffoldBackgroundColor
// .withOpacity(0.9),
// alignment: Alignment.center,
// child: const Icon(
// Icons.upload_outlined,
// size: 100,
// ),
// ),
// Pangea#
),
// #Pangea
// if (controller.dragging)
// Container(
// color: Theme.of(context)
// .scaffoldBackgroundColor
// .withOpacity(0.9),
// alignment: Alignment.center,
// child: const Icon(
// Icons.upload_outlined,
// size: 100,
// ),
// ),
Positioned(
left: 0,
right: 0,
bottom: 16,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (!controller.selectMode)
Container(
margin: EdgeInsets.only(
bottom: 10,
left: bottomSheetPadding,
right: bottomSheetPadding,
),
constraints: const BoxConstraints(
maxWidth: FluffyThemes.columnWidth * 2.4,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
StartIGCButton(
controller: controller,
),
ChatFloatingActionButton(
controller: controller,
),
],
),
),
Container(
margin: EdgeInsets.only(
bottom: bottomSheetPadding,
left: bottomSheetPadding,
right: bottomSheetPadding,
),
constraints: const BoxConstraints(
maxWidth: FluffyThemes.columnWidth * 2.5,
),
alignment: Alignment.center,
child: Material(
clipBehavior: Clip.hardEdge,
color: Theme.of(context)
.colorScheme
.surfaceContainerHighest,
borderRadius: const BorderRadius.all(
Radius.circular(24),
),
child: Column(
children: [
const ConnectionStatusHeader(),
ITBar(
choreographer: controller.choreographer,
),
ReactionsPicker(controller),
ReplyDisplay(controller),
ChatInputRow(controller),
ChatEmojiPicker(controller),
],
),
),
),
],
),
),
// Pangea#
],
),
);

View file

@ -463,7 +463,7 @@ class InputBar extends StatelessWidget {
debounceDuration: const Duration(milliseconds: 50),
// show suggestions after 50ms idle time (default is 300)
// #Pangea
key: controller!.choreographer.inputLayerLinkAndKey.key,
key: controller?.choreographer.inputLayerLinkAndKey.key,
// builder: (context, controller, focusNode) => TextField(
builder: (context, _, focusNode) => TextField(
// Pangea#
@ -504,11 +504,11 @@ class InputBar extends StatelessWidget {
onSubmitted!(text);
},
// #Pangea
style: controller?.isMaxLength ?? false
style: controller?.exceededMaxLength ?? false
? const TextStyle(color: Colors.red)
: null,
onTap: () {
controller!.onInputTap(
controller?.onInputTap(
context,
fNode: focusNode,
);

View file

@ -6,7 +6,6 @@ import 'package:fluffychat/pages/settings/settings.dart';
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_description_button.dart';
import 'package:fluffychat/pangea/utils/set_class_name.dart';
import 'package:fluffychat/pangea/widgets/class/add_space_toggles.dart';
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_settings.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/app_lock.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -43,8 +42,9 @@ class ChatDetailsController extends State<ChatDetails> {
// #Pangea
final GlobalKey<AddToSpaceState> addToSpaceKey = GlobalKey<AddToSpaceState>();
final GlobalKey<ConversationBotSettingsState> addConversationBotKey =
GlobalKey<ConversationBotSettingsState>();
final GlobalKey<ChatDetailsController>
addConversationBotKey =
GlobalKey<ChatDetailsController>();
bool displayAddStudentOptions = false;
void toggleAddStudentOptions() =>

View file

@ -138,17 +138,20 @@ class ChatDetailsView extends StatelessWidget {
Avatar.defaultSize * 2.5,
),
),
child: Hero(
tag: controller.widget
.embeddedCloseButton !=
null
? 'embedded_content_banner'
: 'content_banner',
child: Avatar(
mxContent: room.avatar,
name: displayname,
size: Avatar.defaultSize * 2.5,
),
// #Pangea
// Hero animation is causing weird visual glitch
// Probably not worth keeping
// child: Hero(
// tag: controller.widget
// .embeddedCloseButton !=
// null
// ? 'embedded_content_banner'
// : 'content_banner',
// Pangea#
child: Avatar(
mxContent: room.avatar,
name: displayname,
size: Avatar.defaultSize * 2.5,
),
),
if (!room.isDirectChat &&

View file

@ -863,7 +863,7 @@ class ChatListController extends State<ChatList>
if (space.canSendDefaultStates) {
for (final roomId in selectedRoomIds) {
await space.pangeaSetSpaceChild(roomId);
await space.pangeaSetSpaceChild(roomId, suggested: true);
}
}
// Pangea#
@ -911,11 +911,12 @@ class ChatListController extends State<ChatList>
await client.roomsLoading;
await client.accountDataLoading;
await client.userDeviceKeysLoading;
if (client.prevBatch == null) {
// #Pangea
// See here for explanation of this change: https://github.com/pangeachat/client/pull/539
// if (client.prevBatch == null) {
if (client.onSync.value?.nextBatch == null) {
// Pangea#
await client.onSync.stream.first;
// #Pangea
pangeaController.startChatWithBotIfNotPresent();
//Pangea#
// Display first login bootstrap if enabled
// #Pangea
@ -930,9 +931,19 @@ class ChatListController extends State<ChatList>
}
// #Pangea
await _initPangeaControllers(client);
// Pangea#
if (!mounted) return;
setState(() {
waitForFirstSync = true;
});
}
// #Pangea
Future<void> _initPangeaControllers(Client client) async {
if (mounted) {
// TODO try not to await so much
GoogleAnalytics.analyticsUserUpdate(client.userID);
pangeaController.startChatWithBotIfNotPresent();
await pangeaController.subscriptionController.initialize();
await pangeaController.myAnalytics.initialize();
pangeaController.afterSyncAndFirstLoginInitialization(context);
@ -943,14 +954,9 @@ class ChatListController extends State<ChatList>
ErrorHandler.logError(
m: "didn't run afterSyncAndFirstLoginInitialization because not mounted",
);
// debugger(when: kDebugMode);
}
// Pangea#
if (!mounted) return;
setState(() {
waitForFirstSync = true;
});
}
// Pangea#
void cancelAction() {
if (selectMode == SelectMode.share) {

View file

@ -579,7 +579,12 @@ class _SpaceViewState extends State<SpaceView> {
: null,
);
}
await activeSpace.setSpaceChild(roomId);
await activeSpace.setSpaceChild(
roomId,
// #Pangea
suggested: true,
// Pangea#
);
},
);
if (result.error != null) return;

View file

@ -370,15 +370,17 @@ class UserBottomSheetView extends StatelessWidget {
onTap: () => controller
.participantAction(UserBottomSheetAction.unban),
),
if (user != null && user.id != client.userID)
ListTile(
textColor: Theme.of(context).colorScheme.onErrorContainer,
iconColor: Theme.of(context).colorScheme.onErrorContainer,
title: Text(L10n.of(context)!.reportUser),
leading: const Icon(Icons.report_outlined),
onTap: () => controller
.participantAction(UserBottomSheetAction.report),
),
// #Pangea
// if (user != null && user.id != client.userID)
// ListTile(
// textColor: Theme.of(context).colorScheme.onErrorContainer,
// iconColor: Theme.of(context).colorScheme.onErrorContainer,
// title: Text(L10n.of(context)!.reportUser),
// leading: const Icon(Icons.report_outlined),
// onTap: () => controller
// .participantAction(UserBottomSheetAction.report),
// ),
// Pangea#
if (profileSearchError != null)
ListTile(
leading: const Icon(

View file

@ -42,7 +42,6 @@ class Choreographer {
bool isFetching = false;
Timer? debounceTimer;
String? _roomId;
ChoreoRecord choreoRecord = ChoreoRecord.newRecord;
// last checked by IGC or translation
String? _lastChecked;
@ -464,10 +463,7 @@ class Choreographer {
setState();
}
get roomId => _roomId;
void setRoomId(String? roomId) {
_roomId = roomId ?? '';
}
get roomId => chatController.roomId;
bool get _useCustomInput => [
EditType.keyboard,

View file

@ -233,11 +233,11 @@ class MyAnalyticsController {
if (userL2 == null || _client.userID == null) return;
// analytics room for the user and current target language
final Room analyticsRoom = await _client.getMyAnalyticsRoom(userL2!);
final Room? analyticsRoom = await _client.getMyAnalyticsRoom(userL2!);
// get the last time analytics were updated for this room
final DateTime? l2AnalyticsLastUpdated =
await analyticsRoom.analyticsLastUpdated(
await analyticsRoom?.analyticsLastUpdated(
PangeaEventTypes.summaryAnalytics,
_client.userID!,
);
@ -311,7 +311,7 @@ class MyAnalyticsController {
// if there's new content to be sent, or if lastUpdated hasn't been
// set yet for this room, send the analytics events
if (summaryContent.isNotEmpty || l2AnalyticsLastUpdated == null) {
await analyticsRoom.sendSummaryAnalyticsEvent(
await analyticsRoom?.sendSummaryAnalyticsEvent(
summaryContent,
);
}
@ -351,7 +351,7 @@ class MyAnalyticsController {
// );
if (recentConstructUses.isNotEmpty) {
await analyticsRoom.sendConstructsEvent(
await analyticsRoom?.sendConstructsEvent(
recentConstructUses,
);
}

View file

@ -83,6 +83,15 @@ class UserController extends BaseController {
createdAt: DateTime.now(),
);
final newProfile = Profile(userSettings: userSettings);
// we don't use the pangea profile anymore, but we still need
// it to get access token for the choreographer, so create one
await PUserRepo.repoCreatePangeaUser(
userID: userId!,
dob: dob.toIso8601String(),
fullName: fullname!,
matrixAccessToken: _matrixAccessToken!,
);
await newProfile.saveProfileData(waitForDataInSync: true);
}
@ -155,13 +164,25 @@ class UserController extends BaseController {
_pangeaController.pStoreService.read(PLocalKey.access);
if (localAccessToken == null || needNewJWT(localAccessToken)) {
final PangeaProfileResponse? userModel =
await PUserRepo.fetchPangeaUserInfo(
PangeaProfileResponse? userModel = await PUserRepo.fetchPangeaUserInfo(
userID: userId!,
matrixAccessToken: _matrixAccessToken!,
);
// Oops, some accounts were made without creating pangea profiles, so they
// don't have access to an access token yet. In that case, create a pangea profile.
if (userModel?.access == null) {
throw ("Trying to get accessToken with null userModel");
final dob = profile.userSettings.dateOfBirth;
if (dob != null) {
userModel = await PUserRepo.repoCreatePangeaUser(
userID: userId!,
dob: dob.toIso8601String(),
fullName: fullname!,
matrixAccessToken: _matrixAccessToken!,
);
if (userModel?.access == null) {
throw ("Trying to get accessToken with null userModel");
}
}
}
_pangeaController.pStoreService.save(
PLocalKey.access,

View file

@ -71,12 +71,11 @@ extension MessageModeExtension on MessageMode {
switch (this) {
case MessageMode.translation:
case MessageMode.textToSpeech:
case MessageMode.practiceActivity:
case MessageMode.definition:
return event.messageType == MessageTypes.Text;
case MessageMode.speechToText:
return event.messageType == MessageTypes.Audio;
default:
case MessageMode.practiceActivity:
return true;
}
}

View file

@ -3,7 +3,7 @@ part of "client_extension.dart";
extension AnalyticsClientExtension on Client {
/// Get the logged in user's analytics room matching
/// a given langCode. If not present, create it.
Future<Room> _getMyAnalyticsRoom(String langCode) async {
Future<Room?> _getMyAnalyticsRoom(String langCode) async {
final Room? analyticsRoom = _analyticsRoomLocal(langCode);
if (analyticsRoom != null) return analyticsRoom;
return _makeAnalyticsRoom(langCode);
@ -35,7 +35,11 @@ extension AnalyticsClientExtension on Client {
///
/// If the room does not appear immediately after creation, this method waits for it to appear in sync.
/// Returns the created [Room] object.
Future<Room> _makeAnalyticsRoom(String langCode) async {
Future<Room?> _makeAnalyticsRoom(String langCode) async {
if (userID == null || userID == BotName.byEnvironment) {
return null;
}
final String roomID = await createRoom(
creationContent: {
'type': PangeaRoomTypes.analytics,
@ -74,6 +78,7 @@ extension AnalyticsClientExtension on Client {
// migration function to change analytics rooms' vsibility to public
// so they will appear in the space hierarchy
Future<void> _updateAnalyticsRoomVisibility() async {
if (userID == null || userID == BotName.byEnvironment) return;
await Future.wait(
allMyAnalyticsRooms.map((room) async {
final visability = await getRoomVisibilityOnDirectory(room.id);
@ -91,6 +96,7 @@ extension AnalyticsClientExtension on Client {
/// so teachers can join them via space hierarchy.
/// Allows teachers to join analytics rooms without being invited.
void _addAnalyticsRoomsToAllSpaces() {
if (userID == null || userID == BotName.byEnvironment) return;
for (final Room room in allMyAnalyticsRooms) {
room.addAnalyticsRoomToSpaces();
}
@ -100,6 +106,7 @@ extension AnalyticsClientExtension on Client {
/// Handles case when students cannot add analytics room to space(s)
/// so teacher is still able to get analytics data for this student
void _inviteAllTeachersToAllAnalyticsRooms() {
if (userID == null || userID == BotName.byEnvironment) return;
for (final Room room in allMyAnalyticsRooms) {
room.inviteTeachersToAnalyticsRoom();
}

View file

@ -21,7 +21,7 @@ extension PangeaClient on Client {
/// Get the logged in user's analytics room matching
/// a given langCode. If not present, create it.
Future<Room> getMyAnalyticsRoom(String langCode) async =>
Future<Room?> getMyAnalyticsRoom(String langCode) async =>
await _getMyAnalyticsRoom(langCode);
/// Get local analytics room for a given langCode and

View file

@ -140,8 +140,6 @@ class IGCTextData {
matches.removeAt(matchIndex);
for (final match in matches) {
final matchOffset = match.match.offset;
final matchLength = match.match.length;
match.match.fullText = originalInput;
if (match.match.offset > pangeaMatch.match.offset) {
match.match.offset += replacement.length - pangeaMatch.match.length;
@ -305,7 +303,7 @@ class IGCTextData {
// create a pointer to the current index in the original input
// and iterate until the pointer has reached the end of the input
int currentIndex = 0;
while (currentIndex < originalInput.characters.length - 1) {
while (currentIndex < originalInput.characters.length) {
// check if the pointer is at a match, and if so, get the index of the match
final int matchIndex = matchRanges.indexWhere(
(range) => currentIndex >= range[0] && currentIndex < range[1],

View file

@ -1,4 +1,3 @@
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/models/space_model.dart';
import 'package:fluffychat/pangea/pages/settings_learning/settings_learning.dart';
import 'package:fluffychat/pangea/widgets/user_settings/country_picker_tile.dart';
@ -32,15 +31,15 @@ class SettingsLearningView extends StatelessWidget {
const SizedBox(height: 8),
const Divider(height: 1),
const SizedBox(height: 8),
if (controller.pangeaController.permissionsController.isUser18())
SwitchListTile.adaptive(
activeColor: AppConfig.activeToggleColor,
title: Text(L10n.of(context)!.publicProfileTitle),
subtitle: Text(L10n.of(context)!.publicProfileDesc),
value: controller.pangeaController.userController.isPublic,
onChanged: (bool isPublicProfile) =>
controller.setPublicProfile(isPublicProfile),
),
// if (controller.pangeaController.permissionsController.isUser18())
// SwitchListTile.adaptive(
// activeColor: AppConfig.activeToggleColor,
// title: Text(L10n.of(context)!.publicProfileTitle),
// subtitle: Text(L10n.of(context)!.publicProfileDesc),
// value: controller.pangeaController.userController.isPublic,
// onChanged: (bool isPublicProfile) =>
// controller.setPublicProfile(isPublicProfile),
// ),
ListTile(
subtitle: Text(L10n.of(context)!.toggleToolSettingsDescription),
),

View file

@ -2,6 +2,7 @@ import 'dart:convert';
import 'dart:developer';
import 'package:fluffychat/pangea/constants/model_keys.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:http/http.dart';
import '../models/user_model.dart';
@ -10,6 +11,34 @@ import '../network/requests.dart';
import '../network/urls.dart';
class PUserRepo {
static Future<PangeaProfileResponse?> repoCreatePangeaUser({
required String userID,
required String dob,
required fullName,
required String matrixAccessToken,
}) async {
try {
final Requests req = Requests(
baseUrl: PApiUrls.baseAPI,
matrixAccessToken: matrixAccessToken,
);
final Map<String, dynamic> body = {
ModelKey.userFullName: fullName,
ModelKey.userPangeaUserId: userID,
ModelKey.userDateOfBirth: dob,
};
final resp = await req.post(
url: PApiUrls.createUser,
body: body,
);
return PangeaProfileResponse.fromJson(jsonDecode(resp.body));
} catch (err, s) {
ErrorHandler.logError(e: err, s: s);
return null;
}
}
static Future<PangeaProfileResponse?> fetchPangeaUserInfo({
required String userID,
required String matrixAccessToken,

View file

@ -435,7 +435,11 @@ class MessageToolbarState extends State<MessageToolbar> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
Container(
constraints: const BoxConstraints(
minWidth: 300,
maxHeight: 228,
),
child: SingleChildScrollView(
child: AnimatedSize(
duration: FluffyThemes.animationDuration,

View file

@ -16,7 +16,6 @@ class ConversationBotCustomZone extends StatelessWidget {
@override
Widget build(BuildContext context) {
print(initialBotOptions.toJson());
return Column(
children: [
const SizedBox(height: 12),

View file

@ -1,22 +1,19 @@
import 'dart:developer';
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
import 'package:fluffychat/pangea/models/bot_options_model.dart';
import 'package:fluffychat/pangea/utils/bot_name.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/pangea/widgets/common/bot_face_svg.dart';
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart';
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_mode_select.dart';
import 'package:fluffychat/pangea/widgets/space/language_level_dropdown.dart';
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_settings_form.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 'package:matrix/matrix.dart';
import '../../../widgets/matrix.dart';
import '../../constants/pangea_event_types.dart';
import '../../extensions/pangea_room_extension/pangea_room_extension.dart';
import '../../utils/error_handler.dart';
class ConversationBotSettings extends StatefulWidget {
final Room? room;
final bool startOpen;
@ -36,6 +33,7 @@ class ConversationBotSettings extends StatefulWidget {
class ConversationBotSettingsState extends State<ConversationBotSettings> {
late BotOptionsModel botOptions;
late bool isOpen;
late bool isCreating;
bool addBot = false;
Room? parentSpace;
@ -56,6 +54,22 @@ class ConversationBotSettingsState extends State<ConversationBotSettings> {
parentSpace = widget.activeSpaceId != null
? Matrix.of(context).client.getRoomById(widget.activeSpaceId!)
: null;
isCreating = widget.room == null;
}
Future<void> setBotOption() async {
if (widget.room == null) return;
try {
await Matrix.of(context).client.setRoomStateWithKey(
widget.room!.id,
PangeaEventTypes.botOptions,
'',
botOptions.toJson(),
);
} catch (err, stack) {
debugger(when: kDebugMode);
ErrorHandler.logError(e: err, s: stack);
}
}
Future<void> updateBotOption(void Function() makeLocalChange) async {
@ -74,196 +88,191 @@ class ConversationBotSettingsState extends State<ConversationBotSettings> {
);
}
Future<void> setBotOption() async {
if (widget.room == null) return;
try {
await Matrix.of(context).client.setRoomStateWithKey(
widget.room!.id,
PangeaEventTypes.botOptions,
'',
botOptions.toJson(),
);
} catch (err, stack) {
debugger(when: kDebugMode);
ErrorHandler.logError(e: err, s: stack);
}
}
@override
Widget build(BuildContext context) => Column(
Widget build(BuildContext context) {
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
title: Text(
L10n.of(context)!.convoBotSettingsTitle,
isCreating
? L10n.of(context)!.addConversationBot
: L10n.of(context)!.botConfig,
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.bold,
),
),
subtitle: Text(L10n.of(context)!.convoBotSettingsDescription),
subtitle: isCreating
? Text(L10n.of(context)!.addConversationBotDesc)
: null,
leading: CircleAvatar(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
foregroundColor: Theme.of(context).textTheme.bodyLarge!.color,
child: const Icon(Icons.psychology_outlined),
child: const BotFace(
width: 30.0,
expression: BotExpression.idle,
),
),
trailing: Icon(
isOpen
? Icons.keyboard_arrow_down_outlined
: Icons.keyboard_arrow_right_outlined,
),
onTap: () => setState(() => isOpen = !isOpen),
),
if (isOpen)
AnimatedContainer(
duration: const Duration(milliseconds: 300),
height: isOpen ? null : 0,
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 16),
child: ListTile(
title: Text(
L10n.of(context)!.addConversationBot,
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.bold,
),
),
subtitle: Text(L10n.of(context)!.addConversationBotDesc),
leading: CircleAvatar(
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
foregroundColor:
Theme.of(context).textTheme.bodyLarge!.color,
child: const BotFace(
width: 30.0,
expression: BotExpression.idle,
),
),
trailing: ElevatedButton(
onPressed: () async {
final bool? confirm = await showDialog<bool>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: addBot
trailing: isCreating
? ElevatedButton(
onPressed: () async {
final bool? confirm = await showDialog<bool>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: addBot
? Text(
L10n.of(context)!
.addConversationBotButtonTitleRemove,
)
: Text(
L10n.of(context)!
.addConversationBotDialogTitleInvite,
),
actions: <Widget>[
TextButton(
onPressed: () {
Navigator.of(context).pop(false);
},
child: Text(L10n.of(context)!.cancel),
),
TextButton(
onPressed: () {
Navigator.of(context).pop(!addBot);
},
child: addBot
? Text(
L10n.of(context)!
.addConversationBotButtonTitleRemove,
.addConversationBotDialogRemoveConfirmation,
)
: Text(
L10n.of(context)!
.addConversationBotDialogTitleInvite,
.addConversationBotDialogInviteConfirmation,
),
actions: <Widget>[
TextButton(
onPressed: () {
Navigator.of(context).pop(false);
},
child: Text(L10n.of(context)!.cancel),
),
TextButton(
onPressed: () {
Navigator.of(context).pop(!addBot);
},
child: addBot
? Text(
L10n.of(context)!
.addConversationBotDialogRemoveConfirmation,
)
: Text(
L10n.of(context)!
.addConversationBotDialogInviteConfirmation,
),
),
],
);
},
);
if (confirm == true) {
setState(() => addBot = true);
widget.room?.invite(BotName.byEnvironment);
} else {
setState(() => addBot = false);
widget.room?.kick(BotName.byEnvironment);
}
},
child: addBot
? Text(
L10n.of(context)!
.addConversationBotButtonRemove,
)
: Text(
L10n.of(context)!
.addConversationBotButtonInvite,
),
),
),
),
if (addBot) ...[
Padding(
padding: const EdgeInsets.fromLTRB(32, 16, 0, 0),
child: Text(
L10n.of(context)!.conversationLanguageLevel,
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
Padding(
padding: const EdgeInsets.only(left: 16),
child: LanguageLevelDropdown(
initialLevel: botOptions.languageLevel,
onChanged: (int? newValue) => updateBotOption(() {
botOptions.languageLevel = newValue!;
}),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(32, 16, 0, 0),
child: Text(
L10n.of(context)!.conversationBotModeSelectDescription,
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
Padding(
padding: const EdgeInsets.only(left: 16),
child: ConversationBotModeSelect(
initialMode: botOptions.mode,
onChanged: (String? mode) => updateBotOption(
() {
botOptions.mode = mode ?? "discussion";
},
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(28, 0, 12, 0),
child: ConversationBotModeDynamicZone(
initialBotOptions: botOptions,
onChanged: (BotOptionsModel? newOptions) {
updateBotOption(() {
if (newOptions != null) {
botOptions = newOptions;
}
});
],
);
},
),
),
const SizedBox(height: 16),
],
],
),
);
if (confirm == true) {
setState(() => addBot = true);
widget.room?.invite(BotName.byEnvironment);
} else {
setState(() => addBot = false);
widget.room?.kick(BotName.byEnvironment);
}
},
child: addBot
? Text(
L10n.of(context)!.addConversationBotButtonRemove,
)
: Text(
L10n.of(context)!.addConversationBotButtonInvite,
),
)
: const Icon(Icons.settings),
onTap: isCreating
? null
: () async {
final bool? confirm = await showDialog<bool>(
context: context,
builder: (BuildContext context) {
return StatefulBuilder(
builder: (context, setState) => AlertDialog(
title: Text(
L10n.of(context)!.botConfig,
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding:
const EdgeInsets.fromLTRB(0, 0, 0, 12),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
L10n.of(context)!.conversationBotStatus,
),
Switch(
value: addBot,
onChanged: (value) {
setState(
() => addBot = value,
);
},
),
],
),
),
if (addBot)
Flexible(
child: SingleChildScrollView(
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context)
.colorScheme
.secondary,
width: 0.5,
),
borderRadius: const BorderRadius.all(
Radius.circular(10),
),
),
child: ConversationBotSettingsForm(
botOptions: botOptions,
),
),
),
),
],
),
actions: <Widget>[
TextButton(
onPressed: () {
Navigator.of(context).pop(false);
},
child: Text(L10n.of(context)!.cancel),
),
TextButton(
onPressed: () {
Navigator.of(context).pop(true);
},
child: Text(
L10n.of(context)!
.conversationBotConfigConfirmChange,
),
),
],
),
);
},
);
if (confirm == true) {
if (addBot) {
await widget.room?.invite(BotName.byEnvironment);
} else {
await widget.room?.kick(BotName.byEnvironment);
}
updateBotOption(() {
botOptions = botOptions;
});
}
},
),
if (isCreating && addBot)
ConversationBotSettingsForm(
botOptions: botOptions,
),
],
);
),
);
}
}

View file

@ -0,0 +1,88 @@
import 'package:fluffychat/pangea/models/bot_options_model.dart';
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart';
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_mode_select.dart';
import 'package:fluffychat/pangea/widgets/space/language_level_dropdown.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
class ConversationBotSettingsForm extends StatefulWidget {
final BotOptionsModel botOptions;
const ConversationBotSettingsForm({
super.key,
required this.botOptions,
});
@override
ConversationBotSettingsFormState createState() =>
ConversationBotSettingsFormState();
}
class ConversationBotSettingsFormState
extends State<ConversationBotSettingsForm> {
final formKey = GlobalKey<FormState>();
late BotOptionsModel botOptions;
@override
void initState() {
super.initState();
botOptions = widget.botOptions;
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Padding(
padding: const EdgeInsets.all(12.0),
child: Text(
L10n.of(context)!.conversationLanguageLevel,
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
LanguageLevelDropdown(
initialLevel: botOptions.languageLevel,
onChanged: (int? newValue) => {
setState(() {
botOptions.languageLevel = newValue!;
}),
},
),
Text(
L10n.of(context)!.conversationBotModeSelectDescription,
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
ConversationBotModeSelect(
initialMode: botOptions.mode,
onChanged: (String? mode) => {
setState(() {
botOptions.mode = mode ?? "discussion";
}),
},
),
Padding(
padding: const EdgeInsets.all(12),
child: ConversationBotModeDynamicZone(
initialBotOptions: botOptions,
onChanged: (BotOptionsModel? newOptions) {
if (newOptions != null) {
setState(() {
botOptions = newOptions;
});
}
},
),
),
],
);
}
}

View file

@ -27,7 +27,7 @@ class PangeaTextController extends TextEditingController {
}
static const int maxLength = 1000;
bool get isMaxLength => text.length == 1000;
bool get exceededMaxLength => text.length >= maxLength;
bool forceKeepOpen = false;

View file

@ -6,7 +6,7 @@ description: Learn a language while texting your friends.
# Pangea#
publish_to: none
# On version bump also increase the build number for F-Droid
version: 1.21.1+3533
version: 1.21.2+3534
environment:
sdk: ">=3.0.0 <4.0.0"