Merged main into blue-branch
This commit is contained in:
commit
d4c24e2639
187 changed files with 5787 additions and 59492 deletions
2
.github/workflows/main_deploy.yaml
vendored
2
.github/workflows/main_deploy.yaml
vendored
|
|
@ -79,5 +79,7 @@ jobs:
|
|||
with:
|
||||
name: web
|
||||
path: build/web
|
||||
- name: Update packages
|
||||
run: flutter pub get
|
||||
- name: Update sentry
|
||||
run: flutter packages pub run sentry_dart_plugin
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
|
||||
# Special thanks
|
||||
|
||||
* Pangea Chat is a fork of [FluffyChat](https://fluffychat.im), is an open source, nonprofit and cute [[matrix](https://matrix.org)] client written in [Flutter](https://flutter.dev). The goal of FluffyChat is to create an easy to use instant messenger which is open source and accessible for everyone. You can [support the primary maker of FluffyChat directly here.](https://ko-fi.com/C1C86VN53)
|
||||
* Pangea Chat is a fork of [FluffyChat](https://fluffychat.im) which is a [[matrix](https://matrix.org)] client written in [Flutter](https://flutter.dev). You can [support the primary maker of FluffyChat directly here.](https://ko-fi.com/C1C86VN53)
|
||||
|
||||
* <a href="https://github.com/fabiyamada">Fabiyamada</a> is a graphics designer and has made the fluffychat logo and the banner. Big thanks for her great designs.
|
||||
|
||||
|
|
|
|||
|
|
@ -410,7 +410,6 @@
|
|||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"classes": "Classes",
|
||||
"chooseAStrongPassword": "Choose a strong password",
|
||||
"@chooseAStrongPassword": {
|
||||
"type": "text",
|
||||
|
|
@ -616,16 +615,11 @@
|
|||
}
|
||||
},
|
||||
"createGroup": "Create group",
|
||||
"createNewSpace": "Create an exchange space",
|
||||
"createNewGroup": "Create a new chat",
|
||||
"@createNewGroup": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@createNewSpace": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"currentlyActive": "Currently active",
|
||||
"@currentlyActive": {
|
||||
"type": "text",
|
||||
|
|
@ -2401,7 +2395,7 @@
|
|||
"@noKeyForThisMessage": {},
|
||||
"newGroup": "New chat",
|
||||
"@newGroup": {},
|
||||
"newSpace": "New class",
|
||||
"newSpace": "New space",
|
||||
"@newSpace": {},
|
||||
"enterSpace": "Enter space",
|
||||
"@enterSpace": {},
|
||||
|
|
@ -2540,7 +2534,7 @@
|
|||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"interactiveTranslatorNotAllowedDesc": "Translation assistance is disabled in space group chats for all participants. This restriction does not apply to Class/Exchange Admin or direct chats.",
|
||||
"interactiveTranslatorNotAllowedDesc": "Translation assistance is disabled in space group chats for all participants. This restriction does not apply to space admins or direct chats.",
|
||||
"@interactiveTranslatorNotAllowedDesc": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
|
|
@ -2550,7 +2544,7 @@
|
|||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"interactiveTranslatorRequiredDesc": "Students cannot turn off translation assistance. They can choose not to accept the translation suggestions. This restriction does not apply to Class/Exchange or direct chats.",
|
||||
"interactiveTranslatorRequiredDesc": "Students cannot turn off translation assistance. They can choose not to accept the translation suggestions. This restriction does not apply to spaces or direct chats.",
|
||||
"@interactiveTranslatorRequiredDesc": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
|
|
@ -2560,12 +2554,7 @@
|
|||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"multiLingualClass": "Multilingual Class",
|
||||
"classAnalytics": "Class Analytics",
|
||||
"@classAnalytics": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"multiLingualSpace": "Multilingual Space",
|
||||
"allClasses": "All Classes",
|
||||
"@allClasses": {
|
||||
"type": "text",
|
||||
|
|
@ -2601,18 +2590,8 @@
|
|||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"requestAnExchange": "Request an Exchange",
|
||||
"@requestAnExchange": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"findLanguageExchange": "Find a class exchange partner",
|
||||
"@findLanguageExchange": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"classAnalyticsDesc": "Detailed information on student engagement and language use",
|
||||
"@classAnalyticsDesc": {
|
||||
"spaceAnalyticsDesc": "Detailed information on student engagement and language use",
|
||||
"@spaceAnalyticsDesc": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
|
|
@ -2629,28 +2608,28 @@
|
|||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"classSettings": "Class Settings",
|
||||
"@classSettings": {
|
||||
"languageSettings": "Language Settings",
|
||||
"@languageSettings": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"classSettingsDesc": "Edit class languages and proficiency level.",
|
||||
"@classSettingsDesc": {
|
||||
"languageSettingsDesc": "Edit space languages and proficiency level.",
|
||||
"@languageSettingsDesc": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"selectClassRoomDominantLanguage": "What is the base language of your class?",
|
||||
"@selectClassRoomDominantLanguage": {
|
||||
"selectSpaceDominantLanguage": "What's the most common language of space members?",
|
||||
"@selectSpaceDominantLanguage": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"selectTargetLanguage": "What language are you teaching?",
|
||||
"@selectTargetLanguage": {
|
||||
"selectSpaceTargetLanguage": "What is the most common target language of the space?",
|
||||
"@selectSpaceTargetLanguage": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"whatIsYourClassLanguageLevel": "What is the average language level of your class?",
|
||||
"@whatIsYourClassLanguageLevel": {
|
||||
"whatIsYourSpaceLanguageLevel": "What is the average language level of the space?",
|
||||
"@whatIsYourSpaceLanguageLevel": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
|
|
@ -2679,7 +2658,7 @@
|
|||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"createGroupChatsDesc": "Toggle this on to allow students to create group chats within the class/exchange space.",
|
||||
"createGroupChatsDesc": "Toggle this on to allow students to create group chats within the space.",
|
||||
"@createGroupChatsDesc": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
|
|
@ -2794,12 +2773,12 @@
|
|||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"joinWithClassCode": "Join class or exchange",
|
||||
"joinWithClassCode": "Join space",
|
||||
"@joinWithClassCode": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"joinWithClassCodeDesc": "Connect to a class or exchange space with the 6-digit invite code provided by the space administrator.",
|
||||
"joinWithClassCodeDesc": "Connect to a space with the 6-digit invite code provided by the space administrator.",
|
||||
"@joinWithClassCodeDesc": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
|
|
@ -2809,7 +2788,7 @@
|
|||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"unableToFindClass": "We are unable to find the class or exchange. Please double-check the information with the space administrator. If you are still experiencing an issue, please contact support@pangea.chat.",
|
||||
"unableToFindClass": "We are unable to find the space. Please double-check the information with the space administrator. If you are still experiencing an issue, please contact support@pangea.chat.",
|
||||
"@unableToFindClass": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
|
|
@ -2869,12 +2848,12 @@
|
|||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"welcomeToPangea18Plus": "Welcome to Pangea Chat! 🙂\nWhat's next?\nCreate or join a class!\nOr search for a conversation partner!",
|
||||
"welcomeToPangea18Plus": "Welcome to Pangea Chat! 🙂\nWhat's next?\nCreate or join a space!\nOr search for a conversation partner!",
|
||||
"@welcomeToPangea18Plus": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"welcomeToPangeaMinor": "Welcome to Pangea Chat! 🙂\nWhat's next?\nJoin a class!\nAsk your teacher for an invite code.",
|
||||
"welcomeToPangeaMinor": "Welcome to Pangea Chat! 🙂\nWhat's next?\nJoin a space!\nAsk your teacher for an invite code.",
|
||||
"@welcomeToPangeaMinor": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
|
|
@ -2915,21 +2894,23 @@
|
|||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"helpMeTranslate": "Help me translate!",
|
||||
"helpMeTranslate": "Yes!",
|
||||
"@helpMeTranslate": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"needsItShortMessage": "Try interactive translation!",
|
||||
"needsItShortMessage": "Out of target",
|
||||
"needsIGCShortMessage": "Try interactive grammar assistance!",
|
||||
"@needsItShortMessage": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"needsItMessage": "This message has too many words in your base language.",
|
||||
"needsItMessage": "Wait, that's not {targetLanguage}! Do you need help translating?",
|
||||
"@needsItMessage": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
"placeholders": {
|
||||
"targetLanguage": {}
|
||||
}
|
||||
},
|
||||
"needsIgcMessage": "This message has a grammar error.",
|
||||
"tokenTranslationTitle": "A word is in your base language.",
|
||||
|
|
@ -3130,7 +3111,7 @@
|
|||
"prettyGood": "Pretty good! Here's what I would have said.",
|
||||
"letMeThink": "Hmm, let's see how you did!",
|
||||
"clickMessageTitle": "Need help?",
|
||||
"clickMessageBody": "Click messages to access definitions, translations, and audio!",
|
||||
"clickMessageBody": "Click a message for language help! Click and hold to react 😀.",
|
||||
"understandingMessagesTitle": "Definitions and translations!",
|
||||
"understandingMessagesBody": "Click underlined words for definitions. Translate with message options (upper right).",
|
||||
"allDone": "All done!",
|
||||
|
|
@ -3174,7 +3155,7 @@
|
|||
"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.",
|
||||
"alreadyInClass": "You are already in this space.",
|
||||
"pleaseLoginFirst": "Please login or sign up first and then you will be added to your class/exchange space.",
|
||||
"pleaseLoginFirst": "Please login or sign up first and then you will be added to your space.",
|
||||
"originalMessage": "Original Message",
|
||||
"sentMessage": "Sent Message",
|
||||
"useType": "Use Type",
|
||||
|
|
@ -3185,16 +3166,13 @@
|
|||
"definitionsToolDescription": "When enabled, words underlined in blue can be clicked for definitions. Click messages to access definitions.",
|
||||
"translationsToolDescrption": "When enabled, click a message and the translation icon to see a message in your base language.",
|
||||
"welcomeBack": "Welcome back! If you were part of the 2023-2024 pilot, please contact us for your special pilot subscription. If you are a teacher who has (or whose institution has) purchased licenses for your class, contact us for your teacher subscription.",
|
||||
"classExchanges": "Exchanges",
|
||||
"createNewClass": "New class space",
|
||||
"newExchange": "New exchange space",
|
||||
"kickAllStudents": "Kick All Students",
|
||||
"kickAllStudentsConfirmation": "Are you sure you want to kick all students?",
|
||||
"inviteAllStudents": "Invite All Students",
|
||||
"inviteAllStudentsConfirmation": "Are you sure you want to invite all students?",
|
||||
"inviteStudentsFromOtherClasses": "Invite students from other spaces",
|
||||
"inviteUsersFromPangea": "Add teachers",
|
||||
"allExchanges": "All Exchanges",
|
||||
"redeemPromoCode": "Redeem Promo Code",
|
||||
"enterPromoCode": "Enter Promo Code",
|
||||
"downloadTxtFile": "Download Text File",
|
||||
|
|
@ -3652,22 +3630,24 @@
|
|||
"pay": "Pay",
|
||||
"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",
|
||||
"addToClassDesc": "Adding an exchange to a class will make the exchange appear within the class for students and give them access to all chats within the exchange.",
|
||||
"addToClassOrExchange": "Add chat to class or exchange",
|
||||
"addToClassOrExchangeDesc": "Adding a chat to a class or exchange will make the chat appear within the class or exchange for students and give them access.",
|
||||
"invitedToClassOrExchange": "{user} has invited you to join a space: {classOrExchange}! Do you wish to accept?",
|
||||
"@invitedToClassOrExchange": {
|
||||
"copyClassCodeDesc": "Students who are already in the app can 'Join space' via the main menu.",
|
||||
"addToSpaceDesc": "Adding a chat to a space will make the chat appear within the space for students and give them access.",
|
||||
"@addToSpaceDesc": {
|
||||
"placeholders": {
|
||||
"classOrExchange": {},
|
||||
"roomtype": {}
|
||||
}
|
||||
},
|
||||
"invitedToSpace": "{user} has invited you to join a space: {space}! Do you wish to accept?",
|
||||
"@invitedToSpace": {
|
||||
"placeholders": {
|
||||
"space": {},
|
||||
"user": {}
|
||||
}
|
||||
},
|
||||
"declinedInvitation": "Declined invitation",
|
||||
"acceptedInvitation": "Accepted invitation",
|
||||
"youreInvited": "📩 You're invited!",
|
||||
"studentPermissionsDesc": "Set permissions for this space. They will only apply to the class/exchange space. They will override individual user settings.",
|
||||
"studentPermissionsDesc": "Set permissions for this space. They will only apply to the space. They will override individual user settings.",
|
||||
"noEligibleSpaces": "There are no eligible spaces to add this to.",
|
||||
"youAddedToSpace": "You added {child} to {space}",
|
||||
"@youAddedToSpace": {
|
||||
|
|
@ -3705,9 +3685,9 @@
|
|||
},
|
||||
"emptyChatNameWarning": "Please enter a name for this chat",
|
||||
"emptyClassNameWarning": "Please enter a name for this class",
|
||||
"emptyExchangeNameWarning": "Please enter a name for this exchange",
|
||||
"emptySpaceNameWarning": "Please enter a name for this space",
|
||||
"blurMeansTranslateTitle": "Why is the message blurred?",
|
||||
"blurMeansTranslateBody": "While Immersion Mode is on, messages that are sent in your base language will be blurred while Pangea Bot translates them to your target language. Immersion Mode can be toggled in individual and class settings.",
|
||||
"blurMeansTranslateBody": "While Immersion Mode is on, messages that are sent in your base language will be blurred while Pangea Bot translates them to your target language. Immersion Mode can be toggled in individual and space settings.",
|
||||
"someErrorTitle": "Hm, something's not right",
|
||||
"someErrorBody": "It could be an error or something in your base language.",
|
||||
"bestCorrectionFeedback": "That's correct!",
|
||||
|
|
@ -3717,7 +3697,7 @@
|
|||
"practiceDefaultPrompt": "What is the best answer?",
|
||||
"correctionDefaultPrompt": "What is the best replacement?",
|
||||
"itStartDefaultPrompt": "Do you want help translating?",
|
||||
"languageLevelWarning": "Please select a class language level",
|
||||
"languageLevelWarning": "Please select a space language level",
|
||||
"lockedChatWarning": "🔒 This chat has been locked",
|
||||
"lockSpace": "Lock Space",
|
||||
"lockChat": "Lock Chat",
|
||||
|
|
@ -3730,7 +3710,6 @@
|
|||
"why": "Why?",
|
||||
"definition": "Definition",
|
||||
"exampleSentence": "Example Sentence",
|
||||
"addToClassTitle": "Add Exchange to Class",
|
||||
"reportToTeacher": "Who do you want to report this message to?",
|
||||
"reportMessageTitle": "{reportingUserId} has reported a message from {reportedUserId} in the chat {roomName}",
|
||||
"@reportMessageTitle": {
|
||||
|
|
@ -3777,7 +3756,6 @@
|
|||
},
|
||||
"searchChatsRooms": "Search for #chats, @users...",
|
||||
"createClass": "Create class",
|
||||
"createExchange": "Create exchange",
|
||||
"viewArchive": "View Archive",
|
||||
"trialExpiration": "Your free trial expires on {expiration}",
|
||||
"@trialExpiration": {
|
||||
|
|
@ -3787,10 +3765,10 @@
|
|||
},
|
||||
"freeTrialDesc": "New users recieve a one week free trial of Pangea Chat",
|
||||
"activateTrial": "Activate Free Trial",
|
||||
"inNoSpaces": "You are not a member of any classes or exchanges",
|
||||
"inNoSpaces": "You are not a member of any spaces",
|
||||
"successfullySubscribed": "You have successfully subscribed!",
|
||||
"clickToManageSubscription": "Click here to manage your subscription.",
|
||||
"emptyInviteWarning": "Add this chat to a class or exchange to invite other users.",
|
||||
"emptyInviteWarning": "Add this chat to a space to invite other users.",
|
||||
"errorGettingAudio": "Error getting audio. Please refresh and try again.",
|
||||
"nothingFound": "Nothing found...",
|
||||
"groupName": "Group name",
|
||||
|
|
@ -4022,6 +4000,10 @@
|
|||
"conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel": "Hours between discussion prompts",
|
||||
"conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel": "Responds on ⏩ reaction",
|
||||
"conversationBotDiscussionZone_discussionTriggerReactionKeyLabel": "Reaction to send discussion prompt",
|
||||
"conversationBotCustomZone_title": "Custom Settings",
|
||||
"conversationBotCustomZone_customSystemPromptLabel": "System prompt",
|
||||
"conversationBotCustomZone_customSystemPromptPlaceholder": "Set custom system prompt",
|
||||
"conversationBotCustomZone_customTriggerReactionEnabledLabel": "Responds on ⏩ reaction",
|
||||
"addConversationBotDialogTitleInvite": "Confirm inviting conversation bot",
|
||||
"addConversationBotButtonInvite": "Invite",
|
||||
"addConversationBotDialogInviteConfirmation": "Invite",
|
||||
|
|
@ -4034,9 +4016,9 @@
|
|||
"wordsPerMinute": "Words per minute",
|
||||
"autoIGCToolName": "Run Language Assistance Automatically",
|
||||
"autoIGCToolDescription": "Automatically run language assistance after typing messages",
|
||||
"runGrammarCorrection": "Run grammar correction",
|
||||
"runGrammarCorrection": "Check message",
|
||||
"grammarCorrectionFailed": "Issues to address",
|
||||
"grammarCorrectionComplete": "Grammar correction complete",
|
||||
"grammarCorrectionComplete": "Looks good!",
|
||||
"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.",
|
||||
|
|
@ -4069,27 +4051,39 @@
|
|||
"nonexistentSelection": "Selection no longer exists.",
|
||||
"cantAddSpaceChild": "You do not have permission to add a child to this space.",
|
||||
"roomAddedToSpace": "Room(s) have been added to the selected space.",
|
||||
"versionNotFound": "Version Not Found",
|
||||
"fetchingVersion": "Fetching version...",
|
||||
"versionFetchError": "Error fetching version",
|
||||
"connectedToStaging": "Connected to Staging",
|
||||
"versionText": "Version: {version}+{buildNumber}",
|
||||
"@versionText": {
|
||||
"description": "Text displaying the app version and build number.",
|
||||
"createNewSpace": "New space",
|
||||
"@createNewSpace": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"addChatToSpaceDesc": "Adding a chat to a space will make the chat appear within the space for students and give them access.",
|
||||
"addSpaceToSpaceDesc": "Adding a space to another space will make the child space appear within the parent space for students and give them access.",
|
||||
"spaceAnalytics": "Space Analytics",
|
||||
"changeAnalyticsLanguage": "Change Analytics Language",
|
||||
"suggestToSpace": "Suggest this space",
|
||||
"suggestToSpaceDesc": "Suggested spaces will appear in the chat lists for their parent spaces",
|
||||
"practice": "Practice",
|
||||
"noLanguagesSet": "No languages set",
|
||||
"noActivitiesFound": "No practice activities found for this message",
|
||||
"hintTitle": "Hint:",
|
||||
"speechToTextBody": "See how well you did by looking at your Accuracy and Words Per Minute scores",
|
||||
"previous": "Previous",
|
||||
"languageButtonLabel": "Language: {currentLanguage}",
|
||||
"@languageButtonLabel": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"version": {
|
||||
"type": "String",
|
||||
"description": "The current version of the app."
|
||||
},
|
||||
"buildNumber": {
|
||||
"type": "String",
|
||||
"description": "The build number of the app."
|
||||
}
|
||||
"currentLanguage": {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
},
|
||||
"interactiveTranslatorAutoPlaySliderHeader": "Autoplay translation",
|
||||
"@interactiveTranslatorAutoPlaySliderHeader": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"interactiveTranslatorAutoPlayDesc": "Launches the interactive translator without asking.",
|
||||
"@interactiveTranslatorAutoPlayDesc": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"changeAnalyticsView": "Change Analytics View"
|
||||
}
|
||||
|
|
@ -2752,31 +2752,11 @@
|
|||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"openToExchanges": "¿Abierto a intercambios?",
|
||||
"@openToExchanges": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"oneToOneChatsWithinExchanges": "Chats Privados dentro de Intercambios",
|
||||
"@oneToOneChatsWithinExchanges": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"createGroupChats": "Crear Chats Grupales",
|
||||
"@createGroupChats": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"createGroupChatsInExchanges": "Crear Chats Grupales en Intercambios",
|
||||
"@createGroupChatsInExchanges": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"createGroupChatsInExchangesDesc": "Active esta opción para permitir que los estudiantes creen chats grupales dentro de los intercambios.",
|
||||
"@createGroupChatsInExchangesDesc": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"shareStories": "Subir historias",
|
||||
"@shareStories": {
|
||||
"type": "text",
|
||||
|
|
@ -2917,21 +2897,11 @@
|
|||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"findLanguageExchange": "Encuentre una clase de intercambio",
|
||||
"@findLanguageExchange": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"requestToEnroll": "Solicitar inscripción",
|
||||
"@requestToEnroll": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"requestAnExchange": "Solicitar un intercambio",
|
||||
"@requestAnExchange": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"targetLanguage": "Idioma a aprender",
|
||||
"@targetLanguage": {
|
||||
"type": "text",
|
||||
|
|
@ -2982,11 +2952,6 @@
|
|||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"classSettings": "Ajustes de clase",
|
||||
"@classSettings": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"suggestToClass": "Sugiera el chat a",
|
||||
"@suggestToClass": {
|
||||
"type": "text",
|
||||
|
|
@ -3032,8 +2997,8 @@
|
|||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"classAnalyticsDesc": "Información detallada sobre la participación de los estudiantes y el uso del idioma",
|
||||
"@classAnalyticsDesc": {
|
||||
"spaceAnalyticsDesc": "Información detallada sobre la participación de los estudiantes y el uso del idioma",
|
||||
"@spaceAnalyticsDesc": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
|
|
@ -3099,6 +3064,17 @@
|
|||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"interactiveTranslatorAutoPlaySliderHeader": "Traducción de reproducción automática",
|
||||
"interactiveTranslatorAutoPlay": "Traductora interactiva de reproducción automática",
|
||||
"@interactiveTranslatorAutoPlay": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"interactiveTranslatorAutoPlayDesc": "Inicia el traductor interactivo sin preguntar.",
|
||||
"@interactiveTranslatorAutoPlayDesc": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"grammarAssistance": "Asistencia gramatical",
|
||||
"@grammarAssistance": {
|
||||
"type": "text",
|
||||
|
|
@ -3161,10 +3137,16 @@
|
|||
"translationSeemsFinished": "La traducción parece estar terminada.",
|
||||
"needsItShortMessage": "¡Pruebe la traducción interactiva!",
|
||||
"needsIGCShortMessage": "¡Pruebe el corrector gramatical interactivo!",
|
||||
"needsItMessage": "Este mensaje tiene demasiadas palabras en su idioma base.",
|
||||
"needsItMessage": "Espera, ¡ese no es {targetLanguage}! ¿Necesitas ayuda para traducir?",
|
||||
"@needsItMessage": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"targetLanguage": {}
|
||||
}
|
||||
},
|
||||
"needsIgcMessage": "Este mensaje tiene un error gramatical.",
|
||||
"tokenTranslationTitle": "Una palabra está en su idioma base.",
|
||||
"helpMeTranslate": "¡Ayúdeme a traducir!",
|
||||
"helpMeTranslate": "¡Sí!",
|
||||
"setToPublicSettingsTitle": "¿Quiere encontrar un compañero de conversación?",
|
||||
"setToPublicSettingsDesc": "Antes de que pueda buscar un compañero de conversación, debe configurar la visibilidad de su perfil como pública.",
|
||||
"publicProfileTitle": "Perfil público",
|
||||
|
|
@ -3815,7 +3797,6 @@
|
|||
"sentMessage": "Mensaje enviado",
|
||||
"useType": "Tipo de Uso",
|
||||
"notAvailable": "No disponible",
|
||||
"classExchanges": "Intercambios",
|
||||
"kickAllStudents": "Patear a todos los estudiantes",
|
||||
"kickAllStudentsConfirmation": "¿Estás seguro de que quieres echar a todos los estudiantes?",
|
||||
"inviteAllStudents": "Invitar a todos los estudiantes",
|
||||
|
|
@ -4296,7 +4277,6 @@
|
|||
"copyClassLink": "Copiar enlace de invitación",
|
||||
"copyClassLinkDesc": "Al hacer clic en este enlace, los usuarios accederán a la aplicación, se abrirán una cuenta y se unirán automáticamente a este espacio.",
|
||||
"copyClassCode": "Copiar código de invitación",
|
||||
"classSettingsDesc": "Editar idiomas de clase y nivel",
|
||||
"createGroupChatsDesc": "Active esta opción para permitir a los estudiantes crear chats de grupo dentro del espacio de clase/intercambio.",
|
||||
"joinWithClassCode": "Únete a una clase o a un intercambio",
|
||||
"joinWithClassCodeDesc": "Conéctese a una clase o espacio de intercambio con el código de invitación de 6 dígitos proporcionado por el administrador del espacio.",
|
||||
|
|
@ -4304,8 +4284,6 @@
|
|||
"unableToFindClass": "No podemos encontrar la clase o el intercambio. Por favor, vuelva a comprobar la información con el administrador del espacio. Si sigue teniendo problemas, póngase en contacto con support@pangea.chat.",
|
||||
"welcomeToYourNewClass": "Bienvenido 🙂",
|
||||
"welcomeToClass": "Bienvenido! 🙂\n- ¡Prueba a unirte a un chat!\n- ¡Diviértete chateando!",
|
||||
"welcomeToPangea18Plus": "Bienvenido al chat de Pangea 🙂 .\n¿Qué es lo siguiente?\n¡Crea una clase o únete a ella!\n¡O busca un compañero de conversación!",
|
||||
"welcomeToPangeaMinor": "Bienvenido al chat de Pangea 🙂 .\n¿Qué es lo siguiente?\n¡Únete a una clase!\nPide a tu profesor un código de invitación.",
|
||||
"unableToFindClassCode": "No se puede encontrar el código.",
|
||||
"errorDisableITClassDesc": "La ayuda a la traducción está desactivada para el espacio en el que se encuentra este chat.",
|
||||
"errorDisableIGCClassDesc": "La asistencia gramatical está desactivada para el espacio en el que se encuentra este chat.",
|
||||
|
|
@ -4317,26 +4295,14 @@
|
|||
"pleaseLoginFirst": "Inicie sesión o regístrese primero y, a continuación, se le añadirá a su clase/espacio de intercambio.",
|
||||
"welcomeBack": "¡Bienvenido de nuevo! Si formó parte del piloto 2023-2024, póngase en contacto con nosotros para obtener su suscripción especial de piloto. Si es usted un profesor que ha adquirido (o cuya institución ha adquirido) licencias para su clase, póngase en contacto con nosotros para obtener su suscripción de profesor.",
|
||||
"createNewClass": "Nuevo espacio para clases",
|
||||
"newExchange": "Nuevo espacio de intercambio",
|
||||
"inviteStudentsFromOtherClasses": "Invitar a estudiantes de otros espacios",
|
||||
"allExchanges": "Todos los intercambios",
|
||||
"creatingSpacePleaseWait": "Creando espacio. Por favor, espere...",
|
||||
"pay": "Pagar",
|
||||
"copyClassCodeDesc": "Los estudiantes que ya están en la aplicación pueden 'Unirse a una clase o a un intercambio' a través del menú principal.",
|
||||
"inviteUsersFromPangea": "Añadir profesores",
|
||||
"addToClass": "Añadir intercambio a la clase",
|
||||
"addToClassOrExchange": "Añadir chat a la clase o al intercambio",
|
||||
"classAnalytics": "Análisis de clase",
|
||||
"myLearning": "Mis análisis",
|
||||
"addToClassDesc": "Añadir un intercambio a una clase hará que el intercambio aparezca dentro de la clase para los estudiantes y les dará acceso a todos los chats dentro del intercambio.",
|
||||
"addToClassOrExchangeDesc": "Añadir un chat a una clase o intercambio hará que el chat aparezca dentro de la clase o intercambio para los estudiantes y les dará acceso.",
|
||||
"invitedToClassOrExchange": "{user} te ha invitado a unirte a ¡{classOrExchange}! ¿Deseas aceptar?",
|
||||
"@invitedToClassOrExchange": {
|
||||
"placeholders": {
|
||||
"classOrExchange": {},
|
||||
"user": {}
|
||||
}
|
||||
},
|
||||
"decline": "Disminución",
|
||||
"declinedInvitation": "Invitación rechazada",
|
||||
"acceptedInvitation": "Invitación aceptada",
|
||||
|
|
@ -4369,14 +4335,11 @@
|
|||
},
|
||||
"emptyChatNameWarning": "Introduzca un nombre para este chat",
|
||||
"emptyClassNameWarning": "Introduzca un nombre para esta clase",
|
||||
"emptyExchangeNameWarning": "Introduzca un nombre para este intercambio",
|
||||
"blurMeansTranslateTitle": "¿Por qué está borroso el mensaje?",
|
||||
"blurMeansTranslateBody": "Mientras el Modo Inmersión esté activado, los mensajes que se envíen en su idioma base aparecerán borrosos mientras Pangea Bot los traduce a su idioma de destino. El modo inmersión puede activarse en los ajustes individuales y de clase.",
|
||||
"monthlySubscription": "Mensualmente",
|
||||
"yearlySubscription": "Anualmente",
|
||||
"defaultSubscription": "Suscripción al chat de Pangea",
|
||||
"freeTrial": "Prueba gratuita",
|
||||
"languageLevelWarning": "Seleccione un nivel de idioma de clase",
|
||||
"lockedChatWarning": "🔒 Este chat ha sido bloqueado",
|
||||
"lockSpace": "Espacio de bloqueo",
|
||||
"lockChat": "Chat de bloqueo",
|
||||
|
|
@ -4393,7 +4356,6 @@
|
|||
"itStartDefaultPrompt": "¿Quiere ayuda para traducir?",
|
||||
"suggestTo": "Sugerir a {spaceName}",
|
||||
"suggestChatDesc": "Los chats sugeridos aparecerán en la lista de chats de {spaceName}.",
|
||||
"suggestExchangeDesc": "Los intercambios sugeridos aparecerán en la lista de chat de {spaceName}.",
|
||||
"acceptSelection": "Aceptar corrección",
|
||||
"acceptSelectionAnyway": "Use esto de todos modos",
|
||||
"makingActivity": "Actividad de fabricación",
|
||||
|
|
@ -4460,7 +4422,6 @@
|
|||
"groupCanBeFoundViaSearch": "El grupo se puede encontrar a través de la búsqueda",
|
||||
"inNoSpaces": "No es miembro de ninguna clase o bolsa",
|
||||
"createClass": "Crear clase",
|
||||
"createExchange": "Crear intercambio",
|
||||
"viewArchive": "Ver archivo",
|
||||
"trialExpiration": "Su prueba gratuita caduca el {expiration}.",
|
||||
"@trialExpiration": {
|
||||
|
|
@ -4568,7 +4529,7 @@
|
|||
"definitions": "definiciones",
|
||||
"subscribedToUnlockTools": "Suscríbase para desbloquear herramientas lingüísticas, como",
|
||||
"clickMessageTitle": "¿Necesitas ayuda?",
|
||||
"clickMessageBody": "Haga clic en los mensajes para acceder a las definiciones, traducciones y audio.",
|
||||
"clickMessageBody": "¡Lame un mensaje para obtener ayuda con el idioma! Haz clic y mantén presionado para reaccionar 😀",
|
||||
"more": "Más",
|
||||
"translationTooltip": "Traducir",
|
||||
"audioTooltip": "Reproducir audio",
|
||||
|
|
@ -4665,9 +4626,9 @@
|
|||
"enterNumber": "Introduzca un valor numérico entero.",
|
||||
"autoIGCToolName": "Ejecutar automáticamente la asistencia lingüística",
|
||||
"autoIGCToolDescription": "Ejecutar automáticamente la asistencia lingüística después de escribir mensajes",
|
||||
"runGrammarCorrection": "Corregir la gramática",
|
||||
"runGrammarCorrection": "Comprobar mensaje",
|
||||
"grammarCorrectionFailed": "Cuestiones a tratar",
|
||||
"grammarCorrectionComplete": "Corrección gramatical completa",
|
||||
"grammarCorrectionComplete": "¡Se ve bien!",
|
||||
"leaveRoomDescription": "El chat se moverá al archivo. Los demás usuarios podrán ver que has abandonado el chat.",
|
||||
"archiveSpaceDescription": "Todos los chats de este espacio se moverán al archivo para ti y otros usuarios que no sean administradores.",
|
||||
"leaveSpaceDescription": "Todos los chats dentro de este espacio se moverán al archivo. Los demás usuarios podrán ver que has abandonado el espacio.",
|
||||
|
|
@ -4676,24 +4637,85 @@
|
|||
"tooltipInstructionsMobileBody": "Mantenga pulsados los elementos para ver la información sobre herramientas.",
|
||||
"tooltipInstructionsBrowserBody": "Pase el ratón sobre los elementos para ver información sobre herramientas.",
|
||||
"buildTranslation": "Construye tu traducción a partir de las opciones anteriores",
|
||||
"versionNotFound": "Versión no encontrada",
|
||||
"fetchingVersion": "Obteniendo versión...",
|
||||
"versionFetchError": "Error al obtener la versión",
|
||||
"connectedToStaging": "Conectado al entorno de pruebas",
|
||||
"versionText": "Versión: {version}+{buildNumber}",
|
||||
"@versionText": {
|
||||
"description": "Texto que muestra la versión y el número de compilación de la aplicación.",
|
||||
"appLockDescription": "Bloquea la aplicación cuando no la uses con un código pin",
|
||||
"swipeRightToLeftToReply": "Desliza el dedo de derecha a izquierda para responder",
|
||||
"globalChatId": "ID global del chat",
|
||||
"accessAndVisibility": "Acceso y visibilidad",
|
||||
"accessAndVisibilityDescription": "Quién puede participar en este chat y cómo se puede descubrir el chat.",
|
||||
"calls": "Llamadas",
|
||||
"customEmojisAndStickers": "Emojis y pegatinas personalizados",
|
||||
"customEmojisAndStickersBody": "Añade o comparte emojis o stickers personalizados que podrás utilizar en cualquier chat.",
|
||||
"hideRedactedMessages": "Ocultar mensajes redactados",
|
||||
"hideRedactedMessagesBody": "Si alguien borra un mensaje, éste ya no será visible en el chat.",
|
||||
"hideInvalidOrUnknownMessageFormats": "Ocultar formatos de mensaje no válidos o desconocidos",
|
||||
"hideMemberChangesInPublicChats": "Ocultar los cambios de los miembros en los chats públicos",
|
||||
"hideMemberChangesInPublicChatsBody": "No mostrar en la línea de tiempo del chat si alguien se une o abandona un chat público para mejorar la legibilidad.",
|
||||
"overview": "Visión general",
|
||||
"notifyMeFor": "Notificarme para",
|
||||
"passwordRecoverySettings": "Configuración de recuperación de contraseña",
|
||||
"usersMustKnock": "Los usuarios deben golpear",
|
||||
"userWouldLikeToChangeTheChat": "{user} desea unirse al chat.",
|
||||
"@userWouldLikeToChangeTheChat": {
|
||||
"placeholders": {
|
||||
"user": {}
|
||||
}
|
||||
},
|
||||
"noPublicLinkHasBeenCreatedYet": "Aún no se ha creado ningún enlace público",
|
||||
"knock": "Knock",
|
||||
"multiLingualSpace": "Espacio multilingüe",
|
||||
"languageSettings": "Ajustes de idioma",
|
||||
"languageSettingsDesc": "Editar idiomas espaciales y nivel de competencia.",
|
||||
"selectSpaceDominantLanguage": "¿Cuál es la lengua más común de los miembros del espacio?",
|
||||
"selectSpaceTargetLanguage": "¿Cuál es la lengua de destino más común del espacio?",
|
||||
"whatIsYourSpaceLanguageLevel": "¿Cuál es el nivel lingüístico medio del espacio?",
|
||||
"welcomeToPangea18Plus": "Bienvenido al chat de Pangea 🙂 .\n¿Qué es lo siguiente?\n¡Crea o únete a un espacio!\n¡O busca un compañero de conversación!",
|
||||
"welcomeToPangeaMinor": "Bienvenido al chat de Pangea 🙂 .\n¿Qué es lo siguiente?\n¡Únete a un espacio!\nPide a tu profesor un código de invitación.",
|
||||
"addToSpaceDesc": "Añadir un chat a un espacio hará que el chat aparezca dentro del espacio para los estudiantes y les dará acceso.",
|
||||
"invitedToSpace": "{user} te ha invitado a unirte a un espacio: ¡{space}! ¿Deseas aceptar?",
|
||||
"@invitedToSpace": {
|
||||
"placeholders": {
|
||||
"space": {},
|
||||
"user": {}
|
||||
}
|
||||
},
|
||||
"emptySpaceNameWarning": "Introduzca un nombre para este espacio",
|
||||
"blurMeansTranslateBody": "Mientras el Modo Inmersión esté activado, los mensajes que se envíen en tu idioma base aparecerán borrosos mientras Pangea Bot los traduce a tu idioma de destino. El Modo Inmersión puede activarse en los ajustes individuales y espaciales.",
|
||||
"languageLevelWarning": "Seleccione un nivel de lengua espacial",
|
||||
"knocking": "Golpeando",
|
||||
"chatCanBeDiscoveredViaSearchOnServer": "El chat puede descubrirse mediante la búsqueda en {server}.",
|
||||
"@chatCanBeDiscoveredViaSearchOnServer": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"version": {
|
||||
"type": "String",
|
||||
"description": "La versión actual de la aplicación."
|
||||
},
|
||||
"buildNumber": {
|
||||
"type": "String",
|
||||
"description": "El número de compilación de la aplicación."
|
||||
}
|
||||
"server": {}
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
"publicChatAddresses": "Direcciones de chat públicas",
|
||||
"createNewAddress": "Crear una nueva dirección",
|
||||
"userRole": "Función del usuario",
|
||||
"minimumPowerLevel": "{level} es el nivel mínimo de potencia.",
|
||||
"@minimumPowerLevel": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"level": {}
|
||||
}
|
||||
},
|
||||
"searchMore": "Buscar más...",
|
||||
"gallery": "Galería",
|
||||
"files": "Archivos",
|
||||
"addSpaceToSpaceDescription": "Seleccione un espacio para añadir como padre",
|
||||
"noDatabaseEncryption": "La encriptación de la base de datos no es compatible con esta plataforma",
|
||||
"thereAreCountUsersBlocked": "Ahora mismo hay {count} usuarios bloqueados.",
|
||||
"@thereAreCountUsersBlocked": {
|
||||
"type": "text",
|
||||
"count": {}
|
||||
},
|
||||
"restricted": "Restringido",
|
||||
"knockRestricted": "Golpe restringido",
|
||||
"nonexistentSelection": "La selección ya no existe.",
|
||||
"cantAddSpaceChild": "No tiene permiso para añadir un niño a este espacio.",
|
||||
"roomAddedToSpace": "Se han añadido habitaciones al espacio seleccionado.",
|
||||
"addChatToSpaceDesc": "Añadir un chat a un espacio hará que el chat aparezca dentro del espacio para los estudiantes y les dará acceso.",
|
||||
"addSpaceToSpaceDesc": "Añadir un espacio a otro espacio hará que el espacio hijo aparezca dentro del espacio padre para los estudiantes y les dará acceso.",
|
||||
"spaceAnalytics": "Analítica espacial",
|
||||
"changeAnalyticsLanguage": "Cambiar el lenguaje analítico"
|
||||
}
|
||||
BIN
assets/pangea/bot_faces/pangea_bot.riv
Normal file
BIN
assets/pangea/bot_faces/pangea_bot.riv
Normal file
Binary file not shown.
|
|
@ -1,5 +1,5 @@
|
|||
# Uncomment this line to define a global platform for your project
|
||||
platform :ios, '12.0'
|
||||
platform :ios, '13.0'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
|
|
|||
|
|
@ -475,7 +475,7 @@
|
|||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
|
|
@ -568,7 +568,7 @@
|
|||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
|
|
@ -617,7 +617,7 @@
|
|||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ import 'package:fluffychat/pages/settings_style/settings_style.dart';
|
|||
import 'package:fluffychat/pangea/enum/bar_chart_view_enum.dart';
|
||||
import 'package:fluffychat/pangea/guard/p_vguard.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/student_analytics/student_analytics.dart';
|
||||
import 'package:fluffychat/pangea/pages/exchange/add_exchange_to_class.dart';
|
||||
import 'package:fluffychat/pangea/pages/find_partner/find_partner.dart';
|
||||
import 'package:fluffychat/pangea/pages/p_user_age/p_user_age.dart';
|
||||
import 'package:fluffychat/pangea/pages/settings_learning/settings_learning.dart';
|
||||
|
|
@ -43,8 +42,8 @@ import 'package:fluffychat/widgets/matrix.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../pangea/pages/analytics/class_analytics/class_analytics.dart';
|
||||
import '../pangea/pages/analytics/class_list/class_list.dart';
|
||||
import '../pangea/pages/analytics/space_analytics/space_analytics.dart';
|
||||
import '../pangea/pages/analytics/space_list/space_list.dart';
|
||||
|
||||
abstract class AppRoutes {
|
||||
static FutureOr<String?> loggedInRedirect(
|
||||
|
|
@ -171,79 +170,30 @@ abstract class AppRoutes {
|
|||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const StudentAnalyticsPage(),
|
||||
const StudentAnalyticsPage(
|
||||
selectedView: BarChartViewSelection.messages,
|
||||
),
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'messages',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const StudentAnalyticsPage(
|
||||
selectedView: BarChartViewSelection.messages,
|
||||
),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'errors',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const StudentAnalyticsPage(
|
||||
selectedView: BarChartViewSelection.grammar,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: 'analytics',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const AnalyticsClassList(),
|
||||
const AnalyticsSpaceList(),
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: ':classid',
|
||||
redirect: loggedOutRedirect,
|
||||
path: ':spaceid',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const ClassAnalyticsPage(),
|
||||
const SpaceAnalyticsPage(
|
||||
selectedView: BarChartViewSelection.messages,
|
||||
),
|
||||
),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'messages',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
ClassAnalyticsPage(
|
||||
// when going to sub-space from within a parent space's analytics, the
|
||||
// analytics list tiles do not properly update. Adding a unique key to this page is the best fix
|
||||
// I can find at the moment
|
||||
key: UniqueKey(),
|
||||
selectedView: BarChartViewSelection.messages,
|
||||
),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'errors',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
ClassAnalyticsPage(
|
||||
// when going to sub-space from within a parent space's analytics, the
|
||||
// analytics list tiles do not properly update. Adding a unique key to this page is the best fix
|
||||
// I can find at the moment
|
||||
key: UniqueKey(),
|
||||
selectedView: BarChartViewSelection.grammar,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -319,24 +269,6 @@ abstract class AppRoutes {
|
|||
redirect: loggedOutRedirect,
|
||||
),
|
||||
// #Pangea
|
||||
GoRoute(
|
||||
path: 'newspace/:newexchange',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const NewSpace(),
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
),
|
||||
GoRoute(
|
||||
path: 'join_exchange/:exchangeid',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const AddExchangeToClass(),
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
),
|
||||
GoRoute(
|
||||
path: 'partner',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
|
|
|
|||
|
|
@ -16,12 +16,11 @@ import 'package:fluffychat/pages/chat/recording_dialog.dart';
|
|||
import 'package:fluffychat/pages/chat_details/chat_details.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/use_type.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/models/choreo_record.dart';
|
||||
import 'package:fluffychat/pangea/models/class_model.dart';
|
||||
import 'package:fluffychat/pangea/models/representation_content_model.dart';
|
||||
import 'package:fluffychat/pangea/models/space_model.dart';
|
||||
import 'package:fluffychat/pangea/models/tokens_event_content_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/utils/firebase_analytics.dart';
|
||||
|
|
@ -317,10 +316,10 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
Future.delayed(const Duration(seconds: 1), () async {
|
||||
if (!mounted) return;
|
||||
debugPrint(
|
||||
"chat.dart l1 ${pangeaController.languageController.activeL1Code(roomID: roomId)}",
|
||||
"chat.dart l1 ${pangeaController.languageController.userL1?.langCode}",
|
||||
);
|
||||
debugPrint(
|
||||
"chat.dart l2 ${pangeaController.languageController.activeL2Code(roomID: roomId)}",
|
||||
"chat.dart l2 ${pangeaController.languageController.userL2?.langCode}",
|
||||
);
|
||||
if (mounted) {
|
||||
pangeaController.languageController.showDialogOnEmptyLanguage(
|
||||
|
|
@ -586,7 +585,6 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
PangeaMessageTokens? tokensSent,
|
||||
PangeaMessageTokens? tokensWritten,
|
||||
ChoreoRecord? choreo,
|
||||
UseType? useType,
|
||||
}) async {
|
||||
// Pangea#
|
||||
if (sendController.text.trim().isEmpty) return;
|
||||
|
|
@ -630,7 +628,6 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
tokensSent: tokensSent,
|
||||
tokensWritten: tokensWritten,
|
||||
choreo: choreo,
|
||||
useType: useType,
|
||||
)
|
||||
.then(
|
||||
(String? msgEventId) async {
|
||||
|
|
@ -644,7 +641,6 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
GoogleAnalytics.sendMessage(
|
||||
room.id,
|
||||
room.classCode,
|
||||
useType ?? UseType.un,
|
||||
);
|
||||
|
||||
if (msgEventId == null) {
|
||||
|
|
@ -654,8 +650,6 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
);
|
||||
return;
|
||||
}
|
||||
// ensure that analytics room exists / is created for the active langCode
|
||||
await room.ensureAnalyticsRoomExists();
|
||||
},
|
||||
onError: (err, stack) => ErrorHandler.logError(e: err, s: stack),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import 'package:fluffychat/pages/chat/events/message.dart';
|
|||
import 'package:fluffychat/pages/chat/seen_by_row.dart';
|
||||
import 'package:fluffychat/pages/chat/typing_indicators.dart';
|
||||
import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart';
|
||||
import 'package:fluffychat/pangea/enum/instructions_enum.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/utils/instructions.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/locked_chat_message.dart';
|
||||
import 'package:fluffychat/utils/account_config.dart';
|
||||
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
|
||||
|
|
@ -45,7 +45,8 @@ class ChatEventList extends StatelessWidget {
|
|||
// after the chat event list mounts, if the user hasn't yet seen this instruction
|
||||
// card, attach it on top of the first shown message
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
controller.pangeaController.instructions.show(
|
||||
if (events.isEmpty) return;
|
||||
controller.pangeaController.instructions.showInstructionsPopup(
|
||||
context,
|
||||
InstructionsEnum.clickMessage,
|
||||
events[0].eventId,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import 'package:animations/animations.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/it_bar.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/send_button.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_keys.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_constants.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
|
@ -55,9 +54,6 @@ class ChatInputRow extends StatelessWidget {
|
|||
|
||||
return Column(
|
||||
children: [
|
||||
ITBar(
|
||||
choreographer: controller.choreographer,
|
||||
),
|
||||
Row(
|
||||
// crossAxisAlignment: CrossAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import 'package:future_loading_dialog/future_loading_dialog.dart';
|
|||
import 'package:go_router/go_router.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../pangea/choreographer/widgets/it_bar.dart';
|
||||
import '../../utils/stream_extension.dart';
|
||||
import 'chat_emoji_picker.dart';
|
||||
import 'chat_input_row.dart';
|
||||
|
|
@ -440,6 +441,9 @@ class ChatView extends StatelessWidget {
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const ConnectionStatusHeader(),
|
||||
ITBar(
|
||||
choreographer: controller.choreographer,
|
||||
),
|
||||
ReactionsPicker(controller),
|
||||
ReplyDisplay(controller),
|
||||
ChatInputRow(controller),
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'package:fluffychat/pages/chat/chat.dart';
|
|||
import 'package:fluffychat/pangea/enum/use_type.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/models/language_model.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_buttons.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
|
||||
import 'package:fluffychat/utils/date_time_extension.dart';
|
||||
import 'package:fluffychat/utils/string_color.dart';
|
||||
|
|
@ -469,7 +470,7 @@ class Message extends StatelessWidget {
|
|||
?.showUseType ??
|
||||
false) ...[
|
||||
pangeaMessageEvent!
|
||||
.useType
|
||||
.msgUseType
|
||||
.iconView(
|
||||
context,
|
||||
textColor
|
||||
|
|
@ -524,7 +525,14 @@ class Message extends StatelessWidget {
|
|||
Widget container;
|
||||
final showReceiptsRow =
|
||||
event.hasAggregatedEvents(timeline, RelationshipTypes.reaction);
|
||||
if (showReceiptsRow || displayTime || selected || displayReadMarker) {
|
||||
// #Pangea
|
||||
// if (showReceiptsRow || displayTime || selected || displayReadMarker) {
|
||||
if (showReceiptsRow ||
|
||||
displayTime ||
|
||||
selected ||
|
||||
displayReadMarker ||
|
||||
(pangeaMessageEvent?.showMessageButtons ?? false)) {
|
||||
// Pangea#
|
||||
container = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment:
|
||||
|
|
@ -561,7 +569,11 @@ class Message extends StatelessWidget {
|
|||
AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
child: !showReceiptsRow
|
||||
// #Pangea
|
||||
child: !showReceiptsRow &&
|
||||
!(pangeaMessageEvent?.showMessageButtons ?? false)
|
||||
// child: !showReceiptsRow
|
||||
// Pangea#
|
||||
? const SizedBox.shrink()
|
||||
: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
|
|
@ -569,7 +581,19 @@ class Message extends StatelessWidget {
|
|||
left: (ownMessage ? 0 : Avatar.defaultSize) + 12.0,
|
||||
right: ownMessage ? 0 : 12.0,
|
||||
),
|
||||
child: MessageReactions(event, timeline),
|
||||
// #Pangea
|
||||
child: Row(
|
||||
mainAxisAlignment: ownMessage
|
||||
? MainAxisAlignment.end
|
||||
: MainAxisAlignment.start,
|
||||
children: [
|
||||
if (pangeaMessageEvent?.showMessageButtons ?? false)
|
||||
MessageButtons(toolbarController: toolbarController),
|
||||
MessageReactions(event, timeline),
|
||||
],
|
||||
),
|
||||
// child: MessageReactions(event, timeline),
|
||||
// Pangea#
|
||||
),
|
||||
),
|
||||
if (displayReadMarker)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -2,7 +2,6 @@ import 'dart:async';
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pages/chat_list/chat_list_view.dart';
|
||||
|
|
@ -213,32 +212,12 @@ class ChatListController extends State<ChatList>
|
|||
}
|
||||
|
||||
List<Room> get filteredRooms => Matrix.of(context)
|
||||
.client
|
||||
.rooms
|
||||
.where(
|
||||
getRoomFilterByActiveFilter(activeFilter),
|
||||
)
|
||||
// #Pangea
|
||||
.sorted((roomA, roomB) {
|
||||
// put rooms with unread messages at the top of the list
|
||||
if (roomA.membership == Membership.invite &&
|
||||
roomB.membership != Membership.invite) {
|
||||
return -1;
|
||||
}
|
||||
if (roomA.membership != Membership.invite &&
|
||||
roomB.membership == Membership.invite) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
final bool aUnread = roomA.notificationCount > 0 || roomA.markedUnread;
|
||||
final bool bUnread = roomB.notificationCount > 0 || roomB.markedUnread;
|
||||
if (aUnread && !bUnread) return -1;
|
||||
if (!aUnread && bUnread) return 1;
|
||||
|
||||
return 0;
|
||||
})
|
||||
// Pangea#
|
||||
.toList();
|
||||
.client
|
||||
.rooms
|
||||
.where(
|
||||
getRoomFilterByActiveFilter(activeFilter),
|
||||
)
|
||||
.toList();
|
||||
|
||||
bool isSearchMode = false;
|
||||
Future<QueryPublicRoomsResponse>? publicRoomsResponse;
|
||||
|
|
@ -938,7 +917,7 @@ class ChatListController extends State<ChatList>
|
|||
if (mounted) {
|
||||
GoogleAnalytics.analyticsUserUpdate(client.userID);
|
||||
await pangeaController.subscriptionController.initialize();
|
||||
await pangeaController.myAnalytics.addEventsListener();
|
||||
await pangeaController.myAnalytics.initialize();
|
||||
pangeaController.afterSyncAndFirstLoginInitialization(context);
|
||||
await pangeaController.inviteBotToExistingSpaces();
|
||||
await pangeaController.setPangeaPushRules();
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:fluffychat/pangea/constants/class_default_values.dart';
|
||||
import 'package:fluffychat/pangea/utils/class_code.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/utils/find_conversation_partner_dialog.dart';
|
||||
import 'package:fluffychat/pangea/utils/logout.dart';
|
||||
import 'package:fluffychat/pangea/utils/space_code.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
|
@ -58,17 +59,19 @@ class ClientChooserButton extends StatelessWidget {
|
|||
room.isSpace &&
|
||||
room.ownPowerLevel >= ClassDefaultValues.powerLevelOfAdmin,
|
||||
),
|
||||
value: SettingsAction.classAnalytics,
|
||||
value: SettingsAction.spaceAnalytics,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.analytics_outlined),
|
||||
const SizedBox(width: 18),
|
||||
Expanded(child: Text(L10n.of(context)!.classAnalytics)),
|
||||
Expanded(child: Text(L10n.of(context)!.spaceAnalytics)),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
enabled: matrix.client.rooms.isNotEmpty,
|
||||
enabled: matrix.client.rooms.any(
|
||||
(room) => !room.isSpace && !room.isArchived && !room.isAnalyticsRoom,
|
||||
),
|
||||
value: SettingsAction.myAnalytics,
|
||||
child: Row(
|
||||
children: [
|
||||
|
|
@ -84,7 +87,7 @@ class ClientChooserButton extends StatelessWidget {
|
|||
children: [
|
||||
const Icon(Icons.school),
|
||||
const SizedBox(width: 18),
|
||||
Expanded(child: Text(L10n.of(context)!.createNewClass)),
|
||||
Expanded(child: Text(L10n.of(context)!.createNewSpace)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -98,16 +101,6 @@ class ClientChooserButton extends StatelessWidget {
|
|||
// ],
|
||||
// ),
|
||||
// ),
|
||||
PopupMenuItem(
|
||||
value: SettingsAction.newExchange,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.connecting_airports),
|
||||
const SizedBox(width: 18),
|
||||
Expanded(child: Text(L10n.of(context)!.newExchange)),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (controller.pangeaController.permissionsController.isUser18())
|
||||
PopupMenuItem(
|
||||
value: SettingsAction.findAConversationPartner,
|
||||
|
|
@ -397,11 +390,8 @@ class ClientChooserButton extends StatelessWidget {
|
|||
case SettingsAction.newClass:
|
||||
context.go('/rooms/newspace');
|
||||
break;
|
||||
case SettingsAction.newExchange:
|
||||
context.go('/rooms/newspace/exchange');
|
||||
break;
|
||||
case SettingsAction.joinWithClassCode:
|
||||
ClassCodeUtil.joinWithClassCodeDialog(
|
||||
SpaceCodeUtil.joinWithSpaceCodeDialog(
|
||||
context,
|
||||
controller.pangeaController,
|
||||
);
|
||||
|
|
@ -412,7 +402,7 @@ class ClientChooserButton extends StatelessWidget {
|
|||
controller.pangeaController,
|
||||
);
|
||||
break;
|
||||
case SettingsAction.classAnalytics:
|
||||
case SettingsAction.spaceAnalytics:
|
||||
context.go('/rooms/analytics');
|
||||
break;
|
||||
case SettingsAction.myAnalytics:
|
||||
|
|
@ -507,11 +497,10 @@ enum SettingsAction {
|
|||
// #Pangea
|
||||
learning,
|
||||
joinWithClassCode,
|
||||
classAnalytics,
|
||||
spaceAnalytics,
|
||||
myAnalytics,
|
||||
findAConversationPartner,
|
||||
logout,
|
||||
newClass,
|
||||
newExchange
|
||||
// Pangea#
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,11 +35,26 @@ class ChatPermissionsSettingsView extends StatelessWidget {
|
|||
final powerLevels = Map<String, dynamic>.from(powerLevelsContent)
|
||||
// #Pangea
|
||||
// ..removeWhere((k, v) => v is! int);
|
||||
..removeWhere((k, v) => v is! int || k.equals("m.call.invite"));
|
||||
..removeWhere(
|
||||
(k, v) =>
|
||||
v is! int ||
|
||||
k.equals("m.call.invite") ||
|
||||
k.equals("historical") ||
|
||||
k.equals("state_default"),
|
||||
);
|
||||
// Pangea#
|
||||
final eventsPowerLevels = Map<String, int?>.from(
|
||||
powerLevelsContent.tryGetMap<String, int?>('events') ?? {},
|
||||
)..removeWhere((k, v) => v is! int);
|
||||
// #Pangea
|
||||
)..removeWhere(
|
||||
(k, v) =>
|
||||
v is! int ||
|
||||
k.equals("m.space.child") ||
|
||||
k.equals("pangea.usranalytics") ||
|
||||
k.equals(EventTypes.RoomPowerLevels),
|
||||
);
|
||||
// )..removeWhere((k, v) => v is! int);
|
||||
// Pangea#
|
||||
return Column(
|
||||
children: [
|
||||
Column(
|
||||
|
|
@ -57,51 +72,59 @@ class ChatPermissionsSettingsView extends StatelessWidget {
|
|||
),
|
||||
canEdit: room.canChangePowerLevel,
|
||||
),
|
||||
Divider(color: Theme.of(context).dividerColor),
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10n.of(context)!.notifications,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
const key = 'rooms';
|
||||
final value = powerLevelsContent
|
||||
.containsKey('notifications')
|
||||
? powerLevelsContent
|
||||
.tryGetMap<String, Object?>('notifications')
|
||||
?.tryGet<int>('rooms') ??
|
||||
0
|
||||
: 0;
|
||||
return PermissionsListTile(
|
||||
permissionKey: key,
|
||||
permission: value,
|
||||
category: 'notifications',
|
||||
canEdit: room.canChangePowerLevel,
|
||||
onChanged: (level) => controller.editPowerLevel(
|
||||
context,
|
||||
key,
|
||||
value,
|
||||
newLevel: level,
|
||||
category: 'notifications',
|
||||
// #Pangea
|
||||
// Why would teacher need to stop students from seeing notifications?
|
||||
// Divider(color: Theme.of(context).dividerColor),
|
||||
// ListTile(
|
||||
// title: Text(
|
||||
// L10n.of(context)!.notifications,
|
||||
// style: TextStyle(
|
||||
// color: Theme.of(context).colorScheme.primary,
|
||||
// fontWeight: FontWeight.bold,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// Builder(
|
||||
// builder: (context) {
|
||||
// const key = 'rooms';
|
||||
// final value = powerLevelsContent
|
||||
// .containsKey('notifications')
|
||||
// ? powerLevelsContent
|
||||
// .tryGetMap<String, Object?>('notifications')
|
||||
// ?.tryGet<int>('rooms') ??
|
||||
// 0
|
||||
// : 0;
|
||||
// return PermissionsListTile(
|
||||
// permissionKey: key,
|
||||
// permission: value,
|
||||
// category: 'notifications',
|
||||
// canEdit: room.canChangePowerLevel,
|
||||
// onChanged: (level) => controller.editPowerLevel(
|
||||
// context,
|
||||
// key,
|
||||
// value,
|
||||
// newLevel: level,
|
||||
// category: 'notifications',
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
// Only show if there are actually items in this category
|
||||
if (eventsPowerLevels.isNotEmpty)
|
||||
// Pangea#
|
||||
Divider(color: Theme.of(context).dividerColor),
|
||||
// #Pangea
|
||||
if (eventsPowerLevels.isNotEmpty)
|
||||
// Pangea#
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10n.of(context)!.configureChat,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Divider(color: Theme.of(context).dividerColor),
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10n.of(context)!.configureChat,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
for (final entry in eventsPowerLevels.entries)
|
||||
PermissionsListTile(
|
||||
permissionKey: entry.key,
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@ class NewGroupController extends State<NewGroup> {
|
|||
void initState() {
|
||||
Future.delayed(Duration.zero, () {
|
||||
chatTopic.langCode =
|
||||
pangeaController.languageController.activeL2Code(roomID: null) ??
|
||||
pangeaController.languageController.userL2?.langCode ??
|
||||
pangeaController.pLanguageStore.targetOptions.first.langCode;
|
||||
setState(() {});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pages/new_group/new_group.dart';
|
||||
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_capacity_button.dart';
|
||||
import 'package:fluffychat/pangea/widgets/class/add_class_and_invite.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/localized_exception_extension.dart';
|
||||
|
|
@ -100,7 +99,6 @@ class NewGroupView extends StatelessWidget {
|
|||
key: controller.addToSpaceKey,
|
||||
startOpen: true,
|
||||
activeSpaceId: controller.activeSpaceId,
|
||||
mode: AddToClassMode.chat,
|
||||
),
|
||||
// const SizedBox(height: 16),
|
||||
// SwitchListTile.adaptive(
|
||||
|
|
|
|||
|
|
@ -8,16 +8,14 @@ import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_capa
|
|||
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_rules_editor.dart';
|
||||
import 'package:fluffychat/pangea/utils/bot_name.dart';
|
||||
import 'package:fluffychat/pangea/utils/class_chat_power_levels.dart';
|
||||
import 'package:fluffychat/pangea/utils/class_code.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/utils/firebase_analytics.dart';
|
||||
import 'package:fluffychat/pangea/utils/space_code.dart';
|
||||
import 'package:fluffychat/pangea/widgets/class/add_space_toggles.dart';
|
||||
import 'package:fluffychat/pangea/widgets/space/class_settings.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:go_router/go_router.dart';
|
||||
import 'package:matrix/matrix.dart' as sdk;
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
|
|
@ -36,8 +34,9 @@ class NewSpaceController extends State<NewSpace> {
|
|||
bool publicGroup = true;
|
||||
final GlobalKey<RoomRulesState> rulesEditorKey = GlobalKey<RoomRulesState>();
|
||||
final GlobalKey<AddToSpaceState> addToSpaceKey = GlobalKey<AddToSpaceState>();
|
||||
final GlobalKey<ClassSettingsState> classSettingsKey =
|
||||
GlobalKey<ClassSettingsState>();
|
||||
// commenting out language settings in spaces for now
|
||||
// final GlobalKey<LanguageSettingsState> languageSettingsKey =
|
||||
// GlobalKey<LanguageSettingsState>();
|
||||
final GlobalKey<RoomCapacityButtonState> addCapacityKey =
|
||||
GlobalKey<RoomCapacityButtonState>();
|
||||
|
||||
|
|
@ -68,8 +67,6 @@ class NewSpaceController extends State<NewSpace> {
|
|||
void setPublicGroup(bool b) => setState(() => publicGroup = b);
|
||||
|
||||
// #Pangea
|
||||
bool newClassMode = true;
|
||||
|
||||
List<StateEvent> get initialState {
|
||||
final events = <StateEvent>[];
|
||||
|
||||
|
|
@ -95,11 +92,11 @@ class NewSpaceController extends State<NewSpace> {
|
|||
} else {
|
||||
debugger(when: kDebugMode);
|
||||
}
|
||||
if (classSettingsKey.currentState != null) {
|
||||
events.add(classSettingsKey.currentState!.classSettings.toStateEvent);
|
||||
} else {
|
||||
debugger(when: kDebugMode && newClassMode);
|
||||
}
|
||||
// commenting out language settings in spaces for now
|
||||
// if (languageSettingsKey.currentState != null) {
|
||||
// events
|
||||
// .add(languageSettingsKey.currentState!.languageSettings.toStateEvent);
|
||||
// }
|
||||
|
||||
return events;
|
||||
}
|
||||
|
|
@ -117,33 +114,30 @@ class NewSpaceController extends State<NewSpace> {
|
|||
debugger(when: kDebugMode);
|
||||
return;
|
||||
}
|
||||
if (classSettingsKey.currentState != null &&
|
||||
classSettingsKey.currentState!.sameLanguages) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(L10n.of(context)!.noIdenticalLanguages),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (newClassMode) {
|
||||
final int? languageLevel =
|
||||
classSettingsKey.currentState!.classSettings.languageLevel;
|
||||
if (languageLevel == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(L10n.of(context)!.languageLevelWarning)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// commenting out language settings in spaces for now
|
||||
// if (languageSettingsKey.currentState != null &&
|
||||
// languageSettingsKey.currentState!.sameLanguages) {
|
||||
// ScaffoldMessenger.of(context).showSnackBar(
|
||||
// SnackBar(
|
||||
// content: Text(L10n.of(context)!.noIdenticalLanguages),
|
||||
// ),
|
||||
// );
|
||||
// return;
|
||||
// }
|
||||
// final int? languageLevel =
|
||||
// languageSettingsKey.currentState!.languageSettings.languageLevel;
|
||||
// if (languageLevel == null) {
|
||||
// ScaffoldMessenger.of(context).showSnackBar(
|
||||
// SnackBar(content: Text(L10n.of(context)!.languageLevelWarning)),
|
||||
// );
|
||||
// return;
|
||||
// }
|
||||
// Pangea#
|
||||
if (nameController.text.isEmpty) {
|
||||
setState(() {
|
||||
// #Pangea
|
||||
// nameError = L10n.of(context)!.pleaseChoose;
|
||||
final String warning = newClassMode
|
||||
? L10n.of(context)!.emptyClassNameWarning
|
||||
: L10n.of(context)!.emptyExchangeNameWarning;
|
||||
final String warning = L10n.of(context)!.emptySpaceNameWarning;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(warning)),
|
||||
);
|
||||
|
|
@ -168,7 +162,7 @@ class NewSpaceController extends State<NewSpace> {
|
|||
// roomAliasName: publicGroup
|
||||
// ? nameController.text.trim().toLowerCase().replaceAll(' ', '_')
|
||||
// : null,
|
||||
roomAliasName: ClassCodeUtil.generateClassCode(),
|
||||
roomAliasName: SpaceCodeUtil.generateSpaceCode(),
|
||||
// Pangea#
|
||||
name: nameController.text.trim(),
|
||||
topic: topicController.text.isEmpty ? null : topicController.text,
|
||||
|
|
@ -224,15 +218,10 @@ class NewSpaceController extends State<NewSpace> {
|
|||
}
|
||||
|
||||
room.setSpaceChild(newChatRoomId, suggested: true);
|
||||
newClassMode
|
||||
? GoogleAnalytics.addParent(
|
||||
newChatRoomId,
|
||||
room.classCode,
|
||||
)
|
||||
: GoogleAnalytics.addChatToExchange(
|
||||
newChatRoomId,
|
||||
room.classCode,
|
||||
);
|
||||
GoogleAnalytics.addParent(
|
||||
newChatRoomId,
|
||||
room.classCode,
|
||||
);
|
||||
|
||||
GoogleAnalytics.createClass(room.name, room.classCode);
|
||||
try {
|
||||
|
|
@ -267,8 +256,6 @@ class NewSpaceController extends State<NewSpace> {
|
|||
// #Pangea
|
||||
// Widget build(BuildContext context) => NewSpaceView(this);
|
||||
Widget build(BuildContext context) {
|
||||
newClassMode =
|
||||
GoRouterState.of(context).pathParameters['newexchange'] != 'exchange';
|
||||
return NewSpaceView(this);
|
||||
}
|
||||
// Pangea#
|
||||
|
|
|
|||
|
|
@ -1,17 +1,14 @@
|
|||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart';
|
||||
import 'package:fluffychat/pangea/models/class_model.dart';
|
||||
import 'package:fluffychat/pangea/models/space_model.dart';
|
||||
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_capacity_button.dart';
|
||||
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_rules_editor.dart';
|
||||
import 'package:fluffychat/pangea/widgets/class/add_class_and_invite.dart';
|
||||
import 'package:fluffychat/pangea/widgets/class/add_space_toggles.dart';
|
||||
import 'package:fluffychat/pangea/widgets/space/class_settings.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'new_space.dart';
|
||||
|
||||
|
|
@ -32,29 +29,8 @@ class NewSpaceView extends StatelessWidget {
|
|||
appBar: AppBar(
|
||||
// #Pangea
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
controller.newClassMode
|
||||
? L10n.of(context)!.createNewClass
|
||||
: L10n.of(context)!.newExchange,
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.class_outlined),
|
||||
selectedIcon: const Icon(Icons.class_),
|
||||
color: controller.newClassMode ? activeColor : null,
|
||||
isSelected: controller.newClassMode,
|
||||
onPressed: () => context.go('/rooms/newspace'),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.connecting_airports),
|
||||
selectedIcon: const Icon(Icons.connecting_airports),
|
||||
color: !controller.newClassMode ? activeColor : null,
|
||||
isSelected: !controller.newClassMode,
|
||||
onPressed: () => context.go('/rooms/newspace/exchange'),
|
||||
),
|
||||
],
|
||||
// title: Text(L10n.of(context)!.createNewSpace),
|
||||
// Pangea#
|
||||
title: Text(L10n.of(context)!.createNewSpace),
|
||||
),
|
||||
body: MaxWidthBody(
|
||||
child: Column(
|
||||
|
|
@ -135,42 +111,48 @@ class NewSpaceView extends StatelessWidget {
|
|||
RoomCapacityButton(
|
||||
key: controller.addCapacityKey,
|
||||
),
|
||||
if (controller.newClassMode)
|
||||
ClassSettings(
|
||||
key: controller.classSettingsKey,
|
||||
roomId: null,
|
||||
startOpen: true,
|
||||
initialSettings:
|
||||
Matrix.of(context).client.lastUpdatedClassSettings,
|
||||
),
|
||||
if (!controller.newClassMode)
|
||||
AddToSpaceToggles(
|
||||
key: controller.addToSpaceKey,
|
||||
startOpen: true,
|
||||
mode: !controller.newClassMode
|
||||
? AddToClassMode.exchange
|
||||
: AddToClassMode.chat,
|
||||
),
|
||||
FutureBuilder<PangeaRoomRules?>(
|
||||
future: Matrix.of(context).client.lastUpdatedRoomRules,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
return RoomRulesEditor(
|
||||
key: controller.rulesEditorKey,
|
||||
roomId: null,
|
||||
startOpen: false,
|
||||
initialRules: snapshot.data,
|
||||
);
|
||||
} else {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Center(
|
||||
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
// commenting out language settings in spaces for now
|
||||
// LanguageSettings(
|
||||
// key: controller.languageSettingsKey,
|
||||
// roomId: null,
|
||||
// startOpen: true,
|
||||
// initialSettings:
|
||||
// Matrix.of(context).client.lastUpdatedLanguageSettings,
|
||||
// ),
|
||||
AddToSpaceToggles(
|
||||
key: controller.addToSpaceKey,
|
||||
startOpen: true,
|
||||
spaceMode: true,
|
||||
),
|
||||
if (controller.rulesEditorKey.currentState != null)
|
||||
RoomRulesEditor(
|
||||
key: controller.rulesEditorKey,
|
||||
roomId: null,
|
||||
startOpen: false,
|
||||
initialRules: controller.rulesEditorKey.currentState!.rules,
|
||||
),
|
||||
if (controller.rulesEditorKey.currentState == null)
|
||||
FutureBuilder<PangeaRoomRules?>(
|
||||
future: Matrix.of(context).client.lastUpdatedRoomRules,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
return RoomRulesEditor(
|
||||
key: controller.rulesEditorKey,
|
||||
roomId: null,
|
||||
startOpen: false,
|
||||
initialRules: snapshot.data,
|
||||
);
|
||||
} else {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Center(
|
||||
child:
|
||||
CircularProgressIndicator.adaptive(strokeWidth: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
// SwitchListTile.adaptive(
|
||||
// title: Text(L10n.of(context)!.spaceIsPublic),
|
||||
// value: controller.publicGroup,
|
||||
|
|
@ -194,12 +176,7 @@ class NewSpaceView extends StatelessWidget {
|
|||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
// #Pangea
|
||||
// L10n.of(context)!.createNewSpace,
|
||||
controller.newClassMode
|
||||
? L10n.of(context)!.createClass
|
||||
: L10n.of(context)!.createExchange,
|
||||
// Pangea#
|
||||
L10n.of(context)!.createNewSpace,
|
||||
),
|
||||
),
|
||||
Icon(Icons.adaptive.arrow_forward_outlined),
|
||||
|
|
|
|||
|
|
@ -209,6 +209,12 @@ class SettingsView extends StatelessWidget {
|
|||
Icons.chevron_right_outlined,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.psychology_outlined),
|
||||
title: Text(L10n.of(context)!.learningSettings),
|
||||
onTap: () => context.go('/rooms/settings/learning'),
|
||||
trailing: const Icon(Icons.chevron_right_outlined),
|
||||
),
|
||||
// Pangea#
|
||||
ListTile(
|
||||
leading: const Icon(Icons.shield_outlined),
|
||||
|
|
|
|||
|
|
@ -5,17 +5,16 @@ import 'package:fluffychat/pages/chat/chat.dart';
|
|||
import 'package:fluffychat/pangea/choreographer/controllers/alternative_translator.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/igc_controller.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/message_options.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_keys.dart';
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/subscription_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/assistance_state_enum.dart';
|
||||
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/space_model.dart';
|
||||
import 'package:fluffychat/pangea/models/tokens_event_content_model.dart';
|
||||
import 'package:fluffychat/pangea/models/user_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/any_state_holder.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/utils/overlay.dart';
|
||||
|
|
@ -25,7 +24,6 @@ import 'package:flutter/material.dart';
|
|||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import '../../../widgets/matrix.dart';
|
||||
import '../../enum/use_type.dart';
|
||||
import '../../models/choreo_record.dart';
|
||||
import '../../models/language_model.dart';
|
||||
import '../../models/pangea_match_model.dart';
|
||||
|
|
@ -95,63 +93,59 @@ class Choreographer {
|
|||
}
|
||||
|
||||
Future<void> _sendWithIGC(BuildContext context) async {
|
||||
if (igc.canSendMessage) {
|
||||
final PangeaRepresentation? originalWritten =
|
||||
choreoRecord.includedIT && itController.sourceText != null
|
||||
? PangeaRepresentation(
|
||||
langCode: l1LangCode ?? LanguageKeys.unknownLanguage,
|
||||
text: itController.sourceText!,
|
||||
originalWritten: true,
|
||||
originalSent: false,
|
||||
)
|
||||
: null;
|
||||
|
||||
final PangeaRepresentation originalSent = PangeaRepresentation(
|
||||
langCode: langCodeOfCurrentText ?? LanguageKeys.unknownLanguage,
|
||||
text: currentText,
|
||||
originalSent: true,
|
||||
originalWritten: originalWritten == null,
|
||||
);
|
||||
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");
|
||||
|
||||
chatController.send(
|
||||
// PTODO - turn this back on in conjunction with saving tokens
|
||||
// we need to save those tokens as well, in order for exchanges to work
|
||||
// properly. in an exchange, the other user will want
|
||||
// originalWritten: originalWritten,
|
||||
originalSent: originalSent,
|
||||
tokensSent: igc.igcTextData?.tokens != null
|
||||
? PangeaMessageTokens(tokens: igc.igcTextData!.tokens)
|
||||
: null,
|
||||
//TODO - save originalwritten tokens
|
||||
choreo: applicableChoreo,
|
||||
useType: useType,
|
||||
);
|
||||
|
||||
clear();
|
||||
} else {
|
||||
if (!igc.canSendMessage) {
|
||||
igc.showFirstMatch(context);
|
||||
return;
|
||||
}
|
||||
|
||||
final PangeaRepresentation? originalWritten =
|
||||
choreoRecord.includedIT && itController.sourceText != null
|
||||
? PangeaRepresentation(
|
||||
langCode: l1LangCode ?? LanguageKeys.unknownLanguage,
|
||||
text: itController.sourceText!,
|
||||
originalWritten: true,
|
||||
originalSent: false,
|
||||
)
|
||||
: null;
|
||||
|
||||
// TODO - why does both it and igc need to be enabled for choreo to be applicable?
|
||||
// final ChoreoRecord? applicableChoreo =
|
||||
// isITandIGCEnabled && igc.igcTextData != null ? choreoRecord : null;
|
||||
|
||||
// if tokens or language detection are not available, we should get them
|
||||
// notes
|
||||
// 1) we probably need to move this to after we clear the input field
|
||||
// or the user could experience some lag here.
|
||||
// 2) that this call is being made after we've determined if we have an applicable choreo in order to
|
||||
// say whether correction was run on the message. we may eventually want
|
||||
// to edit the useType after
|
||||
if (igc.igcTextData?.tokens == null ||
|
||||
igc.igcTextData?.detectedLanguage == null) {
|
||||
await igc.getIGCTextData(onlyTokensAndLanguageDetection: true);
|
||||
}
|
||||
|
||||
final PangeaRepresentation originalSent = PangeaRepresentation(
|
||||
langCode:
|
||||
igc.igcTextData?.detectedLanguage ?? LanguageKeys.unknownLanguage,
|
||||
text: currentText,
|
||||
originalSent: true,
|
||||
originalWritten: originalWritten == null,
|
||||
);
|
||||
|
||||
final PangeaMessageTokens? tokensSent = igc.igcTextData?.tokens != null
|
||||
? PangeaMessageTokens(tokens: igc.igcTextData!.tokens)
|
||||
: null;
|
||||
|
||||
chatController.send(
|
||||
// originalWritten: originalWritten,
|
||||
originalSent: originalSent,
|
||||
tokensSent: tokensSent,
|
||||
//TODO - save originalwritten tokens
|
||||
// choreo: applicableChoreo,
|
||||
choreo: choreoRecord,
|
||||
);
|
||||
|
||||
clear();
|
||||
}
|
||||
|
||||
_resetDebounceTimer() {
|
||||
|
|
@ -167,7 +161,7 @@ class Choreographer {
|
|||
}
|
||||
choreoMode = ChoreoMode.it;
|
||||
itController.initializeIT(
|
||||
ITStartData(_textController.text, igc.detectedLangCode),
|
||||
ITStartData(_textController.text, igc.igcTextData?.detectedLanguage),
|
||||
);
|
||||
itMatch.status = PangeaMatchStatus.accepted;
|
||||
|
||||
|
|
@ -180,6 +174,7 @@ class Choreographer {
|
|||
_textController.setSystemText("", EditType.itStart);
|
||||
}
|
||||
|
||||
/// Handles any changes to the text input
|
||||
_onChangeListener() {
|
||||
if (_noChange) {
|
||||
return;
|
||||
|
|
@ -188,21 +183,26 @@ class Choreographer {
|
|||
if ([
|
||||
EditType.igc,
|
||||
].contains(_textController.editType)) {
|
||||
// this may be unnecessary now that tokens are not used
|
||||
// to allow click of words in the input field and we're getting this at the end
|
||||
// TODO - turn it off and tested that this is fine
|
||||
igc.justGetTokensAndAddThemToIGCTextData();
|
||||
|
||||
// we set editType to keyboard here because that is the default for it
|
||||
// and we want to make sure that the next change is treated as a keyboard change
|
||||
// unless the system explicity sets it to something else. this
|
||||
textController.editType = EditType.keyboard;
|
||||
return;
|
||||
}
|
||||
|
||||
// not sure if this is necessary now
|
||||
MatrixState.pAnyState.closeOverlay();
|
||||
|
||||
if (errorService.isError) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if (igc.igcTextData != null) {
|
||||
igc.clear();
|
||||
// setState();
|
||||
// }
|
||||
|
||||
_resetDebounceTimer();
|
||||
|
||||
|
|
@ -212,7 +212,9 @@ class Choreographer {
|
|||
() => getLanguageHelp(),
|
||||
);
|
||||
} else {
|
||||
getLanguageHelp(ChoreoMode.it == choreoMode);
|
||||
getLanguageHelp(
|
||||
onlyTokensAndLanguageDetection: ChoreoMode.it == choreoMode,
|
||||
);
|
||||
}
|
||||
|
||||
//Note: we don't set the keyboard type on each keyboard stroke so this is how we default to
|
||||
|
|
@ -221,10 +223,14 @@ class Choreographer {
|
|||
textController.editType = EditType.keyboard;
|
||||
}
|
||||
|
||||
Future<void> getLanguageHelp([
|
||||
bool tokensOnly = false,
|
||||
/// Fetches the language help for the current text, including grammar correction, language detection,
|
||||
/// tokens, and translations. Includes logic to exit the flow if the user is not subscribed, if the tools are not enabled, or
|
||||
/// or if autoIGC is not enabled and the user has not manually requested it.
|
||||
/// [onlyTokensAndLanguageDetection] will
|
||||
Future<void> getLanguageHelp({
|
||||
bool onlyTokensAndLanguageDetection = false,
|
||||
bool manual = false,
|
||||
]) async {
|
||||
}) async {
|
||||
try {
|
||||
if (errorService.isError) return;
|
||||
final CanSendStatus canSendStatus =
|
||||
|
|
@ -239,13 +245,15 @@ class Choreographer {
|
|||
startLoading();
|
||||
if (choreoMode == ChoreoMode.it &&
|
||||
itController.isTranslationDone &&
|
||||
!tokensOnly) {
|
||||
!onlyTokensAndLanguageDetection) {
|
||||
// debugger(when: kDebugMode);
|
||||
}
|
||||
|
||||
await (choreoMode == ChoreoMode.it && !itController.isTranslationDone
|
||||
? itController.getTranslationData(_useCustomInput)
|
||||
: igc.getIGCTextData(tokensOnly: tokensOnly));
|
||||
: igc.getIGCTextData(
|
||||
onlyTokensAndLanguageDetection: onlyTokensAndLanguageDetection,
|
||||
));
|
||||
} catch (err, stack) {
|
||||
ErrorHandler.logError(e: err, s: stack);
|
||||
} finally {
|
||||
|
|
@ -443,25 +451,15 @@ class Choreographer {
|
|||
}
|
||||
|
||||
LanguageModel? get l2Lang {
|
||||
return pangeaController.languageController.activeL2Model(
|
||||
roomID: roomId,
|
||||
);
|
||||
return pangeaController.languageController.activeL2Model();
|
||||
}
|
||||
|
||||
String? get l2LangCode => l2Lang?.langCode;
|
||||
|
||||
LanguageModel? get l1Lang =>
|
||||
pangeaController.languageController.activeL1Model(
|
||||
roomID: roomId,
|
||||
);
|
||||
String? get l1LangCode => l1Lang?.langCode;
|
||||
pangeaController.languageController.activeL1Model();
|
||||
|
||||
String? get classId => roomId != null
|
||||
? pangeaController.matrixState.client
|
||||
.getRoomById(roomId)
|
||||
?.firstParentWithState(PangeaEventTypes.classSettings)
|
||||
?.id
|
||||
: null;
|
||||
String? get l1LangCode => l1Lang?.langCode;
|
||||
|
||||
String? get userId => pangeaController.userController.userId;
|
||||
|
||||
|
|
@ -492,14 +490,6 @@ class Choreographer {
|
|||
|
||||
bool get editTypeIsKeyboard => EditType.keyboard == _textController.editType;
|
||||
|
||||
String? get langCodeOfCurrentText {
|
||||
if (igc.detectedLangCode != null) return igc.detectedLangCode!;
|
||||
|
||||
if (itController.isOpen) return l2LangCode!;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
setState() {
|
||||
if (!stateListener.isClosed) {
|
||||
stateListener.add(0);
|
||||
|
|
@ -533,6 +523,12 @@ class Choreographer {
|
|||
chatController.room,
|
||||
);
|
||||
|
||||
bool get itAutoPlayEnabled =>
|
||||
pangeaController.pStoreService.read(
|
||||
MatrixProfile.itAutoPlay.title,
|
||||
) ??
|
||||
false;
|
||||
|
||||
bool get definitionsEnabled =>
|
||||
pangeaController.permissionsController.isToolEnabled(
|
||||
ToolSetting.definitions,
|
||||
|
|
@ -582,13 +578,3 @@ class Choreographer {
|
|||
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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,18 +3,17 @@ import 'dart:developer';
|
|||
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart';
|
||||
import 'package:fluffychat/pangea/controllers/span_data_controller.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/span_data_controller.dart';
|
||||
import 'package:fluffychat/pangea/models/igc_text_data_model.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_match_model.dart';
|
||||
import 'package:fluffychat/pangea/repo/igc_repo.dart';
|
||||
import 'package:fluffychat/pangea/repo/tokens_repo.dart';
|
||||
import 'package:fluffychat/pangea/widgets/igc/span_card.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import '../../models/language_detection_model.dart';
|
||||
import '../../models/span_card_model.dart';
|
||||
import '../../repo/tokens_repo.dart';
|
||||
import '../../utils/error_handler.dart';
|
||||
import '../../utils/overlay.dart';
|
||||
|
||||
|
|
@ -29,59 +28,42 @@ class IgcController {
|
|||
spanDataController = SpanDataController(choreographer);
|
||||
}
|
||||
|
||||
Future<void> getIGCTextData({required bool tokensOnly}) async {
|
||||
Future<void> getIGCTextData({
|
||||
required bool onlyTokensAndLanguageDetection,
|
||||
}) async {
|
||||
try {
|
||||
if (choreographer.currentText.isEmpty) return clear();
|
||||
|
||||
// the error spans are going to be reloaded, so clear the cache
|
||||
spanDataController.clearCache();
|
||||
debugPrint('getIGCTextData called with ${choreographer.currentText}');
|
||||
debugPrint('getIGCTextData called with tokensOnly = $tokensOnly');
|
||||
debugPrint(
|
||||
'getIGCTextData called with tokensOnly = $onlyTokensAndLanguageDetection',
|
||||
);
|
||||
|
||||
final IGCRequestBody reqBody = IGCRequestBody(
|
||||
fullText: choreographer.currentText,
|
||||
userL1: choreographer.l1LangCode!,
|
||||
userL2: choreographer.l2LangCode!,
|
||||
enableIGC: choreographer.igcEnabled && !tokensOnly,
|
||||
enableIT: choreographer.itEnabled && !tokensOnly,
|
||||
tokensOnly: tokensOnly,
|
||||
enableIGC: choreographer.igcEnabled && !onlyTokensAndLanguageDetection,
|
||||
enableIT: choreographer.itEnabled && !onlyTokensAndLanguageDetection,
|
||||
);
|
||||
|
||||
final IGCTextData igcTextDataResponse = await IgcRepo.getIGC(
|
||||
await choreographer.accessToken,
|
||||
igcRequest: reqBody,
|
||||
);
|
||||
// temp fix
|
||||
igcTextDataResponse.originalInput = reqBody.fullText;
|
||||
|
||||
//this will happen when the user changes the input while igc is fetching results
|
||||
// this will happen when the user changes the input while igc is fetching results
|
||||
if (igcTextDataResponse.originalInput != choreographer.currentText) {
|
||||
// final current = choreographer.currentText;
|
||||
// final igctext = igcTextDataResponse.originalInput;
|
||||
// Sentry.addBreadcrumb(
|
||||
// Breadcrumb(message: "igc return input does not match current text"),
|
||||
// );
|
||||
// debugger(when: kDebugMode);
|
||||
return;
|
||||
}
|
||||
|
||||
//TO-DO: in api call, specify turning off IT and/or grammar checking
|
||||
if (!choreographer.igcEnabled) {
|
||||
igcTextDataResponse.matches = igcTextDataResponse.matches
|
||||
.where((match) => !match.isGrammarMatch)
|
||||
.toList();
|
||||
}
|
||||
if (!choreographer.itEnabled) {
|
||||
igcTextDataResponse.matches = igcTextDataResponse.matches
|
||||
.where((match) => !match.isOutOfTargetMatch)
|
||||
.toList();
|
||||
}
|
||||
if (!choreographer.itEnabled && !choreographer.igcEnabled) {
|
||||
igcTextDataResponse.matches = [];
|
||||
}
|
||||
|
||||
igcTextData = igcTextDataResponse;
|
||||
|
||||
// TODO - for each new match,
|
||||
// check if existing igcTextData has one and only one match with the same error text and correction
|
||||
// if so, keep the original match and discard the new one
|
||||
// if not, add the new match to the existing igcTextData
|
||||
|
||||
// After fetching igc data, pre-call span details for each match optimistically.
|
||||
// This will make the loading of span details faster for the user
|
||||
if (igcTextData?.matches.isNotEmpty ?? false) {
|
||||
|
|
@ -170,6 +152,13 @@ class IgcController {
|
|||
const int firstMatchIndex = 0;
|
||||
final PangeaMatch match = igcTextData!.matches[firstMatchIndex];
|
||||
|
||||
if (match.isITStart &&
|
||||
choreographer.itAutoPlayEnabled &&
|
||||
igcTextData != null) {
|
||||
choreographer.onITStart(igcTextData!.matches[firstMatchIndex]);
|
||||
return;
|
||||
}
|
||||
|
||||
OverlayUtil.showPositionedCard(
|
||||
context: context,
|
||||
cardToShow: SpanCard(
|
||||
|
|
@ -189,7 +178,7 @@ class IgcController {
|
|||
),
|
||||
roomId: choreographer.roomId,
|
||||
),
|
||||
cardSize: match.isITStart ? const Size(350, 220) : const Size(350, 400),
|
||||
cardSize: match.isITStart ? const Size(350, 260) : const Size(350, 400),
|
||||
transformTargetId: choreographer.inputTransformTargetKey,
|
||||
);
|
||||
}
|
||||
|
|
@ -206,14 +195,6 @@ class IgcController {
|
|||
return true;
|
||||
}
|
||||
|
||||
String? get detectedLangCode {
|
||||
if (!hasRelevantIGCTextData) return null;
|
||||
|
||||
final LanguageDetection first = igcTextData!.detections.first;
|
||||
|
||||
return first.langCode;
|
||||
}
|
||||
|
||||
clear() {
|
||||
igcTextData = null;
|
||||
spanDataController.clearCache();
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import 'dart:developer';
|
|||
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart';
|
||||
import 'package:fluffychat/pangea/constants/choreo_constants.dart';
|
||||
import 'package:fluffychat/pangea/repo/full_text_translation_repo.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -21,6 +20,7 @@ class ITController {
|
|||
Choreographer choreographer;
|
||||
|
||||
bool _isOpen = false;
|
||||
bool _willOpen = false;
|
||||
bool _isEditingSourceText = false;
|
||||
bool showChoiceFeedback = false;
|
||||
|
||||
|
|
@ -36,6 +36,7 @@ class ITController {
|
|||
|
||||
void clear() {
|
||||
_isOpen = false;
|
||||
_willOpen = false;
|
||||
showChoiceFeedback = false;
|
||||
_isEditingSourceText = false;
|
||||
|
||||
|
|
@ -54,6 +55,7 @@ class ITController {
|
|||
}
|
||||
|
||||
Future<void> initializeIT(ITStartData itStartData) async {
|
||||
_willOpen = true;
|
||||
Future.delayed(const Duration(microseconds: 100), () {
|
||||
_isOpen = true;
|
||||
});
|
||||
|
|
@ -61,17 +63,14 @@ class ITController {
|
|||
}
|
||||
|
||||
void closeIT() {
|
||||
//if they close it before choosing anything, just put their text back
|
||||
//if they close it before completing, just put their text back
|
||||
//PTODO - explore using last itStep
|
||||
if (choreographer.currentText.isEmpty) {
|
||||
choreographer.textController.text = sourceText ?? "";
|
||||
}
|
||||
choreographer.textController.text = sourceText ?? "";
|
||||
clear();
|
||||
}
|
||||
|
||||
/// if IGC isn't positive that text is full L1 then translate to L1
|
||||
Future<void> _setSourceText() async {
|
||||
// try {
|
||||
if (_itStartData == null || _itStartData!.text.isEmpty) {
|
||||
Sentry.addBreadcrumb(
|
||||
Breadcrumb(
|
||||
|
|
@ -85,32 +84,23 @@ class ITController {
|
|||
throw Exception("null _itStartData or empty text in _setSourceText");
|
||||
}
|
||||
debugPrint("_setSourceText with detectedLang ${_itStartData!.langCode}");
|
||||
if (_itStartData!.langCode == choreographer.l1LangCode) {
|
||||
sourceText = _itStartData!.text;
|
||||
return;
|
||||
}
|
||||
|
||||
final FullTextTranslationResponseModel res =
|
||||
await FullTextTranslationRepo.translate(
|
||||
accessToken: await choreographer.accessToken,
|
||||
request: FullTextTranslationRequestModel(
|
||||
text: _itStartData!.text,
|
||||
tgtLang: choreographer.l1LangCode!,
|
||||
srcLang: choreographer.l2LangCode,
|
||||
userL1: choreographer.l1LangCode!,
|
||||
userL2: choreographer.l2LangCode!,
|
||||
),
|
||||
);
|
||||
sourceText = res.bestTranslation;
|
||||
// } catch (err, stack) {
|
||||
// debugger(when: kDebugMode);
|
||||
// if (_itStartData?.text.isNotEmpty ?? false) {
|
||||
// ErrorHandler.logError(e: err, s: stack);
|
||||
// sourceText = _itStartData!.text;
|
||||
// } else {
|
||||
// rethrow;
|
||||
// }
|
||||
// if (_itStartData!.langCode == choreographer.l1LangCode) {
|
||||
sourceText = _itStartData!.text;
|
||||
return;
|
||||
// }
|
||||
|
||||
// final FullTextTranslationResponseModel res =
|
||||
// await FullTextTranslationRepo.translate(
|
||||
// accessToken: await choreographer.accessToken,
|
||||
// request: FullTextTranslationRequestModel(
|
||||
// text: _itStartData!.text,
|
||||
// tgtLang: choreographer.l1LangCode!,
|
||||
// srcLang: _itStartData!.langCode,
|
||||
// userL1: choreographer.l1LangCode!,
|
||||
// userL2: choreographer.l2LangCode!,
|
||||
// ),
|
||||
// );
|
||||
// sourceText = res.bestTranslation;
|
||||
}
|
||||
|
||||
// used 1) at very beginning (with custom input = null)
|
||||
|
|
@ -166,7 +156,7 @@ class ITController {
|
|||
|
||||
if (isTranslationDone) {
|
||||
choreographer.altTranslator.setTranslationFeedback();
|
||||
choreographer.getLanguageHelp(true);
|
||||
choreographer.getLanguageHelp(onlyTokensAndLanguageDetection: true);
|
||||
} else {
|
||||
getNextTranslationData();
|
||||
}
|
||||
|
|
@ -217,31 +207,17 @@ class ITController {
|
|||
|
||||
Future<void> onEditSourceTextSubmit(String newSourceText) async {
|
||||
try {
|
||||
sourceText = newSourceText;
|
||||
_isOpen = true;
|
||||
_isEditingSourceText = false;
|
||||
final String currentText = choreographer.currentText;
|
||||
_itStartData = ITStartData(newSourceText, choreographer.l1LangCode);
|
||||
completedITSteps = [];
|
||||
currentITStep = null;
|
||||
nextITStep = null;
|
||||
goldRouteTracker = GoldRouteTracker.defaultTracker;
|
||||
payLoadIds = [];
|
||||
|
||||
choreographer.startLoading();
|
||||
|
||||
final List<ITResponseModel> responses = await Future.wait([
|
||||
_customInputTranslation(""),
|
||||
_customInputTranslation(choreographer.currentText),
|
||||
]);
|
||||
if (responses[0].goldContinuances != null &&
|
||||
responses[0].goldContinuances!.isNotEmpty) {
|
||||
goldRouteTracker = GoldRouteTracker(
|
||||
responses[0].goldContinuances!,
|
||||
sourceText!,
|
||||
);
|
||||
}
|
||||
currentITStep = CurrentITStep(
|
||||
sourceText: sourceText!,
|
||||
currentText: currentText,
|
||||
responseModel: responses[1],
|
||||
storedGoldContinuances: goldRouteTracker.continuances,
|
||||
);
|
||||
|
||||
_addPayloadId(responses[1]);
|
||||
_setSourceText();
|
||||
getTranslationData(false);
|
||||
} catch (err, stack) {
|
||||
debugger(when: kDebugMode);
|
||||
if (err is! http.Response) {
|
||||
|
|
@ -252,6 +228,7 @@ class ITController {
|
|||
);
|
||||
} finally {
|
||||
choreographer.stopLoading();
|
||||
choreographer.textController.text = "";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -265,7 +242,6 @@ class ITController {
|
|||
targetLangCode: targetLangCode,
|
||||
userId: choreographer.userId!,
|
||||
roomId: choreographer.roomId!,
|
||||
classId: choreographer.classId,
|
||||
goldTranslation: goldRouteTracker.fullTranslation,
|
||||
goldContinuances: goldRouteTracker.continuances,
|
||||
),
|
||||
|
|
@ -283,7 +259,6 @@ class ITController {
|
|||
translationId: translationId,
|
||||
targetLangCode: targetLangCode,
|
||||
sourceLangCode: sourceLangCode,
|
||||
classId: choreographer.classId,
|
||||
),
|
||||
);
|
||||
|
||||
|
|
@ -336,15 +311,14 @@ class ITController {
|
|||
|
||||
bool get isOpen => _isOpen;
|
||||
|
||||
bool get willOpen => _willOpen;
|
||||
|
||||
String get targetLangCode => choreographer.l2LangCode!;
|
||||
|
||||
String get sourceLangCode => choreographer.l1LangCode!;
|
||||
|
||||
bool get isLoading => choreographer.isFetching;
|
||||
|
||||
bool get correctChoicesSelected =>
|
||||
completedITSteps.every((ITStep step) => step.isCorrect);
|
||||
|
||||
String latestChoiceFeedback(BuildContext context) =>
|
||||
completedITSteps.isNotEmpty
|
||||
? completedITSteps.last.choiceFeedback(context)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_keys.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/models/language_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/firebase_analytics.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -3,14 +3,12 @@ import 'dart:math';
|
|||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../utils/bot_style.dart';
|
||||
import 'it_shimmer.dart';
|
||||
|
||||
class ChoicesArray extends StatelessWidget {
|
||||
class ChoicesArray extends StatefulWidget {
|
||||
final bool isLoading;
|
||||
final List<Choice>? choices;
|
||||
final void Function(int) onPressed;
|
||||
|
|
@ -18,6 +16,10 @@ class ChoicesArray extends StatelessWidget {
|
|||
final int? selectedChoiceIndex;
|
||||
final String originalSpan;
|
||||
final String Function(int) uniqueKeyForLayerLink;
|
||||
|
||||
/// some uses of this widget want to disable the choices
|
||||
final bool isActive;
|
||||
|
||||
const ChoicesArray({
|
||||
super.key,
|
||||
required this.isLoading,
|
||||
|
|
@ -26,26 +28,55 @@ class ChoicesArray extends StatelessWidget {
|
|||
required this.originalSpan,
|
||||
required this.uniqueKeyForLayerLink,
|
||||
required this.selectedChoiceIndex,
|
||||
this.isActive = true,
|
||||
this.onLongPress,
|
||||
});
|
||||
|
||||
@override
|
||||
ChoicesArrayState createState() => ChoicesArrayState();
|
||||
}
|
||||
|
||||
class ChoicesArrayState extends State<ChoicesArray> {
|
||||
bool interactionDisabled = false;
|
||||
|
||||
void disableInteraction() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
setState(() {
|
||||
interactionDisabled = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void enableInteractions() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
setState(() {
|
||||
interactionDisabled = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
return isLoading && (choices == null || choices!.length <= 1)
|
||||
? ItShimmer(originalSpan: originalSpan)
|
||||
return widget.isLoading &&
|
||||
(widget.choices == null || widget.choices!.length <= 1)
|
||||
? ItShimmer(originalSpan: widget.originalSpan)
|
||||
: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
children: choices
|
||||
children: widget.choices
|
||||
?.asMap()
|
||||
.entries
|
||||
.map(
|
||||
(entry) => ChoiceItem(
|
||||
theme: theme,
|
||||
onLongPress: onLongPress,
|
||||
onPressed: onPressed,
|
||||
onLongPress:
|
||||
widget.isActive ? widget.onLongPress : null,
|
||||
onPressed: widget.isActive ? widget.onPressed : (_) {},
|
||||
entry: entry,
|
||||
isSelected: selectedChoiceIndex == entry.key,
|
||||
interactionDisabled: interactionDisabled,
|
||||
enableInteraction: enableInteractions,
|
||||
disableInteraction: disableInteraction,
|
||||
isSelected: widget.selectedChoiceIndex == entry.key,
|
||||
),
|
||||
)
|
||||
.toList() ??
|
||||
|
|
@ -74,6 +105,9 @@ class ChoiceItem extends StatelessWidget {
|
|||
required this.onPressed,
|
||||
required this.entry,
|
||||
required this.isSelected,
|
||||
required this.interactionDisabled,
|
||||
required this.enableInteraction,
|
||||
required this.disableInteraction,
|
||||
});
|
||||
|
||||
final MapEntry<int, Choice> entry;
|
||||
|
|
@ -81,6 +115,9 @@ class ChoiceItem extends StatelessWidget {
|
|||
final void Function(int p1)? onLongPress;
|
||||
final void Function(int p1) onPressed;
|
||||
final bool isSelected;
|
||||
final bool interactionDisabled;
|
||||
final VoidCallback enableInteraction;
|
||||
final VoidCallback disableInteraction;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -94,6 +131,8 @@ class ChoiceItem extends StatelessWidget {
|
|||
key: ValueKey(entry.value.text),
|
||||
selected: entry.value.color != null,
|
||||
isGold: entry.value.isGold,
|
||||
enableInteraction: enableInteraction,
|
||||
disableInteraction: disableInteraction,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(2),
|
||||
padding: EdgeInsets.zero,
|
||||
|
|
@ -109,27 +148,29 @@ class ChoiceItem extends StatelessWidget {
|
|||
: null,
|
||||
child: TextButton(
|
||||
style: ButtonStyle(
|
||||
padding: MaterialStateProperty.all(
|
||||
padding: WidgetStateProperty.all(
|
||||
const EdgeInsets.symmetric(horizontal: 7),
|
||||
),
|
||||
//if index is selected, then give the background a slight primary color
|
||||
backgroundColor: MaterialStateProperty.all<Color>(
|
||||
backgroundColor: WidgetStateProperty.all<Color>(
|
||||
entry.value.color != null
|
||||
? entry.value.color!.withOpacity(0.2)
|
||||
: theme.colorScheme.primary.withOpacity(0.1),
|
||||
),
|
||||
textStyle: MaterialStateProperty.all(
|
||||
textStyle: WidgetStateProperty.all(
|
||||
BotStyle.text(context),
|
||||
),
|
||||
shape: MaterialStateProperty.all(
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
onLongPress:
|
||||
onLongPress != null ? () => onLongPress!(entry.key) : null,
|
||||
onPressed: () => onPressed(entry.key),
|
||||
onLongPress: onLongPress != null && !interactionDisabled
|
||||
? () => onLongPress!(entry.key)
|
||||
: null,
|
||||
onPressed:
|
||||
interactionDisabled ? null : () => onPressed(entry.key),
|
||||
child: Text(
|
||||
entry.value.text,
|
||||
style: BotStyle.text(context),
|
||||
|
|
@ -149,11 +190,15 @@ class ChoiceAnimationWidget extends StatefulWidget {
|
|||
final Widget child;
|
||||
final bool selected;
|
||||
final bool isGold;
|
||||
final VoidCallback enableInteraction;
|
||||
final VoidCallback disableInteraction;
|
||||
|
||||
const ChoiceAnimationWidget({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.selected,
|
||||
required this.enableInteraction,
|
||||
required this.disableInteraction,
|
||||
this.isGold = false,
|
||||
});
|
||||
|
||||
|
|
@ -161,11 +206,13 @@ class ChoiceAnimationWidget extends StatefulWidget {
|
|||
ChoiceAnimationWidgetState createState() => ChoiceAnimationWidgetState();
|
||||
}
|
||||
|
||||
enum AnimationState { ready, forward, reverse, finished }
|
||||
|
||||
class ChoiceAnimationWidgetState extends State<ChoiceAnimationWidget>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
late final Animation<double> _animation;
|
||||
bool animationPlayed = false;
|
||||
AnimationState animationState = AnimationState.ready;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -177,33 +224,45 @@ class ChoiceAnimationWidgetState extends State<ChoiceAnimationWidget>
|
|||
);
|
||||
|
||||
_animation = widget.isGold
|
||||
? Tween<double>(begin: 1.0, end: 1.2).animate(_controller)
|
||||
: TweenSequence<double>([
|
||||
TweenSequenceItem<double>(
|
||||
tween: Tween<double>(begin: 0, end: -8 * pi / 180),
|
||||
weight: 1.0,
|
||||
),
|
||||
TweenSequenceItem<double>(
|
||||
tween: Tween<double>(begin: -8 * pi / 180, end: 16 * pi / 180),
|
||||
weight: 2.0,
|
||||
),
|
||||
TweenSequenceItem<double>(
|
||||
tween: Tween<double>(begin: 16 * pi / 180, end: 0),
|
||||
weight: 1.0,
|
||||
),
|
||||
]).animate(_controller);
|
||||
? Tween<double>(begin: 1.0, end: 1.2).animate(_controller)
|
||||
: TweenSequence<double>([
|
||||
TweenSequenceItem<double>(
|
||||
tween: Tween<double>(begin: 0, end: -8 * pi / 180),
|
||||
weight: 1.0,
|
||||
),
|
||||
TweenSequenceItem<double>(
|
||||
tween: Tween<double>(begin: -8 * pi / 180, end: 16 * pi / 180),
|
||||
weight: 2.0,
|
||||
),
|
||||
TweenSequenceItem<double>(
|
||||
tween: Tween<double>(begin: 16 * pi / 180, end: 0),
|
||||
weight: 1.0,
|
||||
),
|
||||
]).animate(_controller);
|
||||
|
||||
if (widget.selected && !animationPlayed) {
|
||||
widget.enableInteraction();
|
||||
|
||||
if (widget.selected && animationState == AnimationState.ready) {
|
||||
widget.disableInteraction();
|
||||
_controller.forward();
|
||||
animationPlayed = true;
|
||||
setState(() {});
|
||||
setState(() {
|
||||
animationState = AnimationState.forward;
|
||||
});
|
||||
}
|
||||
_controller.addStatusListener((status) {
|
||||
if (status == AnimationStatus.completed) {
|
||||
if (status == AnimationStatus.completed &&
|
||||
animationState == AnimationState.forward) {
|
||||
_controller.reverse();
|
||||
} else if (status == AnimationStatus.dismissed) {
|
||||
_controller.stop();
|
||||
_controller.reset();
|
||||
setState(() {
|
||||
animationState = AnimationState.reverse;
|
||||
});
|
||||
}
|
||||
if (status == AnimationStatus.dismissed &&
|
||||
animationState == AnimationState.reverse) {
|
||||
widget.enableInteraction();
|
||||
setState(() {
|
||||
animationState = AnimationState.finished;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -211,38 +270,40 @@ class ChoiceAnimationWidgetState extends State<ChoiceAnimationWidget>
|
|||
@override
|
||||
void didUpdateWidget(ChoiceAnimationWidget oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.selected && !animationPlayed) {
|
||||
if (widget.selected && animationState == AnimationState.ready) {
|
||||
widget.disableInteraction();
|
||||
_controller.forward();
|
||||
animationPlayed = true;
|
||||
setState(() {});
|
||||
setState(() {
|
||||
animationState = AnimationState.forward;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.isGold
|
||||
? AnimatedBuilder(
|
||||
key: UniqueKey(),
|
||||
animation: _animation,
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: _animation.value,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: widget.child,
|
||||
)
|
||||
: AnimatedBuilder(
|
||||
key: UniqueKey(),
|
||||
animation: _animation,
|
||||
builder: (context, child) {
|
||||
return Transform.rotate(
|
||||
angle: _animation.value,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: widget.child,
|
||||
);
|
||||
? AnimatedBuilder(
|
||||
key: UniqueKey(),
|
||||
animation: _animation,
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: _animation.value,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: widget.child,
|
||||
)
|
||||
: AnimatedBuilder(
|
||||
key: UniqueKey(),
|
||||
animation: _animation,
|
||||
builder: (context, child) {
|
||||
return Transform.rotate(
|
||||
angle: _animation.value,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:fluffychat/pangea/choreographer/widgets/it_feedback_card.dart';
|
|||
import 'package:fluffychat/pangea/choreographer/widgets/translation_finished_flow.dart';
|
||||
import 'package:fluffychat/pangea/constants/choreo_constants.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
|
@ -21,93 +22,107 @@ class ITBar extends StatelessWidget {
|
|||
final Choreographer choreographer;
|
||||
const ITBar({super.key, required this.choreographer});
|
||||
|
||||
ITController get controller => choreographer.itController;
|
||||
ITController get itController => choreographer.itController;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!controller.isOpen) return const SizedBox();
|
||||
|
||||
return CompositedTransformTarget(
|
||||
link: choreographer.itBarLinkAndKey.link,
|
||||
child: Container(
|
||||
key: choreographer.itBarLinkAndKey.key,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).brightness == Brightness.light
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(AppConfig.borderRadius),
|
||||
topRight: Radius.circular(AppConfig.borderRadius),
|
||||
),
|
||||
),
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.fromLTRB(0, 3, 3, 3),
|
||||
child: Stack(
|
||||
children: [
|
||||
SingleChildScrollView(
|
||||
child: Column(
|
||||
return AnimatedSize(
|
||||
duration: itController.willOpen
|
||||
? const Duration(milliseconds: 2000)
|
||||
: const Duration(milliseconds: 500),
|
||||
curve: Curves.fastOutSlowIn,
|
||||
clipBehavior: Clip.none,
|
||||
child: !itController.willOpen
|
||||
? const SizedBox()
|
||||
: CompositedTransformTarget(
|
||||
link: choreographer.itBarLinkAndKey.link,
|
||||
child: AnimatedOpacity(
|
||||
duration: itController.willOpen
|
||||
? const Duration(milliseconds: 2000)
|
||||
: const Duration(milliseconds: 500),
|
||||
opacity: itController.willOpen ? 1.0 : 0.0,
|
||||
child: Container(
|
||||
key: choreographer.itBarLinkAndKey.key,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).brightness == Brightness.light
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(AppConfig.borderRadius),
|
||||
topRight: Radius.circular(AppConfig.borderRadius),
|
||||
),
|
||||
),
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.fromLTRB(0, 3, 3, 3),
|
||||
child: Stack(
|
||||
children: [
|
||||
// Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// children: [
|
||||
// // Row(
|
||||
// // mainAxisAlignment: MainAxisAlignment.start,
|
||||
// // crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// // children: [
|
||||
// // CounterDisplay(
|
||||
// // correct: controller.correctChoices,
|
||||
// // custom: controller.customChoices,
|
||||
// // incorrect: controller.incorrectChoices,
|
||||
// // yellow: controller.wildcardChoices,
|
||||
// // ),
|
||||
// // CompositedTransformTarget(
|
||||
// // link: choreographer.itBotLayerLinkAndKey.link,
|
||||
// // child: ITBotButton(
|
||||
// // key: choreographer.itBotLayerLinkAndKey.key,
|
||||
// // choreographer: choreographer,
|
||||
// // ),
|
||||
// // ),
|
||||
// // ],
|
||||
// // ),
|
||||
// ITCloseButton(choreographer: choreographer),
|
||||
// ],
|
||||
// ),
|
||||
// const SizedBox(height: 40.0),
|
||||
OriginalText(controller: controller),
|
||||
const SizedBox(height: 7.0),
|
||||
IntrinsicHeight(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(minHeight: 80),
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: Center(
|
||||
child: controller.choreographer.errorService.isError
|
||||
? ITError(
|
||||
error: controller
|
||||
SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
// Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// children: [
|
||||
// // Row(
|
||||
// // mainAxisAlignment: MainAxisAlignment.start,
|
||||
// // crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// // children: [
|
||||
// // CounterDisplay(
|
||||
// // correct: controller.correctChoices,
|
||||
// // custom: controller.customChoices,
|
||||
// // incorrect: controller.incorrectChoices,
|
||||
// // yellow: controller.wildcardChoices,
|
||||
// // ),
|
||||
// // CompositedTransformTarget(
|
||||
// // link: choreographer.itBotLayerLinkAndKey.link,
|
||||
// // child: ITBotButton(
|
||||
// // key: choreographer.itBotLayerLinkAndKey.key,
|
||||
// // choreographer: choreographer,
|
||||
// // ),
|
||||
// // ),
|
||||
// // ],
|
||||
// // ),
|
||||
// ITCloseButton(choreographer: choreographer),
|
||||
// ],
|
||||
// ),
|
||||
// const SizedBox(height: 40.0),
|
||||
OriginalText(controller: itController),
|
||||
const SizedBox(height: 7.0),
|
||||
IntrinsicHeight(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(minHeight: 80),
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: Center(
|
||||
child: itController.choreographer.errorService.isError
|
||||
? ITError(
|
||||
error: itController
|
||||
.choreographer.errorService.error!,
|
||||
controller: controller,
|
||||
controller: itController,
|
||||
)
|
||||
: controller.showChoiceFeedback
|
||||
? ChoiceFeedbackText(controller: controller)
|
||||
: controller.isTranslationDone
|
||||
? TranslationFeedback(
|
||||
controller: controller,
|
||||
)
|
||||
: ITChoices(controller: controller),
|
||||
),
|
||||
: itController.showChoiceFeedback
|
||||
? ChoiceFeedbackText(controller: itController)
|
||||
: itController.isTranslationDone
|
||||
? TranslationFeedback(
|
||||
controller: itController,
|
||||
)
|
||||
: ITChoices(controller: itController),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 0.0,
|
||||
right: 0.0,
|
||||
child: ITCloseButton(choreographer: choreographer),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 0.0,
|
||||
right: 0.0,
|
||||
child: ITCloseButton(choreographer: choreographer),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -184,10 +199,23 @@ class OriginalText extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
if (!controller.isEditingSourceText && controller.sourceText != null)
|
||||
IconButton(
|
||||
onPressed: () => controller.setIsEditingSourceText(true),
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
if (
|
||||
!controller.isEditingSourceText
|
||||
&& controller.sourceText != null
|
||||
)
|
||||
AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
opacity: controller.nextITStep != null
|
||||
? 1.0
|
||||
: 0.0,
|
||||
child: IconButton(
|
||||
onPressed: () => {
|
||||
if (controller.nextITStep != null) {
|
||||
controller.setIsEditingSourceText(true),
|
||||
},
|
||||
},
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/instructions_enum.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/utils/instructions.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import '../../widgets/common/bot_face_svg.dart';
|
||||
import '../controllers/choreographer.dart';
|
||||
import '../controllers/it_controller.dart';
|
||||
|
|
@ -37,7 +37,7 @@ class ITBotButton extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
choreographer.pangeaController.instructions.show(
|
||||
choreographer.pangeaController.instructions.showInstructionsPopup(
|
||||
context,
|
||||
InstructionsEnum.itInstructions,
|
||||
choreographer.itBotTransformTargetKey,
|
||||
|
|
@ -45,8 +45,9 @@ class ITBotButton extends StatelessWidget {
|
|||
);
|
||||
|
||||
return IconButton(
|
||||
icon: const BotFace(width: 40.0, expression: BotExpression.right),
|
||||
onPressed: () => choreographer.pangeaController.instructions.show(
|
||||
icon: const BotFace(width: 40.0, expression: BotExpression.idle),
|
||||
onPressed: () =>
|
||||
choreographer.pangeaController.instructions.showInstructionsPopup(
|
||||
context,
|
||||
InstructionsEnum.itInstructions,
|
||||
choreographer.itBotTransformTargetKey,
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ class ITFeedbackCardView extends StatelessWidget {
|
|||
children: [
|
||||
CardHeader(
|
||||
text: controller.widget.req.chosenContinuance,
|
||||
botExpression: BotExpression.down,
|
||||
botExpression: BotExpression.nonGold,
|
||||
),
|
||||
Text(
|
||||
controller.widget.choiceFeedback,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import 'dart:developer';
|
|||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
||||
import 'package:fluffychat/pangea/models/class_model.dart';
|
||||
import 'package:fluffychat/pangea/models/space_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
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:fluffychat/pangea/controllers/subscription_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/assistance_state_enum.dart';
|
||||
import 'package:fluffychat/pangea/widgets/user_settings/p_language_dialog.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
|
|
@ -53,15 +53,15 @@ class StartIGCButtonState extends State<StartIGCButton>
|
|||
setState(() => prevState = assistanceState);
|
||||
}
|
||||
|
||||
bool get itEnabled => widget.controller.choreographer.itEnabled;
|
||||
bool get igcEnabled => widget.controller.choreographer.igcEnabled;
|
||||
CanSendStatus get canSendStatus =>
|
||||
widget.controller.pangeaController.subscriptionController.canSendStatus;
|
||||
bool get grammarCorrectionEnabled =>
|
||||
(itEnabled || igcEnabled) && canSendStatus == CanSendStatus.subscribed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool itEnabled = widget.controller.choreographer.itEnabled;
|
||||
final bool igcEnabled = widget.controller.choreographer.igcEnabled;
|
||||
final CanSendStatus canSendStatus =
|
||||
widget.controller.pangeaController.subscriptionController.canSendStatus;
|
||||
final bool grammarCorrectionEnabled =
|
||||
(itEnabled || igcEnabled) && canSendStatus == CanSendStatus.subscribed;
|
||||
|
||||
if (!grammarCorrectionEnabled ||
|
||||
widget.controller.choreographer.isAutoIGCEnabled ||
|
||||
widget.controller.choreographer.choreoMode == ChoreoMode.it) {
|
||||
|
|
@ -77,92 +77,67 @@ class StartIGCButtonState extends State<StartIGCButton>
|
|||
return SizedBox(
|
||||
height: 50,
|
||||
width: 50,
|
||||
child: FloatingActionButton(
|
||||
tooltip: assistanceState.tooltip(
|
||||
L10n.of(context)!,
|
||||
),
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
disabledElevation: 0,
|
||||
shape: const CircleBorder(),
|
||||
onPressed: () {
|
||||
if (assistanceState != AssistanceState.complete) {
|
||||
widget.controller.choreographer
|
||||
.getLanguageHelp(
|
||||
false,
|
||||
true,
|
||||
)
|
||||
.then((_) {
|
||||
if (widget.controller.choreographer.igc.igcTextData != null &&
|
||||
widget.controller.choreographer.igc.igcTextData!.matches
|
||||
.isNotEmpty) {
|
||||
widget.controller.choreographer.igc.showFirstMatch(context);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
child: Stack(
|
||||
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: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
child: InkWell(
|
||||
customBorder: const CircleBorder(),
|
||||
onLongPress: () => pLanguageDialog(context, () {}),
|
||||
child: FloatingActionButton(
|
||||
tooltip: assistanceState.tooltip(
|
||||
L10n.of(context)!,
|
||||
),
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
disabledElevation: 0,
|
||||
shape: const CircleBorder(),
|
||||
onPressed: () {
|
||||
if (assistanceState != AssistanceState.fetching) {
|
||||
widget.controller.choreographer
|
||||
.getLanguageHelp(
|
||||
onlyTokensAndLanguageDetection: false,
|
||||
manual: true,
|
||||
)
|
||||
.then((_) {
|
||||
if (widget.controller.choreographer.igc.igcTextData != null &&
|
||||
widget.controller.choreographer.igc.igcTextData!.matches
|
||||
.isNotEmpty) {
|
||||
widget.controller.choreographer.igc.showFirstMatch(context);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
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: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: 20,
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: assistanceState.stateColor(context),
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
size: 16,
|
||||
Icons.check,
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: 20,
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: assistanceState.stateColor(context),
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
size: 16,
|
||||
Icons.check,
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension AssistanceStateExtension on AssistanceState {
|
||||
Color stateColor(context) {
|
||||
switch (this) {
|
||||
case AssistanceState.noMessage:
|
||||
case AssistanceState.notFetched:
|
||||
case AssistanceState.fetching:
|
||||
return Theme.of(context).colorScheme.primary;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ class Environment {
|
|||
DateTime.utc(2023, 1, 25).isBefore(DateTime.now());
|
||||
|
||||
static String get fileName {
|
||||
return ".env";
|
||||
return ".local_choreo.env";
|
||||
}
|
||||
|
||||
static bool get isStaging => synapsURL.contains("staging");
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
class PrefKey {
|
||||
static const lastFetched = 'LAST_FETCHED';
|
||||
static const flags = 'flags';
|
||||
}
|
||||
24
lib/pangea/constants/language_constants.dart
Normal file
24
lib/pangea/constants/language_constants.dart
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import 'package:fluffychat/pangea/models/language_detection_model.dart';
|
||||
|
||||
class LanguageKeys {
|
||||
static const unknownLanguage = "unk";
|
||||
static const mixedLanguage = "mixed";
|
||||
static const defaultLanguage = "en";
|
||||
static const multiLanguage = "multi";
|
||||
}
|
||||
|
||||
class LanguageLevelType {
|
||||
static List<int> get allInts => [0, 1, 2, 3, 4, 5, 6];
|
||||
}
|
||||
|
||||
class PrefKey {
|
||||
static const lastFetched = 'p_lang_lastfetched';
|
||||
static const flags = 'p_lang_flag';
|
||||
}
|
||||
|
||||
final LanguageDetection unknownLanguageDetection = LanguageDetection(
|
||||
langCode: LanguageKeys.unknownLanguage,
|
||||
confidence: 0.5,
|
||||
);
|
||||
|
||||
const double languageDetectionConfidenceThreshold = 0.95;
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
class LanguageKeys {
|
||||
static const unknownLanguage = "unk";
|
||||
static const mixedLanguage = "mixed";
|
||||
static const defaultLanguage = "en";
|
||||
static const multiLanguage = "multi";
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
class LanguageLevelType {
|
||||
static List<int> get allInts => [0, 1, 2, 3, 4, 5, 6];
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
class PrefKey {
|
||||
static const lastFetched = 'p_lang_lastfetched';
|
||||
static const flags = 'p_lang_flag';
|
||||
}
|
||||
|
|
@ -11,5 +11,6 @@ class PLocalKey {
|
|||
static const String dismissedPaywall = 'dismissedPaywall';
|
||||
static const String paywallBackoff = 'paywallBackoff';
|
||||
static const String autoPlayMessages = 'autoPlayMessages';
|
||||
static const String itAutoPlay = 'itAutoPlay';
|
||||
static const String messagesSinceUpdate = 'messagesSinceLastUpdate';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,11 +27,8 @@ class ModelKey {
|
|||
|
||||
static const String clientIsPublic = "isPublic";
|
||||
static const String clientIsOpenEnrollment = 'isOpenEnrollment';
|
||||
static const String clientIsOpenExchange = 'isOpenExchange';
|
||||
static const String clientIsOneToOneChatClass = 'oneToOneChatClass';
|
||||
static const String clientIsOneToOneChatExchange = 'oneToOneChatExchange';
|
||||
static const String clientIsCreateRooms = 'isCreateRooms';
|
||||
static const String clientIsCreateRoomsExchange = 'isCreateRoomsExchange';
|
||||
static const String clientIsShareVideo = 'isShareVideo';
|
||||
static const String clientIsSharePhoto = 'isSharePhoto';
|
||||
static const String clientIsShareFiles = 'isShareFiles';
|
||||
|
|
@ -39,7 +36,6 @@ class ModelKey {
|
|||
static const String clientIsCreateStories = 'isCreateStories';
|
||||
static const String clientIsVoiceNotes = 'isVoiceNotes';
|
||||
static const String clientIsInviteOnlyStudents = 'isInviteOnlyStudents';
|
||||
static const String clientIsInviteOnlyExchanges = 'isInviteOnlyExchanges';
|
||||
|
||||
static const String userL1 = "user_l1";
|
||||
static const String userL2 = "user_l2";
|
||||
|
|
@ -70,7 +66,6 @@ class ModelKey {
|
|||
static const String tokensSent = "tokens_sent";
|
||||
static const String tokensWritten = "tokens_written";
|
||||
static const String choreoRecord = "choreo_record";
|
||||
static const String useType = "use_type";
|
||||
|
||||
static const String baseDefinition = "base_definition";
|
||||
static const String targetDefinition = "target_definition";
|
||||
|
|
@ -99,17 +94,16 @@ class ModelKey {
|
|||
static const String languageLevel = "difficulty";
|
||||
static const String safetyModeration = "safety_moderation";
|
||||
static const String mode = "mode";
|
||||
static const String custom = "custom";
|
||||
static const String discussionTopic = "discussion_topic";
|
||||
static const String discussionKeywords = "discussion_keywords";
|
||||
static const String discussionTriggerScheduleEnabled =
|
||||
"discussion_trigger_schedule_enabled";
|
||||
static const String discussionTriggerScheduleHourInterval =
|
||||
"discussion_trigger_schedule_hour_interval";
|
||||
static const String discussionTriggerReactionEnabled =
|
||||
"discussion_trigger_reaction_enabled";
|
||||
static const String discussionTriggerReactionKey =
|
||||
"discussion_trigger_reaction_key";
|
||||
static const String customSystemPrompt = "custom_system_prompt";
|
||||
static const String customTriggerReactionEnabled =
|
||||
"custom_trigger_reaction_enabled";
|
||||
static const String customTriggerReactionKey = "custom_trigger_reaction_key";
|
||||
|
||||
static const String prevEventId = "prev_event_id";
|
||||
static const String prevLastUpdated = "prev_last_updated";
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
class PangeaEventTypes {
|
||||
static const classSettings = "pangea.class";
|
||||
static const pangeaExchange = "p.exchange";
|
||||
static const languageSettings = "pangea.class";
|
||||
|
||||
static const transcript = "pangea.transcript";
|
||||
|
||||
|
|
@ -26,4 +25,14 @@ class PangeaEventTypes {
|
|||
|
||||
static const String report = 'm.report';
|
||||
static const textToSpeechRule = "p.rule.text_to_speech";
|
||||
|
||||
/// A request to the server to generate activities
|
||||
static const activityRequest = "pangea.activity_req";
|
||||
|
||||
/// A practice activity that is related to a message
|
||||
static const pangeaActivity = "pangea.activity_res";
|
||||
|
||||
/// A record of completion of an activity. There
|
||||
/// can be one per user per activity.
|
||||
static const activityRecord = "pangea.activity_completion";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
class PangeaRoomTypes {
|
||||
static const analytics = 'p.analytics';
|
||||
static const exchange = 'p.exchange';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
|||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/extensions/client_extension/client_extension.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/utils/class_code.dart';
|
||||
import 'package:fluffychat/pangea/models/space_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/utils/space_code.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
|
@ -34,12 +34,11 @@ class ClassController extends BaseController {
|
|||
|
||||
Future<void> fixClassPowerLevels() async {
|
||||
try {
|
||||
final List<Future<void>> classFixes = [];
|
||||
final teacherSpaces = await _pangeaController
|
||||
.matrixState.client.classesAndExchangesImTeaching;
|
||||
for (final room in teacherSpaces) {
|
||||
classFixes.add(room.setClassPowerLevels());
|
||||
}
|
||||
final teacherSpaces =
|
||||
await _pangeaController.matrixState.client.spacesImTeaching;
|
||||
final List<Future<void>> classFixes = List<Room>.from(teacherSpaces)
|
||||
.map((adminSpace) => adminSpace.setClassPowerLevels())
|
||||
.toList();
|
||||
await Future.wait(classFixes);
|
||||
} catch (err, stack) {
|
||||
debugger(when: kDebugMode);
|
||||
|
|
@ -65,7 +64,7 @@ class ClassController extends BaseController {
|
|||
classCode,
|
||||
).onError(
|
||||
(error, stackTrace) =>
|
||||
ClassCodeUtil.messageSnack(context, ErrorCopy(context, error).body),
|
||||
SpaceCodeUtil.messageSnack(context, ErrorCopy(context, error).body),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -78,8 +77,7 @@ class ClassController extends BaseController {
|
|||
if (!room.isDirectChat) return [];
|
||||
final List<String> existingParentsIds =
|
||||
room.pangeaSpaceParents.map((e) => e.id).toList();
|
||||
final List<Room> spaces =
|
||||
_pangeaController.matrixState.client.classesAndExchangesImIn;
|
||||
final List<Room> spaces = _pangeaController.matrixState.client.spacesImIn;
|
||||
|
||||
//make sure we have the latest participants
|
||||
await Future.wait(spaces.map((e) => e.requestParticipants()));
|
||||
|
|
@ -121,7 +119,7 @@ class ClassController extends BaseController {
|
|||
});
|
||||
|
||||
if (classChunk == null) {
|
||||
ClassCodeUtil.messageSnack(
|
||||
SpaceCodeUtil.messageSnack(
|
||||
context,
|
||||
L10n.of(context)!.unableToFindClass,
|
||||
);
|
||||
|
|
@ -131,7 +129,7 @@ class ClassController extends BaseController {
|
|||
if (_pangeaController.matrixState.client.rooms
|
||||
.any((room) => room.id == classChunk.roomId)) {
|
||||
setActiveSpaceIdInChatListController(classChunk.roomId);
|
||||
ClassCodeUtil.messageSnack(context, L10n.of(context)!.alreadyInClass);
|
||||
SpaceCodeUtil.messageSnack(context, L10n.of(context)!.alreadyInClass);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -170,9 +168,6 @@ class ClassController extends BaseController {
|
|||
final Room? joinedSpace =
|
||||
_pangeaController.matrixState.client.getRoomById(classChunk.roomId);
|
||||
|
||||
// ensure that the user has an analytics room for this space's language
|
||||
await joinedSpace?.ensureAnalyticsRoomExists();
|
||||
|
||||
// when possible, add user's analytics room the to space they joined
|
||||
await joinedSpace?.addAnalyticsRoomsToSpace();
|
||||
|
||||
|
|
@ -181,7 +176,7 @@ class ClassController extends BaseController {
|
|||
GoogleAnalytics.joinClass(classCode);
|
||||
return;
|
||||
} catch (err) {
|
||||
ClassCodeUtil.messageSnack(
|
||||
SpaceCodeUtil.messageSnack(
|
||||
context,
|
||||
ErrorCopy(context, err).body,
|
||||
);
|
||||
|
|
@ -199,7 +194,7 @@ class ClassController extends BaseController {
|
|||
final Room? room = _pangeaController.matrixState.client.getRoomById(roomId);
|
||||
if (room == null) return;
|
||||
|
||||
if (room.classSettings != null && room.pangeaRoomRules == null) {
|
||||
if (room.isSpace && room.isRoomAdmin && room.pangeaRoomRules == null) {
|
||||
try {
|
||||
await _pangeaController.matrixState.client.setRoomStateWithKey(
|
||||
roomId,
|
||||
|
|
|
|||
|
|
@ -1,14 +1,11 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/language_keys.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/controllers/language_list_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.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/language_model.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import '../widgets/user_settings/p_language_dialog.dart';
|
||||
|
|
@ -63,50 +60,51 @@ class LanguageController {
|
|||
return _userL2Code != null ? PangeaLanguage.byLangCode(_userL2Code!) : null;
|
||||
}
|
||||
|
||||
String? activeL1Code({String? roomID}) {
|
||||
final String? activeL2 = activeL2Code(roomID: roomID);
|
||||
if (roomID == null || activeL2 != _userL1Code) {
|
||||
return _userL1Code;
|
||||
}
|
||||
final ClassSettingsModel? classContext = _pangeaController
|
||||
.matrixState.client
|
||||
.getRoomById(roomID)
|
||||
?.firstLanguageSettings;
|
||||
final String? classL1 = classContext?.dominantLanguage;
|
||||
if (classL1 == LanguageKeys.mixedLanguage ||
|
||||
classL1 == LanguageKeys.multiLanguage ||
|
||||
classL1 == null) {
|
||||
if (_userL2Code != _userL1Code) {
|
||||
return _userL2Code;
|
||||
}
|
||||
return LanguageKeys.unknownLanguage;
|
||||
}
|
||||
return classL1;
|
||||
String? activeL1Code() {
|
||||
return _userL1Code;
|
||||
// final String? activeL2 = activeL2Code(roomID: roomID);
|
||||
// if (roomID == null || activeL2 != _userL1Code) {
|
||||
// return _userL1Code;
|
||||
// }
|
||||
// final LanguageSettingsModel? classContext = _pangeaController
|
||||
// .matrixState.client
|
||||
// .getRoomById(roomID)
|
||||
// ?.firstLanguageSettings;
|
||||
// final String? classL1 = classContext?.dominantLanguage;
|
||||
// if (classL1 == LanguageKeys.mixedLanguage ||
|
||||
// classL1 == LanguageKeys.multiLanguage ||
|
||||
// classL1 == null) {
|
||||
// if (_userL2Code != _userL1Code) {
|
||||
// return _userL2Code;
|
||||
// }
|
||||
// return LanguageKeys.unknownLanguage;
|
||||
// }
|
||||
// return classL1;
|
||||
}
|
||||
|
||||
/// Class languages override user languages within a class context
|
||||
String? activeL2Code({String? roomID}) {
|
||||
if (roomID == null) {
|
||||
return _userL2Code;
|
||||
}
|
||||
final ClassSettingsModel? classContext = _pangeaController
|
||||
.matrixState.client
|
||||
.getRoomById(roomID)
|
||||
?.firstLanguageSettings;
|
||||
return classContext?.targetLanguage ?? _userL2Code;
|
||||
String? activeL2Code() {
|
||||
return _userL2Code;
|
||||
// if (roomID == null) {
|
||||
// return _userL2Code;
|
||||
// }
|
||||
// final LanguageSettingsModel? classContext = _pangeaController
|
||||
// .matrixState.client
|
||||
// .getRoomById(roomID)
|
||||
// ?.firstLanguageSettings;
|
||||
// return classContext?.targetLanguage ?? _userL2Code;
|
||||
}
|
||||
|
||||
LanguageModel? activeL1Model({String? roomID}) {
|
||||
final activeL1 = activeL1Code(roomID: roomID);
|
||||
return activeL1 != null ? PangeaLanguage.byLangCode(activeL1) : null;
|
||||
LanguageModel? activeL1Model() {
|
||||
return userL1;
|
||||
// final activeL1 = activeL1Code(roomID: roomID);
|
||||
// return activeL1 != null ? PangeaLanguage.byLangCode(activeL1) : null;
|
||||
}
|
||||
|
||||
LanguageModel? activeL2Model({String? roomID}) {
|
||||
final activeL2 = activeL2Code(roomID: roomID);
|
||||
final model = activeL2 != null ? PangeaLanguage.byLangCode(activeL2) : null;
|
||||
return model;
|
||||
LanguageModel? activeL2Model() {
|
||||
return userL2;
|
||||
// final activeL2 = activeL2Code(roomID: roomID);
|
||||
// final model = activeL2 != null ? PangeaLanguage.byLangCode(activeL2) : null;
|
||||
// return model;
|
||||
}
|
||||
|
||||
bool equalActiveL1AndActiveL2({Room? room}) =>
|
||||
activeL1Code() == activeL2Code(roomID: room?.id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'dart:async';
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:fluffychat/pangea/config/environment.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_constants.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';
|
||||
|
|
@ -75,19 +76,21 @@ class LanguageDetectionResponse {
|
|||
};
|
||||
}
|
||||
|
||||
LanguageDetection? get _bestDetection {
|
||||
/// Return the highest confidence detection.
|
||||
/// If there are no detections, the unknown language detection is returned.
|
||||
LanguageDetection get highestConfidenceDetection {
|
||||
detections.sort((a, b) => b.confidence.compareTo(a.confidence));
|
||||
return detections.isNotEmpty ? detections.first : null;
|
||||
return detections.firstOrNull ?? unknownLanguageDetection;
|
||||
}
|
||||
|
||||
final double _confidenceThreshold = 0.95;
|
||||
|
||||
LanguageDetection? bestDetection({double? threshold}) {
|
||||
threshold ??= _confidenceThreshold;
|
||||
return (_bestDetection?.confidence ?? 0) >= _confidenceThreshold
|
||||
? _bestDetection!
|
||||
: null;
|
||||
}
|
||||
/// Returns the highest validated detection based on the confidence threshold.
|
||||
/// If the highest confidence detection is below the threshold, the unknown language
|
||||
/// detection is returned.
|
||||
LanguageDetection highestValidatedDetection({double? threshold}) =>
|
||||
highestConfidenceDetection.confidence >=
|
||||
(threshold ?? languageDetectionConfidenceThreshold)
|
||||
? highestConfidenceDetection
|
||||
: unknownLanguageDetection;
|
||||
}
|
||||
|
||||
class _LanguageDetectionCacheItem {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/language_keys.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/models/language_model.dart';
|
||||
import 'package:fluffychat/pangea/repo/language_repo.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../constants/language_list_keys.dart';
|
||||
import '../utils/shared_prefs.dart';
|
||||
|
||||
class PangeaLanguage {
|
||||
|
|
@ -27,7 +26,7 @@ class PangeaLanguage {
|
|||
|
||||
static Future<void> initialize() async {
|
||||
try {
|
||||
_langList = await _getCahedFlags();
|
||||
_langList = await _getCachedFlags();
|
||||
if (await _shouldFetch || _langList.isEmpty) {
|
||||
_langList = await LanguageRepo.fetchLanguages();
|
||||
|
||||
|
|
@ -77,7 +76,7 @@ class PangeaLanguage {
|
|||
await MyShared.saveJson(PrefKey.flags, flagMap);
|
||||
}
|
||||
|
||||
static Future<List<LanguageModel>> _getCahedFlags() async {
|
||||
static Future<List<LanguageModel>> _getCachedFlags() async {
|
||||
final Map<dynamic, dynamic>? flagsMap =
|
||||
await MyShared.readJson(PrefKey.flags);
|
||||
if (flagsMap == null) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/models/class_model.dart';
|
||||
import 'package:fluffychat/pangea/models/space_model.dart';
|
||||
|
||||
class LocalSettings {
|
||||
late PangeaController _pangeaController;
|
||||
|
|
|
|||
|
|
@ -4,11 +4,13 @@ import 'dart:developer';
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/pangea/constants/match_rule_ids.dart';
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/controllers/language_list_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/time_span.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/analytics_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/summary_analytics_event.dart';
|
||||
import 'package:fluffychat/pangea/models/language_model.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/base_analytics.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
|
@ -60,35 +62,65 @@ class AnalyticsController extends BaseController {
|
|||
timeSpan.toString(),
|
||||
local: true,
|
||||
);
|
||||
setState();
|
||||
}
|
||||
|
||||
///////// SPACE ANALYTICS LANGUAGES //////////
|
||||
String get _analyticsSpaceLangKey => "ANALYTICS_SPACE_LANG_KEY";
|
||||
|
||||
LanguageModel get currentAnalyticsLang {
|
||||
try {
|
||||
final String? str = _pangeaController.pStoreService.read(
|
||||
_analyticsSpaceLangKey,
|
||||
local: true,
|
||||
);
|
||||
return str != null
|
||||
? PangeaLanguage.byLangCode(str)
|
||||
: _pangeaController.languageController.userL2 ??
|
||||
_pangeaController.pLanguageStore.targetOptions.first;
|
||||
} catch (err) {
|
||||
debugger(when: kDebugMode);
|
||||
return _pangeaController.pLanguageStore.targetOptions.first;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setCurrentAnalyticsLang(LanguageModel lang) async {
|
||||
await _pangeaController.pStoreService.save(
|
||||
_analyticsSpaceLangKey,
|
||||
lang.langCode,
|
||||
local: true,
|
||||
);
|
||||
setState();
|
||||
}
|
||||
|
||||
/// given an analytics event type and the current analytics language,
|
||||
/// get the last time the user updated their analytics
|
||||
Future<DateTime?> myAnalyticsLastUpdated(String type) async {
|
||||
// given an analytics event type, get the last updated times
|
||||
// for each of the user's analytics rooms and return the most recent
|
||||
// Most Recent instead of the oldest because, for instance:
|
||||
// My last Spanish event was sent 3 days ago.
|
||||
// My last English event was sent 1 day ago.
|
||||
// When I go to check if the cached data is out of date, the cached item was set 2 days ago.
|
||||
// I know there’s new data available because the English update data (the most recent) is after the cache’s creation time.
|
||||
// So, I should update the cache.
|
||||
final List<Room> analyticsRooms = _pangeaController
|
||||
.matrixState.client.allMyAnalyticsRooms
|
||||
.where((room) => room.isAnalyticsRoom)
|
||||
.toList();
|
||||
|
||||
final List<DateTime> lastUpdates = [];
|
||||
final Map<String, DateTime> langCodeLastUpdates = {};
|
||||
for (final Room analyticsRoom in analyticsRooms) {
|
||||
final String? roomLang = analyticsRoom.madeForLang;
|
||||
if (roomLang == null) continue;
|
||||
final DateTime? lastUpdated = await analyticsRoom.analyticsLastUpdated(
|
||||
type,
|
||||
_pangeaController.matrixState.client.userID!,
|
||||
);
|
||||
if (lastUpdated != null) {
|
||||
lastUpdates.add(lastUpdated);
|
||||
langCodeLastUpdates[roomLang] = lastUpdated;
|
||||
}
|
||||
}
|
||||
|
||||
if (lastUpdates.isEmpty) return null;
|
||||
return lastUpdates.reduce(
|
||||
if (langCodeLastUpdates.isEmpty) return null;
|
||||
final String? l2Code =
|
||||
_pangeaController.languageController.userL2?.langCode;
|
||||
if (l2Code != null && langCodeLastUpdates.containsKey(l2Code)) {
|
||||
return langCodeLastUpdates[l2Code];
|
||||
}
|
||||
return langCodeLastUpdates.values.reduce(
|
||||
(check, mostRecent) => check.isAfter(mostRecent) ? check : mostRecent,
|
||||
);
|
||||
}
|
||||
|
|
@ -96,7 +128,6 @@ class AnalyticsController extends BaseController {
|
|||
Future<DateTime?> spaceAnalyticsLastUpdated(
|
||||
String type,
|
||||
Room space,
|
||||
String langCode,
|
||||
) async {
|
||||
// check if any students have recently updated their analytics
|
||||
// if any have, then the cache needs to be updated
|
||||
|
|
@ -106,7 +137,7 @@ class AnalyticsController extends BaseController {
|
|||
final List<Future<DateTime?>> lastUpdatedFutures = [];
|
||||
for (final student in space.students) {
|
||||
final Room? analyticsRoom = _pangeaController.matrixState.client
|
||||
.analyticsRoomLocal(langCode, student.id);
|
||||
.analyticsRoomLocal(currentAnalyticsLang.langCode, student.id);
|
||||
if (analyticsRoom == null) continue;
|
||||
lastUpdatedFutures.add(
|
||||
analyticsRoom.analyticsLastUpdated(
|
||||
|
|
@ -132,35 +163,37 @@ class AnalyticsController extends BaseController {
|
|||
// private chat analytics to determine which children are already visible
|
||||
// in the chat list
|
||||
final Map<String, List<String>> _lastFetchedHierarchies = {};
|
||||
|
||||
void setLatestHierarchy(String spaceId, GetSpaceHierarchyResponse resp) {
|
||||
final List<String> roomIds = resp.rooms.map((room) => room.roomId).toList();
|
||||
_lastFetchedHierarchies[spaceId] = roomIds;
|
||||
}
|
||||
|
||||
Future<List<String>> getLatestSpaceHierarchy(String spaceId) async {
|
||||
if (!_lastFetchedHierarchies.containsKey(spaceId)) {
|
||||
final resp =
|
||||
await _pangeaController.matrixState.client.getSpaceHierarchy(spaceId);
|
||||
setLatestHierarchy(spaceId, resp);
|
||||
}
|
||||
return _lastFetchedHierarchies[spaceId] ?? [];
|
||||
}
|
||||
|
||||
//////////////////////////// MESSAGE SUMMARY ANALYTICS ////////////////////////////
|
||||
|
||||
/// get all the summary analytics events for the current user
|
||||
/// in the current language's analytics room
|
||||
Future<List<SummaryAnalyticsEvent>> mySummaryAnalytics() async {
|
||||
// gets all the summary analytics events for the user
|
||||
// since the current timespace's cut off date
|
||||
final analyticsRooms =
|
||||
_pangeaController.matrixState.client.allMyAnalyticsRooms;
|
||||
final Room? analyticsRoom = _pangeaController.matrixState.client
|
||||
.analyticsRoomLocal(currentAnalyticsLang.langCode);
|
||||
if (analyticsRoom == null) return [];
|
||||
|
||||
final List<SummaryAnalyticsEvent> allEvents = [];
|
||||
|
||||
// TODO switch to using list of futures
|
||||
for (final Room analyticsRoom in analyticsRooms) {
|
||||
final List<AnalyticsEvent>? roomEvents =
|
||||
await analyticsRoom.getAnalyticsEvents(
|
||||
type: PangeaEventTypes.summaryAnalytics,
|
||||
since: currentAnalyticsTimeSpan.cutOffDate,
|
||||
userId: _pangeaController.matrixState.client.userID!,
|
||||
);
|
||||
|
||||
allEvents.addAll(
|
||||
roomEvents?.cast<SummaryAnalyticsEvent>() ?? [],
|
||||
);
|
||||
}
|
||||
return allEvents;
|
||||
final List<AnalyticsEvent>? roomEvents =
|
||||
await analyticsRoom.getAnalyticsEvents(
|
||||
type: PangeaEventTypes.summaryAnalytics,
|
||||
since: currentAnalyticsTimeSpan.cutOffDate,
|
||||
userId: _pangeaController.matrixState.client.userID!,
|
||||
);
|
||||
return roomEvents?.cast<SummaryAnalyticsEvent>() ?? [];
|
||||
}
|
||||
|
||||
Future<List<SummaryAnalyticsEvent>> spaceMemberAnalytics(
|
||||
|
|
@ -168,9 +201,6 @@ class AnalyticsController extends BaseController {
|
|||
) async {
|
||||
// gets all the summary analytics events for the students
|
||||
// in a space since the current timespace's cut off date
|
||||
final langCode = _pangeaController.languageController.activeL2Code(
|
||||
roomID: space.id,
|
||||
);
|
||||
|
||||
// ensure that all the space's events are loaded (mainly the for langCode)
|
||||
// and that the participants are loaded
|
||||
|
|
@ -181,7 +211,7 @@ class AnalyticsController extends BaseController {
|
|||
final List<SummaryAnalyticsEvent> analyticsEvents = [];
|
||||
for (final student in space.students) {
|
||||
final Room? analyticsRoom = _pangeaController.matrixState.client
|
||||
.analyticsRoomLocal(langCode, student.id);
|
||||
.analyticsRoomLocal(currentAnalyticsLang.langCode, student.id);
|
||||
|
||||
if (analyticsRoom != null) {
|
||||
final List<AnalyticsEvent>? roomEvents =
|
||||
|
|
@ -225,7 +255,8 @@ class AnalyticsController extends BaseController {
|
|||
(e.defaultSelected.id == defaultSelected.id) &&
|
||||
(e.defaultSelected.type == defaultSelected.type) &&
|
||||
(e.selected?.id == selected?.id) &&
|
||||
(e.selected?.type == selected?.type),
|
||||
(e.selected?.type == selected?.type) &&
|
||||
(e.langCode == currentAnalyticsLang.langCode),
|
||||
);
|
||||
|
||||
if (index != -1) {
|
||||
|
|
@ -253,6 +284,7 @@ class AnalyticsController extends BaseController {
|
|||
chartAnalyticsModel: chartAnalyticsModel,
|
||||
defaultSelected: defaultSelected,
|
||||
selected: selected,
|
||||
langCode: currentAnalyticsLang.langCode,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -267,16 +299,16 @@ class AnalyticsController extends BaseController {
|
|||
return filtered;
|
||||
}
|
||||
|
||||
List<SummaryAnalyticsEvent> filterRoomAnalytics(
|
||||
Future<List<SummaryAnalyticsEvent>> filterRoomAnalytics(
|
||||
List<SummaryAnalyticsEvent> unfiltered,
|
||||
String? roomID,
|
||||
) {
|
||||
) async {
|
||||
List<SummaryAnalyticsEvent> filtered = [...unfiltered];
|
||||
Room? room;
|
||||
if (roomID != null) {
|
||||
room = _pangeaController.matrixState.client.getRoomById(roomID);
|
||||
if (room?.isSpace == true) {
|
||||
return filterSpaceAnalytics(unfiltered, roomID);
|
||||
return await filterSpaceAnalytics(unfiltered, roomID);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -295,16 +327,10 @@ class AnalyticsController extends BaseController {
|
|||
|
||||
Future<List<SummaryAnalyticsEvent>> filterPrivateChatAnalytics(
|
||||
List<SummaryAnalyticsEvent> unfiltered,
|
||||
Room? space,
|
||||
Room space,
|
||||
) async {
|
||||
if (space != null && !_lastFetchedHierarchies.containsKey(space.id)) {
|
||||
final resp = await _pangeaController.matrixState.client
|
||||
.getSpaceHierarchy(space.id);
|
||||
setLatestHierarchy(space.id, resp);
|
||||
}
|
||||
|
||||
final List<String> privateChatIds = space?.allSpaceChildRoomIds ?? [];
|
||||
final List<String> lastFetched = _lastFetchedHierarchies[space!.id] ?? [];
|
||||
final List<String> privateChatIds = space.allSpaceChildRoomIds;
|
||||
final List<String> lastFetched = await getLatestSpaceHierarchy(space.id);
|
||||
for (final id in lastFetched) {
|
||||
privateChatIds.removeWhere((e) => e == id);
|
||||
}
|
||||
|
|
@ -324,19 +350,11 @@ class AnalyticsController extends BaseController {
|
|||
return filtered;
|
||||
}
|
||||
|
||||
List<SummaryAnalyticsEvent> filterSpaceAnalytics(
|
||||
Future<List<SummaryAnalyticsEvent>> filterSpaceAnalytics(
|
||||
List<SummaryAnalyticsEvent> unfiltered,
|
||||
String spaceId,
|
||||
) {
|
||||
final selectedSpace =
|
||||
_pangeaController.matrixState.client.getRoomById(spaceId);
|
||||
final List<String> chatIds = selectedSpace?.spaceChildren
|
||||
.map((e) => e.roomId)
|
||||
.where((e) => e != null)
|
||||
.cast<String>()
|
||||
.toList() ??
|
||||
[];
|
||||
|
||||
) async {
|
||||
final List<String> chatIds = await getLatestSpaceHierarchy(spaceId);
|
||||
List<SummaryAnalyticsEvent> filtered =
|
||||
List<SummaryAnalyticsEvent>.from(unfiltered);
|
||||
|
||||
|
|
@ -384,12 +402,15 @@ class AnalyticsController extends BaseController {
|
|||
if (defaultSelected.type == AnalyticsEntryType.student) {
|
||||
throw "private chat filtering not available for my analytics";
|
||||
}
|
||||
if (space == null) {
|
||||
throw "space is null in filterAnalytics with selected type privateChats";
|
||||
}
|
||||
return await filterPrivateChatAnalytics(
|
||||
unfilteredAnalytics,
|
||||
space,
|
||||
);
|
||||
case AnalyticsEntryType.space:
|
||||
return filterSpaceAnalytics(unfilteredAnalytics, selected!.id);
|
||||
return await filterSpaceAnalytics(unfilteredAnalytics, selected!.id);
|
||||
default:
|
||||
throw Exception("invalid filter type - ${selected?.type}");
|
||||
}
|
||||
|
|
@ -405,7 +426,6 @@ class AnalyticsController extends BaseController {
|
|||
|
||||
// if the user is looking at space analytics, then fetch the space
|
||||
Room? space;
|
||||
String? langCode;
|
||||
if (defaultSelected.type == AnalyticsEntryType.space) {
|
||||
space = _pangeaController.matrixState.client.getRoomById(
|
||||
defaultSelected.id,
|
||||
|
|
@ -423,23 +443,7 @@ class AnalyticsController extends BaseController {
|
|||
timeSpan: currentAnalyticsTimeSpan,
|
||||
);
|
||||
}
|
||||
|
||||
await space.postLoad();
|
||||
langCode = _pangeaController.languageController.activeL2Code(
|
||||
roomID: space.id,
|
||||
);
|
||||
if (langCode == null) {
|
||||
ErrorHandler.logError(
|
||||
m: "langCode missing in getAnalytics",
|
||||
data: {
|
||||
"space": space,
|
||||
},
|
||||
);
|
||||
return ChartAnalyticsModel(
|
||||
msgs: [],
|
||||
timeSpan: currentAnalyticsTimeSpan,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DateTime? lastUpdated;
|
||||
|
|
@ -456,7 +460,6 @@ class AnalyticsController extends BaseController {
|
|||
lastUpdated = await spaceAnalyticsLastUpdated(
|
||||
PangeaEventTypes.summaryAnalytics,
|
||||
space!,
|
||||
langCode!,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -467,8 +470,10 @@ class AnalyticsController extends BaseController {
|
|||
lastUpdated: lastUpdated,
|
||||
);
|
||||
if (local != null && !forceUpdate) {
|
||||
debugPrint("returning local analytics");
|
||||
return local;
|
||||
}
|
||||
debugPrint("fetching new analytics");
|
||||
|
||||
// get all the relevant summary analytics events for the current timespan
|
||||
final List<SummaryAnalyticsEvent> summaryEvents =
|
||||
|
|
@ -515,20 +520,18 @@ class AnalyticsController extends BaseController {
|
|||
//////////////////////////// CONSTRUCTS ////////////////////////////
|
||||
|
||||
Future<List<ConstructAnalyticsEvent>> allMyConstructs() async {
|
||||
final List<Room> analyticsRooms =
|
||||
_pangeaController.matrixState.client.allMyAnalyticsRooms;
|
||||
final Room? analyticsRoom = _pangeaController.matrixState.client
|
||||
.analyticsRoomLocal(currentAnalyticsLang.langCode);
|
||||
if (analyticsRoom == null) return [];
|
||||
|
||||
final List<ConstructAnalyticsEvent> allConstructs = [];
|
||||
for (final Room analyticsRoom in analyticsRooms) {
|
||||
final List<ConstructAnalyticsEvent>? roomEvents =
|
||||
(await analyticsRoom.getAnalyticsEvents(
|
||||
type: PangeaEventTypes.construct,
|
||||
since: currentAnalyticsTimeSpan.cutOffDate,
|
||||
userId: _pangeaController.matrixState.client.userID!,
|
||||
))
|
||||
?.cast<ConstructAnalyticsEvent>();
|
||||
allConstructs.addAll(roomEvents ?? []);
|
||||
}
|
||||
final List<ConstructAnalyticsEvent>? roomEvents =
|
||||
(await analyticsRoom.getAnalyticsEvents(
|
||||
type: PangeaEventTypes.construct,
|
||||
since: currentAnalyticsTimeSpan.cutOffDate,
|
||||
userId: _pangeaController.matrixState.client.userID!,
|
||||
))
|
||||
?.cast<ConstructAnalyticsEvent>();
|
||||
final List<ConstructAnalyticsEvent> allConstructs = roomEvents ?? [];
|
||||
|
||||
final List<String> adminSpaceRooms =
|
||||
await _pangeaController.matrixState.client.teacherRoomIds;
|
||||
|
|
@ -548,24 +551,10 @@ class AnalyticsController extends BaseController {
|
|||
) async {
|
||||
await space.postLoad();
|
||||
await space.requestParticipants();
|
||||
final String? langCode = _pangeaController.languageController.activeL2Code(
|
||||
roomID: space.id,
|
||||
);
|
||||
|
||||
if (langCode == null) {
|
||||
ErrorHandler.logError(
|
||||
m: "langCode missing in allSpaceMemberConstructs",
|
||||
data: {
|
||||
"space": space,
|
||||
},
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
final List<ConstructAnalyticsEvent> constructEvents = [];
|
||||
for (final student in space.students) {
|
||||
final Room? analyticsRoom = _pangeaController.matrixState.client
|
||||
.analyticsRoomLocal(langCode, student.id);
|
||||
.analyticsRoomLocal(currentAnalyticsLang.langCode, student.id);
|
||||
if (analyticsRoom != null) {
|
||||
final List<ConstructAnalyticsEvent>? roomEvents =
|
||||
(await analyticsRoom.getAnalyticsEvents(
|
||||
|
|
@ -614,31 +603,30 @@ class AnalyticsController extends BaseController {
|
|||
return filtered;
|
||||
}
|
||||
|
||||
List<ConstructAnalyticsEvent> filterPrivateChatConstructs(
|
||||
Future<List<ConstructAnalyticsEvent>> filterPrivateChatConstructs(
|
||||
List<ConstructAnalyticsEvent> unfilteredConstructs,
|
||||
Room parentSpace,
|
||||
) {
|
||||
final List<String> directChatIds = [];
|
||||
Room space,
|
||||
) async {
|
||||
final List<String> privateChatIds = space.allSpaceChildRoomIds;
|
||||
final List<String> lastFetched = await getLatestSpaceHierarchy(space.id);
|
||||
for (final id in lastFetched) {
|
||||
privateChatIds.removeWhere((e) => e == id);
|
||||
}
|
||||
final List<ConstructAnalyticsEvent> filtered =
|
||||
List<ConstructAnalyticsEvent>.from(unfilteredConstructs);
|
||||
for (final construct in filtered) {
|
||||
construct.content.uses.removeWhere(
|
||||
(use) => !directChatIds.contains(use.chatId),
|
||||
(use) => !privateChatIds.contains(use.chatId),
|
||||
);
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
List<ConstructAnalyticsEvent> filterSpaceConstructs(
|
||||
Future<List<ConstructAnalyticsEvent>> filterSpaceConstructs(
|
||||
List<ConstructAnalyticsEvent> unfilteredConstructs,
|
||||
Room space,
|
||||
) {
|
||||
final List<String> chatIds = space.spaceChildren
|
||||
.map((e) => e.roomId)
|
||||
.where((e) => e != null)
|
||||
.cast<String>()
|
||||
.toList();
|
||||
|
||||
) async {
|
||||
final List<String> chatIds = await getLatestSpaceHierarchy(space.id);
|
||||
final List<ConstructAnalyticsEvent> filtered =
|
||||
List<ConstructAnalyticsEvent>.from(unfilteredConstructs);
|
||||
|
||||
|
|
@ -653,7 +641,7 @@ class AnalyticsController extends BaseController {
|
|||
|
||||
List<ConstructAnalyticsEvent>? getConstructsLocal({
|
||||
required TimeSpan timeSpan,
|
||||
required ConstructType constructType,
|
||||
required ConstructTypeEnum constructType,
|
||||
required AnalyticsSelected defaultSelected,
|
||||
AnalyticsSelected? selected,
|
||||
DateTime? lastUpdated,
|
||||
|
|
@ -665,7 +653,8 @@ class AnalyticsController extends BaseController {
|
|||
e.defaultSelected.id == defaultSelected.id &&
|
||||
e.defaultSelected.type == defaultSelected.type &&
|
||||
e.selected?.id == selected?.id &&
|
||||
e.selected?.type == selected?.type,
|
||||
e.selected?.type == selected?.type &&
|
||||
e.langCode == currentAnalyticsLang.langCode,
|
||||
);
|
||||
|
||||
if (index > -1) {
|
||||
|
|
@ -680,7 +669,7 @@ class AnalyticsController extends BaseController {
|
|||
}
|
||||
|
||||
void cacheConstructs({
|
||||
required ConstructType constructType,
|
||||
required ConstructTypeEnum constructType,
|
||||
required List<ConstructAnalyticsEvent> events,
|
||||
required AnalyticsSelected defaultSelected,
|
||||
AnalyticsSelected? selected,
|
||||
|
|
@ -691,13 +680,14 @@ class AnalyticsController extends BaseController {
|
|||
events: List.from(events),
|
||||
defaultSelected: defaultSelected,
|
||||
selected: selected,
|
||||
langCode: currentAnalyticsLang.langCode,
|
||||
);
|
||||
_cachedConstructs.add(entry);
|
||||
}
|
||||
|
||||
Future<List<ConstructAnalyticsEvent>> getMyConstructs({
|
||||
required AnalyticsSelected defaultSelected,
|
||||
required ConstructType constructType,
|
||||
required ConstructTypeEnum constructType,
|
||||
AnalyticsSelected? selected,
|
||||
}) async {
|
||||
final List<ConstructAnalyticsEvent> unfilteredConstructs =
|
||||
|
|
@ -716,7 +706,7 @@ class AnalyticsController extends BaseController {
|
|||
}
|
||||
|
||||
Future<List<ConstructAnalyticsEvent>> getSpaceConstructs({
|
||||
required ConstructType constructType,
|
||||
required ConstructTypeEnum constructType,
|
||||
required Room space,
|
||||
required AnalyticsSelected defaultSelected,
|
||||
AnalyticsSelected? selected,
|
||||
|
|
@ -769,25 +759,25 @@ class AnalyticsController extends BaseController {
|
|||
case AnalyticsEntryType.privateChats:
|
||||
return defaultSelected.type == AnalyticsEntryType.student
|
||||
? throw "private chat filtering not available for my analytics"
|
||||
: filterPrivateChatConstructs(unfilteredConstructs, space!);
|
||||
: await filterPrivateChatConstructs(unfilteredConstructs, space!);
|
||||
case AnalyticsEntryType.space:
|
||||
return filterSpaceConstructs(unfilteredConstructs, space!);
|
||||
return await filterSpaceConstructs(unfilteredConstructs, space!);
|
||||
default:
|
||||
throw Exception("invalid filter type - ${selected?.type}");
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<ConstructAnalyticsEvent>?> getConstructs({
|
||||
required ConstructType constructType,
|
||||
required ConstructTypeEnum constructType,
|
||||
required AnalyticsSelected defaultSelected,
|
||||
AnalyticsSelected? selected,
|
||||
bool removeIT = true,
|
||||
bool forceUpdate = false,
|
||||
}) async {
|
||||
debugPrint("getting constructs");
|
||||
await _pangeaController.matrixState.client.roomsLoading;
|
||||
|
||||
Room? space;
|
||||
String? langCode;
|
||||
if (defaultSelected.type == AnalyticsEntryType.space) {
|
||||
space = _pangeaController.matrixState.client.getRoomById(
|
||||
defaultSelected.id,
|
||||
|
|
@ -803,18 +793,6 @@ class AnalyticsController extends BaseController {
|
|||
return [];
|
||||
}
|
||||
await space.postLoad();
|
||||
langCode = _pangeaController.languageController.activeL2Code(
|
||||
roomID: space.id,
|
||||
);
|
||||
if (langCode == null) {
|
||||
ErrorHandler.logError(
|
||||
m: "langCode missing in setConstructs",
|
||||
data: {
|
||||
"space": space,
|
||||
},
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
DateTime? lastUpdated;
|
||||
|
|
@ -831,7 +809,6 @@ class AnalyticsController extends BaseController {
|
|||
lastUpdated = await spaceAnalyticsLastUpdated(
|
||||
PangeaEventTypes.construct,
|
||||
space!,
|
||||
langCode!,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -843,8 +820,10 @@ class AnalyticsController extends BaseController {
|
|||
lastUpdated: lastUpdated,
|
||||
);
|
||||
if (local != null && !forceUpdate) {
|
||||
debugPrint("returning local constructs");
|
||||
return local;
|
||||
}
|
||||
debugPrint("fetching new constructs");
|
||||
|
||||
final filteredConstructs = space == null
|
||||
? await getMyConstructs(
|
||||
|
|
@ -884,6 +863,7 @@ class AnalyticsController extends BaseController {
|
|||
}
|
||||
|
||||
abstract class CacheEntry {
|
||||
final String langCode;
|
||||
final TimeSpan timeSpan;
|
||||
final AnalyticsSelected defaultSelected;
|
||||
AnalyticsSelected? selected;
|
||||
|
|
@ -892,6 +872,7 @@ abstract class CacheEntry {
|
|||
CacheEntry({
|
||||
required this.timeSpan,
|
||||
required this.defaultSelected,
|
||||
required this.langCode,
|
||||
this.selected,
|
||||
}) {
|
||||
_createdAt = DateTime.now();
|
||||
|
|
@ -917,24 +898,26 @@ abstract class CacheEntry {
|
|||
}
|
||||
|
||||
class ConstructCacheEntry extends CacheEntry {
|
||||
final ConstructType type;
|
||||
final ConstructTypeEnum type;
|
||||
final List<ConstructAnalyticsEvent> events;
|
||||
|
||||
ConstructCacheEntry({
|
||||
required this.type,
|
||||
required this.events,
|
||||
required super.timeSpan,
|
||||
required super.langCode,
|
||||
required super.defaultSelected,
|
||||
super.selected,
|
||||
});
|
||||
}
|
||||
|
||||
class AnalyticsCacheModel extends CacheEntry {
|
||||
ChartAnalyticsModel chartAnalyticsModel;
|
||||
final ChartAnalyticsModel chartAnalyticsModel;
|
||||
|
||||
AnalyticsCacheModel({
|
||||
required this.chartAnalyticsModel,
|
||||
required super.timeSpan,
|
||||
required super.langCode,
|
||||
required super.defaultSelected,
|
||||
super.selected,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -176,14 +176,20 @@ class MessageDataController extends BaseController {
|
|||
required String target,
|
||||
required Room room,
|
||||
}) async {
|
||||
if (_pangeaController.languageController.userL2 == null ||
|
||||
_pangeaController.languageController.userL1 == null) {
|
||||
ErrorHandler.logError(
|
||||
e: "userL1 or userL2 is null in _getPangeaRepresentation",
|
||||
s: StackTrace.current,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
final req = FullTextTranslationRequestModel(
|
||||
text: text,
|
||||
tgtLang: target,
|
||||
srcLang: source,
|
||||
userL2:
|
||||
_pangeaController.languageController.activeL2Code(roomID: room.id)!,
|
||||
userL1:
|
||||
_pangeaController.languageController.activeL1Code(roomID: room.id)!,
|
||||
userL2: _pangeaController.languageController.userL2!.langCode,
|
||||
userL1: _pangeaController.languageController.userL1!.langCode,
|
||||
);
|
||||
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@ import 'dart:developer';
|
|||
|
||||
import 'package:fluffychat/pangea/constants/local.key.dart';
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/controllers/base_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/analytics_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/analytics_model.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_record_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
|
@ -15,53 +15,72 @@ import 'package:matrix/matrix.dart';
|
|||
import '../extensions/client_extension/client_extension.dart';
|
||||
import '../extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
|
||||
// controls the sending of analytics events
|
||||
class MyAnalyticsController extends BaseController {
|
||||
/// handles the processing of analytics for
|
||||
/// 1) messages sent by the user and
|
||||
/// 2) constructs used by the user, both in sending messages and doing practice activities
|
||||
class MyAnalyticsController {
|
||||
late PangeaController _pangeaController;
|
||||
Timer? _updateTimer;
|
||||
|
||||
/// the max number of messages that will be cached before
|
||||
/// an automatic update is triggered
|
||||
final int _maxMessagesCached = 10;
|
||||
|
||||
/// the number of minutes before an automatic update is triggered
|
||||
final int _minutesBeforeUpdate = 5;
|
||||
|
||||
/// the time since the last update that will trigger an automatic update
|
||||
final Duration _timeSinceUpdate = const Duration(days: 1);
|
||||
|
||||
MyAnalyticsController(PangeaController pangeaController) {
|
||||
_pangeaController = pangeaController;
|
||||
}
|
||||
|
||||
// adds the listener that handles when to run automatic updates
|
||||
// to analytics - either after a certain number of messages sent/
|
||||
// received or after a certain amount of time without an update
|
||||
Future<void> addEventsListener() async {
|
||||
final Client client = _pangeaController.matrixState.client;
|
||||
/// adds the listener that handles when to run automatic updates
|
||||
/// to analytics - either after a certain number of messages sent
|
||||
/// received or after a certain amount of time [_timeSinceUpdate] without an update
|
||||
Future<void> initialize() async {
|
||||
final lastUpdated = await _refreshAnalyticsIfOutdated();
|
||||
|
||||
// if analytics haven't been updated in the last day, update them
|
||||
DateTime? lastUpdated = await _pangeaController.analytics
|
||||
.myAnalyticsLastUpdated(PangeaEventTypes.summaryAnalytics);
|
||||
final DateTime yesterday = DateTime.now().subtract(const Duration(days: 1));
|
||||
if (lastUpdated?.isBefore(yesterday) ?? true) {
|
||||
debugPrint("analytics out-of-date, updating");
|
||||
await updateAnalytics();
|
||||
lastUpdated = await _pangeaController.analytics
|
||||
.myAnalyticsLastUpdated(PangeaEventTypes.summaryAnalytics);
|
||||
}
|
||||
|
||||
client.onSync.stream
|
||||
// listen for new messages and updateAnalytics timer
|
||||
// we are doing this in an attempt to update analytics when activitiy is low
|
||||
// both in messages sent by this client and other clients that you're connected with
|
||||
// doesn't account for messages sent by other clients that you're not connected with
|
||||
_client.onSync.stream
|
||||
.where((SyncUpdate update) => update.rooms?.join != null)
|
||||
.listen((update) {
|
||||
updateAnalyticsTimer(update, lastUpdated);
|
||||
});
|
||||
}
|
||||
|
||||
// given an update from sync stream, check if the update contains
|
||||
// messages for which analytics will be saved. If so, reset the timer
|
||||
// and add the event ID to the cache of un-added event IDs
|
||||
/// If analytics haven't been updated in the last day, update them
|
||||
Future<DateTime?> _refreshAnalyticsIfOutdated() async {
|
||||
DateTime? lastUpdated = await _pangeaController.analytics
|
||||
.myAnalyticsLastUpdated(PangeaEventTypes.summaryAnalytics);
|
||||
final DateTime yesterday = DateTime.now().subtract(_timeSinceUpdate);
|
||||
|
||||
if (lastUpdated?.isBefore(yesterday) ?? true) {
|
||||
debugPrint("analytics out-of-date, updating");
|
||||
await updateAnalytics();
|
||||
lastUpdated = await _pangeaController.analytics
|
||||
.myAnalyticsLastUpdated(PangeaEventTypes.summaryAnalytics);
|
||||
}
|
||||
return lastUpdated;
|
||||
}
|
||||
|
||||
Client get _client => _pangeaController.matrixState.client;
|
||||
|
||||
/// Given an update from sync stream, check if the update contains
|
||||
/// messages for which analytics will be saved. If so, reset the timer
|
||||
/// and add the event ID to the cache of un-added event IDs
|
||||
void updateAnalyticsTimer(SyncUpdate update, DateTime? lastUpdated) {
|
||||
for (final entry in update.rooms!.join!.entries) {
|
||||
final Room room =
|
||||
_pangeaController.matrixState.client.getRoomById(entry.key)!;
|
||||
final Room room = _client.getRoomById(entry.key)!;
|
||||
|
||||
// get the new events in this sync that are messages
|
||||
final List<Event>? events = entry.value.timeline?.events
|
||||
?.map((event) => Event.fromMatrixEvent(event, room))
|
||||
.where((event) => eventHasAnalytics(event, lastUpdated))
|
||||
.where((event) => hasUserAnalyticsToCache(event, lastUpdated))
|
||||
.toList();
|
||||
|
||||
// add their event IDs to the cache of un-added event IDs
|
||||
|
|
@ -81,8 +100,9 @@ class MyAnalyticsController extends BaseController {
|
|||
}
|
||||
|
||||
// checks if event from sync update is a message that should have analytics
|
||||
bool eventHasAnalytics(Event event, DateTime? lastUpdated) {
|
||||
return (lastUpdated == null || event.originServerTs.isAfter(lastUpdated)) &&
|
||||
bool hasUserAnalyticsToCache(Event event, DateTime? lastUpdated) {
|
||||
return event.senderId == _client.userID &&
|
||||
(lastUpdated == null || event.originServerTs.isAfter(lastUpdated)) &&
|
||||
event.type == EventTypes.Message &&
|
||||
event.messageType == MessageTypes.Text &&
|
||||
!(event.eventId.contains("web") &&
|
||||
|
|
@ -157,6 +177,7 @@ class MyAnalyticsController extends BaseController {
|
|||
_updateCompleter = Completer<void>();
|
||||
try {
|
||||
await _updateAnalytics();
|
||||
clearMessagesSinceUpdate();
|
||||
} catch (err, s) {
|
||||
ErrorHandler.logError(
|
||||
e: err,
|
||||
|
|
@ -169,170 +190,135 @@ class MyAnalyticsController extends BaseController {
|
|||
}
|
||||
}
|
||||
|
||||
String? get userL2 => _pangeaController.languageController.activeL2Code();
|
||||
|
||||
/// top level analytics sending function. Gather recent messages and activity records,
|
||||
/// convert them into the correct formats, and send them to the analytics room
|
||||
Future<void> _updateAnalytics() async {
|
||||
// top level analytics sending function. Send analytics
|
||||
// for each type of analytics event
|
||||
// to each of the applicable analytics rooms
|
||||
clearMessagesSinceUpdate();
|
||||
|
||||
// fetch a list of all the chats that the user is studying
|
||||
// and a list of all the spaces in which the user is studying
|
||||
await setStudentChats();
|
||||
await setStudentSpaces();
|
||||
|
||||
// get all the analytics rooms that the user has
|
||||
// and create any missing analytics rooms (if the user is studying
|
||||
// in a class but doesn't have an analytics room for that class's L2)
|
||||
final List<Room> analyticsRooms =
|
||||
_pangeaController.matrixState.client.allMyAnalyticsRooms;
|
||||
analyticsRooms.addAll(await createMissingAnalyticsRooms());
|
||||
|
||||
// finally, send an analytics event for each analytics room and
|
||||
// each type of analytics event
|
||||
for (final Room analyticsRoom in analyticsRooms) {
|
||||
for (final String type in AnalyticsEvent.analyticsEventTypes) {
|
||||
await sendAnalyticsEvent(analyticsRoom, type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> sendAnalyticsEvent(
|
||||
Room analyticsRoom,
|
||||
String type,
|
||||
) async {
|
||||
// given an analytics room for a language and a type of analytics event
|
||||
// gathers all the relevant data and sends it to the analytics room
|
||||
|
||||
// get the language code for the analytics room
|
||||
final String? langCode = analyticsRoom.madeForLang;
|
||||
if (langCode == null) {
|
||||
ErrorHandler.logError(
|
||||
e: "no lang code found for analytics room: ${analyticsRoom.id}",
|
||||
s: StackTrace.current,
|
||||
);
|
||||
// if missing important info, don't send analytics
|
||||
if (userL2 == null || _client.userID == null) {
|
||||
debugger(when: kDebugMode);
|
||||
return;
|
||||
}
|
||||
|
||||
// get the last time an analytics event of this type was sent to this room
|
||||
final DateTime? lastUpdated = await analyticsRoom.analyticsLastUpdated(
|
||||
type,
|
||||
_pangeaController.matrixState.client.userID!,
|
||||
// analytics room for the user and current target language
|
||||
final Room analyticsRoom = await _client.getMyAnalyticsRoom(userL2!);
|
||||
|
||||
// get the last time analytics were updated for this room
|
||||
final DateTime? l2AnalyticsLastUpdated =
|
||||
await analyticsRoom.analyticsLastUpdated(
|
||||
PangeaEventTypes.summaryAnalytics,
|
||||
_client.userID!,
|
||||
);
|
||||
|
||||
// each type of analytics event has a format for storing per-message data
|
||||
// for SummaryAnalytics events, this is RecentMessageRecord
|
||||
// for Construct events, this is OneConstructUse
|
||||
// analyticsContent is a list of these formatted data
|
||||
final List<dynamic> analyticsContent = [];
|
||||
// all chats in which user is a student
|
||||
final List<Room> chats = _client.rooms
|
||||
.where((room) => !room.isSpace && !room.isAnalyticsRoom)
|
||||
.toList();
|
||||
|
||||
for (final Room chat in _studentChats) {
|
||||
// for each chat the student studies in, check if the langCode
|
||||
// matches the langCode of the analytics room
|
||||
final String? chatLangCode =
|
||||
_pangeaController.languageController.activeL2Code(roomID: chat.id);
|
||||
if (chatLangCode != langCode) continue;
|
||||
// get the recent message events and activity records for each chat
|
||||
final List<Future<List<Event>>> recentMsgFutures = [];
|
||||
final List<Future<List<Event>>> recentActivityFutures = [];
|
||||
for (final Room chat in chats) {
|
||||
recentMsgFutures.add(
|
||||
chat.getEventsBySender(
|
||||
type: EventTypes.Message,
|
||||
sender: _client.userID!,
|
||||
since: l2AnalyticsLastUpdated,
|
||||
),
|
||||
);
|
||||
recentActivityFutures.add(
|
||||
chat.getEventsBySender(
|
||||
type: PangeaEventTypes.activityRecord,
|
||||
sender: _client.userID!,
|
||||
since: l2AnalyticsLastUpdated,
|
||||
),
|
||||
);
|
||||
}
|
||||
final List<List<Event>> recentMsgs =
|
||||
(await Future.wait(recentMsgFutures)).toList();
|
||||
final List<PracticeActivityRecordEvent> recentActivityRecords =
|
||||
(await Future.wait(recentActivityFutures))
|
||||
.expand((e) => e)
|
||||
.map((event) => PracticeActivityRecordEvent(event: event))
|
||||
.toList();
|
||||
|
||||
// get messages the logged in user has sent in all chats
|
||||
// since the last analytics event was sent
|
||||
List<PangeaMessageEvent>? recentMsgs;
|
||||
try {
|
||||
recentMsgs = await chat.myMessageEventsInChat(
|
||||
since: lastUpdated,
|
||||
);
|
||||
} catch (err) {
|
||||
debugPrint("failed to fetch messages for chat ${chat.id}");
|
||||
continue;
|
||||
}
|
||||
// get the timelines for each chat
|
||||
final List<Future<Timeline>> timelineFutures = [];
|
||||
for (final chat in chats) {
|
||||
timelineFutures.add(chat.getTimeline());
|
||||
}
|
||||
final List<Timeline> timelines = await Future.wait(timelineFutures);
|
||||
final Map<String, Timeline> timelineMap =
|
||||
Map.fromIterables(chats.map((e) => e.id), timelines);
|
||||
|
||||
if (lastUpdated != null) {
|
||||
recentMsgs.removeWhere(
|
||||
(msg) => msg.event.originServerTs.isBefore(lastUpdated),
|
||||
);
|
||||
}
|
||||
|
||||
// then format that data into analytics data and add the formatted
|
||||
// data to the list of analyticsContent
|
||||
analyticsContent.addAll(
|
||||
AnalyticsModel.formatAnalyticsContent(recentMsgs, type),
|
||||
//convert into PangeaMessageEvents
|
||||
final List<List<PangeaMessageEvent>> recentPangeaMessageEvents = [];
|
||||
for (final (index, eventList) in recentMsgs.indexed) {
|
||||
recentPangeaMessageEvents.add(
|
||||
eventList
|
||||
.map(
|
||||
(event) => PangeaMessageEvent(
|
||||
event: event,
|
||||
timeline: timelines[index],
|
||||
ownMessage: true,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
// send the analytics data to the analytics room
|
||||
// if there is no data to send, don't send an event,
|
||||
// unless no events have been sent yet. In that case, send an event
|
||||
// with no data to indicate that the the system checked for data
|
||||
// and found none, so the system doesn't repeatedly check for data
|
||||
if (analyticsContent.isEmpty && lastUpdated != null) return;
|
||||
await AnalyticsEvent.sendEvent(
|
||||
analyticsRoom,
|
||||
type,
|
||||
analyticsContent,
|
||||
);
|
||||
}
|
||||
final List<PangeaMessageEvent> allRecentMessages =
|
||||
recentPangeaMessageEvents.expand((e) => e).toList();
|
||||
|
||||
// on the off chance that the user is in a class but doesn't have an analytics
|
||||
// room for the target language of that class, create the analytics room(s)
|
||||
Future<List<Room>> createMissingAnalyticsRooms() async {
|
||||
List<String> targetLangs = [];
|
||||
final String? userL2 = _pangeaController.languageController.activeL2Code();
|
||||
if (userL2 != null) targetLangs.add(userL2);
|
||||
final List<String?> spaceL2s = studentSpaces
|
||||
.map(
|
||||
(space) => _pangeaController.languageController.activeL2Code(
|
||||
roomID: space.id,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
targetLangs.addAll(spaceL2s.where((l2) => l2 != null).cast<String>());
|
||||
targetLangs = targetLangs.toSet().toList();
|
||||
for (final String langCode in targetLangs) {
|
||||
await _pangeaController.matrixState.client.getMyAnalyticsRoom(langCode);
|
||||
final List<RecentMessageRecord> summaryContent =
|
||||
SummaryAnalyticsModel.formatSummaryContent(allRecentMessages);
|
||||
// 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(
|
||||
summaryContent,
|
||||
);
|
||||
}
|
||||
return _pangeaController.matrixState.client.allMyAnalyticsRooms;
|
||||
}
|
||||
|
||||
List<Room> _studentChats = [];
|
||||
|
||||
Future<void> setStudentChats() async {
|
||||
final List<String> teacherRoomIds =
|
||||
await _pangeaController.matrixState.client.teacherRoomIds;
|
||||
_studentChats = _pangeaController.matrixState.client.rooms
|
||||
.where(
|
||||
(r) =>
|
||||
!r.isSpace &&
|
||||
!r.isAnalyticsRoom &&
|
||||
!teacherRoomIds.contains(r.id),
|
||||
)
|
||||
.toList();
|
||||
setState(data: _studentChats);
|
||||
}
|
||||
|
||||
List<Room> get studentChats {
|
||||
try {
|
||||
if (_studentChats.isNotEmpty) return _studentChats;
|
||||
setStudentChats();
|
||||
return _studentChats;
|
||||
} catch (err) {
|
||||
debugger(when: kDebugMode);
|
||||
return [];
|
||||
// get constructs for messages
|
||||
final List<OneConstructUse> recentConstructUses = [];
|
||||
for (final PangeaMessageEvent message in allRecentMessages) {
|
||||
recentConstructUses.addAll(message.allConstructUses);
|
||||
}
|
||||
}
|
||||
|
||||
List<Room> _studentSpaces = [];
|
||||
// get constructs for practice activities
|
||||
final List<Future<List<OneConstructUse>>> constructFutures = [];
|
||||
for (final PracticeActivityRecordEvent activity in recentActivityRecords) {
|
||||
final Timeline? timeline = timelineMap[activity.event.roomId!];
|
||||
if (timeline == null) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
m: "PracticeActivityRecordEvent has null timeline",
|
||||
data: activity.event.toJson(),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
constructFutures.add(activity.uses(timeline));
|
||||
}
|
||||
final List<List<OneConstructUse>> constructLists =
|
||||
await Future.wait(constructFutures);
|
||||
|
||||
Future<void> setStudentSpaces() async {
|
||||
_studentSpaces = await _pangeaController
|
||||
.matrixState.client.classesAndExchangesImStudyingIn;
|
||||
}
|
||||
recentConstructUses.addAll(constructLists.expand((e) => e));
|
||||
|
||||
List<Room> get studentSpaces {
|
||||
try {
|
||||
if (_studentSpaces.isNotEmpty) return _studentSpaces;
|
||||
setStudentSpaces();
|
||||
return _studentSpaces;
|
||||
} catch (err) {
|
||||
debugger(when: kDebugMode);
|
||||
return [];
|
||||
//TODO - confirm that this is the correct construct content
|
||||
// debugger(
|
||||
// when: kDebugMode,
|
||||
// );
|
||||
// ; debugger(
|
||||
// when: kDebugMode &&
|
||||
// (allRecentMessages.isNotEmpty || recentActivityRecords.isNotEmpty),
|
||||
// );
|
||||
|
||||
if (recentConstructUses.isNotEmpty) {
|
||||
await analyticsRoom.sendConstructsEvent(
|
||||
recentConstructUses,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'dart:math';
|
||||
|
||||
|
|
@ -12,6 +13,8 @@ import 'package:fluffychat/pangea/controllers/local_settings.dart';
|
|||
import 'package:fluffychat/pangea/controllers/message_data_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/permissions_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/practice_activity_generation_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/practice_activity_record_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/speech_to_text_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/subscription_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart';
|
||||
|
|
@ -53,6 +56,8 @@ class PangeaController {
|
|||
late TextToSpeechController textToSpeech;
|
||||
late SpeechToTextController speechToText;
|
||||
late LanguageDetectionController languageDetection;
|
||||
late PracticeActivityRecordController activityRecordController;
|
||||
late PracticeGenerationController practiceGenerationController;
|
||||
|
||||
///store Services
|
||||
late PLocalStore pStoreService;
|
||||
|
|
@ -101,6 +106,8 @@ class PangeaController {
|
|||
textToSpeech = TextToSpeechController(this);
|
||||
speechToText = SpeechToTextController(this);
|
||||
languageDetection = LanguageDetectionController(this);
|
||||
activityRecordController = PracticeActivityRecordController(this);
|
||||
practiceGenerationController = PracticeGenerationController();
|
||||
PAuthGaurd.pController = this;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
|||
import 'package:fluffychat/pangea/controllers/base_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.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/space_model.dart';
|
||||
import 'package:fluffychat/pangea/models/user_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/p_extension.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
|
@ -117,27 +117,4 @@ class PermissionsController extends BaseController {
|
|||
return isToolEnabled(ToolSetting.interactiveTranslator, room) &&
|
||||
isToolEnabled(ToolSetting.interactiveGrammar, room);
|
||||
}
|
||||
|
||||
// bool get showChatListStartChatFloatingActionButton {
|
||||
// //for now, I'm turning off chat button when not in the context of a clas
|
||||
// //it will still be possible to private chat outside of a class
|
||||
// //need to investigate if private chats can be put in a space. i suppose they can
|
||||
// //if so, do we want that?
|
||||
// try {
|
||||
// if (_pangeaController.classController.activeClass == null) return false;
|
||||
|
||||
// // final isExchange =
|
||||
// // (_pangeaController.classController.activeClass?.isExchange ?? false);
|
||||
// const isExchange = false;
|
||||
// final regular = (canUserPrivateChat() || canUserGroupChat());
|
||||
// final inExchange =
|
||||
// (canUserPrivateChatExchanges() || canUserGroupChatExchanges());
|
||||
// final theAnswer = isExchange ? inExchange : regular;
|
||||
// // debugger(when: kDebugMode && !theAnswer);
|
||||
// return theAnswer;
|
||||
// } catch (e, s) {
|
||||
// ErrorHandler.logError(e: e, s: s);
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,102 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/enum/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
/// Represents an item in the completion cache.
|
||||
class _RequestCacheItem {
|
||||
PracticeActivityRequest req;
|
||||
|
||||
Future<PracticeActivityEvent?> practiceActivityEvent;
|
||||
|
||||
_RequestCacheItem({
|
||||
required this.req,
|
||||
required this.practiceActivityEvent,
|
||||
});
|
||||
}
|
||||
|
||||
/// Controller for handling activity completions.
|
||||
class PracticeGenerationController {
|
||||
static final Map<int, _RequestCacheItem> _cache = {};
|
||||
Timer? _cacheClearTimer;
|
||||
|
||||
PracticeGenerationController() {
|
||||
_initializeCacheClearing();
|
||||
}
|
||||
|
||||
void _initializeCacheClearing() {
|
||||
const duration = Duration(minutes: 2);
|
||||
_cacheClearTimer = Timer.periodic(duration, (Timer t) => _clearCache());
|
||||
}
|
||||
|
||||
void _clearCache() {
|
||||
_cache.clear();
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_cacheClearTimer?.cancel();
|
||||
}
|
||||
|
||||
Future<PracticeActivityEvent?> _sendAndPackageEvent(
|
||||
PracticeActivityModel model,
|
||||
PangeaMessageEvent pangeaMessageEvent,
|
||||
) async {
|
||||
final Event? activityEvent = await pangeaMessageEvent.room.sendPangeaEvent(
|
||||
content: model.toJson(),
|
||||
parentEventId: pangeaMessageEvent.eventId,
|
||||
type: PangeaEventTypes.pangeaActivity,
|
||||
);
|
||||
|
||||
if (activityEvent == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return PracticeActivityEvent(
|
||||
event: activityEvent,
|
||||
timeline: pangeaMessageEvent.timeline,
|
||||
);
|
||||
}
|
||||
|
||||
Future<PracticeActivityEvent?> getPracticeActivity(
|
||||
PracticeActivityRequest req,
|
||||
PangeaMessageEvent event,
|
||||
) async {
|
||||
final int cacheKey = req.hashCode;
|
||||
|
||||
if (_cache.containsKey(cacheKey)) {
|
||||
return _cache[cacheKey]!.practiceActivityEvent;
|
||||
} else {
|
||||
//TODO - send request to server/bot, either via API or via event of type pangeaActivityReq
|
||||
// for now, just make and send the event from the client
|
||||
final Future<PracticeActivityEvent?> eventFuture =
|
||||
_sendAndPackageEvent(dummyModel(event), event);
|
||||
|
||||
_cache[cacheKey] =
|
||||
_RequestCacheItem(req: req, practiceActivityEvent: eventFuture);
|
||||
|
||||
return _cache[cacheKey]!.practiceActivityEvent;
|
||||
}
|
||||
}
|
||||
|
||||
PracticeActivityModel dummyModel(PangeaMessageEvent event) =>
|
||||
PracticeActivityModel(
|
||||
tgtConstructs: [
|
||||
ConstructIdentifier(lemma: "be", type: ConstructTypeEnum.vocab),
|
||||
],
|
||||
activityType: ActivityTypeEnum.multipleChoice,
|
||||
langCode: event.messageDisplayLangCode,
|
||||
msgId: event.eventId,
|
||||
multipleChoice: MultipleChoice(
|
||||
question: "What is a synonym for 'happy'?",
|
||||
choices: ["sad", "angry", "joyful", "tired"],
|
||||
answer: "joyful",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
/// Represents an item in the completion cache.
|
||||
class _RecordCacheItem {
|
||||
PracticeActivityRecordModel data;
|
||||
|
||||
Future<Event?> recordEvent;
|
||||
|
||||
_RecordCacheItem({required this.data, required this.recordEvent});
|
||||
}
|
||||
|
||||
/// Controller for handling activity completions.
|
||||
class PracticeActivityRecordController {
|
||||
static final Map<int, _RecordCacheItem> _cache = {};
|
||||
late final PangeaController _pangeaController;
|
||||
Timer? _cacheClearTimer;
|
||||
|
||||
PracticeActivityRecordController(this._pangeaController) {
|
||||
_initializeCacheClearing();
|
||||
}
|
||||
|
||||
void _initializeCacheClearing() {
|
||||
const duration = Duration(minutes: 2);
|
||||
_cacheClearTimer = Timer.periodic(duration, (Timer t) => _clearCache());
|
||||
}
|
||||
|
||||
void _clearCache() {
|
||||
_cache.clear();
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_cacheClearTimer?.cancel();
|
||||
}
|
||||
|
||||
/// Sends a practice activity record to the server and returns the corresponding event.
|
||||
///
|
||||
/// The [recordModel] parameter is the model representing the practice activity record.
|
||||
/// The [practiceActivityEvent] parameter is the event associated with the practice activity.
|
||||
/// Note that the system will send a new event if the model has changed in any way ie it is
|
||||
/// a new completion of the practice activity. However, it will cache previous sends to ensure
|
||||
/// that opening and closing of the widget does not result in multiple sends of the same data.
|
||||
/// It allows checks the data to make sure that it contains responses to the practice activity
|
||||
/// and does not represent a blank record with no actual completion to be saved.
|
||||
///
|
||||
/// Returns a [Future] that completes with the corresponding [Event] object.
|
||||
Future<Event?> send(
|
||||
PracticeActivityRecordModel recordModel,
|
||||
PracticeActivityEvent practiceActivityEvent,
|
||||
) async {
|
||||
final int cacheKey = recordModel.hashCode;
|
||||
|
||||
if (recordModel.responses.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_cache.containsKey(cacheKey)) {
|
||||
return _cache[cacheKey]!.recordEvent;
|
||||
} else {
|
||||
final Future<Event?> eventFuture = practiceActivityEvent.event.room
|
||||
.sendPangeaEvent(
|
||||
content: recordModel.toJson(),
|
||||
parentEventId: practiceActivityEvent.event.eventId,
|
||||
type: PangeaEventTypes.activityRecord,
|
||||
)
|
||||
.catchError((e) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: StackTrace.current,
|
||||
data: {
|
||||
'recordModel': recordModel.toJson(),
|
||||
'practiceActivityEvent': practiceActivityEvent.event.toJson(),
|
||||
},
|
||||
);
|
||||
return null;
|
||||
});
|
||||
|
||||
_cache[cacheKey] =
|
||||
_RecordCacheItem(data: recordModel, recordEvent: eventFuture);
|
||||
|
||||
return _cache[cacheKey]!.recordEvent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
|||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import '../models/class_model.dart';
|
||||
import '../models/space_model.dart';
|
||||
|
||||
class RoomRulesEditController {
|
||||
final Room? room;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_keys.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/controllers/base_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
|
|
@ -123,6 +123,7 @@ class UserController extends BaseController {
|
|||
: null;
|
||||
|
||||
final bool? autoPlay = migratedProfileInfo(MatrixProfile.autoPlayMessages);
|
||||
final bool? itAutoPlay = migratedProfileInfo(MatrixProfile.itAutoPlay);
|
||||
final bool? trial = migratedProfileInfo(MatrixProfile.activatedFreeTrial);
|
||||
final bool? interactiveTranslator =
|
||||
migratedProfileInfo(MatrixProfile.interactiveTranslator);
|
||||
|
|
@ -142,6 +143,7 @@ class UserController extends BaseController {
|
|||
await updateMatrixProfile(
|
||||
dateOfBirth: dob,
|
||||
autoPlayMessages: autoPlay,
|
||||
itAutoPlay: itAutoPlay,
|
||||
activatedFreeTrial: trial,
|
||||
interactiveTranslator: interactiveTranslator,
|
||||
interactiveGrammar: interactiveGrammar,
|
||||
|
|
@ -223,6 +225,7 @@ class UserController extends BaseController {
|
|||
Future<void> updateMatrixProfile({
|
||||
String? dateOfBirth,
|
||||
bool? autoPlayMessages,
|
||||
bool? itAutoPlay,
|
||||
bool? activatedFreeTrial,
|
||||
bool? interactiveTranslator,
|
||||
bool? interactiveGrammar,
|
||||
|
|
@ -251,6 +254,12 @@ class UserController extends BaseController {
|
|||
autoPlayMessages,
|
||||
);
|
||||
}
|
||||
if (itAutoPlay != null) {
|
||||
await _pangeaController.pStoreService.save(
|
||||
MatrixProfile.itAutoPlay.title,
|
||||
itAutoPlay,
|
||||
);
|
||||
}
|
||||
if (activatedFreeTrial != null) {
|
||||
await _pangeaController.pStoreService.save(
|
||||
MatrixProfile.activatedFreeTrial.title,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import 'package:fluffychat/pangea/constants/language_keys.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/repo/word_repo.dart';
|
||||
import '../models/word_data_model.dart';
|
||||
import 'base_controller.dart';
|
||||
|
|
|
|||
16
lib/pangea/enum/activity_type_enum.dart
Normal file
16
lib/pangea/enum/activity_type_enum.dart
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
enum ActivityTypeEnum { multipleChoice, freeResponse, listening, speaking }
|
||||
|
||||
extension ActivityTypeExtension on ActivityTypeEnum {
|
||||
String get string {
|
||||
switch (this) {
|
||||
case ActivityTypeEnum.multipleChoice:
|
||||
return 'multiple_choice';
|
||||
case ActivityTypeEnum.freeResponse:
|
||||
return 'free_response';
|
||||
case ActivityTypeEnum.listening:
|
||||
return 'listening';
|
||||
case ActivityTypeEnum.speaking:
|
||||
return 'speaking';
|
||||
}
|
||||
}
|
||||
}
|
||||
43
lib/pangea/enum/assistance_state_enum.dart
Normal file
43
lib/pangea/enum/assistance_state_enum.dart
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
// 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
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/constants/colors.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
enum AssistanceState {
|
||||
noMessage,
|
||||
notFetched,
|
||||
fetching,
|
||||
fetched,
|
||||
complete,
|
||||
}
|
||||
|
||||
extension AssistanceStateExtension on AssistanceState {
|
||||
Color stateColor(context) {
|
||||
switch (this) {
|
||||
case AssistanceState.noMessage:
|
||||
case AssistanceState.notFetched:
|
||||
case AssistanceState.fetching:
|
||||
return Theme.of(context).colorScheme.primary;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -29,15 +29,4 @@ extension BarChartViewSelectionExtension on BarChartViewSelection {
|
|||
return Icons.spellcheck_outlined;
|
||||
}
|
||||
}
|
||||
|
||||
String get route {
|
||||
switch (this) {
|
||||
case BarChartViewSelection.messages:
|
||||
return 'messages';
|
||||
// case BarChartViewSelection.vocab:
|
||||
// return 'vocab';
|
||||
case BarChartViewSelection.grammar:
|
||||
return 'errors';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +1,30 @@
|
|||
enum ConstructType {
|
||||
enum ConstructTypeEnum {
|
||||
grammar,
|
||||
vocab,
|
||||
}
|
||||
|
||||
extension ConstructExtension on ConstructType {
|
||||
extension ConstructExtension on ConstructTypeEnum {
|
||||
String get string {
|
||||
switch (this) {
|
||||
case ConstructType.grammar:
|
||||
case ConstructTypeEnum.grammar:
|
||||
return 'grammar';
|
||||
case ConstructType.vocab:
|
||||
case ConstructTypeEnum.vocab:
|
||||
return 'vocab';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ConstructTypeUtil {
|
||||
static ConstructType fromString(String? string) {
|
||||
static ConstructTypeEnum fromString(String? string) {
|
||||
switch (string) {
|
||||
case 'g':
|
||||
case 'grammar':
|
||||
return ConstructType.grammar;
|
||||
return ConstructTypeEnum.grammar;
|
||||
case 'v':
|
||||
case 'vocab':
|
||||
return ConstructType.vocab;
|
||||
return ConstructTypeEnum.vocab;
|
||||
default:
|
||||
return ConstructType.vocab;
|
||||
return ConstructTypeEnum.vocab;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
93
lib/pangea/enum/construct_use_type_enum.dart
Normal file
93
lib/pangea/enum/construct_use_type_enum.dart
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
enum ConstructUseTypeEnum {
|
||||
/// produced in chat by user, igc was run, and we've judged it to be a correct use
|
||||
wa,
|
||||
|
||||
/// produced in chat by user, igc was run, and we've judged it to be a incorrect use
|
||||
/// Note: if the IGC match is ignored, this is not counted as an incorrect use
|
||||
ga,
|
||||
|
||||
/// produced in chat by user and igc was not run
|
||||
unk,
|
||||
|
||||
/// selected correctly in IT flow
|
||||
corIt,
|
||||
|
||||
/// encountered as IT distractor and correctly ignored it
|
||||
ignIt,
|
||||
|
||||
/// encountered as it distractor and selected it
|
||||
incIt,
|
||||
|
||||
/// encountered in igc match and ignored match
|
||||
ignIGC,
|
||||
|
||||
/// selected correctly in IGC flow
|
||||
corIGC,
|
||||
|
||||
/// encountered as distractor in IGC flow and selected it
|
||||
incIGC,
|
||||
|
||||
/// selected correctly in practice activity flow
|
||||
corPA,
|
||||
|
||||
/// was target construct in practice activity but user did not select correctly
|
||||
incPA,
|
||||
}
|
||||
|
||||
extension ConstructUseTypeExtension on ConstructUseTypeEnum {
|
||||
String get string {
|
||||
switch (this) {
|
||||
case ConstructUseTypeEnum.ga:
|
||||
return 'ga';
|
||||
case ConstructUseTypeEnum.wa:
|
||||
return 'wa';
|
||||
case ConstructUseTypeEnum.corIt:
|
||||
return 'corIt';
|
||||
case ConstructUseTypeEnum.incIt:
|
||||
return 'incIt';
|
||||
case ConstructUseTypeEnum.ignIt:
|
||||
return 'ignIt';
|
||||
case ConstructUseTypeEnum.ignIGC:
|
||||
return 'ignIGC';
|
||||
case ConstructUseTypeEnum.corIGC:
|
||||
return 'corIGC';
|
||||
case ConstructUseTypeEnum.incIGC:
|
||||
return 'incIGC';
|
||||
case ConstructUseTypeEnum.unk:
|
||||
return 'unk';
|
||||
case ConstructUseTypeEnum.corPA:
|
||||
return 'corPA';
|
||||
case ConstructUseTypeEnum.incPA:
|
||||
return 'incPA';
|
||||
}
|
||||
}
|
||||
|
||||
IconData get icon {
|
||||
switch (this) {
|
||||
case ConstructUseTypeEnum.ga:
|
||||
return Icons.check;
|
||||
case ConstructUseTypeEnum.wa:
|
||||
return Icons.thumb_up_sharp;
|
||||
case ConstructUseTypeEnum.corIt:
|
||||
return Icons.check;
|
||||
case ConstructUseTypeEnum.incIt:
|
||||
return Icons.close;
|
||||
case ConstructUseTypeEnum.ignIt:
|
||||
return Icons.close;
|
||||
case ConstructUseTypeEnum.ignIGC:
|
||||
return Icons.close;
|
||||
case ConstructUseTypeEnum.corIGC:
|
||||
return Icons.check;
|
||||
case ConstructUseTypeEnum.incIGC:
|
||||
return Icons.close;
|
||||
case ConstructUseTypeEnum.corPA:
|
||||
return Icons.check;
|
||||
case ConstructUseTypeEnum.incPA:
|
||||
return Icons.close;
|
||||
case ConstructUseTypeEnum.unk:
|
||||
return Icons.help;
|
||||
}
|
||||
}
|
||||
}
|
||||
45
lib/pangea/enum/instructions_enum.dart
Normal file
45
lib/pangea/enum/instructions_enum.dart
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
enum InstructionsEnum {
|
||||
itInstructions,
|
||||
clickMessage,
|
||||
blurMeansTranslate,
|
||||
tooltipInstructions,
|
||||
speechToText,
|
||||
}
|
||||
|
||||
extension Copy on InstructionsEnum {
|
||||
String title(BuildContext context) {
|
||||
switch (this) {
|
||||
case InstructionsEnum.itInstructions:
|
||||
return L10n.of(context)!.itInstructionsTitle;
|
||||
case InstructionsEnum.clickMessage:
|
||||
return L10n.of(context)!.clickMessageTitle;
|
||||
case InstructionsEnum.blurMeansTranslate:
|
||||
return L10n.of(context)!.blurMeansTranslateTitle;
|
||||
case InstructionsEnum.tooltipInstructions:
|
||||
return L10n.of(context)!.tooltipInstructionsTitle;
|
||||
case InstructionsEnum.speechToText:
|
||||
return L10n.of(context)!.hintTitle;
|
||||
}
|
||||
}
|
||||
|
||||
String body(BuildContext context) {
|
||||
switch (this) {
|
||||
case InstructionsEnum.itInstructions:
|
||||
return L10n.of(context)!.itInstructionsBody;
|
||||
case InstructionsEnum.clickMessage:
|
||||
return L10n.of(context)!.clickMessageBody;
|
||||
case InstructionsEnum.blurMeansTranslate:
|
||||
return L10n.of(context)!.blurMeansTranslateBody;
|
||||
case InstructionsEnum.speechToText:
|
||||
return L10n.of(context)!.speechToTextBody;
|
||||
case InstructionsEnum.tooltipInstructions:
|
||||
return PlatformInfos.isMobile
|
||||
? L10n.of(context)!.tooltipInstructionsMobileBody
|
||||
: L10n.of(context)!.tooltipInstructionsBrowserBody;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,16 @@
|
|||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
enum MessageMode { translation, definition, speechToText, textToSpeech }
|
||||
enum MessageMode {
|
||||
translation,
|
||||
definition,
|
||||
speechToText,
|
||||
textToSpeech,
|
||||
practiceActivity
|
||||
}
|
||||
|
||||
extension MessageModeExtension on MessageMode {
|
||||
IconData get icon {
|
||||
|
|
@ -17,6 +24,8 @@ extension MessageModeExtension on MessageMode {
|
|||
//TODO change icon for audio messages
|
||||
case MessageMode.definition:
|
||||
return Icons.book;
|
||||
case MessageMode.practiceActivity:
|
||||
return Symbols.fitness_center;
|
||||
default:
|
||||
return Icons.error; // Icon to indicate an error or unsupported mode
|
||||
}
|
||||
|
|
@ -32,6 +41,8 @@ extension MessageModeExtension on MessageMode {
|
|||
return L10n.of(context)!.speechToTextTooltip;
|
||||
case MessageMode.definition:
|
||||
return L10n.of(context)!.definitions;
|
||||
case MessageMode.practiceActivity:
|
||||
return L10n.of(context)!.practice;
|
||||
default:
|
||||
return L10n.of(context)!
|
||||
.oopsSomethingWentWrong; // Title to indicate an error or unsupported mode
|
||||
|
|
@ -48,6 +59,8 @@ extension MessageModeExtension on MessageMode {
|
|||
return L10n.of(context)!.speechToTextTooltip;
|
||||
case MessageMode.definition:
|
||||
return L10n.of(context)!.define;
|
||||
case MessageMode.practiceActivity:
|
||||
return L10n.of(context)!.practice;
|
||||
default:
|
||||
return L10n.of(context)!
|
||||
.oopsSomethingWentWrong; // Title to indicate an error or unsupported mode
|
||||
|
|
@ -58,6 +71,7 @@ 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:
|
||||
|
|
@ -66,4 +80,29 @@ extension MessageModeExtension on MessageMode {
|
|||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Color? iconColor(
|
||||
PangeaMessageEvent event,
|
||||
MessageMode? currentMode,
|
||||
BuildContext context,
|
||||
) {
|
||||
final bool isPracticeActivity = this == MessageMode.practiceActivity;
|
||||
final bool practicing = currentMode == MessageMode.practiceActivity;
|
||||
final bool practiceEnabled = event.hasUncompletedActivity;
|
||||
|
||||
// if this is the practice activity icon, and there's no practice activities available,
|
||||
// and the current mode is not practice, return lower opacity color.
|
||||
if (isPracticeActivity && !practicing && !practiceEnabled) {
|
||||
return Theme.of(context).iconTheme.color?.withOpacity(0.5);
|
||||
}
|
||||
|
||||
// if this is not a practice activity icon, and practice activities are available,
|
||||
// then return lower opacity color if the current mode is practice.
|
||||
if (!isPracticeActivity && practicing && practiceEnabled) {
|
||||
return Theme.of(context).iconTheme.color?.withOpacity(0.5);
|
||||
}
|
||||
|
||||
// if this is the current mode, return primary color.
|
||||
return currentMode == this ? Theme.of(context).colorScheme.primary : null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
enum SpanDataTypeEnum {
|
||||
|
|
@ -32,7 +32,10 @@ extension SpanDataTypeEnumExt on SpanDataTypeEnum {
|
|||
case SpanDataTypeEnum.correction:
|
||||
return L10n.of(context)!.correctionDefaultPrompt;
|
||||
case SpanDataTypeEnum.itStart:
|
||||
return L10n.of(context)!.needsItMessage;
|
||||
return L10n.of(context)!.needsItMessage(
|
||||
MatrixState.pangeaController.languageController.userL2?.displayName ??
|
||||
L10n.of(context)!.targetLanguage,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import '../models/choreo_record.dart';
|
||||
import '../utils/bot_style.dart';
|
||||
|
||||
enum UseType { wa, ta, ga, un }
|
||||
|
|
@ -93,17 +91,3 @@ extension UseTypeMethods on UseType {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
UseType useTypeCalculator(
|
||||
ChoreoRecord? choreoRecord,
|
||||
) {
|
||||
if (choreoRecord == null) {
|
||||
return UseType.un;
|
||||
} else if (choreoRecord.includedIT) {
|
||||
return UseType.ta;
|
||||
} else if (choreoRecord.hasAcceptedMatches) {
|
||||
return UseType.ga;
|
||||
} else {
|
||||
return UseType.wa;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,83 +0,0 @@
|
|||
part of "client_extension.dart";
|
||||
|
||||
extension ClassesAndExchangesClientExtension on Client {
|
||||
List<Room> get _classes => rooms.where((e) => e.isPangeaClass).toList();
|
||||
|
||||
List<Room> get _classesImTeaching => rooms
|
||||
.where(
|
||||
(e) =>
|
||||
e.isPangeaClass &&
|
||||
e.ownPowerLevel == ClassDefaultValues.powerLevelOfAdmin,
|
||||
)
|
||||
.toList();
|
||||
|
||||
Future<List<Room>> get _classesAndExchangesImTeaching async {
|
||||
final allSpaces = rooms.where((room) => room.isSpace);
|
||||
for (final Room space in allSpaces) {
|
||||
if (space.getState(EventTypes.RoomPowerLevels) == null) {
|
||||
await space.postLoad();
|
||||
}
|
||||
}
|
||||
|
||||
final spaces = rooms
|
||||
.where(
|
||||
(e) =>
|
||||
(e.isPangeaClass || e.isExchange) &&
|
||||
e.ownPowerLevel == ClassDefaultValues.powerLevelOfAdmin,
|
||||
)
|
||||
.toList();
|
||||
return spaces;
|
||||
}
|
||||
|
||||
List<Room> get _classesImIn => rooms
|
||||
.where(
|
||||
(e) =>
|
||||
e.isPangeaClass &&
|
||||
e.ownPowerLevel < ClassDefaultValues.powerLevelOfAdmin,
|
||||
)
|
||||
.toList();
|
||||
|
||||
Future<List<Room>> get _classesAndExchangesImStudyingIn async {
|
||||
final List<Room> joinedSpaces = rooms
|
||||
.where(
|
||||
(room) => room.isSpace && room.membership == Membership.join,
|
||||
)
|
||||
.toList();
|
||||
|
||||
for (final Room space in joinedSpaces) {
|
||||
if (space.getState(EventTypes.RoomPowerLevels) == null) {
|
||||
await space.postLoad();
|
||||
}
|
||||
}
|
||||
|
||||
final spaces = rooms
|
||||
.where(
|
||||
(e) =>
|
||||
(e.isPangeaClass || e.isExchange) &&
|
||||
e.ownPowerLevel < ClassDefaultValues.powerLevelOfAdmin,
|
||||
)
|
||||
.toList();
|
||||
return spaces;
|
||||
}
|
||||
|
||||
List<Room> get _classesAndExchangesImIn =>
|
||||
rooms.where((e) => e.isPangeaClass || e.isExchange).toList();
|
||||
|
||||
Future<PangeaRoomRules?> get _lastUpdatedRoomRules async =>
|
||||
(await _classesAndExchangesImTeaching)
|
||||
.where((space) => space.rulesUpdatedAt != null)
|
||||
.sorted(
|
||||
(a, b) => b.rulesUpdatedAt!.compareTo(a.rulesUpdatedAt!),
|
||||
)
|
||||
.firstOrNull
|
||||
?.pangeaRoomRules;
|
||||
|
||||
ClassSettingsModel? get _lastUpdatedClassSettings => classesImTeaching
|
||||
.where((space) => space.classSettingsUpdatedAt != null)
|
||||
.sorted(
|
||||
(a, b) =>
|
||||
b.classSettingsUpdatedAt!.compareTo(a.classSettingsUpdatedAt!),
|
||||
)
|
||||
.firstOrNull
|
||||
?.classSettings;
|
||||
}
|
||||
|
|
@ -123,7 +123,7 @@ extension AnalyticsClientExtension on Client {
|
|||
// Allows teachers to join analytics rooms without being invited
|
||||
Future<void> _joinAnalyticsRoomsInAllSpaces() async {
|
||||
final List<Future> joinFutures = [];
|
||||
for (final Room space in (await _classesAndExchangesImTeaching)) {
|
||||
for (final Room space in (await _spacesImTeaching)) {
|
||||
joinFutures.add(space.joinAnalyticsRoomsInSpace());
|
||||
}
|
||||
await Future.wait(joinFutures);
|
||||
|
|
@ -154,4 +154,17 @@ extension AnalyticsClientExtension on Client {
|
|||
await _joinInvitedAnalyticsRooms();
|
||||
await _joinAnalyticsRoomsInAllSpaces();
|
||||
}
|
||||
|
||||
Future<Map<String, DateTime?>> _allAnalyticsRoomsLastUpdated() async {
|
||||
// get the last updated time for each analytics room
|
||||
final Map<String, DateTime?> lastUpdatedMap = {};
|
||||
for (final analyticsRoom in allMyAnalyticsRooms) {
|
||||
final DateTime? lastUpdated = await analyticsRoom.analyticsLastUpdated(
|
||||
PangeaEventTypes.summaryAnalytics,
|
||||
userID!,
|
||||
);
|
||||
lastUpdatedMap[analyticsRoom.id] = lastUpdated;
|
||||
}
|
||||
return lastUpdatedMap;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,17 +3,18 @@ import 'dart:developer';
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/pangea/constants/class_default_values.dart';
|
||||
import 'package:fluffychat/pangea/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.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/models/class_model.dart';
|
||||
import 'package:fluffychat/pangea/models/space_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/bot_name.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
part "classes_and_exchanges_extension.dart";
|
||||
part "client_analytics_extension.dart";
|
||||
part "general_info_extension.dart";
|
||||
part "space_extension.dart";
|
||||
|
||||
extension PangeaClient on Client {
|
||||
// analytics
|
||||
|
|
@ -43,27 +44,22 @@ extension PangeaClient on Client {
|
|||
|
||||
Future<void> migrateAnalyticsRooms() async => await _migrateAnalyticsRooms();
|
||||
|
||||
// classes_and_exchanges
|
||||
Future<Map<String, DateTime?>> allAnalyticsRoomsLastUpdated() async =>
|
||||
await _allAnalyticsRoomsLastUpdated();
|
||||
|
||||
List<Room> get classes => _classes;
|
||||
// spaces
|
||||
|
||||
List<Room> get classesImTeaching => _classesImTeaching;
|
||||
Future<List<Room>> get spacesImTeaching async => await _spacesImTeaching;
|
||||
|
||||
Future<List<Room>> get classesAndExchangesImTeaching async =>
|
||||
await _classesAndExchangesImTeaching;
|
||||
Future<List<Room>> get chatsImAStudentIn async => await _chatsImAStudentIn;
|
||||
|
||||
List<Room> get classesImIn => _classesImIn;
|
||||
Future<List<Room>> get spaceImAStudentIn async => await _spacesImStudyingIn;
|
||||
|
||||
Future<List<Room>> get classesAndExchangesImStudyingIn async =>
|
||||
await _classesAndExchangesImStudyingIn;
|
||||
|
||||
List<Room> get classesAndExchangesImIn => _classesAndExchangesImIn;
|
||||
List<Room> get spacesImIn => _spacesImIn;
|
||||
|
||||
Future<PangeaRoomRules?> get lastUpdatedRoomRules async =>
|
||||
await _lastUpdatedRoomRules;
|
||||
|
||||
ClassSettingsModel? get lastUpdatedClassSettings => _lastUpdatedClassSettings;
|
||||
|
||||
// general_info
|
||||
|
||||
Future<List<String>> get teacherRoomIds async => await _teacherRoomIds;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ part of "client_extension.dart";
|
|||
extension GeneralInfoClientExtension on Client {
|
||||
Future<List<String>> get _teacherRoomIds async {
|
||||
final List<String> adminRoomIds = [];
|
||||
for (final Room adminSpace in (await _classesAndExchangesImTeaching)) {
|
||||
for (final Room adminSpace in (await _spacesImTeaching)) {
|
||||
adminRoomIds.add(adminSpace.id);
|
||||
final List<String> adminSpaceRooms = adminSpace.allSpaceChildRoomIds;
|
||||
adminRoomIds.addAll(adminSpaceRooms);
|
||||
|
|
@ -13,7 +13,7 @@ extension GeneralInfoClientExtension on Client {
|
|||
|
||||
Future<List<User>> get _myTeachers async {
|
||||
final List<User> teachers = [];
|
||||
for (final classRoom in classesAndExchangesImIn) {
|
||||
for (final classRoom in spacesImIn) {
|
||||
for (final teacher in await classRoom.teachers) {
|
||||
// If person requesting list of teachers is a teacher in another classroom, don't add them to the list
|
||||
if (!teachers.any((e) => e.id == teacher.id) && userID != teacher.id) {
|
||||
|
|
|
|||
76
lib/pangea/extensions/client_extension/space_extension.dart
Normal file
76
lib/pangea/extensions/client_extension/space_extension.dart
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
part of "client_extension.dart";
|
||||
|
||||
extension SpaceClientExtension on Client {
|
||||
Future<List<Room>> get _spacesImTeaching async {
|
||||
final allSpaces = rooms.where((room) => room.isSpace);
|
||||
for (final Room space in allSpaces) {
|
||||
if (space.getState(EventTypes.RoomPowerLevels) == null) {
|
||||
await space.postLoad();
|
||||
}
|
||||
}
|
||||
|
||||
final spaces = rooms
|
||||
.where(
|
||||
(e) =>
|
||||
(e.isSpace) &&
|
||||
e.ownPowerLevel == ClassDefaultValues.powerLevelOfAdmin,
|
||||
)
|
||||
.toList();
|
||||
return spaces;
|
||||
}
|
||||
|
||||
Future<List<Room>> get _chatsImAStudentIn async {
|
||||
final List<String> nowteacherRoomIds = await teacherRoomIds;
|
||||
return rooms
|
||||
.where(
|
||||
(r) =>
|
||||
!r.isSpace &&
|
||||
!r.isAnalyticsRoom &&
|
||||
!nowteacherRoomIds.contains(r.id),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<List<Room>> get _spacesImStudyingIn async {
|
||||
final List<Room> joinedSpaces = rooms
|
||||
.where(
|
||||
(room) => room.isSpace && room.membership == Membership.join,
|
||||
)
|
||||
.toList();
|
||||
|
||||
for (final Room space in joinedSpaces) {
|
||||
if (space.getState(EventTypes.RoomPowerLevels) == null) {
|
||||
await space.postLoad();
|
||||
}
|
||||
}
|
||||
|
||||
final spaces = rooms
|
||||
.where(
|
||||
(e) =>
|
||||
e.isSpace &&
|
||||
e.ownPowerLevel < ClassDefaultValues.powerLevelOfAdmin,
|
||||
)
|
||||
.toList();
|
||||
return spaces;
|
||||
}
|
||||
|
||||
List<Room> get _spacesImIn => rooms.where((e) => e.isSpace).toList();
|
||||
|
||||
Future<PangeaRoomRules?> get _lastUpdatedRoomRules async =>
|
||||
(await _spacesImTeaching)
|
||||
.where((space) => space.rulesUpdatedAt != null)
|
||||
.sorted(
|
||||
(a, b) => b.rulesUpdatedAt!.compareTo(a.rulesUpdatedAt!),
|
||||
)
|
||||
.firstOrNull
|
||||
?.pangeaRoomRules;
|
||||
|
||||
// LanguageSettingsModel? get _lastUpdatedLanguageSettings => rooms
|
||||
// .where((room) => room.isSpace && room.languageSettingsUpdatedAt != null)
|
||||
// .sorted(
|
||||
// (a, b) => b.languageSettingsUpdatedAt!
|
||||
// .compareTo(a.languageSettingsUpdatedAt!),
|
||||
// )
|
||||
// .firstOrNull
|
||||
// ?.languageSettings;
|
||||
}
|
||||
|
|
@ -2,6 +2,8 @@ import 'dart:developer';
|
|||
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/models/choreo_record.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart';
|
||||
import 'package:fluffychat/pangea/models/representation_content_model.dart';
|
||||
import 'package:fluffychat/pangea/models/tokens_event_content_model.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
|
@ -26,7 +28,12 @@ extension PangeaEvent on Event {
|
|||
return PangeaRepresentation.fromJson(json) as V;
|
||||
case PangeaEventTypes.choreoRecord:
|
||||
return ChoreoRecord.fromJson(json) as V;
|
||||
case PangeaEventTypes.pangeaActivity:
|
||||
return PracticeActivityModel.fromJson(json) as V;
|
||||
case PangeaEventTypes.activityRecord:
|
||||
return PracticeActivityRecordModel.fromJson(json) as V;
|
||||
default:
|
||||
debugger(when: kDebugMode);
|
||||
throw Exception("$type events do not have pangea content");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ extension ChildrenAndParentsRoomExtension on Room {
|
|||
//resolve somehow if multiple rooms have the state?
|
||||
//check logic
|
||||
Room? _firstParentWithState(String stateType) {
|
||||
if (![PangeaEventTypes.classSettings, PangeaEventTypes.rules]
|
||||
if (![PangeaEventTypes.languageSettings, PangeaEventTypes.rules]
|
||||
.contains(stateType)) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -77,13 +77,6 @@ extension ChildrenAndParentsRoomExtension on Room {
|
|||
return null;
|
||||
}
|
||||
|
||||
/// find any parents and return the rooms
|
||||
List<Room> get _immediateClassParents => pangeaSpaceParents
|
||||
.where(
|
||||
(element) => element.isPangeaClass,
|
||||
)
|
||||
.toList();
|
||||
|
||||
List<Room> get _pangeaSpaceParents => client.rooms
|
||||
.where(
|
||||
(r) => r.isSpace,
|
||||
|
|
@ -98,16 +91,16 @@ extension ChildrenAndParentsRoomExtension on Room {
|
|||
String _nameIncludingParents(BuildContext context) {
|
||||
String nameSoFar = getLocalizedDisplayname(MatrixLocals(L10n.of(context)!));
|
||||
Room currentRoom = this;
|
||||
if (currentRoom.immediateClassParents.isEmpty) {
|
||||
if (currentRoom.pangeaSpaceParents.isEmpty) {
|
||||
return nameSoFar;
|
||||
}
|
||||
currentRoom = currentRoom.immediateClassParents.first;
|
||||
currentRoom = currentRoom.pangeaSpaceParents.first;
|
||||
var nameToAdd =
|
||||
currentRoom.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!));
|
||||
nameToAdd =
|
||||
nameToAdd.length <= 10 ? nameToAdd : "${nameToAdd.substring(0, 10)}...";
|
||||
nameSoFar = '$nameToAdd > $nameSoFar';
|
||||
if (currentRoom.immediateClassParents.isEmpty) {
|
||||
if (currentRoom.pangeaSpaceParents.isEmpty) {
|
||||
return nameSoFar;
|
||||
}
|
||||
return "... > $nameSoFar";
|
||||
|
|
@ -130,11 +123,19 @@ extension ChildrenAndParentsRoomExtension on Room {
|
|||
|
||||
// Checks if has permissions to add child chat
|
||||
// Or whether potential child space is ancestor of this
|
||||
bool _canAddAsParentOf(Room? child) {
|
||||
if (child == null || !child.isSpace) {
|
||||
return _canIAddSpaceChild(child);
|
||||
}
|
||||
if (id == child.id) return false;
|
||||
return !child._allSpaceChildRoomIds.contains(id);
|
||||
bool _canAddAsParentOf(Room? child, {bool spaceMode = false}) {
|
||||
// don't add a room to itself
|
||||
if (id == child?.id) return false;
|
||||
spaceMode = child?.isSpace ?? spaceMode;
|
||||
|
||||
// get the bool for adding chats to spaces
|
||||
final bool canAddChild = _canIAddSpaceChild(child, spaceMode: spaceMode);
|
||||
if (!spaceMode) return canAddChild;
|
||||
|
||||
// if adding space to a space, check if the child is an ancestor
|
||||
// to prevent cycles
|
||||
final bool isCycle =
|
||||
child != null ? child._allSpaceChildRoomIds.contains(id) : false;
|
||||
return canAddChild && !isCycle;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -229,7 +229,6 @@ extension EventsRoomExtension on Room {
|
|||
PangeaMessageTokens? tokensSent,
|
||||
PangeaMessageTokens? tokensWritten,
|
||||
ChoreoRecord? choreo,
|
||||
UseType? useType,
|
||||
}) {
|
||||
// if (parseCommands) {
|
||||
// return client.parseAndRunCommand(this, message,
|
||||
|
|
@ -247,7 +246,6 @@ extension EventsRoomExtension on Room {
|
|||
ModelKey.originalWritten: originalWritten?.toJson(),
|
||||
ModelKey.tokensSent: tokensSent?.toJson(),
|
||||
ModelKey.tokensWritten: tokensWritten?.toJson(),
|
||||
ModelKey.useType: useType?.string,
|
||||
};
|
||||
if (parseMarkdown) {
|
||||
final html = markdown(
|
||||
|
|
@ -347,7 +345,7 @@ extension EventsRoomExtension on Room {
|
|||
RecentMessageRecord(
|
||||
eventId: event.eventId,
|
||||
chatId: id,
|
||||
useType: pMsgEvent.useType,
|
||||
useType: pMsgEvent.msgUseType,
|
||||
time: event.originServerTs,
|
||||
),
|
||||
);
|
||||
|
|
@ -426,26 +424,6 @@ extension EventsRoomExtension on Room {
|
|||
// }
|
||||
// }
|
||||
|
||||
Future<List<PangeaMessageEvent>> myMessageEventsInChat({
|
||||
DateTime? since,
|
||||
}) async {
|
||||
final List<Event> msgEvents = await getEventsBySender(
|
||||
type: EventTypes.Message,
|
||||
sender: client.userID!,
|
||||
since: since,
|
||||
);
|
||||
final Timeline timeline = await getTimeline();
|
||||
return msgEvents
|
||||
.where((event) => (event.content['msgtype'] == MessageTypes.Text))
|
||||
.map((event) {
|
||||
return PangeaMessageEvent(
|
||||
event: event,
|
||||
timeline: timeline,
|
||||
ownMessage: true,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// fetch event of a certain type by a certain sender
|
||||
// since a certain time or up to a certain amount
|
||||
Future<List<Event>> getEventsBySender({
|
||||
|
|
|
|||
|
|
@ -4,15 +4,19 @@ 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/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/constants/pangea_room_types.dart';
|
||||
import 'package:fluffychat/pangea/controllers/language_list_controller.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/analytics_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/summary_analytics_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart';
|
||||
import 'package:fluffychat/pangea/models/bot_options_model.dart';
|
||||
import 'package:fluffychat/pangea/models/class_model.dart';
|
||||
import 'package:fluffychat/pangea/models/language_model.dart';
|
||||
import 'package:fluffychat/pangea/models/space_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';
|
||||
|
|
@ -30,17 +34,16 @@ import 'package:sentry_flutter/sentry_flutter.dart';
|
|||
|
||||
import '../../../config/app_config.dart';
|
||||
import '../../constants/pangea_event_types.dart';
|
||||
import '../../enum/use_type.dart';
|
||||
import '../../models/choreo_record.dart';
|
||||
import '../../models/representation_content_model.dart';
|
||||
import '../client_extension/client_extension.dart';
|
||||
|
||||
part "children_and_parents_extension.dart";
|
||||
part "class_and_exchange_settings_extension.dart";
|
||||
part "events_extension.dart";
|
||||
part "room_analytics_extension.dart";
|
||||
part "room_information_extension.dart";
|
||||
part "room_settings_extension.dart";
|
||||
part "space_settings_extension.dart";
|
||||
part "user_permissions_extension.dart";
|
||||
|
||||
extension PangeaRoom on Room {
|
||||
|
|
@ -49,9 +52,6 @@ extension PangeaRoom on Room {
|
|||
Future<void> joinAnalyticsRoomsInSpace() async =>
|
||||
await _joinAnalyticsRoomsInSpace();
|
||||
|
||||
Future<void> ensureAnalyticsRoomExists() async =>
|
||||
await _ensureAnalyticsRoomExists();
|
||||
|
||||
Future<void> addAnalyticsRoomToSpace(Room analyticsRoom) async =>
|
||||
await _addAnalyticsRoomToSpace(analyticsRoom);
|
||||
|
||||
|
|
@ -105,8 +105,6 @@ extension PangeaRoom on Room {
|
|||
Room? firstParentWithState(String stateType) =>
|
||||
_firstParentWithState(stateType);
|
||||
|
||||
List<Room> get immediateClassParents => _immediateClassParents;
|
||||
|
||||
List<Room> get pangeaSpaceParents => _pangeaSpaceParents;
|
||||
|
||||
String nameIncludingParents(BuildContext context) =>
|
||||
|
|
@ -114,7 +112,9 @@ extension PangeaRoom on Room {
|
|||
|
||||
List<String> get allSpaceChildRoomIds => _allSpaceChildRoomIds;
|
||||
|
||||
bool canAddAsParentOf(Room? child) => _canAddAsParentOf(child);
|
||||
bool canAddAsParentOf(Room? child, {bool spaceMode = false}) {
|
||||
return _canAddAsParentOf(child, spaceMode: spaceMode);
|
||||
}
|
||||
|
||||
// class_and_exchange_settings
|
||||
|
||||
|
|
@ -130,15 +130,10 @@ extension PangeaRoom on Room {
|
|||
|
||||
Future<void> setClassPowerLevels() async => await _setClassPowerLevels();
|
||||
|
||||
DateTime? get classSettingsUpdatedAt => _classSettingsUpdatedAt;
|
||||
|
||||
ClassSettingsModel? get classSettings => _classSettings;
|
||||
|
||||
Event? get languageSettingsStateEvent => _languageSettingsStateEvent;
|
||||
|
||||
Event? get pangeaRoomRulesStateEvent => _pangeaRoomRulesStateEvent;
|
||||
|
||||
ClassSettingsModel? get firstLanguageSettings => _firstLanguageSettings;
|
||||
Future<List<LanguageModel>> targetLanguages() async =>
|
||||
await _targetLanguages();
|
||||
|
||||
// events
|
||||
|
||||
|
|
@ -185,7 +180,6 @@ extension PangeaRoom on Room {
|
|||
PangeaMessageTokens? tokensSent,
|
||||
PangeaMessageTokens? tokensWritten,
|
||||
ChoreoRecord? choreo,
|
||||
UseType? useType,
|
||||
}) =>
|
||||
_pangeaSendTextEvent(
|
||||
message,
|
||||
|
|
@ -202,7 +196,6 @@ extension PangeaRoom on Room {
|
|||
tokensSent: tokensSent,
|
||||
tokensWritten: tokensWritten,
|
||||
choreo: choreo,
|
||||
useType: useType,
|
||||
);
|
||||
|
||||
Future<String> updateStateEvent(Event stateEvent) =>
|
||||
|
|
@ -222,8 +215,6 @@ extension PangeaRoom on Room {
|
|||
|
||||
bool isFirstOrSecondChild(String roomId) => _isFirstOrSecondChild(roomId);
|
||||
|
||||
bool get isExchange => _isExchange;
|
||||
|
||||
bool get isDirectChatWithoutMe => _isDirectChatWithoutMe;
|
||||
|
||||
// bool isMadeForLang(String langCode) => _isMadeForLang(langCode);
|
||||
|
|
@ -234,8 +225,6 @@ extension PangeaRoom on Room {
|
|||
|
||||
bool get isLocked => _isLocked;
|
||||
|
||||
bool get isPangeaClass => _isPangeaClass;
|
||||
|
||||
bool isAnalyticsRoomOfUser(String userId) => _isAnalyticsRoomOfUser(userId);
|
||||
|
||||
bool get isAnalyticsRoom => _isAnalyticsRoom;
|
||||
|
|
@ -283,7 +272,9 @@ extension PangeaRoom on Room {
|
|||
|
||||
bool get canDelete => _canDelete;
|
||||
|
||||
bool canIAddSpaceChild(Room? room) => _canIAddSpaceChild(room);
|
||||
bool canIAddSpaceChild(Room? room, {bool spaceMode = false}) {
|
||||
return _canIAddSpaceChild(room, spaceMode: spaceMode);
|
||||
}
|
||||
|
||||
bool get canIAddSpaceParents => _canIAddSpaceParents;
|
||||
|
||||
|
|
|
|||
|
|
@ -55,14 +55,6 @@ extension AnalyticsRoomExtension on Room {
|
|||
}
|
||||
}
|
||||
|
||||
// check if analytics room exists for a given language code
|
||||
// and if not, create it
|
||||
Future<void> _ensureAnalyticsRoomExists() async {
|
||||
await postLoad();
|
||||
if (firstLanguageSettings?.targetLanguage == null) return;
|
||||
await client.getMyAnalyticsRoom(firstLanguageSettings!.targetLanguage);
|
||||
}
|
||||
|
||||
// add 1 analytics room to 1 space
|
||||
Future<void> _addAnalyticsRoomToSpace(Room analyticsRoom) async {
|
||||
if (!isSpace) {
|
||||
|
|
@ -107,7 +99,7 @@ extension AnalyticsRoomExtension on Room {
|
|||
return;
|
||||
}
|
||||
|
||||
for (final Room space in (await client.classesAndExchangesImStudyingIn)) {
|
||||
for (final Room space in (await client.spaceImAStudentIn)) {
|
||||
if (space.spaceChildren.any((sc) => sc.roomId == id)) continue;
|
||||
await space.addAnalyticsRoomToSpace(this);
|
||||
}
|
||||
|
|
@ -183,7 +175,7 @@ extension AnalyticsRoomExtension on Room {
|
|||
return;
|
||||
}
|
||||
|
||||
for (final Room space in (await client.classesAndExchangesImStudyingIn)) {
|
||||
for (final Room space in (await client.spaceImAStudentIn)) {
|
||||
await space.inviteSpaceTeachersToAnalyticsRoom(this);
|
||||
}
|
||||
}
|
||||
|
|
@ -202,7 +194,7 @@ extension AnalyticsRoomExtension on Room {
|
|||
final List<Event> events = await getEventsBySender(
|
||||
type: type,
|
||||
sender: userId,
|
||||
count: 1,
|
||||
count: 10,
|
||||
);
|
||||
if (events.isEmpty) return null;
|
||||
final Event event = events.first;
|
||||
|
|
@ -257,4 +249,31 @@ extension AnalyticsRoomExtension on Room {
|
|||
return creationContent?.tryGet<String>(ModelKey.langCode) == langCode ||
|
||||
creationContent?.tryGet<String>(ModelKey.oldLangCode) == langCode;
|
||||
}
|
||||
|
||||
Future<String?> sendSummaryAnalyticsEvent(
|
||||
List<RecentMessageRecord> records,
|
||||
) async {
|
||||
final SummaryAnalyticsModel analyticsModel = SummaryAnalyticsModel(
|
||||
messages: records,
|
||||
);
|
||||
final String? eventId = await sendEvent(
|
||||
analyticsModel.toJson(),
|
||||
type: PangeaEventTypes.summaryAnalytics,
|
||||
);
|
||||
return eventId;
|
||||
}
|
||||
|
||||
Future<String?> sendConstructsEvent(
|
||||
List<OneConstructUse> uses,
|
||||
) async {
|
||||
final ConstructAnalyticsModel constructsModel = ConstructAnalyticsModel(
|
||||
uses: uses,
|
||||
);
|
||||
|
||||
final String? eventId = await sendEvent(
|
||||
constructsModel.toJson(),
|
||||
type: PangeaEventTypes.construct,
|
||||
);
|
||||
return eventId;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,11 +40,6 @@ extension RoomInformationRoomExtension on Room {
|
|||
));
|
||||
}
|
||||
|
||||
bool get _isExchange =>
|
||||
isSpace &&
|
||||
languageSettingsStateEvent == null &&
|
||||
pangeaRoomRulesStateEvent != null;
|
||||
|
||||
bool get _isDirectChatWithoutMe =>
|
||||
isDirectChat && !getParticipants().any((e) => e.id == client.userID);
|
||||
|
||||
|
|
@ -83,8 +78,6 @@ extension RoomInformationRoomExtension on Room {
|
|||
ClassDefaultValues.powerLevelOfAdmin;
|
||||
}
|
||||
|
||||
bool get _isPangeaClass => isSpace && languageSettingsStateEvent != null;
|
||||
|
||||
bool _isAnalyticsRoomOfUser(String userId) =>
|
||||
isAnalyticsRoom && isMadeByUser(userId);
|
||||
|
||||
|
|
|
|||
|
|
@ -40,8 +40,7 @@ extension RoomSettingsRoomExtension on Room {
|
|||
|
||||
IconData? get _roomTypeIcon {
|
||||
if (membership == Membership.invite) return Icons.add;
|
||||
if (isPangeaClass) return Icons.school;
|
||||
if (isExchange) return Icons.connecting_airports;
|
||||
if (isSpace) return Icons.school;
|
||||
if (isAnalyticsRoom) return Icons.analytics;
|
||||
if (isDirectChat) return Icons.forum;
|
||||
return Icons.group;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
part of "pangea_room_extension.dart";
|
||||
|
||||
extension ClassAndExchangeSettingsRoomExtension on Room {
|
||||
extension SpaceRoomExtension on Room {
|
||||
DateTime? get _rulesUpdatedAt {
|
||||
if (!isSpace) return null;
|
||||
return pangeaRoomRulesStateEvent?.originServerTs ?? creationTime;
|
||||
|
|
@ -9,7 +9,7 @@ extension ClassAndExchangeSettingsRoomExtension on Room {
|
|||
String get _classCode {
|
||||
if (!isSpace) {
|
||||
for (final Room potentialClassRoom in pangeaSpaceParents) {
|
||||
if (potentialClassRoom.isPangeaClass) {
|
||||
if (potentialClassRoom.isSpace) {
|
||||
return potentialClassRoom.classCode;
|
||||
}
|
||||
}
|
||||
|
|
@ -84,46 +84,6 @@ extension ClassAndExchangeSettingsRoomExtension on Room {
|
|||
}
|
||||
}
|
||||
|
||||
DateTime? get _classSettingsUpdatedAt {
|
||||
if (!isSpace) return null;
|
||||
return languageSettingsStateEvent?.originServerTs ?? creationTime;
|
||||
}
|
||||
|
||||
/// the pangeaClass event is listed an importantStateEvent so, if event exists,
|
||||
/// it's already local. If it's an old class and doesn't, then the class_controller
|
||||
/// should automatically migrate during this same session, when the space is first loaded
|
||||
ClassSettingsModel? get _classSettings {
|
||||
try {
|
||||
if (!isSpace) {
|
||||
return null;
|
||||
}
|
||||
final Map<String, dynamic>? content = languageSettingsStateEvent?.content;
|
||||
if (content != null) {
|
||||
final ClassSettingsModel classSettings =
|
||||
ClassSettingsModel.fromJson(content);
|
||||
return classSettings;
|
||||
}
|
||||
return null;
|
||||
} catch (err, s) {
|
||||
Sentry.addBreadcrumb(
|
||||
Breadcrumb(
|
||||
message: "Error in classSettings",
|
||||
data: {"room": toJson()},
|
||||
),
|
||||
);
|
||||
ErrorHandler.logError(e: err, s: s);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Event? get _languageSettingsStateEvent {
|
||||
final dynamic classSettings = getState(PangeaEventTypes.classSettings);
|
||||
if (classSettings is Event) {
|
||||
return classSettings;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Event? get _pangeaRoomRulesStateEvent {
|
||||
final dynamic roomRules = getState(PangeaEventTypes.rules);
|
||||
if (roomRules is Event) {
|
||||
|
|
@ -132,7 +92,76 @@ extension ClassAndExchangeSettingsRoomExtension on Room {
|
|||
return null;
|
||||
}
|
||||
|
||||
ClassSettingsModel? get _firstLanguageSettings =>
|
||||
classSettings ??
|
||||
firstParentWithState(PangeaEventTypes.classSettings)?.classSettings;
|
||||
Future<List<LanguageModel>> _targetLanguages() async {
|
||||
await requestParticipants();
|
||||
final students = _students;
|
||||
|
||||
final Map<LanguageModel, int> langCounts = {};
|
||||
final List<Room> allRooms = client.rooms;
|
||||
for (final User student in students) {
|
||||
for (final Room room in allRooms) {
|
||||
if (!room.isAnalyticsRoomOfUser(student.id)) continue;
|
||||
final String? langCode = room.madeForLang;
|
||||
if (langCode == null ||
|
||||
langCode.isEmpty ||
|
||||
langCode == LanguageKeys.unknownLanguage) {
|
||||
continue;
|
||||
}
|
||||
final LanguageModel lang = PangeaLanguage.byLangCode(langCode);
|
||||
langCounts[lang] ??= 0;
|
||||
langCounts[lang] = langCounts[lang]! + 1;
|
||||
}
|
||||
}
|
||||
// get a list of language models, sorted
|
||||
// by the number of students who are learning that language
|
||||
return langCounts.entries.map((entry) => entry.key).toList()
|
||||
..sort(
|
||||
(a, b) => langCounts[b]!.compareTo(langCounts[a]!),
|
||||
);
|
||||
}
|
||||
|
||||
// DateTime? get _languageSettingsUpdatedAt {
|
||||
// if (!isSpace) return null;
|
||||
// return languageSettingsStateEvent?.originServerTs ?? creationTime;
|
||||
// }
|
||||
|
||||
/// the pangeaClass event is listed an importantStateEvent so, if event exists,
|
||||
/// it's already local. If it's an old class and doesn't, then the class_controller
|
||||
/// should automatically migrate during this same session, when the space is first loaded
|
||||
// LanguageSettingsModel? get _languageSettings {
|
||||
// try {
|
||||
// if (!isSpace) {
|
||||
// return null;
|
||||
// }
|
||||
// final Map<String, dynamic>? content = languageSettingsStateEvent?.content;
|
||||
// if (content != null) {
|
||||
// final LanguageSettingsModel languageSettings =
|
||||
// LanguageSettingsModel.fromJson(content);
|
||||
// return languageSettings;
|
||||
// }
|
||||
// return null;
|
||||
// } catch (err, s) {
|
||||
// Sentry.addBreadcrumb(
|
||||
// Breadcrumb(
|
||||
// message: "Error in languageSettings",
|
||||
// data: {"room": toJson()},
|
||||
// ),
|
||||
// );
|
||||
// ErrorHandler.logError(e: err, s: s);
|
||||
// return null;
|
||||
// }
|
||||
// }
|
||||
|
||||
// Event? get _languageSettingsStateEvent {
|
||||
// final dynamic languageSettings =
|
||||
// getState(PangeaEventTypes.languageSettings);
|
||||
// if (languageSettings is Event) {
|
||||
// return languageSettings;
|
||||
// }
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// LanguageSettingsModel? get _firstLanguageSettings =>
|
||||
// languageSettings ??
|
||||
// firstParentWithState(PangeaEventTypes.languageSettings)?.languageSettings;
|
||||
}
|
||||
|
|
@ -78,39 +78,30 @@ extension UserPermissionsRoomExtension on Room {
|
|||
|
||||
bool get _canDelete => isSpaceAdmin;
|
||||
|
||||
bool _canIAddSpaceChild(Room? room) {
|
||||
bool _canIAddSpaceChild(Room? room, {bool spaceMode = false}) {
|
||||
if (!isSpace) {
|
||||
ErrorHandler.logError(
|
||||
m: "should not call canIAddSpaceChildren on non-space room",
|
||||
m: "should not call canIAddSpaceChildren on non-space room. Room id: $id",
|
||||
data: toJson(),
|
||||
s: StackTrace.current,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (room != null && !room._isRoomAdmin) {
|
||||
return false;
|
||||
}
|
||||
if (!pangeaCanSendEvent(EventTypes.SpaceChild) && !_isRoomAdmin) {
|
||||
return false;
|
||||
}
|
||||
if (room == null) {
|
||||
return isRoomAdmin || (pangeaRoomRules?.isCreateRooms ?? false);
|
||||
}
|
||||
if (room.isExchange) {
|
||||
return isRoomAdmin;
|
||||
}
|
||||
if (!room.isSpace) {
|
||||
return pangeaRoomRules?.isCreateRooms ?? false;
|
||||
}
|
||||
if (room.isPangeaClass) {
|
||||
ErrorHandler.logError(
|
||||
m: "should not call canIAddSpaceChild with class",
|
||||
data: room.toJson(),
|
||||
s: StackTrace.current,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
|
||||
final isSpaceAdmin = isRoomAdmin;
|
||||
final isChildRoomAdmin = room?.isRoomAdmin ?? true;
|
||||
|
||||
// if user is not admin of child room, return false
|
||||
if (!isChildRoomAdmin) return false;
|
||||
|
||||
// if the child room is a space, or will be a space,
|
||||
// then the user must be an admin of the parent space
|
||||
if (room?.isSpace ?? spaceMode) return isSpaceAdmin;
|
||||
|
||||
// otherwise, the user can add the child room to the parent
|
||||
// if they're the admin of the parent or if the parent creation
|
||||
// of group chats
|
||||
return isSpaceAdmin || (pangeaRoomRules?.isCreateRooms ?? false);
|
||||
}
|
||||
|
||||
bool get _canIAddSpaceParents =>
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
// relates to a pangea representation event
|
||||
// the matrix even fits the form of a regular matrix audio event
|
||||
// but with something to distinguish it as a pangea audio event
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
class PangeaAudioEvent {
|
||||
Event? _event;
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
|
@ -23,7 +25,7 @@ class ChoreoEvent {
|
|||
_content ??= event.getPangeaContent<ChoreoRecord>();
|
||||
return _content;
|
||||
} catch (err, s) {
|
||||
if (kDebugMode) rethrow;
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: err, s: s);
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,34 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/pangea/constants/choreo_constants.dart';
|
||||
import 'package:fluffychat/pangea/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/audio_encoding_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_representation_event.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/models/choreo_record.dart';
|
||||
import 'package:fluffychat/pangea/models/class_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/models/representation_content_model.dart';
|
||||
import 'package:fluffychat/pangea/models/space_model.dart';
|
||||
import 'package:fluffychat/pangea/models/speech_to_text_models.dart';
|
||||
import 'package:fluffychat/pangea/models/tokens_event_content_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/bot_name.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_audio_card.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import '../../widgets/matrix.dart';
|
||||
import '../constants/language_keys.dart';
|
||||
import '../constants/language_constants.dart';
|
||||
import '../constants/pangea_event_types.dart';
|
||||
import '../enum/use_type.dart';
|
||||
import '../utils/error_handler.dart';
|
||||
|
|
@ -28,7 +37,6 @@ class PangeaMessageEvent {
|
|||
late Event _event;
|
||||
final Timeline timeline;
|
||||
final bool ownMessage;
|
||||
bool _isValidPangeaMessageEvent = true;
|
||||
|
||||
PangeaMessageEvent({
|
||||
required Event event,
|
||||
|
|
@ -36,7 +44,7 @@ class PangeaMessageEvent {
|
|||
required this.ownMessage,
|
||||
}) {
|
||||
if (event.type != EventTypes.Message) {
|
||||
_isValidPangeaMessageEvent = false;
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
m: "${event.type} should not be used to make a PangeaMessageEvent",
|
||||
);
|
||||
|
|
@ -539,7 +547,18 @@ class PangeaMessageEvent {
|
|||
originalWritten: false,
|
||||
);
|
||||
|
||||
UseType get useType => useTypeCalculator(originalSent?.choreo);
|
||||
UseType get msgUseType {
|
||||
final ChoreoRecord? choreoRecord = originalSent?.choreo;
|
||||
if (choreoRecord == null) {
|
||||
return UseType.un;
|
||||
} else if (choreoRecord.includedIT) {
|
||||
return UseType.ta;
|
||||
} else if (choreoRecord.hasAcceptedMatches) {
|
||||
return UseType.ga;
|
||||
} else {
|
||||
return UseType.wa;
|
||||
}
|
||||
}
|
||||
|
||||
bool get showUseType =>
|
||||
!ownMessage &&
|
||||
|
|
@ -549,12 +568,32 @@ class PangeaMessageEvent {
|
|||
_event.messageType != PangeaEventTypes.report &&
|
||||
_event.messageType == MessageTypes.Text;
|
||||
|
||||
// this is just showActivityIcon now but will include
|
||||
// logic for showing
|
||||
bool get showMessageButtons => hasUncompletedActivity;
|
||||
|
||||
/// Returns a boolean value indicating whether to show an activity icon for this message event.
|
||||
///
|
||||
/// The [hasUncompletedActivity] getter checks if the [l2Code] is null, and if so, returns false.
|
||||
/// Otherwise, it retrieves a list of [PracticeActivityEvent] objects using the [practiceActivities] function
|
||||
/// with the [l2Code] as an argument.
|
||||
/// If the list is empty, it returns false.
|
||||
/// Otherwise, it checks if every activity in the list is complete using the [isComplete] property.
|
||||
/// If any activity is not complete, it returns true, indicating that the activity icon should be shown.
|
||||
/// Otherwise, it returns false.
|
||||
bool get hasUncompletedActivity {
|
||||
if (practiceActivities.isEmpty) return false;
|
||||
return practiceActivities.any((activity) => !(activity.isComplete));
|
||||
}
|
||||
|
||||
String? get l2Code =>
|
||||
MatrixState.pangeaController.languageController.activeL2Code();
|
||||
|
||||
String get messageDisplayLangCode {
|
||||
final bool immersionMode = MatrixState
|
||||
.pangeaController.permissionsController
|
||||
.isToolEnabled(ToolSetting.immersionMode, room);
|
||||
final String? l2Code = MatrixState.pangeaController.languageController
|
||||
.activeL2Code(roomID: room.id);
|
||||
|
||||
final String? originalLangCode =
|
||||
(originalWritten ?? originalSent)?.langCode;
|
||||
|
||||
|
|
@ -578,14 +617,219 @@ class PangeaMessageEvent {
|
|||
return steps;
|
||||
}
|
||||
|
||||
// List<SpanData> get activities =>
|
||||
//each match is turned into an activity that other students can access
|
||||
//they're not told the answer but have to find it themselves
|
||||
//the message has a blank piece which they fill in themselves
|
||||
/// Returns a list of all [PracticeActivityEvent] objects
|
||||
/// associated with this message event.
|
||||
List<PracticeActivityEvent> get _practiceActivityEvents {
|
||||
return _latestEdit
|
||||
.aggregatedEvents(
|
||||
timeline,
|
||||
PangeaEventTypes.pangeaActivity,
|
||||
)
|
||||
.map(
|
||||
(e) => PracticeActivityEvent(
|
||||
timeline: timeline,
|
||||
event: e,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
// replication of logic from message_content.dart
|
||||
// bool get isHtml =>
|
||||
// AppConfig.renderHtml && !_event.redacted && _event.isRichMessage;
|
||||
/// Returns a boolean value indicating whether there are any
|
||||
/// activities associated with this message event for the user's active l2
|
||||
bool get hasActivities {
|
||||
try {
|
||||
return practiceActivities.isNotEmpty;
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(e: e, s: s);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a list of [PracticeActivityEvent] objects for the given [langCode].
|
||||
List<PracticeActivityEvent> practiceActivitiesByLangCode(
|
||||
String langCode, {
|
||||
bool debug = false,
|
||||
}) {
|
||||
try {
|
||||
debugger(when: debug);
|
||||
final List<PracticeActivityEvent> activities = [];
|
||||
for (final event in _practiceActivityEvents) {
|
||||
if (event.practiceActivity.langCode == langCode) {
|
||||
activities.add(event);
|
||||
}
|
||||
}
|
||||
return activities;
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: e, s: s, data: event.toJson());
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a list of [PracticeActivityEvent] for the user's active l2.
|
||||
List<PracticeActivityEvent> get practiceActivities =>
|
||||
l2Code == null ? [] : practiceActivitiesByLangCode(l2Code!);
|
||||
|
||||
/// all construct uses for the message, including vocab and grammar
|
||||
List<OneConstructUse> get allConstructUses =>
|
||||
[..._grammarConstructUses, ..._vocabUses, ..._itStepsToConstructUses];
|
||||
|
||||
/// Returns a list of [OneConstructUse] from itSteps for which the continuance
|
||||
/// was selected or ignored. Correct selections are considered in the tokens
|
||||
/// flow. Once all continuances have lemmas, we can do both correct and incorrect
|
||||
/// in this flow. It actually doesn't do anything at all right now, because the
|
||||
/// choregrapher is not returning lemmas for continuances. This is a TODO.
|
||||
/// So currently only the lemmas can be gotten from the tokens for choices that
|
||||
/// are actually in the final message.
|
||||
List<OneConstructUse> get _itStepsToConstructUses {
|
||||
final List<OneConstructUse> uses = [];
|
||||
if (originalSent?.choreo == null) return uses;
|
||||
|
||||
for (final itStep in originalSent!.choreo!.itSteps) {
|
||||
for (final continuance in itStep.continuances) {
|
||||
// this seems to always be false for continuances right now
|
||||
|
||||
if (originalSent!.choreo!.finalMessage.contains(continuance.text)) {
|
||||
continue;
|
||||
}
|
||||
if (continuance.wasClicked) {
|
||||
//PTODO - account for end of flow score
|
||||
if (continuance.level != ChoreoConstants.levelThresholdForGreen) {
|
||||
uses.addAll(
|
||||
_lemmasToVocabUses(
|
||||
continuance.lemmas,
|
||||
ConstructUseTypeEnum.incIt,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (continuance.level != ChoreoConstants.levelThresholdForGreen) {
|
||||
uses.addAll(
|
||||
_lemmasToVocabUses(
|
||||
continuance.lemmas,
|
||||
ConstructUseTypeEnum.ignIt,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return uses;
|
||||
}
|
||||
|
||||
/// get construct uses of type vocab for the message
|
||||
List<OneConstructUse> get _vocabUses {
|
||||
final List<OneConstructUse> uses = [];
|
||||
|
||||
// missing vital info so return
|
||||
if (event.roomId == null || originalSent?.tokens == null) {
|
||||
// debugger(when: kDebugMode);
|
||||
return uses;
|
||||
}
|
||||
|
||||
// for each token, record whether selected in ga, ta, or wa
|
||||
for (final token in originalSent!.tokens!) {
|
||||
uses.addAll(_getVocabUseForToken(token));
|
||||
}
|
||||
|
||||
return uses;
|
||||
}
|
||||
|
||||
/// Returns a list of [OneConstructUse] objects for the given [token]
|
||||
/// If there is no [originalSent] or [originalSent.choreo], the [token] is
|
||||
/// considered to be a [ConstructUseTypeEnum.wa] as long as it matches the target language.
|
||||
/// Later on, we may want to consider putting it in some category of like 'pending'
|
||||
/// If the [token] is in the [originalSent.choreo.acceptedOrIgnoredMatch],
|
||||
/// it is considered to be a [ConstructUseTypeEnum.ga].
|
||||
/// If the [token] is in the [originalSent.choreo.acceptedOrIgnoredMatch.choices],
|
||||
/// it is considered to be a [ConstructUseTypeEnum.corIt].
|
||||
/// If the [token] is not included in any choreoStep, it is considered to be a [ConstructUseTypeEnum.wa].
|
||||
List<OneConstructUse> _getVocabUseForToken(PangeaToken token) {
|
||||
if (originalSent?.choreo == null) {
|
||||
final bool inUserL2 = originalSent?.langCode == l2Code;
|
||||
return _lemmasToVocabUses(
|
||||
token.lemmas,
|
||||
inUserL2 ? ConstructUseTypeEnum.wa : ConstructUseTypeEnum.unk,
|
||||
);
|
||||
}
|
||||
|
||||
for (final step in originalSent!.choreo!.choreoSteps) {
|
||||
/// if 1) accepted match 2) token is in the replacement and 3) replacement
|
||||
/// is in the overall step text, then token was a ga
|
||||
if (step.acceptedOrIgnoredMatch?.status == PangeaMatchStatus.accepted &&
|
||||
(step.acceptedOrIgnoredMatch!.match.choices?.any(
|
||||
(r) =>
|
||||
r.value.contains(token.text.content) &&
|
||||
step.text.contains(r.value),
|
||||
) ??
|
||||
false)) {
|
||||
return _lemmasToVocabUses(token.lemmas, ConstructUseTypeEnum.ga);
|
||||
}
|
||||
if (step.itStep != null) {
|
||||
final bool pickedThroughIT =
|
||||
step.itStep!.chosenContinuance?.text.contains(token.text.content) ??
|
||||
false;
|
||||
if (pickedThroughIT) {
|
||||
return _lemmasToVocabUses(token.lemmas, ConstructUseTypeEnum.corIt);
|
||||
//PTODO - check if added via custom input in IT flow
|
||||
}
|
||||
}
|
||||
}
|
||||
return _lemmasToVocabUses(token.lemmas, ConstructUseTypeEnum.wa);
|
||||
}
|
||||
|
||||
/// Convert a list of [lemmas] into a list of vocab uses
|
||||
/// with the given [type]
|
||||
List<OneConstructUse> _lemmasToVocabUses(
|
||||
List<Lemma> lemmas,
|
||||
ConstructUseTypeEnum type,
|
||||
) {
|
||||
final List<OneConstructUse> uses = [];
|
||||
for (final lemma in lemmas) {
|
||||
if (lemma.saveVocab) {
|
||||
uses.add(
|
||||
OneConstructUse(
|
||||
useType: type,
|
||||
chatId: event.roomId!,
|
||||
timeStamp: event.originServerTs,
|
||||
lemma: lemma.text,
|
||||
form: lemma.form,
|
||||
msgId: event.eventId,
|
||||
constructType: ConstructTypeEnum.vocab,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return uses;
|
||||
}
|
||||
|
||||
/// get construct uses of type grammar for the message
|
||||
List<OneConstructUse> get _grammarConstructUses {
|
||||
final List<OneConstructUse> uses = [];
|
||||
|
||||
if (originalSent?.choreo == null || event.roomId == null) return uses;
|
||||
|
||||
for (final step in originalSent!.choreo!.choreoSteps) {
|
||||
if (step.acceptedOrIgnoredMatch?.status == PangeaMatchStatus.accepted) {
|
||||
final String name = step.acceptedOrIgnoredMatch!.match.rule?.id ??
|
||||
step.acceptedOrIgnoredMatch!.match.shortMessage ??
|
||||
step.acceptedOrIgnoredMatch!.match.type.typeName.name;
|
||||
uses.add(
|
||||
OneConstructUse(
|
||||
useType: ConstructUseTypeEnum.ga,
|
||||
chatId: event.roomId!,
|
||||
timeStamp: event.originServerTs,
|
||||
lemma: name,
|
||||
form: name,
|
||||
msgId: event.eventId,
|
||||
constructType: ConstructTypeEnum.grammar,
|
||||
id: "${event.eventId}_${step.acceptedOrIgnoredMatch!.match.offset}_${step.acceptedOrIgnoredMatch!.match.length}",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return uses;
|
||||
}
|
||||
}
|
||||
|
||||
class URLFinder {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import 'package:matrix/src/utils/markdown.dart';
|
|||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import '../../widgets/matrix.dart';
|
||||
import '../constants/language_keys.dart';
|
||||
import '../constants/language_constants.dart';
|
||||
import '../constants/pangea_event_types.dart';
|
||||
import '../models/choreo_record.dart';
|
||||
import '../models/representation_content_model.dart';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart';
|
||||
import 'package:fluffychat/pangea/models/tokens_event_content_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../constants/pangea_event_types.dart';
|
||||
|
|
@ -22,6 +25,7 @@ class TokensEvent {
|
|||
_content ??= event.getPangeaContent<PangeaMessageTokens>();
|
||||
return _content!;
|
||||
} catch (err, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: err, s: s);
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_record_event.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../constants/pangea_event_types.dart';
|
||||
|
||||
class PracticeActivityEvent {
|
||||
Event event;
|
||||
Timeline? timeline;
|
||||
PracticeActivityModel? _content;
|
||||
|
||||
PracticeActivityEvent({
|
||||
required this.event,
|
||||
required this.timeline,
|
||||
content,
|
||||
}) {
|
||||
if (content != null) {
|
||||
if (!kDebugMode) {
|
||||
throw Exception(
|
||||
"content should not be set on product, just a dev placeholder",
|
||||
);
|
||||
} else {
|
||||
_content = content;
|
||||
}
|
||||
}
|
||||
if (event.type != PangeaEventTypes.pangeaActivity) {
|
||||
throw Exception(
|
||||
"${event.type} should not be used to make a PracticeActivityEvent",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PracticeActivityModel get practiceActivity {
|
||||
_content ??= event.getPangeaContent<PracticeActivityModel>();
|
||||
return _content!;
|
||||
}
|
||||
|
||||
/// All completion records assosiated with this activity
|
||||
List<PracticeActivityRecordEvent> get allRecords {
|
||||
if (timeline == null) {
|
||||
debugger(when: kDebugMode);
|
||||
return [];
|
||||
}
|
||||
final List<Event> records = event
|
||||
.aggregatedEvents(timeline!, PangeaEventTypes.activityRecord)
|
||||
.toList();
|
||||
|
||||
return records
|
||||
.map((event) => PracticeActivityRecordEvent(event: event))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// Completion record assosiated with this activity
|
||||
/// for the logged in user, null if there is none
|
||||
PracticeActivityRecordEvent? get userRecord {
|
||||
final List<PracticeActivityRecordEvent> records = allRecords
|
||||
.where(
|
||||
(recordEvent) =>
|
||||
recordEvent.event.senderId ==
|
||||
recordEvent.event.room.client.userID,
|
||||
)
|
||||
.toList();
|
||||
if (records.length > 1) {
|
||||
debugPrint("There should only be one record per user per activity");
|
||||
debugger(when: kDebugMode);
|
||||
}
|
||||
return records.firstOrNull;
|
||||
}
|
||||
|
||||
String get parentMessageId => event.relationshipEventId!;
|
||||
|
||||
/// Checks if there are any user records in the list for this activity,
|
||||
/// and, if so, then the activity is complete
|
||||
bool get isComplete => userRecord != null;
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../constants/pangea_event_types.dart';
|
||||
|
||||
class PracticeActivityRecordEvent {
|
||||
Event event;
|
||||
|
||||
PracticeActivityRecordModel? _content;
|
||||
|
||||
PracticeActivityRecordEvent({required this.event}) {
|
||||
if (event.type != PangeaEventTypes.activityRecord) {
|
||||
throw Exception(
|
||||
"${event.type} should not be used to make a PracticeActivityRecordEvent",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PracticeActivityRecordModel get record {
|
||||
_content ??= event.getPangeaContent<PracticeActivityRecordModel>();
|
||||
return _content!;
|
||||
}
|
||||
|
||||
Future<List<OneConstructUse>> uses(Timeline timeline) async {
|
||||
try {
|
||||
final String? parent = event.relationshipEventId;
|
||||
if (parent == null) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
m: "PracticeActivityRecordEvent has null event.relationshipEventId",
|
||||
data: event.toJson(),
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
final Event? practiceEvent =
|
||||
await timeline.getEventById(event.relationshipEventId!);
|
||||
|
||||
if (practiceEvent == null) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
m: "PracticeActivityRecordEvent has null practiceActivityEvent with id $parent",
|
||||
data: event.toJson(),
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
final PracticeActivityEvent practiceActivity = PracticeActivityEvent(
|
||||
event: practiceEvent,
|
||||
timeline: timeline,
|
||||
);
|
||||
|
||||
final List<OneConstructUse> uses = [];
|
||||
|
||||
final List<ConstructIdentifier> constructIds =
|
||||
practiceActivity.practiceActivity.tgtConstructs;
|
||||
|
||||
for (final construct in constructIds) {
|
||||
uses.add(
|
||||
OneConstructUse(
|
||||
lemma: construct.lemma,
|
||||
constructType: construct.type,
|
||||
useType: record.useType,
|
||||
//TODO - find form of construct within the message
|
||||
//this is related to the feature of highlighting the target construct in the message
|
||||
form: construct.lemma,
|
||||
chatId: event.roomId ?? practiceEvent.roomId ?? timeline.room.id,
|
||||
msgId: practiceActivity.parentMessageId,
|
||||
timeStamp: event.originServerTs,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return uses;
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: e, s: s, data: event.toJson());
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/analytics_model.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/summary_analytics_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
|
|
@ -28,32 +26,4 @@ abstract class AnalyticsEvent {
|
|||
}
|
||||
return contentCache!;
|
||||
}
|
||||
|
||||
static List<String> analyticsEventTypes = [
|
||||
PangeaEventTypes.summaryAnalytics,
|
||||
PangeaEventTypes.construct,
|
||||
];
|
||||
|
||||
static Future<String?> sendEvent(
|
||||
Room analyticsRoom,
|
||||
String type,
|
||||
List<dynamic> analyticsContent,
|
||||
) async {
|
||||
String? eventId;
|
||||
switch (type) {
|
||||
case PangeaEventTypes.summaryAnalytics:
|
||||
eventId = await SummaryAnalyticsEvent.sendSummaryAnalyticsEvent(
|
||||
analyticsRoom,
|
||||
analyticsContent.cast<RecentMessageRecord>(),
|
||||
);
|
||||
break;
|
||||
case PangeaEventTypes.construct:
|
||||
eventId = await ConstructAnalyticsEvent.sendConstructsEvent(
|
||||
analyticsRoom,
|
||||
analyticsContent.cast<OneConstructUse>(),
|
||||
);
|
||||
break;
|
||||
}
|
||||
return eventId;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,11 @@ abstract class AnalyticsModel {
|
|||
case PangeaEventTypes.summaryAnalytics:
|
||||
return SummaryAnalyticsModel.formatSummaryContent(recentMsgs);
|
||||
case PangeaEventTypes.construct:
|
||||
return ConstructAnalyticsModel.formatConstructsContent(recentMsgs);
|
||||
final List<OneConstructUse> uses = [];
|
||||
for (final msg in recentMsgs) {
|
||||
uses.addAll(msg.allConstructUses);
|
||||
}
|
||||
return uses;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,19 +18,4 @@ class ConstructAnalyticsEvent extends AnalyticsEvent {
|
|||
contentCache ??= ConstructAnalyticsModel.fromJson(event.content);
|
||||
return contentCache as ConstructAnalyticsModel;
|
||||
}
|
||||
|
||||
static Future<String?> sendConstructsEvent(
|
||||
Room analyticsRoom,
|
||||
List<OneConstructUse> uses,
|
||||
) async {
|
||||
final ConstructAnalyticsModel constructsModel = ConstructAnalyticsModel(
|
||||
uses: uses,
|
||||
);
|
||||
|
||||
final String? eventId = await analyticsRoom.sendEvent(
|
||||
constructsModel.toJson(),
|
||||
type: PangeaEventTypes.construct,
|
||||
);
|
||||
return eventId;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/analytics_model.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../enum/construct_type_enum.dart';
|
||||
|
|
@ -24,7 +22,7 @@ class ConstructAnalyticsModel extends AnalyticsModel {
|
|||
if (json[_usesKey] is List) {
|
||||
// This is the new format
|
||||
uses.addAll(
|
||||
json[_usesKey]
|
||||
(json[_usesKey] as List)
|
||||
.map((use) => OneConstructUse.fromJson(use))
|
||||
.cast<OneConstructUse>()
|
||||
.toList(),
|
||||
|
|
@ -39,13 +37,13 @@ class ConstructAnalyticsModel extends AnalyticsModel {
|
|||
final lemmaUses = useValue[_usesKey];
|
||||
for (final useData in lemmaUses) {
|
||||
final use = OneConstructUse(
|
||||
useType: ConstructUseType.ga,
|
||||
useType: ConstructUseTypeEnum.ga,
|
||||
chatId: useData["chatId"],
|
||||
timeStamp: DateTime.parse(useData["timeStamp"]),
|
||||
lemma: lemma,
|
||||
form: useData["form"],
|
||||
msgId: useData["msgId"],
|
||||
constructType: ConstructType.grammar,
|
||||
constructType: ConstructTypeEnum.grammar,
|
||||
);
|
||||
uses.add(use);
|
||||
}
|
||||
|
|
@ -70,122 +68,13 @@ class ConstructAnalyticsModel extends AnalyticsModel {
|
|||
_usesKey: uses.map((use) => use.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
static List<OneConstructUse> formatConstructsContent(
|
||||
List<PangeaMessageEvent> recentMsgs,
|
||||
) {
|
||||
final List<PangeaMessageEvent> filtered = List.from(recentMsgs);
|
||||
final List<OneConstructUse> uses = [];
|
||||
|
||||
for (final msg in filtered) {
|
||||
if (msg.originalSent?.choreo == null) continue;
|
||||
uses.addAll(
|
||||
msg.originalSent!.choreo!.toGrammarConstructUse(
|
||||
msg.eventId,
|
||||
msg.room.id,
|
||||
msg.originServerTs,
|
||||
),
|
||||
);
|
||||
|
||||
final List<PangeaToken>? tokens = msg.originalSent?.tokens;
|
||||
if (tokens == null) continue;
|
||||
uses.addAll(
|
||||
msg.originalSent!.choreo!.toVocabUse(
|
||||
tokens,
|
||||
msg.room.id,
|
||||
msg.eventId,
|
||||
msg.originServerTs,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return uses;
|
||||
}
|
||||
}
|
||||
|
||||
enum ConstructUseType {
|
||||
/// produced in chat by user, igc was run, and we've judged it to be a correct use
|
||||
wa,
|
||||
|
||||
/// produced in chat by user, igc was run, and we've judged it to be a incorrect use
|
||||
/// Note: if the IGC match is ignored, this is not counted as an incorrect use
|
||||
ga,
|
||||
|
||||
/// produced in chat by user and igc was not run
|
||||
unk,
|
||||
|
||||
/// selected correctly in IT flow
|
||||
corIt,
|
||||
|
||||
/// encountered as IT distractor and correctly ignored it
|
||||
ignIt,
|
||||
|
||||
/// encountered as it distractor and selected it
|
||||
incIt,
|
||||
|
||||
/// encountered in igc match and ignored match
|
||||
ignIGC,
|
||||
|
||||
/// selected correctly in IGC flow
|
||||
corIGC,
|
||||
|
||||
/// encountered as distractor in IGC flow and selected it
|
||||
incIGC,
|
||||
}
|
||||
|
||||
extension on ConstructUseType {
|
||||
String get string {
|
||||
switch (this) {
|
||||
case ConstructUseType.ga:
|
||||
return 'ga';
|
||||
case ConstructUseType.wa:
|
||||
return 'wa';
|
||||
case ConstructUseType.corIt:
|
||||
return 'corIt';
|
||||
case ConstructUseType.incIt:
|
||||
return 'incIt';
|
||||
case ConstructUseType.ignIt:
|
||||
return 'ignIt';
|
||||
case ConstructUseType.ignIGC:
|
||||
return 'ignIGC';
|
||||
case ConstructUseType.corIGC:
|
||||
return 'corIGC';
|
||||
case ConstructUseType.incIGC:
|
||||
return 'incIGC';
|
||||
case ConstructUseType.unk:
|
||||
return 'unk';
|
||||
}
|
||||
}
|
||||
|
||||
IconData get icon {
|
||||
switch (this) {
|
||||
case ConstructUseType.ga:
|
||||
return Icons.check;
|
||||
case ConstructUseType.wa:
|
||||
return Icons.thumb_up_sharp;
|
||||
case ConstructUseType.corIt:
|
||||
return Icons.check;
|
||||
case ConstructUseType.incIt:
|
||||
return Icons.close;
|
||||
case ConstructUseType.ignIt:
|
||||
return Icons.close;
|
||||
case ConstructUseType.ignIGC:
|
||||
return Icons.close;
|
||||
case ConstructUseType.corIGC:
|
||||
return Icons.check;
|
||||
case ConstructUseType.incIGC:
|
||||
return Icons.close;
|
||||
case ConstructUseType.unk:
|
||||
return Icons.help;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class OneConstructUse {
|
||||
String? lemma;
|
||||
ConstructType? constructType;
|
||||
ConstructTypeEnum? constructType;
|
||||
String? form;
|
||||
ConstructUseType useType;
|
||||
ConstructUseTypeEnum useType;
|
||||
String chatId;
|
||||
String? msgId;
|
||||
DateTime timeStamp;
|
||||
|
|
@ -204,7 +93,7 @@ class OneConstructUse {
|
|||
|
||||
factory OneConstructUse.fromJson(Map<String, dynamic> json) {
|
||||
return OneConstructUse(
|
||||
useType: ConstructUseType.values
|
||||
useType: ConstructUseTypeEnum.values
|
||||
.firstWhere((e) => e.string == json['useType']),
|
||||
chatId: json['chatId'],
|
||||
timeStamp: DateTime.parse(json['timeStamp']),
|
||||
|
|
@ -248,7 +137,7 @@ class OneConstructUse {
|
|||
|
||||
class ConstructUses {
|
||||
final List<OneConstructUse> uses;
|
||||
final ConstructType constructType;
|
||||
final ConstructTypeEnum constructType;
|
||||
final String lemma;
|
||||
|
||||
ConstructUses({
|
||||
|
|
|
|||
|
|
@ -18,18 +18,4 @@ class SummaryAnalyticsEvent extends AnalyticsEvent {
|
|||
contentCache ??= SummaryAnalyticsModel.fromJson(event.content);
|
||||
return contentCache as SummaryAnalyticsModel;
|
||||
}
|
||||
|
||||
static Future<String?> sendSummaryAnalyticsEvent(
|
||||
Room analyticsRoom,
|
||||
List<RecentMessageRecord> records,
|
||||
) async {
|
||||
final SummaryAnalyticsModel analyticsModel = SummaryAnalyticsModel(
|
||||
messages: records,
|
||||
);
|
||||
final String? eventId = await analyticsRoom.sendEvent(
|
||||
analyticsModel.toJson(),
|
||||
type: PangeaEventTypes.summaryAnalytics,
|
||||
);
|
||||
return eventId;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ class SummaryAnalyticsModel extends AnalyticsModel {
|
|||
(msg) => RecentMessageRecord(
|
||||
eventId: msg.eventId,
|
||||
chatId: msg.room.id,
|
||||
useType: msg.useType,
|
||||
useType: msg.msgUseType,
|
||||
time: msg.originServerTs,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -13,44 +13,66 @@ class BotOptionsModel {
|
|||
List<String> keywords;
|
||||
bool safetyModeration;
|
||||
String mode;
|
||||
String? custom;
|
||||
String? discussionTopic;
|
||||
String? discussionKeywords;
|
||||
bool? discussionTriggerScheduleEnabled;
|
||||
int? discussionTriggerScheduleHourInterval;
|
||||
bool? discussionTriggerReactionEnabled;
|
||||
String? discussionTriggerReactionKey;
|
||||
String? customSystemPrompt;
|
||||
bool? customTriggerReactionEnabled;
|
||||
String? customTriggerReactionKey;
|
||||
|
||||
BotOptionsModel({
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// General Bot Options
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
this.languageLevel,
|
||||
this.topic = "General Conversation",
|
||||
this.keywords = const [],
|
||||
this.safetyModeration = true,
|
||||
this.mode = "discussion",
|
||||
this.custom = "",
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Discussion Mode Options
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
this.discussionTopic,
|
||||
this.discussionKeywords,
|
||||
this.discussionTriggerScheduleEnabled,
|
||||
this.discussionTriggerScheduleHourInterval,
|
||||
this.discussionTriggerReactionEnabled = true,
|
||||
this.discussionTriggerReactionKey,
|
||||
this.discussionTriggerReactionKey = "⏩",
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Custom Mode Options
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
this.customSystemPrompt,
|
||||
this.customTriggerReactionEnabled = true,
|
||||
this.customTriggerReactionKey = "⏩",
|
||||
});
|
||||
|
||||
factory BotOptionsModel.fromJson(json) {
|
||||
return BotOptionsModel(
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// General Bot Options
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
languageLevel: json[ModelKey.languageLevel],
|
||||
safetyModeration: json[ModelKey.safetyModeration] ?? true,
|
||||
mode: json[ModelKey.mode] ?? "discussion",
|
||||
custom: json[ModelKey.custom],
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Discussion Mode Options
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
discussionTopic: json[ModelKey.discussionTopic],
|
||||
discussionKeywords: json[ModelKey.discussionKeywords],
|
||||
discussionTriggerScheduleEnabled:
|
||||
json[ModelKey.discussionTriggerScheduleEnabled],
|
||||
discussionTriggerScheduleHourInterval:
|
||||
json[ModelKey.discussionTriggerScheduleHourInterval],
|
||||
discussionTriggerReactionEnabled:
|
||||
json[ModelKey.discussionTriggerReactionEnabled],
|
||||
discussionTriggerReactionKey: json[ModelKey.discussionTriggerReactionKey],
|
||||
json[ModelKey.discussionTriggerReactionEnabled] ?? true,
|
||||
discussionTriggerReactionKey:
|
||||
json[ModelKey.discussionTriggerReactionKey] ?? "⏩",
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Custom Mode Options
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
customSystemPrompt: json[ModelKey.customSystemPrompt],
|
||||
customTriggerReactionEnabled:
|
||||
json[ModelKey.customTriggerReactionEnabled] ?? true,
|
||||
customTriggerReactionKey: json[ModelKey.customTriggerReactionKey] ?? "⏩",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -61,17 +83,16 @@ class BotOptionsModel {
|
|||
data[ModelKey.languageLevel] = languageLevel;
|
||||
data[ModelKey.safetyModeration] = safetyModeration;
|
||||
data[ModelKey.mode] = mode;
|
||||
data[ModelKey.custom] = custom;
|
||||
data[ModelKey.discussionTopic] = discussionTopic;
|
||||
data[ModelKey.discussionKeywords] = discussionKeywords;
|
||||
data[ModelKey.discussionTriggerScheduleEnabled] =
|
||||
discussionTriggerScheduleEnabled;
|
||||
data[ModelKey.discussionTriggerScheduleHourInterval] =
|
||||
discussionTriggerScheduleHourInterval;
|
||||
data[ModelKey.discussionTriggerReactionEnabled] =
|
||||
discussionTriggerReactionEnabled;
|
||||
discussionTriggerReactionEnabled ?? true;
|
||||
data[ModelKey.discussionTriggerReactionKey] =
|
||||
discussionTriggerReactionKey;
|
||||
discussionTriggerReactionKey ?? "⏩";
|
||||
data[ModelKey.customSystemPrompt] = customSystemPrompt;
|
||||
data[ModelKey.customTriggerReactionEnabled] =
|
||||
customTriggerReactionEnabled ?? true;
|
||||
data[ModelKey.customTriggerReactionKey] = customTriggerReactionKey ?? "⏩";
|
||||
return data;
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
|
|
@ -92,27 +113,27 @@ class BotOptionsModel {
|
|||
case ModelKey.mode:
|
||||
mode = value;
|
||||
break;
|
||||
case ModelKey.custom:
|
||||
custom = value;
|
||||
break;
|
||||
case ModelKey.discussionTopic:
|
||||
discussionTopic = value;
|
||||
break;
|
||||
case ModelKey.discussionKeywords:
|
||||
discussionKeywords = value;
|
||||
break;
|
||||
case ModelKey.discussionTriggerScheduleEnabled:
|
||||
discussionTriggerScheduleEnabled = value;
|
||||
break;
|
||||
case ModelKey.discussionTriggerScheduleHourInterval:
|
||||
discussionTriggerScheduleHourInterval = value;
|
||||
break;
|
||||
case ModelKey.discussionTriggerReactionEnabled:
|
||||
discussionTriggerReactionEnabled = value;
|
||||
break;
|
||||
case ModelKey.discussionTriggerReactionKey:
|
||||
discussionTriggerReactionKey = value;
|
||||
break;
|
||||
case ModelKey.customSystemPrompt:
|
||||
customSystemPrompt = value;
|
||||
break;
|
||||
case ModelKey.customTriggerReactionEnabled:
|
||||
customTriggerReactionEnabled = value;
|
||||
break;
|
||||
case ModelKey.customTriggerReactionKey:
|
||||
customTriggerReactionKey = value;
|
||||
break;
|
||||
default:
|
||||
throw Exception('Invalid key for bot options - $key');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,8 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_match_model.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
|
||||
import '../constants/choreo_constants.dart';
|
||||
import '../enum/construct_type_enum.dart';
|
||||
import 'it_step.dart';
|
||||
import 'lemma.dart';
|
||||
|
||||
/// this class lives within a [PangeaIGCEvent]
|
||||
/// it always has a [RepresentationEvent] parent
|
||||
|
|
@ -111,135 +106,6 @@ class ChoreoRecord {
|
|||
openMatches: [],
|
||||
);
|
||||
|
||||
/// [tokens] is the final list of tokens that were sent
|
||||
/// if no ga or ta,
|
||||
/// make wa use for each and return
|
||||
/// else
|
||||
/// for each saveable vocab in the final message
|
||||
/// if vocab is contained in an accepted replacement, make ga use
|
||||
/// if vocab is contained in ta choice,
|
||||
/// if selected as choice, corIt
|
||||
/// if written as customInput, corIt? (account for score in this)
|
||||
/// for each it step
|
||||
/// for each continuance
|
||||
/// if not within the final message, save ignIT/incIT
|
||||
List<OneConstructUse> toVocabUse(
|
||||
List<PangeaToken> tokens,
|
||||
String chatId,
|
||||
String msgId,
|
||||
DateTime timestamp,
|
||||
) {
|
||||
final List<OneConstructUse> uses = [];
|
||||
final DateTime now = DateTime.now();
|
||||
List<OneConstructUse> lemmasToVocabUses(
|
||||
List<Lemma> lemmas,
|
||||
ConstructUseType type,
|
||||
) {
|
||||
final List<OneConstructUse> uses = [];
|
||||
for (final lemma in lemmas) {
|
||||
if (lemma.saveVocab) {
|
||||
uses.add(
|
||||
OneConstructUse(
|
||||
useType: type,
|
||||
chatId: chatId,
|
||||
timeStamp: timestamp,
|
||||
lemma: lemma.text,
|
||||
form: lemma.form,
|
||||
msgId: msgId,
|
||||
constructType: ConstructType.vocab,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return uses;
|
||||
}
|
||||
|
||||
List<OneConstructUse> getVocabUseForToken(PangeaToken token) {
|
||||
for (final step in choreoSteps) {
|
||||
/// if 1) accepted match 2) token is in the replacement and 3) replacement
|
||||
/// is in the overall step text, then token was a ga
|
||||
if (step.acceptedOrIgnoredMatch?.status == PangeaMatchStatus.accepted &&
|
||||
(step.acceptedOrIgnoredMatch!.match.choices?.any(
|
||||
(r) =>
|
||||
r.value.contains(token.text.content) &&
|
||||
step.text.contains(r.value),
|
||||
) ??
|
||||
false)) {
|
||||
return lemmasToVocabUses(token.lemmas, ConstructUseType.ga);
|
||||
}
|
||||
if (step.itStep != null) {
|
||||
final bool pickedThroughIT = step.itStep!.chosenContinuance?.text
|
||||
.contains(token.text.content) ??
|
||||
false;
|
||||
if (pickedThroughIT) {
|
||||
return lemmasToVocabUses(token.lemmas, ConstructUseType.corIt);
|
||||
//PTODO - check if added via custom input in IT flow
|
||||
}
|
||||
}
|
||||
}
|
||||
return lemmasToVocabUses(token.lemmas, ConstructUseType.wa);
|
||||
}
|
||||
|
||||
/// for each token, record whether selected in ga, ta, or wa
|
||||
for (final token in tokens) {
|
||||
uses.addAll(getVocabUseForToken(token));
|
||||
}
|
||||
|
||||
for (final itStep in itSteps) {
|
||||
for (final continuance in itStep.continuances) {
|
||||
// this seems to always be false for continuances right now
|
||||
|
||||
if (finalMessage.contains(continuance.text)) {
|
||||
continue;
|
||||
}
|
||||
if (continuance.wasClicked) {
|
||||
//PTODO - account for end of flow score
|
||||
if (continuance.level != ChoreoConstants.levelThresholdForGreen) {
|
||||
uses.addAll(
|
||||
lemmasToVocabUses(continuance.lemmas, ConstructUseType.incIt),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (continuance.level != ChoreoConstants.levelThresholdForGreen) {
|
||||
uses.addAll(
|
||||
lemmasToVocabUses(continuance.lemmas, ConstructUseType.ignIt),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return uses;
|
||||
}
|
||||
|
||||
List<OneConstructUse> toGrammarConstructUse(
|
||||
String msgId,
|
||||
String chatId,
|
||||
DateTime timestamp,
|
||||
) {
|
||||
final List<OneConstructUse> uses = [];
|
||||
for (final step in choreoSteps) {
|
||||
if (step.acceptedOrIgnoredMatch?.status == PangeaMatchStatus.accepted) {
|
||||
final String name = step.acceptedOrIgnoredMatch!.match.rule?.id ??
|
||||
step.acceptedOrIgnoredMatch!.match.shortMessage ??
|
||||
step.acceptedOrIgnoredMatch!.match.type.typeName.name;
|
||||
uses.add(
|
||||
OneConstructUse(
|
||||
useType: ConstructUseType.ga,
|
||||
chatId: chatId,
|
||||
timeStamp: timestamp,
|
||||
lemma: name,
|
||||
form: name,
|
||||
msgId: msgId,
|
||||
constructType: ConstructType.grammar,
|
||||
id: "${msgId}_${step.acceptedOrIgnoredMatch!.match.offset}_${step.acceptedOrIgnoredMatch!.match.length}",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return uses;
|
||||
}
|
||||
|
||||
List<ITStep> get itSteps =>
|
||||
choreoSteps.where((e) => e.itStep != null).map((e) => e.itStep!).toList();
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ class CustomInputRequestModel {
|
|||
String targetLangCode;
|
||||
String userId;
|
||||
String roomId;
|
||||
String? classId;
|
||||
|
||||
String? goldTranslation;
|
||||
List<Continuance>? goldContinuances;
|
||||
|
|
@ -20,7 +19,6 @@ class CustomInputRequestModel {
|
|||
required this.targetLangCode,
|
||||
required this.userId,
|
||||
required this.roomId,
|
||||
required this.classId,
|
||||
required this.goldTranslation,
|
||||
required this.goldContinuances,
|
||||
});
|
||||
|
|
@ -32,7 +30,6 @@ class CustomInputRequestModel {
|
|||
targetLangCode: json[ModelKey.tgtLang],
|
||||
userId: json['user_id'],
|
||||
roomId: json['room_id'],
|
||||
classId: json['class_id'],
|
||||
goldTranslation: json['gold_translation'],
|
||||
goldContinuances: json['gold_continuances'] != null
|
||||
? List.from(json['gold_continuances'])
|
||||
|
|
@ -48,7 +45,6 @@ class CustomInputRequestModel {
|
|||
ModelKey.tgtLang: targetLangCode,
|
||||
'user_id': userId,
|
||||
'room_id': roomId,
|
||||
'class_id': classId,
|
||||
'gold_translation': goldTranslation,
|
||||
'gold_continuances': goldContinuances != null
|
||||
? List.from(goldContinuances!.map((e) => e.toJson()))
|
||||
|
|
|
|||
|
|
@ -1,40 +0,0 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'class_model.dart';
|
||||
|
||||
class ExchangeModel {
|
||||
PangeaRoomRules permissions;
|
||||
|
||||
ExchangeModel({
|
||||
required this.permissions,
|
||||
});
|
||||
|
||||
factory ExchangeModel.fromJson(Map<String, dynamic> json) {
|
||||
return ExchangeModel(
|
||||
permissions: PangeaRoomRules.fromJson(json[ModelKey.permissions]),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = <String, dynamic>{};
|
||||
try {
|
||||
data[ModelKey.permissions] = permissions.toJson();
|
||||
return data;
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: e, s: s);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
updateEditableClassField(String key, dynamic value) {
|
||||
switch (key) {
|
||||
default:
|
||||
throw Exception('Invalid key for setting permissions - $key');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
|
@ -154,32 +155,37 @@ class VocabTotals {
|
|||
void addVocabUseBasedOnUseType(List<OneConstructUse> uses) {
|
||||
for (final use in uses) {
|
||||
switch (use.useType) {
|
||||
case ConstructUseType.ga:
|
||||
case ConstructUseTypeEnum.ga:
|
||||
ga++;
|
||||
break;
|
||||
case ConstructUseType.wa:
|
||||
case ConstructUseTypeEnum.wa:
|
||||
wa++;
|
||||
break;
|
||||
case ConstructUseType.corIt:
|
||||
case ConstructUseTypeEnum.corIt:
|
||||
corIt++;
|
||||
break;
|
||||
case ConstructUseType.incIt:
|
||||
case ConstructUseTypeEnum.incIt:
|
||||
incIt++;
|
||||
break;
|
||||
case ConstructUseType.ignIt:
|
||||
case ConstructUseTypeEnum.ignIt:
|
||||
ignIt++;
|
||||
break;
|
||||
//TODO - these shouldn't be counted as such
|
||||
case ConstructUseType.ignIGC:
|
||||
case ConstructUseTypeEnum.ignIGC:
|
||||
ignIt++;
|
||||
break;
|
||||
case ConstructUseType.corIGC:
|
||||
case ConstructUseTypeEnum.corIGC:
|
||||
corIt++;
|
||||
break;
|
||||
case ConstructUseType.incIGC:
|
||||
case ConstructUseTypeEnum.incIGC:
|
||||
incIt++;
|
||||
break;
|
||||
case ConstructUseType.unk:
|
||||
//TODO if we bring back Headwords then we need to add these
|
||||
case ConstructUseTypeEnum.corPA:
|
||||
break;
|
||||
case ConstructUseTypeEnum.incPA:
|
||||
break;
|
||||
case ConstructUseTypeEnum.unk:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/controllers/language_detection_controller.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_match_model.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/models/span_card_model.dart';
|
||||
|
|
@ -13,12 +14,11 @@ import 'package:matrix/matrix.dart';
|
|||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import '../constants/model_keys.dart';
|
||||
import 'language_detection_model.dart';
|
||||
|
||||
// import 'package:language_tool/language_tool.dart';
|
||||
|
||||
class IGCTextData {
|
||||
List<LanguageDetection> detections;
|
||||
LanguageDetectionResponse detections;
|
||||
String originalInput;
|
||||
String? fullTextCorrection;
|
||||
List<PangeaToken> tokens;
|
||||
|
|
@ -42,6 +42,18 @@ class IGCTextData {
|
|||
});
|
||||
|
||||
factory IGCTextData.fromJson(Map<String, dynamic> json) {
|
||||
// changing this to allow for use of the LanguageDetectionResponse methods
|
||||
// TODO - change API after we're sure all clients are updated. not urgent.
|
||||
final LanguageDetectionResponse detections =
|
||||
json[_detectionsKey] is Iterable
|
||||
? LanguageDetectionResponse.fromJson({
|
||||
"detections": json[_detectionsKey],
|
||||
"full_text": json["original_input"],
|
||||
})
|
||||
: LanguageDetectionResponse.fromJson(
|
||||
json[_detectionsKey] as Map<String, dynamic>,
|
||||
);
|
||||
|
||||
return IGCTextData(
|
||||
tokens: (json[_tokensKey] as Iterable)
|
||||
.map<PangeaToken>(
|
||||
|
|
@ -59,12 +71,7 @@ class IGCTextData {
|
|||
.toList()
|
||||
.cast<PangeaMatch>()
|
||||
: [],
|
||||
detections: (json[_detectionsKey] as Iterable)
|
||||
.map<LanguageDetection>(
|
||||
(e) => LanguageDetection.fromJson(e as Map<String, dynamic>),
|
||||
)
|
||||
.toList()
|
||||
.cast<LanguageDetection>(),
|
||||
detections: detections,
|
||||
originalInput: json["original_input"],
|
||||
fullTextCorrection: json["full_text_correction"],
|
||||
userL1: json[ModelKey.userL1],
|
||||
|
|
@ -79,7 +86,7 @@ class IGCTextData {
|
|||
static const String _detectionsKey = "detections";
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
_detectionsKey: detections.map((e) => e.toJson()).toList(),
|
||||
_detectionsKey: detections.toJson(),
|
||||
"original_input": originalInput,
|
||||
"full_text_correction": fullTextCorrection,
|
||||
_tokensKey: tokens.map((e) => e.toJson()).toList(),
|
||||
|
|
@ -90,6 +97,18 @@ class IGCTextData {
|
|||
"enable_igc": enableIGC,
|
||||
};
|
||||
|
||||
/// if we haven't run IGC or IT or there are no matches, we use the highest validated detection
|
||||
/// from [LanguageDetectionResponse.highestValidatedDetection]
|
||||
/// if we have run igc/it and there are no matches, we can relax the threshold
|
||||
/// and use the highest confidence detection
|
||||
String get detectedLanguage {
|
||||
if (!(enableIGC && enableIT) || matches.isNotEmpty) {
|
||||
return detections.highestValidatedDetection().langCode;
|
||||
} else {
|
||||
return detections.highestConfidenceDetection.langCode;
|
||||
}
|
||||
}
|
||||
|
||||
// reconstruct fullText based on accepted match
|
||||
//update offsets in existing matches to reflect the change
|
||||
//if existing matches overlap with the accepted one, remove them??
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue