import 'package:matrix/matrix.dart'; import 'package:fluffychat/pangea/common/constants/model_keys.dart'; import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/instructions/instruction_settings.dart'; import 'package:fluffychat/pangea/learning_settings/gender_enum.dart'; import 'package:fluffychat/pangea/learning_settings/language_level_type_enum.dart'; import 'package:fluffychat/pangea/learning_settings/tool_settings_enum.dart'; import 'package:fluffychat/widgets/matrix.dart'; import '../languages/language_model.dart'; /// The user's settings learning settings. class UserSettings { DateTime? dateOfBirth; DateTime? createdAt; bool? publicProfile; String? targetLanguage; String? sourceLanguage; GenderEnum gender; String? country; LanguageLevelTypeEnum cefrLevel; UserSettings({ this.dateOfBirth, this.createdAt, this.publicProfile, this.targetLanguage, this.sourceLanguage, this.gender = GenderEnum.unselected, this.country, this.cefrLevel = LanguageLevelTypeEnum.a1, }); factory UserSettings.fromJson(Map json) => UserSettings( dateOfBirth: json[ModelKey.userDateOfBirth] != null ? DateTime.parse(json[ModelKey.userDateOfBirth]) : null, createdAt: json[ModelKey.userCreatedAt] != null ? DateTime.parse(json[ModelKey.userCreatedAt]) : null, publicProfile: json[ModelKey.publicProfile], targetLanguage: json[ModelKey.targetLanguage], sourceLanguage: json[ModelKey.sourceLanguage], gender: json[ModelKey.userGender] is String ? GenderEnumExtension.fromString( json[ModelKey.userGender], ) : GenderEnum.unselected, country: json[ModelKey.userCountry], cefrLevel: json[ModelKey.cefrLevel] is String ? LanguageLevelTypeEnum.fromString( json[ModelKey.cefrLevel], ) : LanguageLevelTypeEnum.a1, ); Map toJson() { final Map data = {}; data[ModelKey.userDateOfBirth] = dateOfBirth?.toIso8601String(); data[ModelKey.userCreatedAt] = createdAt?.toIso8601String(); data[ModelKey.publicProfile] = publicProfile; data[ModelKey.targetLanguage] = targetLanguage; data[ModelKey.sourceLanguage] = sourceLanguage; data[ModelKey.userGender] = gender.string; data[ModelKey.userCountry] = country; data[ModelKey.cefrLevel] = cefrLevel.string; return data; } static UserSettings? migrateFromAccountData() { final accountData = MatrixState.pangeaController.matrixState.client.accountData; if (!accountData.containsKey(ModelKey.userDateOfBirth)) return null; final dobContent = accountData[ModelKey.userDateOfBirth]! .content[ModelKey.userDateOfBirth]; String? dobString; if (dobContent != null) { dobString = dobContent as String; } DateTime dob; try { dob = DateTime.parse(dobString!); } catch (_) { return null; } final createdAtContent = accountData[ModelKey.userCreatedAt]?.content[ModelKey.userCreatedAt]; DateTime? createdAt; if (createdAtContent != null) { try { createdAt = DateTime.parse(createdAtContent as String); } catch (_) { createdAt = null; } } return UserSettings( dateOfBirth: dob, createdAt: createdAt, publicProfile: (accountData[ModelKey.publicProfile] ?.content[ModelKey.publicProfile] as bool?) ?? false, targetLanguage: accountData[ModelKey.targetLanguage] ?.content[ModelKey.targetLanguage] as String?, sourceLanguage: accountData[ModelKey.sourceLanguage] ?.content[ModelKey.sourceLanguage] as String?, country: accountData[ModelKey.userCountry]?.content[ModelKey.userCountry] as String?, ); } UserSettings copy() { return UserSettings( dateOfBirth: dateOfBirth, createdAt: createdAt, publicProfile: publicProfile, targetLanguage: targetLanguage, sourceLanguage: sourceLanguage, gender: gender, country: country, cefrLevel: cefrLevel, ); } @override bool operator ==(Object other) { if (identical(this, other)) return true; return other is UserSettings && other.dateOfBirth == dateOfBirth && other.createdAt == createdAt && (other.publicProfile ?? false) == (publicProfile ?? false) && other.targetLanguage == targetLanguage && other.sourceLanguage == sourceLanguage && other.gender == gender && other.country == country && other.cefrLevel == cefrLevel; } @override int get hashCode => Object.hashAll([ dateOfBirth.hashCode, createdAt.hashCode, (publicProfile ?? false).hashCode, targetLanguage.hashCode, sourceLanguage.hashCode, gender.hashCode, country.hashCode, cefrLevel.hashCode, ]); } /// The user's language tool settings. class UserToolSettings { bool interactiveTranslator; bool interactiveGrammar; bool immersionMode; bool definitions; bool autoIGC; bool enableTTS; bool enableAutocorrect; UserToolSettings({ this.interactiveTranslator = true, this.interactiveGrammar = true, this.immersionMode = false, this.definitions = true, this.autoIGC = true, this.enableTTS = true, this.enableAutocorrect = false, }); factory UserToolSettings.fromJson(Map json) => UserToolSettings( interactiveTranslator: json[ToolSetting.interactiveTranslator.toString()] ?? true, interactiveGrammar: json[ToolSetting.interactiveGrammar.toString()] ?? true, immersionMode: false, definitions: json[ToolSetting.definitions.toString()] ?? true, autoIGC: json[ModelKey.autoIGC] ?? true, enableTTS: json[ToolSetting.enableTTS.toString()] ?? true, enableAutocorrect: json["enableAutocorrect"] ?? false, ); Map toJson() { final Map data = {}; data[ToolSetting.interactiveTranslator.toString()] = interactiveTranslator; data[ToolSetting.interactiveGrammar.toString()] = interactiveGrammar; data[ToolSetting.immersionMode.toString()] = immersionMode; data[ToolSetting.definitions.toString()] = definitions; data[ModelKey.autoIGC] = autoIGC; data[ToolSetting.enableTTS.toString()] = enableTTS; data["enableAutocorrect"] = enableAutocorrect; return data; } factory UserToolSettings.migrateFromAccountData() { final accountData = MatrixState.pangeaController.matrixState.client.accountData; return UserToolSettings( interactiveTranslator: (accountData[ToolSetting.interactiveTranslator.toString()] ?.content[ToolSetting.interactiveTranslator.toString()] as bool?) ?? true, interactiveGrammar: (accountData[ToolSetting.interactiveGrammar.toString()] ?.content[ToolSetting.interactiveGrammar.toString()] as bool?) ?? true, immersionMode: false, definitions: (accountData[ToolSetting.definitions.toString()] ?.content[ToolSetting.definitions.toString()] as bool?) ?? true, autoIGC: (accountData[ToolSetting.autoIGC.toString()] ?.content[ToolSetting.autoIGC.toString()] as bool?) ?? true, ); } UserToolSettings copy() { return UserToolSettings( interactiveTranslator: interactiveTranslator, interactiveGrammar: interactiveGrammar, immersionMode: immersionMode, definitions: definitions, autoIGC: autoIGC, enableTTS: enableTTS, enableAutocorrect: enableAutocorrect, ); } @override bool operator ==(Object other) { if (identical(this, other)) return true; return other is UserToolSettings && other.interactiveTranslator == interactiveTranslator && other.interactiveGrammar == interactiveGrammar && other.immersionMode == immersionMode && other.definitions == definitions && other.autoIGC == autoIGC && other.enableTTS == enableTTS && other.enableAutocorrect == enableAutocorrect; } @override int get hashCode => Object.hashAll([ interactiveTranslator.hashCode, interactiveGrammar.hashCode, immersionMode.hashCode, definitions.hashCode, autoIGC.hashCode, enableTTS.hashCode, enableAutocorrect.hashCode, ]); } /// A wrapper around the matrix account data for the user profile. /// Enables easy access to the profile data and saving new data. class Profile { late UserSettings userSettings; late UserToolSettings toolSettings; late InstructionSettings instructionSettings; Profile({ required this.userSettings, UserToolSettings? toolSettings, InstructionSettings? instructionSettings, }) { this.toolSettings = toolSettings ?? UserToolSettings(); this.instructionSettings = instructionSettings ?? InstructionSettings(); } /// Load an instance of profile from the client's account data. static Profile? fromAccountData(Map? profileData) { if (profileData == null) return null; final userSettingsContent = profileData[ModelKey.userSettings]; if (userSettingsContent == null) return null; final toolSettingsContent = profileData[ModelKey.toolSettings]; final instructionSettingsContent = profileData[ModelKey.instructionsSettings]; return Profile( userSettings: UserSettings.fromJson(userSettingsContent as Map), toolSettings: toolSettingsContent != null ? UserToolSettings.fromJson( toolSettingsContent as Map, ) : UserToolSettings(), instructionSettings: instructionSettingsContent != null ? InstructionSettings.fromJson( instructionSettingsContent as Map, ) : InstructionSettings(), ); } Map toJson() { final Map json = { ModelKey.userSettings: userSettings.toJson(), ModelKey.toolSettings: toolSettings.toJson(), ModelKey.instructionsSettings: instructionSettings.toJson(), }; return json; } /// Migrate data from the old matrix account data /// format to the new matrix account data format. static Profile? migrateFromAccountData() { final userSettings = UserSettings.migrateFromAccountData(); if (userSettings == null) return null; final toolSettings = UserToolSettings.migrateFromAccountData(); final instructionSettings = InstructionSettings.migrateFromAccountData(); return Profile( userSettings: userSettings, toolSettings: toolSettings, instructionSettings: instructionSettings, ); } /// Saves the current configuration of the profile to the client's account data. /// If [waitForDataInSync] is true, the function will wait for the updated account /// data to come through in a sync, indicating that it has been set on the matrix server. Future saveProfileData({ waitForDataInSync = false, }) async { final PangeaController pangeaController = MatrixState.pangeaController; final Client client = pangeaController.matrixState.client; final List profileKeys = [ ModelKey.userSettings, ModelKey.toolSettings, ModelKey.instructionsSettings, ]; Future? waitForUpdate; if (waitForDataInSync) { waitForUpdate = client.onSync.stream.firstWhere( (sync) => sync.accountData != null && sync.accountData!.any( (event) => event.content.keys.any((k) => profileKeys.contains(k)), ), ); } await client.setAccountData( client.userID!, ModelKey.userProfile, toJson(), ); if (waitForDataInSync) { await waitForUpdate; } } static Profile get emptyProfile { return Profile( userSettings: UserSettings(), toolSettings: UserToolSettings(), instructionSettings: InstructionSettings(), ); } Profile copy() { return Profile( userSettings: userSettings.copy(), toolSettings: toolSettings.copy(), instructionSettings: instructionSettings.copy(), ); } @override bool operator ==(Object other) { if (identical(this, other)) return true; return other is Profile && other.userSettings == userSettings && other.toolSettings == toolSettings && other.instructionSettings == instructionSettings; } @override int get hashCode => userSettings.hashCode ^ toolSettings.hashCode ^ instructionSettings.hashCode; } /// Model of data from pangea chat server. Not used anymore, in favor of matrix account data. /// This class if used to read in data from the server to be migrated to matrix account data. class PangeaProfile { final String createdAt; final String pangeaUserId; String? dateOfBirth; String? targetLanguage; String? sourceLanguage; String? country; bool? publicProfile; PangeaProfile({ required this.createdAt, required this.pangeaUserId, this.dateOfBirth, this.targetLanguage, this.sourceLanguage, this.country, this.publicProfile, }); factory PangeaProfile.fromJson(Map json) { final l2 = LanguageModel.codeFromNameOrCode( json[ModelKey.targetLanguage], ); final l1 = LanguageModel.codeFromNameOrCode( json[ModelKey.sourceLanguage], ); return PangeaProfile( createdAt: json[ModelKey.userCreatedAt], pangeaUserId: json[ModelKey.userPangeaUserId], dateOfBirth: json[ModelKey.userDateOfBirth], targetLanguage: l2, sourceLanguage: l1, publicProfile: json[ModelKey.publicProfile], country: json[ModelKey.userCountry], ); } Map toJson() { final Map data = {}; data[ModelKey.userCreatedAt] = createdAt; data[ModelKey.userPangeaUserId] = pangeaUserId; data[ModelKey.userDateOfBirth] = dateOfBirth; data[ModelKey.targetLanguage] = targetLanguage; data[ModelKey.sourceLanguage] = sourceLanguage; data[ModelKey.publicProfile] = publicProfile; data[ModelKey.userCountry] = country; return data; } } class PangeaProfileResponse { final PangeaProfile profile; final String access; PangeaProfileResponse({ required this.profile, required this.access, }); factory PangeaProfileResponse.fromJson(Map json) { return PangeaProfileResponse( profile: PangeaProfile.fromJson(json[ModelKey.userProfile]), access: json[ModelKey.userAccess], ); } }