diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index a7fb07449..84703a551 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -1,3963 +1,3974 @@ { - "@@locale": "en", - "@@last_modified": "2021-08-14 12:38:37.885451", - "repeatPassword": "Repeat password", - "@repeatPassword": {}, - "notAnImage": "Not an image file.", - "remove": "Remove", - "importNow": "Import now", - "importEmojis": "Import Emojis", - "importFromZipFile": "Import from .zip file", - "exportEmotePack": "Export Emote pack as .zip", - "replace": "Replace", - "about": "About", - "@about": { - "type": "text", - "placeholders": {} - }, - "accept": "Accept", - "@accept": { - "type": "text", - "placeholders": {} - }, - "acceptedTheInvitation": "👍 {username} accepted the invitation", - "@acceptedTheInvitation": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "account": "Account", - "@account": { - "type": "text", - "placeholders": {} - }, - "accountInformation": "Account information", - "@accountInformation": { - "type": "text", - "placeholders": {} - }, - "activatedEndToEndEncryption": "🔐 {username} activated end to end encryption", - "@activatedEndToEndEncryption": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "addEmail": "Add email", - "@addEmail": { - "type": "text", - "placeholders": {} - }, - "confirmMatrixId": "Please confirm your Matrix ID in order to delete your account.", - "@confirmMatrixId": {}, - "supposedMxid": "This should be {mxid}", - "@supposedMxid": { - "type": "text", - "placeholders": { - "mxid": {} - } - }, - "addGroupDescription": "Add a chat description", - "@addGroupDescription": { - "type": "text", - "placeholders": {} - }, - "addNewFriend": "Add new friend", - "@addNewFriend": { - "type": "text", - "placeholders": {} - }, - "addToSpace": "Add to space", - "@addToSpace": {}, - "admin": "Admin", - "@admin": { - "type": "text", - "placeholders": {} - }, - "alias": "alias", - "@alias": { - "type": "text", - "placeholders": {} - }, - "all": "All", - "@all": { - "type": "text", - "placeholders": {} - }, - "allChats": "All chats", - "@allChats": { - "type": "text", - "placeholders": {} - }, - "alreadyHaveAnAccount": "Already have an account?", - "@alreadyHaveAnAccount": { - "type": "text", - "placeholders": {} - }, - "commandHint_googly": "Send some googly eyes", - "commandHint_cuddle": "Send a cuddle", - "commandHint_hug": "Send a hug", - "googlyEyesContent": "{senderName} sends you googly eyes", - "@googlyEyesContent": { - "type": "text", - "placeholders": { - "senderName": {} - } - }, - "cuddleContent": "{senderName} cuddles you", - "@cuddleContent": { - "type": "text", - "placeholders": { - "senderName": {} - } - }, - "hugContent": "{senderName} hugs you", - "@hugContent": { - "type": "text", - "placeholders": { - "senderName": {} - } - }, - "answeredTheCall": "{senderName} answered the call", - "@answeredTheCall": { - "type": "text", - "placeholders": { - "senderName": {} - } - }, - "anyoneCanJoin": "Anyone can join", - "@anyoneCanJoin": { - "type": "text", - "placeholders": {} - }, - "appLock": "App lock", - "@appLock": { - "type": "text", - "placeholders": {} - }, - "archive": "Archive", - "@archive": { - "type": "text", - "placeholders": {} - }, - "areGuestsAllowedToJoin": "Are guest users allowed to join", - "@areGuestsAllowedToJoin": { - "type": "text", - "placeholders": {} - }, - "areYouSure": "Are you sure?", - "@areYouSure": { - "type": "text", - "placeholders": {} - }, - "areYouSureYouWantToLogout": "Are you sure you want to log out?", - "@areYouSureYouWantToLogout": { - "type": "text", - "placeholders": {} - }, - "askSSSSSign": "To be able to sign the other person, please enter your secure store passphrase or recovery key.", - "@askSSSSSign": { - "type": "text", - "placeholders": {} - }, - "askVerificationRequest": "Accept this verification request from {username}?", - "@askVerificationRequest": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "autoplayImages": "Automatically play animated stickers and emotes", - "@autoplayImages": { - "type": "text", - "placeholder": {} - }, - "badServerLoginTypesException": "The homeserver supports the login types:\n{serverVersions}\nBut this app supports only:\n{supportedVersions}", - "@badServerLoginTypesException": { - "type": "text", - "placeholders": { - "serverVersions": {}, - "supportedVersions": {} - } - }, - "sendTypingNotifications": "Send typing notifications", - "@sendTypingNotifications": {}, - "sendOnEnter": "Always send on enter", - "@sendOnEnter": {}, - "badServerVersionsException": "The homeserver supports the Spec versions:\n{serverVersions}\nBut this app supports only {supportedVersions}", - "@badServerVersionsException": { - "type": "text", - "placeholders": { - "serverVersions": {}, - "supportedVersions": {} - } - }, - "banFromChat": "Ban from chat", - "@banFromChat": { - "type": "text", - "placeholders": {} - }, - "banned": "Banned", - "@banned": { - "type": "text", - "placeholders": {} - }, - "bannedUser": "{username} banned {targetName}", - "@bannedUser": { - "type": "text", - "placeholders": { - "username": {}, - "targetName": {} - } - }, - "blockDevice": "Block Device", - "@blockDevice": { - "type": "text", - "placeholders": {} - }, - "blocked": "Blocked", - "@blocked": { - "type": "text", - "placeholders": {} - }, - "botMessages": "Bot messages", - "@botMessages": { - "type": "text", - "placeholders": {} - }, - "cancel": "Cancel", - "@cancel": { - "type": "text", - "placeholders": {} - }, - "cantOpenUri": "Can't open the URI {uri}", - "@cantOpenUri": { - "type": "text", - "placeholders": { - "uri": {} - } - }, - "changeDeviceName": "Change device name", - "@changeDeviceName": { - "type": "text", - "placeholders": {} - }, - "changedTheChatAvatar": "{username} changed the chat avatar", - "@changedTheChatAvatar": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "changedTheChatDescriptionTo": "{username} changed the chat description to: '{description}'", - "@changedTheChatDescriptionTo": { - "type": "text", - "placeholders": { - "username": {}, - "description": {} - } - }, - "changedTheChatNameTo": "{username} changed the chat name to: '{chatname}'", - "@changedTheChatNameTo": { - "type": "text", - "placeholders": { - "username": {}, - "chatname": {} - } - }, - "changedTheChatPermissions": "{username} changed the chat permissions", - "@changedTheChatPermissions": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "changedTheDisplaynameTo": "{username} changed their displayname to: '{displayname}'", - "@changedTheDisplaynameTo": { - "type": "text", - "placeholders": { - "username": {}, - "displayname": {} - } - }, - "changedTheGuestAccessRules": "{username} changed the guest access rules", - "@changedTheGuestAccessRules": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "changedTheGuestAccessRulesTo": "{username} changed the guest access rules to: {rules}", - "@changedTheGuestAccessRulesTo": { - "type": "text", - "placeholders": { - "username": {}, - "rules": {} - } - }, - "changedTheHistoryVisibility": "{username} changed the history visibility", - "@changedTheHistoryVisibility": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "changedTheHistoryVisibilityTo": "{username} changed the history visibility to: {rules}", - "@changedTheHistoryVisibilityTo": { - "type": "text", - "placeholders": { - "username": {}, - "rules": {} - } - }, - "changedTheJoinRules": "{username} changed the join rules", - "@changedTheJoinRules": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "changedTheJoinRulesTo": "{username} changed the join rules to: {joinRules}", - "@changedTheJoinRulesTo": { - "type": "text", - "placeholders": { - "username": {}, - "joinRules": {} - } - }, - "changedTheProfileAvatar": "{username} changed their avatar", - "@changedTheProfileAvatar": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "changedTheRoomAliases": "{username} changed the room aliases", - "@changedTheRoomAliases": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "changedTheRoomInvitationLink": "{username} changed the invitation link", - "@changedTheRoomInvitationLink": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "changePassword": "Change password", - "@changePassword": { - "type": "text", - "placeholders": {} - }, - "changeTheHomeserver": "Change the homeserver", - "@changeTheHomeserver": { - "type": "text", - "placeholders": {} - }, - "changeTheme": "Change your style", - "@changeTheme": { - "type": "text", - "placeholders": {} - }, - "changeTheNameOfTheGroup": "Change the name of the chat", - "@changeTheNameOfTheGroup": { - "type": "text", - "placeholders": {} - }, - "changeYourAvatar": "Change your avatar", - "@changeYourAvatar": { - "type": "text", - "placeholders": {} - }, - "channelCorruptedDecryptError": "The encryption has been corrupted", - "@channelCorruptedDecryptError": { - "type": "text", - "placeholders": {} - }, - "chat": "Chat", - "@chat": { - "type": "text", - "placeholders": {} - }, - "yourChatBackupHasBeenSetUp": "Your chat backup has been set up.", - "@yourChatBackupHasBeenSetUp": {}, - "chatBackup": "Chat backup", - "@chatBackup": { - "type": "text", - "placeholders": {} - }, - "chatBackupDescription": "Your old messages are secured with a recovery key. Please make sure you don't lose it.", - "@chatBackupDescription": { - "type": "text", - "placeholders": {} - }, - "chatDetails": "Chat details", - "@chatDetails": { - "type": "text", - "placeholders": {} - }, - "chatHasBeenAddedToThisSpace": "Chat has been added to this space", - "@chatHasBeenAddedToThisSpace": {}, - "chats": "Group Chats", - "@chats": { - "type": "text", - "placeholders": {} - }, - "classes": "Classes", - "chooseAStrongPassword": "Choose a strong password", - "@chooseAStrongPassword": { - "type": "text", - "placeholders": {} - }, - "clearArchive": "Clear archive", - "@clearArchive": {}, - "close": "Close", - "@close": { - "type": "text", - "placeholders": {} - }, - "commandHint_markasdm": "Mark as direct message room for the giving Matrix ID", - "@commandHint_markasdm": {}, - "commandHint_markasgroup": "Mark as group", - "@commandHint_markasgroup": {}, - "commandHint_ban": "Ban the given user from this room", - "@commandHint_ban": { - "type": "text", - "description": "Usage hint for the command /ban" - }, - "commandHint_clearcache": "Clear cache", - "@commandHint_clearcache": { - "type": "text", - "description": "Usage hint for the command /clearcache" - }, - "commandHint_create": "Create an empty group chat\nUse --no-encryption to disable encryption", - "@commandHint_create": { - "type": "text", - "description": "Usage hint for the command /create" - }, - "commandHint_discardsession": "Discard session", - "@commandHint_discardsession": { - "type": "text", - "description": "Usage hint for the command /discardsession" - }, - "commandHint_dm": "Start a direct chat\nUse --no-encryption to disable encryption", - "@commandHint_dm": { - "type": "text", - "description": "Usage hint for the command /dm" - }, - "commandHint_html": "Send HTML-formatted text", - "@commandHint_html": { - "type": "text", - "description": "Usage hint for the command /html" - }, - "commandHint_invite": "Invite the given user to this room", - "@commandHint_invite": { - "type": "text", - "description": "Usage hint for the command /invite" - }, - "commandHint_join": "Join the given room", - "@commandHint_join": { - "type": "text", - "description": "Usage hint for the command /join" - }, - "commandHint_kick": "Remove the given user from this room", - "@commandHint_kick": { - "type": "text", - "description": "Usage hint for the command /kick" - }, - "commandHint_leave": "Leave this room", - "@commandHint_leave": { - "type": "text", - "description": "Usage hint for the command /leave" - }, - "commandHint_me": "Describe yourself", - "@commandHint_me": { - "type": "text", - "description": "Usage hint for the command /me" - }, - "commandHint_myroomavatar": "Set your picture for this room (by mxc-uri)", - "@commandHint_myroomavatar": { - "type": "text", - "description": "Usage hint for the command /myroomavatar" - }, - "commandHint_myroomnick": "Set your display name for this room", - "@commandHint_myroomnick": { - "type": "text", - "description": "Usage hint for the command /myroomnick" - }, - "commandHint_op": "Set the given user's power level (default: 50)", - "@commandHint_op": { - "type": "text", - "description": "Usage hint for the command /op" - }, - "commandHint_plain": "Send unformatted text", - "@commandHint_plain": { - "type": "text", - "description": "Usage hint for the command /plain" - }, - "commandHint_react": "Send reply as a reaction", - "@commandHint_react": { - "type": "text", - "description": "Usage hint for the command /react" - }, - "commandHint_send": "Send text", - "@commandHint_send": { - "type": "text", - "description": "Usage hint for the command /send" - }, - "commandHint_unban": "Unban the given user from this room", - "@commandHint_unban": { - "type": "text", - "description": "Usage hint for the command /unban" - }, - "commandInvalid": "Command invalid", - "@commandInvalid": { - "type": "text" - }, - "commandMissing": "{command} is not a command.", - "@commandMissing": { - "type": "text", - "placeholders": { - "command": {} - }, - "description": "State that {command} is not a valid /command." - }, - "compareEmojiMatch": "Please compare the emojis", - "@compareEmojiMatch": { - "type": "text", - "placeholders": {} - }, - "compareNumbersMatch": "Please compare the numbers", - "@compareNumbersMatch": { - "type": "text", - "placeholders": {} - }, - "configureChat": "Configure chat", - "@configureChat": { - "type": "text", - "placeholders": {} - }, - "confirm": "Confirm", - "@confirm": { - "type": "text", - "placeholders": {} - }, - "connect": "Start", - "@connect": { - "type": "text", - "placeholders": {} - }, - "contactHasBeenInvitedToTheGroup": "Contact has been invited to the group", - "@contactHasBeenInvitedToTheGroup": { - "type": "text", - "placeholders": {} - }, - "containsDisplayName": "Contains display name", - "@containsDisplayName": { - "type": "text", - "placeholders": {} - }, - "containsUserName": "Contains username", - "@containsUserName": { - "type": "text", - "placeholders": {} - }, - "contentHasBeenReported": "The content has been reported", - "@contentHasBeenReported": { - "type": "text", - "placeholders": {} - }, - "copiedToClipboard": "Copied to clipboard", - "@copiedToClipboard": { - "type": "text", - "placeholders": {} - }, - "copy": "Copy", - "@copy": { - "type": "text", - "placeholders": {} - }, - "copyToClipboard": "Copy to clipboard", - "@copyToClipboard": { - "type": "text", - "placeholders": {} - }, - "couldNotDecryptMessage": "Could not decrypt message: {error}", - "@couldNotDecryptMessage": { - "type": "text", - "placeholders": { - "error": {} - } - }, - "countParticipants": "{count} participants", - "@countParticipants": { - "type": "text", - "placeholders": { - "count": {} - } - }, - "create": "Create", - "@create": { - "type": "text", - "placeholders": {} - }, - "createdTheChat": "💬 {username} created the chat", - "@createdTheChat": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "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", - "placeholders": {} - }, - "darkTheme": "Dark", - "@darkTheme": { - "type": "text", - "placeholders": {} - }, - "dateAndTimeOfDay": "{date}, {timeOfDay}", - "@dateAndTimeOfDay": { - "type": "text", - "placeholders": { - "date": {}, - "timeOfDay": {} - } - }, - "dateWithoutYear": "{month}-{day}", - "@dateWithoutYear": { - "type": "text", - "placeholders": { - "month": {}, - "day": {} - } - }, - "dateWithYear": "{year}-{month}-{day}", - "@dateWithYear": { - "type": "text", - "placeholders": { - "year": {}, - "month": {}, - "day": {} - } - }, - "deactivateAccountWarning": "This will deactivate your user account. This can not be undone! Are you sure?", - "@deactivateAccountWarning": { - "type": "text", - "placeholders": {} - }, - "defaultPermissionLevel": "Default permission level", - "@defaultPermissionLevel": { - "type": "text", - "placeholders": {} - }, - "delete": "Delete", - "@delete": { - "type": "text", - "placeholders": {} - }, - "deleteAccount": "Delete account", - "@deleteAccount": { - "type": "text", - "placeholders": {} - }, - "deleteMessage": "Delete message", - "@deleteMessage": { - "type": "text", - "placeholders": {} - }, - "device": "Device", - "@device": { - "type": "text", - "placeholders": {} - }, - "deviceId": "Device ID", - "@deviceId": { - "type": "text", - "placeholders": {} - }, - "devices": "Devices", - "@devices": { - "type": "text", - "placeholders": {} - }, - "directChats": "Direct Chats", - "@directChats": { - "type": "text", - "placeholders": {} - }, - "allRooms": "All Group Chats", - "@allRooms": { - "type": "text", - "placeholders": {} - }, - "displaynameHasBeenChanged": "Displayname has been changed", - "@displaynameHasBeenChanged": { - "type": "text", - "placeholders": {} - }, - "downloadFile": "Download file", - "@downloadFile": { - "type": "text", - "placeholders": {} - }, - "edit": "Edit", - "@edit": { - "type": "text", - "placeholders": {} - }, - "editBlockedServers": "Edit blocked servers", - "@editBlockedServers": { - "type": "text", - "placeholders": {} - }, - "chatPermissions": "Chat permissions", - "editChatPermissions": "Edit chat permissions", - "@editChatPermissions": { - "type": "text", - "placeholders": {} - }, - "editDisplayname": "Edit displayname", - "@editDisplayname": { - "type": "text", - "placeholders": {} - }, - "editRoomAliases": "Edit room aliases", - "@editRoomAliases": { - "type": "text", - "placeholders": {} - }, - "editRoomAvatar": "Edit room avatar", - "@editRoomAvatar": { - "type": "text", - "placeholders": {} - }, - "emoteExists": "Emote already exists!", - "@emoteExists": { - "type": "text", - "placeholders": {} - }, - "emoteInvalid": "Invalid emote shortcode!", - "@emoteInvalid": { - "type": "text", - "placeholders": {} - }, - "emoteKeyboardNoRecents": "Recently-used emotes will appear here...", - "@emoteKeyboardNoRecents": { - "type": "text", - "placeholders": {} - }, - "emotePacks": "Emote packs for room", - "@emotePacks": { - "type": "text", - "placeholders": {} - }, - "emoteSettings": "Emote Settings", - "@emoteSettings": { - "type": "text", - "placeholders": {} - }, - "emoteShortcode": "Emote shortcode", - "@emoteShortcode": { - "type": "text", - "placeholders": {} - }, - "emoteWarnNeedToPick": "You need to pick an emote shortcode and an image!", - "@emoteWarnNeedToPick": { - "type": "text", - "placeholders": {} - }, - "emptyChat": "Empty chat", - "@emptyChat": { - "type": "text", - "placeholders": {} - }, - "enableEmotesGlobally": "Enable emote pack globally", - "@enableEmotesGlobally": { - "type": "text", - "placeholders": {} - }, - "enableEncryption": "Enable encryption", - "@enableEncryption": { - "type": "text", - "placeholders": {} - }, - "enableEncryptionWarning": "You won't be able to disable the encryption anymore. Are you sure?", - "@enableEncryptionWarning": { - "type": "text", - "placeholders": {} - }, - "encrypted": "Encrypted", - "@encrypted": { - "type": "text", - "placeholders": {} - }, - "encryption": "Encryption", - "@encryption": { - "type": "text", - "placeholders": {} - }, - "encryptionNotEnabled": "Encryption is not enabled", - "@encryptionNotEnabled": { - "type": "text", - "placeholders": {} - }, - "endedTheCall": "{senderName} ended the call", - "@endedTheCall": { - "type": "text", - "placeholders": { - "senderName": {} - } - }, - "enterAGroupName": "Enter a chat name", - "@enterAGroupName": { - "type": "text", - "placeholders": {} - }, - "enterAnEmailAddress": "Enter an email address", - "@enterAnEmailAddress": { - "type": "text", - "placeholders": {} - }, - "enterASpacepName": "Enter a name", - "@enterASpacepName": {}, - "homeserver": "Homeserver", - "@homeserver": {}, - "enterYourHomeserver": "Enter your homeserver", - "@enterYourHomeserver": { - "type": "text", - "placeholders": {} - }, - "errorObtainingLocation": "Error obtaining location: {error}", - "@errorObtainingLocation": { - "type": "text", - "placeholders": { - "error": {} - } - }, - "everythingReady": "Everything ready!", - "@everythingReady": { - "type": "text", - "placeholders": {} - }, - "extremeOffensive": "Extremely offensive", - "@extremeOffensive": { - "type": "text", - "placeholders": {} - }, - "fileName": "File name", - "@fileName": { - "type": "text", - "placeholders": {} - }, - "fluffychat": "FluffyChat", - "@fluffychat": { - "type": "text", - "placeholders": {} - }, - "fontSize": "Font size", - "@fontSize": { - "type": "text", - "placeholders": {} - }, - "forward": "Forward", - "@forward": { - "type": "text", - "placeholders": {} - }, - "fromJoining": "From joining", - "@fromJoining": { - "type": "text", - "placeholders": {} - }, - "fromTheInvitation": "From the invitation", - "@fromTheInvitation": { - "type": "text", - "placeholders": {} - }, - "goToTheNewRoom": "Go to the new room", - "@goToTheNewRoom": { - "type": "text", - "placeholders": {} - }, - "group": "Chat", - "@group": { - "type": "text", - "placeholders": {} - }, - "chatDescription": "Chat description", - "chatDescriptionHasBeenChanged": "Chat description changed", - "groupIsPublic": "Group is public", - "groupDescription": "Chat description", - "@groupDescription": { - "type": "text", - "placeholders": {} - }, - "groupDescriptionHasBeenChanged": "Chat description changed", - "@groupDescriptionHasBeenChanged": { - "type": "text", - "placeholders": {} - }, - "@groupIsPublic": { - "type": "text", - "placeholders": {} - }, - "groups": "Chats", - "@groups": { - "type": "text", - "placeholders": {} - }, - "groupWith": "Chat with {displayname}", - "@groupWith": { - "type": "text", - "placeholders": { - "displayname": {} - } - }, - "guestsAreForbidden": "Guests are forbidden", - "@guestsAreForbidden": { - "type": "text", - "placeholders": {} - }, - "guestsCanJoin": "Guests can join", - "@guestsCanJoin": { - "type": "text", - "placeholders": {} - }, - "hasWithdrawnTheInvitationFor": "{username} has withdrawn the invitation for {targetName}", - "@hasWithdrawnTheInvitationFor": { - "type": "text", - "placeholders": { - "username": {}, - "targetName": {} - } - }, - "help": "Help", - "@help": { - "type": "text", - "placeholders": {} - }, - "hideRedactedEvents": "Hide redacted events", - "@hideRedactedEvents": { - "type": "text", - "placeholders": {} - }, - "hideUnknownEvents": "Hide unknown events", - "@hideUnknownEvents": { - "type": "text", - "placeholders": {} - }, - "howOffensiveIsThisContent": "How offensive is this content?", - "@howOffensiveIsThisContent": { - "type": "text", - "placeholders": {} - }, - "id": "ID", - "@id": { - "type": "text", - "placeholders": {} - }, - "identity": "Identity", - "@identity": { - "type": "text", - "placeholders": {} - }, - "ignore": "Block", - "@ignore": { - "type": "text", - "placeholders": {} - }, - "ignoredUsers": "Blocked users", - "@ignoredUsers": { - "type": "text", - "placeholders": {} - }, - "ignoreListDescription": "You can block users who are disturbing you. You won't be able to receive any messages or invites from the users on your personal block list.", - "@ignoreListDescription": { - "type": "text", - "placeholders": {} - }, - "ignoreUsername": "Block username", - "@ignoreUsername": { - "type": "text", - "placeholders": {} - }, - "block": "block", - "blockedUsers": "Blocked users", - "blockListDescription": "You can block users who are disturbing you. You won't be able to receive any messages or room invites from the users on your personal block list.", - "blockUsername": "Ignore username", - "iHaveClickedOnLink": "I have clicked on the link", - "@iHaveClickedOnLink": { - "type": "text", - "placeholders": {} - }, - "incorrectPassphraseOrKey": "Incorrect passphrase or recovery key", - "@incorrectPassphraseOrKey": { - "type": "text", - "placeholders": {} - }, - "inoffensive": "Slightly offensive", - "@inoffensive": { - "type": "text", - "placeholders": {} - }, - "inviteContact": "Invite contact", - "@inviteContact": { - "type": "text", - "placeholders": {} - }, - "inviteContactToGroupQuestion": "Do you want to invite {contact} to the chat \"{groupName}\"?", - "@inviteContactToGroup": { - "type": "text", - "placeholders": { - "groupName": {} - } - }, - "inviteContactToGroup": "Invite contact to {groupName}", - "noChatDescriptionYet": "No chat description created yet.", - "tryAgain": "Try again", - "invalidServerName": "Invalid server name", - "invited": "Invited", - "@invited": { - "type": "text", - "placeholders": {} - }, - "redactMessageDescription": "The message will be redacted for all participants in this conversation. This cannot be undone.", - "optionalRedactReason": "(Optional) Reason for redacting this message...", - "invitedUser": "📩 {username} invited {targetName}", - "@invitedUser": { - "type": "text", - "placeholders": { - "username": {}, - "targetName": {} - } - }, - "invitedUsersOnly": "Invited users only", - "@invitedUsersOnly": { - "type": "text", - "placeholders": {} - }, - "inviteForMe": "Invite for me", - "@inviteForMe": { - "type": "text", - "placeholders": {} - }, - "inviteText": "{username} invited you to FluffyChat.\n1. Visit fluffychat.im and install the app \n2. Sign up or sign in \n3. Open the invite link: \n {link}", - "@inviteText": { - "type": "text", - "placeholders": { - "username": {}, - "link": {} - } - }, - "isTyping": "is typing…", - "@isTyping": { - "type": "text", - "placeholders": {} - }, - "joinedTheChat": "👋 {username} joined the chat", - "@joinedTheChat": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "joinRoom": "Join room", - "@joinRoom": { - "type": "text", - "placeholders": {} - }, - "kicked": "👞 {username} kicked {targetName}", - "@kicked": { - "type": "text", - "placeholders": { - "username": {}, - "targetName": {} - } - }, - "kickedAndBanned": "🙅 {username} kicked and banned {targetName}", - "@kickedAndBanned": { - "type": "text", - "placeholders": { - "username": {}, - "targetName": {} - } - }, - "kickFromChat": "Kick from chat", - "@kickFromChat": { - "type": "text", - "placeholders": {} - }, - "lastActiveAgo": "Last active: {localizedTimeShort}", - "@lastActiveAgo": { - "type": "text", - "placeholders": { - "localizedTimeShort": {} - } - }, - "leave": "Leave", - "@leave": { - "type": "text", - "placeholders": {} - }, - "leftTheChat": "Left the chat", - "@leftTheChat": { - "type": "text", - "placeholders": {} - }, - "license": "License", - "@license": { - "type": "text", - "placeholders": {} - }, - "lightTheme": "Light", - "@lightTheme": { - "type": "text", - "placeholders": {} - }, - "loadCountMoreParticipants": "Load {count} more participants", - "@loadCountMoreParticipants": { - "type": "text", - "placeholders": { - "count": {} - } - }, - "dehydrate": "Export session and wipe device", - "@dehydrate": {}, - "dehydrateWarning": "This action cannot be undone. Ensure you safely store the backup file.", - "@dehydrateWarning": {}, - "dehydrateTor": "TOR Users: Export session", - "@dehydrateTor": {}, - "dehydrateTorLong": "For TOR users, it is recommended to export the session before closing the window.", - "@dehydrateTorLong": {}, - "hydrateTor": "TOR Users: Import session export", - "@hydrateTor": {}, - "hydrateTorLong": "Did you export your session last time on TOR? Quickly import it and continue chatting.", - "@hydrateTorLong": {}, - "hydrate": "Restore from backup file", - "@hydrate": {}, - "loadingPleaseWait": "Loading… Please wait.", - "@loadingPleaseWait": { - "type": "text", - "placeholders": {} - }, - "loadMore": "Load more…", - "@loadMore": { - "type": "text", - "placeholders": {} - }, - "locationDisabledNotice": "Location services are disabled. Please enable them to be able to share your location.", - "@locationDisabledNotice": { - "type": "text", - "placeholders": {} - }, - "locationPermissionDeniedNotice": "Location permission denied. Please grant them to be able to share your location.", - "@locationPermissionDeniedNotice": { - "type": "text", - "placeholders": {} - }, - "login": "Login", - "@login": { - "type": "text", - "placeholders": {} - }, - "logInTo": "Log in to {homeserver}", - "@logInTo": { - "type": "text", - "placeholders": { - "homeserver": {} - } - }, - "logout": "Logout", - "@logout": { - "type": "text", - "placeholders": {} - }, - "memberChanges": "Member changes", - "@memberChanges": { - "type": "text", - "placeholders": {} - }, - "mention": "Mention", - "@mention": { - "type": "text", - "placeholders": {} - }, - "messages": "Messages", - "messagesStyle": "Messages:", - "@messages": { - "type": "text", - "placeholders": {} - }, - "moderator": "Moderator", - "@moderator": { - "type": "text", - "placeholders": {} - }, - "muteChat": "Mute chat", - "@muteChat": { - "type": "text", - "placeholders": {} - }, - "needPantalaimonWarning": "Please be aware that you need Pantalaimon to use end-to-end encryption for now.", - "@needPantalaimonWarning": { - "type": "text", - "placeholders": {} - }, - "newChat": "New chat", - "@newChat": { - "type": "text", - "placeholders": {} - }, - "newMessageInFluffyChat": "💬 New message in FluffyChat", - "@newMessageInFluffyChat": { - "type": "text", - "placeholders": {} - }, - "newVerificationRequest": "New verification request!", - "@newVerificationRequest": { - "type": "text", - "placeholders": {} - }, - "next": "Next", - "@next": { - "type": "text", - "placeholders": {} - }, - "no": "No", - "@no": { - "type": "text", - "placeholders": {} - }, - "noConnectionToTheServer": "No connection to the server", - "@noConnectionToTheServer": { - "type": "text", - "placeholders": {} - }, - "noEmotesFound": "No emotes found. 😕", - "@noEmotesFound": { - "type": "text", - "placeholders": {} - }, - "noEncryptionForPublicRooms": "You can only activate encryption as soon as the room is no longer publicly accessible.", - "@noEncryptionForPublicRooms": { - "type": "text", - "placeholders": {} - }, - "noGoogleServicesWarning": "Firebase Cloud Messaging doesn't appear to be available on your device. To still receive push notifications, we recommend installing ntfy. With ntfy or another Unified Push provider you can receive push notifications in a data secure way. You can download ntfy from the PlayStore or from F-Droid.", - "@noGoogleServicesWarning": { - "type": "text", - "placeholders": {} - }, - "noMatrixServer": "{server1} is no matrix server, use {server2} instead?", - "@noMatrixServer": { - "type": "text", - "placeholders": { - "server1": {}, - "server2": {} - } - }, - "shareInviteLink": "Share invite link", - "scanQrCode": "Scan QR code", - "@scanQrCode": {}, - "none": "None", - "@none": { - "type": "text", - "placeholders": {} - }, - "noPasswordRecoveryDescription": "You have not added a way to recover your password yet.", - "@noPasswordRecoveryDescription": { - "type": "text", - "placeholders": {} - }, - "noPermission": "No permission", - "@noPermission": { - "type": "text", - "placeholders": {} - }, - "noRoomsFound": "No rooms found…", - "@noRoomsFound": { - "type": "text", - "placeholders": {} - }, - "notifications": "Notifications", - "@notifications": { - "type": "text", - "placeholders": {} - }, - "notificationsEnabledForThisAccount": "Notifications enabled for this account", - "@notificationsEnabledForThisAccount": { - "type": "text", - "placeholders": {} - }, - "numUsersTyping": "{count} users are typing…", - "@numUsersTyping": { - "type": "text", - "placeholders": { - "count": {} - } - }, - "obtainingLocation": "Obtaining location…", - "@obtainingLocation": { - "type": "text", - "placeholders": {} - }, - "offensive": "Offensive", - "@offensive": { - "type": "text", - "placeholders": {} - }, - "offline": "Offline", - "@offline": { - "type": "text", - "placeholders": {} - }, - "ok": "Ok", - "@ok": { - "type": "text", - "placeholders": {} - }, - "online": "Online", - "@online": { - "type": "text", - "placeholders": {} - }, - "onlineKeyBackupEnabled": "Online Key Backup is enabled", - "@onlineKeyBackupEnabled": { - "type": "text", - "placeholders": {} - }, - "oopsPushError": "Oops! Unfortunately, an error occurred when setting up the push notifications.", - "@oopsPushError": { - "type": "text", - "placeholders": {} - }, - "oopsSomethingWentWrong": "Oops, something went wrong…", - "@oopsSomethingWentWrong": { - "type": "text", - "placeholders": {} - }, - "openAppToReadMessages": "Open app to read messages", - "@openAppToReadMessages": { - "type": "text", - "placeholders": {} - }, - "openCamera": "Open camera", - "@openCamera": { - "type": "text", - "placeholders": {} - }, - "openVideoCamera": "Open camera for a video", - "@openVideoCamera": { - "type": "text", - "placeholders": {} - }, - "oneClientLoggedOut": "One of your clients has been logged out", - "@oneClientLoggedOut": {}, - "addAccount": "Add account", - "@addAccount": {}, - "editBundlesForAccount": "Edit bundles for this account", - "@editBundlesForAccount": {}, - "addToBundle": "Add to bundle", - "@addToBundle": {}, - "removeFromBundle": "Remove from this bundle", - "@removeFromBundle": {}, - "bundleName": "Bundle name", - "@bundleName": {}, - "enableMultiAccounts": "(BETA) Enable multi accounts on this device", - "@enableMultiAccounts": {}, - "openInMaps": "Open in maps", - "@openInMaps": { - "type": "text", - "placeholders": {} - }, - "link": "Link", - "@link": {}, - "serverRequiresEmail": "This server needs to validate your email address for registration.", - "@serverRequiresEmail": {}, - "optionalGroupName": "(Optional) Chat name", - "@optionalGroupName": { - "type": "text", - "placeholders": {} - }, - "or": "Or", - "@or": { - "type": "text", - "placeholders": {} - }, - "participant": "Participant", - "@participant": { - "type": "text", - "placeholders": {} - }, - "passphraseOrKey": "passphrase or recovery key", - "@passphraseOrKey": { - "type": "text", - "placeholders": {} - }, - "password": "Password", - "@password": { - "type": "text", - "placeholders": {} - }, - "passwordForgotten": "Password forgotten", - "@passwordForgotten": { - "type": "text", - "placeholders": {} - }, - "passwordHasBeenChanged": "Password has been changed", - "@passwordHasBeenChanged": { - "type": "text", - "placeholders": {} - }, - "passwordRecovery": "Password recovery", - "@passwordRecovery": { - "type": "text", - "placeholders": {} - }, - "people": "People", - "@people": { - "type": "text", - "placeholders": {} - }, - "pickImage": "Pick an image", - "@pickImage": { - "type": "text", - "placeholders": {} - }, - "pin": "Pin", - "@pin": { - "type": "text", - "placeholders": {} - }, - "play": "Play {fileName}", - "@play": { - "type": "text", - "placeholders": { - "fileName": {} - } - }, - "pleaseChoose": "Please choose", - "@pleaseChoose": { - "type": "text", - "placeholders": {} - }, - "pleaseChooseAPasscode": "Please choose a pass code", - "@pleaseChooseAPasscode": { - "type": "text", - "placeholders": {} - }, - "pleaseClickOnLink": "Please click on the link in the email and then proceed. In rare cases, the email can be sent to spam or take up to 5 minutes to arrive.", - "@pleaseClickOnLink": { - "type": "text", - "placeholders": {} - }, - "pleaseEnter4Digits": "Please enter 4 digits or leave empty to disable app lock.", - "@pleaseEnter4Digits": { - "type": "text", - "placeholders": {} - }, - "pleaseEnterRecoveryKey": "Please enter your recovery key:", - "@pleaseEnterRecoveryKey": {}, - "pleaseEnterYourPassword": "Please enter your password", - "@pleaseEnterYourPassword": { - "type": "text", - "placeholders": {} - }, - "pleaseEnterYourPin": "Please enter your pin", - "@pleaseEnterYourPin": { - "type": "text", - "placeholders": {} - }, - "pleaseEnterYourUsername": "Please enter your username", - "@pleaseEnterYourUsername": { - "type": "text", - "placeholders": {} - }, - "pleaseFollowInstructionsOnWeb": "Please follow the instructions on the website and tap on next.", - "@pleaseFollowInstructionsOnWeb": { - "type": "text", - "placeholders": {} - }, - "privacy": "Privacy", - "@privacy": { - "type": "text", - "placeholders": {} - }, - "publicRooms": "Public Rooms", - "@publicRooms": { - "type": "text", - "placeholders": {} - }, - "pushRules": "Push rules", - "@pushRules": { - "type": "text", - "placeholders": {} - }, - "reason": "Reason", - "@reason": { - "type": "text", - "placeholders": {} - }, - "recording": "Recording", - "@recording": { - "type": "text", - "placeholders": {} - }, - "redactedBy": "Redacted by {username}", - "@redactedBy": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "directChat": "Direct chat", - "redactedByBecause": "Redacted by {username} because: \"{reason}\"", - "@redactedByBecause": { - "type": "text", - "placeholders": { - "username": {}, - "reason": {} - } - }, - "redactedAnEvent": "{username} redacted an event", - "@redactedAnEvent": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "redactMessage": "Redact message", - "@redactMessage": { - "type": "text", - "placeholders": {} - }, - "register": "Register", - "@register": { - "type": "text", - "placeholders": {} - }, - "reject": "Reject", - "@reject": { - "type": "text", - "placeholders": {} - }, - "rejectedTheInvitation": "{username} rejected the invitation", - "@rejectedTheInvitation": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "rejoin": "Rejoin", - "@rejoin": { - "type": "text", - "placeholders": {} - }, - "@remove": { - "type": "text", - "placeholders": {} - }, - "removeAllOtherDevices": "Remove all other devices", - "@removeAllOtherDevices": { - "type": "text", - "placeholders": {} - }, - "removedBy": "Removed by {username}", - "@removedBy": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "removeDevice": "Remove device", - "@removeDevice": { - "type": "text", - "placeholders": {} - }, - "unbanFromChat": "Unban from chat", - "@unbanFromChat": { - "type": "text", - "placeholders": {} - }, - "removeYourAvatar": "Remove your avatar", - "@removeYourAvatar": { - "type": "text", - "placeholders": {} - }, - "replaceRoomWithNewerVersion": "Replace room with newer version", - "@replaceRoomWithNewerVersion": { - "type": "text", - "placeholders": {} - }, - "reply": "Reply", - "@reply": { - "type": "text", - "placeholders": {} - }, - "reportMessage": "Report message", - "@reportMessage": { - "type": "text", - "placeholders": {} - }, - "requestPermission": "Request permission", - "@requestPermission": { - "type": "text", - "placeholders": {} - }, - "roomHasBeenUpgraded": "Room has been upgraded", - "@roomHasBeenUpgraded": { - "type": "text", - "placeholders": {} - }, - "roomVersion": "Room version", - "@roomVersion": { - "type": "text", - "placeholders": {} - }, - "saveFile": "Save file", - "@saveFile": { - "type": "text", - "placeholders": {} - }, - "search": "Search", - "@search": { - "type": "text", - "placeholders": {} - }, - "security": "Security", - "@security": { - "type": "text", - "placeholders": {} - }, - "recoveryKey": "Recovery key", - "@recoveryKey": {}, - "recoveryKeyLost": "Recovery key lost?", - "@recoveryKeyLost": {}, - "seenByUser": "Seen by {username}", - "@seenByUser": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "send": "Send", - "@send": { - "type": "text", - "placeholders": {} - }, - "sendAMessage": "Send a message", - "@sendAMessage": { - "type": "text", - "placeholders": {} - }, - "sendAsText": "Send as text", - "@sendAsText": { - "type": "text" - }, - "sendAudio": "Send audio", - "@sendAudio": { - "type": "text", - "placeholders": {} - }, - "sendFile": "Send file", - "@sendFile": { - "type": "text", - "placeholders": {} - }, - "sendImage": "Send image", - "@sendImage": { - "type": "text", - "placeholders": {} - }, - "sendMessages": "Send messages", - "@sendMessages": { - "type": "text", - "placeholders": {} - }, - "sendOriginal": "Send original", - "@sendOriginal": { - "type": "text", - "placeholders": {} - }, - "sendSticker": "Send sticker", - "@sendSticker": { - "type": "text", - "placeholders": {} - }, - "sendVideo": "Send video", - "@sendVideo": { - "type": "text", - "placeholders": {} - }, - "sentAFile": "📁 {username} sent a file", - "@sentAFile": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "sentAnAudio": "🎤 {username} sent an audio", - "@sentAnAudio": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "sentAPicture": "🖼️ {username} sent a picture", - "@sentAPicture": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "sentASticker": "😊 {username} sent a sticker", - "@sentASticker": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "sentAVideo": "🎥 {username} sent a video", - "@sentAVideo": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "sentCallInformations": "{senderName} sent call information", - "@sentCallInformations": { - "type": "text", - "placeholders": { - "senderName": {} - } - }, - "separateChatTypes": "Separate Direct Chats and Groups", - "@separateChatTypes": { - "type": "text", - "placeholders": {} - }, - "setAsCanonicalAlias": "Set as main alias", - "@setAsCanonicalAlias": { - "type": "text", - "placeholders": {} - }, - "setCustomEmotes": "Set custom emotes", - "@setCustomEmotes": { - "type": "text", - "placeholders": {} - }, - "setChatDescription": "Set chat description", - "setInvitationLink": "Set invitation link", - "@setInvitationLink": { - "type": "text", - "placeholders": {} - }, - "setPermissionsLevel": "Set permissions level", - "@setPermissionsLevel": { - "type": "text", - "placeholders": {} - }, - "setStatus": "Set status", - "@setStatus": { - "type": "text", - "placeholders": {} - }, - "settings": "Settings", - "@settings": { - "type": "text", - "placeholders": {} - }, - "share": "Share", - "@share": { - "type": "text", - "placeholders": {} - }, - "sharedTheLocation": "{username} shared their location", - "@sharedTheLocation": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "shareLocation": "Share location", - "@shareLocation": { - "type": "text", - "placeholders": {} - }, - "showPassword": "Show password", - "@showPassword": { - "type": "text", - "placeholders": {} - }, - "presenceStyle": "Presence:", - "@presenceStyle": { - "type": "text", - "placeholders": {} - }, - "presencesToggle": "Show status messages from other users", - "@presencesToggle": { - "type": "text", - "placeholders": {} - }, - "singlesignon": "Single Sign on", - "@singlesignon": { - "type": "text", - "placeholders": {} - }, - "skip": "Skip", - "@skip": { - "type": "text", - "placeholders": {} - }, - "sourceCode": "Source code", - "@sourceCode": { - "type": "text", - "placeholders": {} - }, - "spaceIsPublic": "Space is public", - "@spaceIsPublic": { - "type": "text", - "placeholders": {} - }, - "spaceName": "Name", - "@spaceName": { - "type": "text", - "placeholders": {} - }, - "startedACall": "{senderName} started a call", - "@startedACall": { - "type": "text", - "placeholders": { - "senderName": {} - } - }, - "startFirstChat": "Start your first chat", - "status": "Status", - "@status": { - "type": "text", - "placeholders": {} - }, - "statusExampleMessage": "How are you today?", - "@statusExampleMessage": { - "type": "text", - "placeholders": {} - }, - "submit": "Submit", - "@submit": { - "type": "text", - "placeholders": {} - }, - "synchronizingPleaseWait": "Synchronizing… Please wait.", - "@synchronizingPleaseWait": { - "type": "text", - "placeholders": {} - }, - "systemTheme": "System", - "@systemTheme": { - "type": "text", - "placeholders": {} - }, - "theyDontMatch": "They Don't Match", - "@theyDontMatch": { - "type": "text", - "placeholders": {} - }, - "theyMatch": "They Match", - "@theyMatch": { - "type": "text", - "placeholders": {} - }, - "title": "FluffyChat", - "@title": { - "description": "Title for the application", - "type": "text", - "placeholders": {} - }, - "toggleFavorite": "Toggle Favorite", - "@toggleFavorite": { - "type": "text", - "placeholders": {} - }, - "toggleMuted": "Toggle Muted", - "@toggleMuted": { - "type": "text", - "placeholders": {} - }, - "toggleUnread": "Mark Read/Unread", - "@toggleUnread": { - "type": "text", - "placeholders": {} - }, - "tooManyRequestsWarning": "Too many requests. Please try again later!", - "@tooManyRequestsWarning": { - "type": "text", - "placeholders": {} - }, - "transferFromAnotherDevice": "Transfer from another device", - "@transferFromAnotherDevice": { - "type": "text", - "placeholders": {} - }, - "tryToSendAgain": "Try to send again", - "@tryToSendAgain": { - "type": "text", - "placeholders": {} - }, - "unavailable": "Unavailable", - "@unavailable": { - "type": "text", - "placeholders": {} - }, - "unbannedUser": "{username} unbanned {targetName}", - "@unbannedUser": { - "type": "text", - "placeholders": { - "username": {}, - "targetName": {} - } - }, - "unblockDevice": "Unblock Device", - "@unblockDevice": { - "type": "text", - "placeholders": {} - }, - "unknownDevice": "Unknown device", - "@unknownDevice": { - "type": "text", - "placeholders": {} - }, - "unknownEncryptionAlgorithm": "Unknown encryption algorithm", - "@unknownEncryptionAlgorithm": { - "type": "text", - "placeholders": {} - }, - "unknownEvent": "Unknown event '{type}'", - "@unknownEvent": { - "type": "text", - "placeholders": { - "type": {} - } - }, - "unmuteChat": "Unmute chat", - "@unmuteChat": { - "type": "text", - "placeholders": {} - }, - "unpin": "Unpin", - "@unpin": { - "type": "text", - "placeholders": {} - }, - "unreadChats": "{unreadCount, plural, =1{1 unread chat} other{{unreadCount} unread chats}}", - "@unreadChats": { - "type": "text", - "placeholders": { - "unreadCount": {} - } - }, - "userAndOthersAreTyping": "{username} and {count} others are typing…", - "@userAndOthersAreTyping": { - "type": "text", - "placeholders": { - "username": {}, - "count": {} - } - }, - "userAndUserAreTyping": "{username} and {username2} are typing…", - "@userAndUserAreTyping": { - "type": "text", - "placeholders": { - "username": {}, - "username2": {} - } - }, - "userIsTyping": "{username} is typing…", - "@userIsTyping": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "userLeftTheChat": "🚪 {username} left the chat", - "@userLeftTheChat": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "username": "Username", - "@username": { - "type": "text", - "placeholders": {} - }, - "userSentUnknownEvent": "{username} sent a {type} event", - "@userSentUnknownEvent": { - "type": "text", - "placeholders": { - "username": {}, - "type": {} - } - }, - "unverified": "Unverified", - "@unverified": {}, - "verified": "Verified", - "@verified": { - "type": "text", - "placeholders": {} - }, - "verify": "Verify", - "@verify": { - "type": "text", - "placeholders": {} - }, - "verifyStart": "Start Verification", - "@verifyStart": { - "type": "text", - "placeholders": {} - }, - "verifySuccess": "You successfully verified!", - "@verifySuccess": { - "type": "text", - "placeholders": {} - }, - "verifyTitle": "Verifying other account", - "@verifyTitle": { - "type": "text", - "placeholders": {} - }, - "videoCall": "Video call", - "@videoCall": { - "type": "text", - "placeholders": {} - }, - "visibilityOfTheChatHistory": "Visibility of the chat history", - "@visibilityOfTheChatHistory": { - "type": "text", - "placeholders": {} - }, - "visibleForAllParticipants": "Visible for all participants", - "@visibleForAllParticipants": { - "type": "text", - "placeholders": {} - }, - "visibleForEveryone": "Visible for everyone", - "@visibleForEveryone": { - "type": "text", - "placeholders": {} - }, - "voiceMessage": "Voice message", - "@voiceMessage": { - "type": "text", - "placeholders": {} - }, - "waitingPartnerAcceptRequest": "Waiting for partner to accept the request…", - "@waitingPartnerAcceptRequest": { - "type": "text", - "placeholders": {} - }, - "waitingPartnerEmoji": "Waiting for partner to accept the emoji…", - "@waitingPartnerEmoji": { - "type": "text", - "placeholders": {} - }, - "waitingPartnerNumbers": "Waiting for partner to accept the numbers…", - "@waitingPartnerNumbers": { - "type": "text", - "placeholders": {} - }, - "wallpaper": "Wallpaper:", - "@wallpaper": { - "type": "text", - "placeholders": {} - }, - "warning": "Warning!", - "@warning": { - "type": "text", - "placeholders": {} - }, - "weSentYouAnEmail": "We sent you an email", - "@weSentYouAnEmail": { - "type": "text", - "placeholders": {} - }, - "whoCanPerformWhichAction": "Who can perform which action", - "@whoCanPerformWhichAction": { - "type": "text", - "placeholders": {} - }, - "whoIsAllowedToJoinThisGroup": "Who is allowed to join this chat", - "@whoIsAllowedToJoinThisGroup": { - "type": "text", - "placeholders": {} - }, - "whyDoYouWantToReportThis": "Why do you want to report this?", - "@whyDoYouWantToReportThis": { - "type": "text", - "placeholders": {} - }, - "wipeChatBackup": "Wipe your chat backup to create a new recovery key?", - "@wipeChatBackup": { - "type": "text", - "placeholders": {} - }, - "withTheseAddressesRecoveryDescription": "With these addresses you can recover your password.", - "@withTheseAddressesRecoveryDescription": { - "type": "text", - "placeholders": {} - }, - "writeAMessage": "Write a message…", - "writeAMessageFlag": "Write a message in {l1flag} or {l2flag}", - "@writeAMessageFlag": { - "type": "text", - "placeholders": { - "l1flag": {}, - "l2flag": {} - } - }, - "yes": "Yes", - "@yes": { - "type": "text", - "placeholders": {} - }, - "you": "You", - "@you": { - "type": "text", - "placeholders": {} - }, - "youAreNoLongerParticipatingInThisChat": "You are no longer participating in this chat", - "@youAreNoLongerParticipatingInThisChat": { - "type": "text", - "placeholders": {} - }, - "youHaveBeenBannedFromThisChat": "You have been banned from this chat", - "@youHaveBeenBannedFromThisChat": { - "type": "text", - "placeholders": {} - }, - "yourPublicKey": "Your public key", - "@yourPublicKey": { - "type": "text", - "placeholders": {} - }, - "messageInfo": "Message info", - "@messageInfo": {}, - "time": "Time", - "@time": {}, - "messageType": "Message Type", - "@messageType": {}, - "sender": "Sender", - "@sender": {}, - "openGallery": "Open gallery", - "@openGallery": {}, - "removeFromSpace": "Remove from space", - "@removeFromSpace": {}, - "addToSpaceDescription": "Select a space to add this chat to it.", - "@addToSpaceDescription": {}, - "start": "Start", - "@start": {}, - "pleaseEnterRecoveryKeyDescription": "To unlock your old messages, please enter your recovery key that has been generated in a previous session. Your recovery key is NOT your password.", - "@pleaseEnterRecoveryKeyDescription": {}, - "publish": "Publish", - "@publish": {}, - "videoWithSize": "Video ({size})", - "@videoWithSize": { - "type": "text", - "placeholders": { - "size": {} - } - }, - "openChat": "Open Chat", - "@openChat": {}, - "markAsRead": "Mark as read", - "@markAsRead": {}, - "reportUser": "Report user", - "@reportUser": {}, - "dismiss": "Dismiss", - "@dismiss": {}, - "reactedWith": "{sender} reacted with {reaction}", - "@reactedWith": { - "type": "text", - "placeholders": { - "sender": {}, - "reaction": {} - } - }, - "pinMessage": "Pin to room", - "@pinMessage": {}, - "confirmEventUnpin": "Are you sure to permanently unpin the event?", - "@confirmEventUnpin": {}, - "emojis": "Emojis", - "@emojis": {}, - "placeCall": "Place call", - "@placeCall": {}, - "voiceCall": "Voice call", - "@voiceCall": {}, - "unsupportedAndroidVersion": "Unsupported Android version", - "@unsupportedAndroidVersion": {}, - "unsupportedAndroidVersionLong": "This feature requires a newer Android version. Please check for updates or Lineage OS support.", - "@unsupportedAndroidVersionLong": {}, - "videoCallsBetaWarning": "Please note that video calls are currently in beta. They might not work as expected or work at all on all platforms.", - "@videoCallsBetaWarning": {}, - "experimentalVideoCalls": "Experimental video calls", - "@experimentalVideoCalls": {}, - "emailOrUsername": "Email or username", - "@emailOrUsername": {}, - "indexedDbErrorTitle": "Private mode issues", - "@indexedDbErrorTitle": {}, - "indexedDbErrorLong": "The message storage is unfortunately not enabled in private mode by default.\nPlease visit\n - about:config\n - set dom.indexedDB.privateBrowsing.enabled to true\nOtherwise, it is not possible to run FluffyChat.", - "@indexedDbErrorLong": {}, - "switchToAccount": "Switch to account {number}", - "@switchToAccount": { - "type": "number", - "placeholders": { - "number": {} - } - }, - "nextAccount": "Next account", - "@nextAccount": {}, - "previousAccount": "Previous account", - "@previousAccount": {}, - "addWidget": "Add widget", - "@addWidget": {}, - "widgetVideo": "Video", - "@widgetVideo": {}, - "widgetEtherpad": "Text note", - "@widgetEtherpad": {}, - "widgetJitsi": "Jitsi Meet", - "@widgetJitsi": {}, - "widgetCustom": "Custom", - "@widgetCustom": {}, - "widgetName": "Name", - "@widgetName": {}, - "widgetUrlError": "This is not a valid URL.", - "@widgetUrlError": {}, - "widgetNameError": "Please provide a display name.", - "@widgetNameError": {}, - "errorAddingWidget": "Error adding the widget.", - "@errorAddingWidget": {}, - "youRejectedTheInvitation": "You rejected the invitation", - "@youRejectedTheInvitation": {}, - "youJoinedTheChat": "You joined the chat", - "@youJoinedTheChat": {}, - "youAcceptedTheInvitation": "👍 You accepted the invitation", - "@youAcceptedTheInvitation": {}, - "youBannedUser": "You banned {user}", - "@youBannedUser": { - "placeholders": { - "user": {} - } - }, - "youHaveWithdrawnTheInvitationFor": "You have withdrawn the invitation for {user}", - "@youHaveWithdrawnTheInvitationFor": { - "placeholders": { - "user": {} - } - }, - "youInvitedToBy": "📩 You have been invited via link to:\n{alias}", - "@youInvitedToBy": { - "placeholders": { - "alias": {} - } - }, - "youInvitedBy": "📩 You have been invited by {user}", - "@youInvitedBy": { - "placeholders": { - "user": {} - } - }, - "youInvitedUser": "📩 You invited {user}", - "@youInvitedUser": { - "placeholders": { - "user": {} - } - }, - "youKicked": "👞 You kicked {user}", - "@youKicked": { - "placeholders": { - "user": {} - } - }, - "youKickedAndBanned": "🙅 You kicked and banned {user}", - "@youKickedAndBanned": { - "placeholders": { - "user": {} - } - }, - "youUnbannedUser": "You unbanned {user}", - "@youUnbannedUser": { - "placeholders": { - "user": {} - } - }, - "hasKnocked": "{user} has knocked", - "@hasKnocked": { - "placeholders": { - "user": {} - } - }, - "users": "Users", - "@users": {}, - "unlockOldMessages": "Unlock old messages", - "@unlockOldMessages": {}, - "storeInSecureStorageDescription": "Store the recovery key in the secure storage of this device.", - "@storeInSecureStorageDescription": {}, - "saveKeyManuallyDescription": "Save this key manually by triggering the system share dialog or clipboard.", - "@saveKeyManuallyDescription": {}, - "storeInAndroidKeystore": "Store in Android KeyStore", - "@storeInAndroidKeystore": {}, - "storeInAppleKeyChain": "Store in Apple KeyChain", - "@storeInAppleKeyChain": {}, - "storeSecurlyOnThisDevice": "Store securely on this device", - "@storeSecurlyOnThisDevice": {}, - "countFiles": "{count} files", - "@countFiles": { - "placeholders": { - "count": {} - } - }, - "user": "User", - "@user": {}, - "custom": "Custom", - "@custom": {}, - "foregroundServiceRunning": "This notification appears when the foreground service is running.", - "@foregroundServiceRunning": {}, - "screenSharingTitle": "screen sharing", - "@screenSharingTitle": {}, - "screenSharingDetail": "You are sharing your screen in FuffyChat", - "@screenSharingDetail": {}, - "callingPermissions": "Calling permissions", - "@callingPermissions": {}, - "callingAccount": "Calling account", - "@callingAccount": {}, - "callingAccountDetails": "Allows FluffyChat to use the native android dialer app.", - "@callingAccountDetails": {}, - "appearOnTop": "Appear on top", - "@appearOnTop": {}, - "appearOnTopDetails": "Allows the app to appear on top (not needed if you already have Fluffychat setup as a calling account)", - "@appearOnTopDetails": {}, - "otherCallingPermissions": "Microphone, camera and other FluffyChat permissions", - "@otherCallingPermissions": {}, - "whyIsThisMessageEncrypted": "Why is this message unreadable?", - "@whyIsThisMessageEncrypted": {}, - "noKeyForThisMessage": "This can happen if the message was sent before you have signed in to your account at this device.\n\nIt is also possible that the sender has blocked your device or something went wrong with the internet connection.\n\nAre you able to read the message on another session? Then you can transfer the message from it! Go to Settings > Devices and make sure that your devices have verified each other. When you open the room the next time and both sessions are in the foreground, the keys will be transmitted automatically.\n\nDo you not want to lose the keys when logging out or switching devices? Make sure that you have enabled the chat backup in the settings.", - "@noKeyForThisMessage": {}, - "newGroup": "New chat", - "@newGroup": {}, - "newSpace": "New class", - "@newSpace": {}, - "enterSpace": "Enter space", - "@enterSpace": {}, - "enterRoom": "Enter room", - "@enterRoom": {}, - "allSpaces": "All spaces", - "@allSpaces": {}, - "numChats": "{number} chats", - "@numChats": { - "type": "number", - "placeholders": { - "number": {} - } - }, - "hideUnimportantStateEvents": "Hide unimportant state events", - "hidePresences": "Hide Status List?", - "doNotShowAgain": "Do not show again", - "wasDirectChatDisplayName": "Empty chat (was {oldDisplayName})", - "@wasDirectChatDisplayName": { - "type": "text", - "placeholders": { - "oldDisplayName": {} - } - }, - "newSpaceDescription": "Spaces allows you to consolidate your chats and build private or public communities.", - "encryptThisChat": "Encrypt this chat", - "disableEncryptionWarning": "For security reasons you can not disable encryption in a chat, where it has been enabled before.", - "sorryThatsNotPossible": "Sorry... that is not possible", - "deviceKeys": "Device keys:", - "reopenChat": "Reopen chat", - "noBackupWarning": "Don't forget your password!", - "noOtherDevicesFound": "No other devices found", - "fileIsTooBigForServer": "The server reports that the file is too large to be sent.", - "fileHasBeenSavedAt": "File has been saved at {path}", - "@fileHasBeenSavedAt": { - "type": "text", - "placeholders": { - "path": {} - } - }, - "jumpToLastReadMessage": "Jump to last read message", - "readUpToHere": "Read up to here", - "jump": "Jump", - "openLinkInBrowser": "Open link in browser", - "reportErrorDescription": "😭 Oh no. Something went wrong. If you want, you can report this bug to the developers.", - "report": "report", - "signInWithPassword": "Sign in with password", - "pleaseTryAgainLaterOrChooseDifferentServer": "Please try again later or choose a different server.", - "signInWith": "Sign in with {provider}", - "@signInWith": { - "type": "text", - "placeholders": { - "provider": {} - } - }, - "profileNotFound": "The user could not be found on the server. Maybe there is a connection problem or the user doesn't exist.", - "setTheme": "Set theme:", - "setColorTheme": "Set color theme:", - "invite": "Invite", - "requests": "Requests", - "inviteGroupChat": "📨 Invite group chat", - "invitePrivateChat": "📨 Invite private chat", - "invalidInput": "Invalid input!", - "wrongPinEntered": "Wrong pin entered! Try again in {seconds} seconds...", - "@wrongPinEntered": { - "type": "text", - "placeholders": { - "seconds": {} - } - }, - "allCorrect": "That's how I would say it! Nice!", - "newWayAllGood": "That's not how I would have said it but it looks good!", - "othersAreBetter": "Hm, there might be a better way to say that.", - "holdForInfo": "Click and hold for word info.", - "greenFeedback": "That's what I would put!", - "yellowFeedback": "Hm, you can try that and see if it works! To use this word, just click it again.", - "redFeedback": "I don't think that's right...", - "customInputFeedbackChoice": "You wrote it in. Nice!", - "itInstructionsTitle": "I can help you translate!", - "itInstructionsBody": "You can click and hold choices for word info.", - "toggleLanguages": "Toggle the language of selected messages.", - "classWelcomeChat": "Welcome Chat", - "@classWelcomeChat": { - "type": "text", - "placeholders": {} - }, - "deleteSpace": "Delete Space", - "deleteGroup": "Delete Group Chat", - "areYouSureDeleteClass": "Are you sure you want to delete this space?", - "areYouSureDeleteGroup": "Are you sure you want to delete this group chat?", - "cannotBeReversed": "This action cannot be reversed", - "enterDeletedClassName": "Enter space name to confirm:", - "incorrectClassName": "Incorrect Space Name", - "oneday": "Last 24 hours", - "@oneday": { - "type": "text", - "placeholders": {} - }, - "oneweek": "Last 7 days", - "@oneweek": { - "type": "text", - "placeholders": {} - }, - "onemonth": "Past month", - "@onemonth": { - "type": "text", - "placeholders": {} - }, - "sixmonth": "Past 6 months", - "@sixmonth": { - "type": "text", - "placeholders": {} - }, - "oneyear": "Past year", - "@oneyear": { - "type": "text", - "placeholders": {} - }, - "gaTooltip": "L2 use with grammar assistance", - "taTooltip": "L2 use with translation assistance", - "unTooltip": "Other", - "interactiveTranslatorSliderHeader": "Interactive Translator", - "interactiveGrammarSliderHeader": "Interactive Grammar Checker", - "interactiveTranslatorNotAllowed": "Disabled", - "@interactiveTranslatorNotAllowed": { - "type": "text", - "placeholders": {} - }, - "interactiveTranslatorAllowed": "Student Choice", - "@interactiveTranslatorAllowed": { - "type": "text", - "placeholders": {} - }, - "interactiveTranslatorRequired": "Required", - "@interactiveTranslatorRequired": { - "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": { - "type": "text", - "placeholders": {} - }, - "interactiveTranslatorAllowedDesc": "Students can choose whether to use translation assistance in space group chats in Main Menu > My Learning Settings.", - "@interactiveTranslatorAllowedDesc": { - "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": { - "type": "text", - "placeholders": {} - }, - "notYetSet": "Not yet set", - "@notYetSet": { - "type": "text", - "placeholders": {} - }, - "multiLingualClass": "Multilingual Class", - "classAnalytics": "Class Analytics", - "@classAnalytics": { - "type": "text", - "placeholders": {} - }, - "allClasses": "All Classes", - "@allClasses": { - "type": "text", - "placeholders": {} - }, - "myLearning": "My Analytics", - "@myLearning": { - "type": "text", - "placeholders": {} - }, - "allChatsAndClasses": "All chats and spaces", - "timeOfLastMessage": "Time of last sent message", - "totalMessages": "Total messages sent", - "waTooltip": "L2 use without assistance", - "changeDateRange": "Change date range", - "numberOfStudents": "Number of Students", - "@numberOfStudents": { - "type": "text", - "placeholders": {} - }, - "classDescription": "Description", - "@classDescription": { - "type": "text", - "placeholders": {} - }, - "classDescriptionDesc": "Set a description", - "@classDescriptionDesc": { - "type": "text", - "placeholders": {} - }, - "requestToEnroll": "Request to Enroll", - "@requestToEnroll": { - "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": { - "type": "text", - "placeholders": {} - }, - "addStudents": "Add students", - "@addStudents": { - "type": "text", - "placeholders": {} - }, - "copyClassLink": "Copy invite link", - "copyClassLinkDesc": "Clicking this link will take students to the app, direct them to make an account and they will automatically join this space.", - "copyClassCode": "Copy invite code", - "inviteStudentByUserName": "Invite student by username", - "@inviteStudentByUserName": { - "type": "text", - "placeholders": {} - }, - "classSettings": "Class Settings", - "@classSettings": { - "type": "text", - "placeholders": {} - }, - "classSettingsDesc": "Edit class languages and proficiency level.", - "@classSettingsDesc": { - "type": "text", - "placeholders": {} - }, - "selectClassRoomDominantLanguage": "What is the base language of your class?", - "@selectClassRoomDominantLanguage": { - "type": "text", - "placeholders": {} - }, - "selectTargetLanguage": "What language are you teaching?", - "@selectTargetLanguage": { - "type": "text", - "placeholders": {} - }, - "whatIsYourClassLanguageLevel": "What is the average language level of your class?", - "@whatIsYourClassLanguageLevel": { - "type": "text", - "placeholders": {} - }, - "studentPermissions": "Student Permissions", - "@studentPermissions": { - "type": "text", - "placeholders": {} - }, - "interactiveTranslator": "Translation assistance", - "@interactiveTranslator": { - "type": "text", - "placeholders": {} - }, - "oneToOneChatsWithinClass": "Private Chats within Space", - "@oneToOneChatsWithinClass": { - "type": "text", - "placeholders": {} - }, - "oneToOneChatsWithinClassDesc": "If you allow private chats, students can initiate and use private chats with other space participants. Otherwise, they can only participate in groups chats.", - "@oneToOneChatsWithinClassDesc": { - "type": "text", - "placeholders": {} - }, - "createGroupChats": "Create Group Chats", - "@createGroupChats": { - "type": "text", - "placeholders": {} - }, - "createGroupChatsDesc": "Toggle this on to allow students to create group chats within the class/exchange space.", - "@createGroupChatsDesc": { - "type": "text", - "placeholders": {} - }, - "shareVideo": "Share Video", - "@shareVideo": { - "type": "text", - "placeholders": {} - }, - "shareVideoDesc": "Toggle this on to allow students to share videos in chats.", - "@shareVideoDesc": { - "type": "text", - "placeholders": {} - }, - "sharePhotos": "Share Photos", - "@sharePhotos": { - "type": "text", - "placeholders": {} - }, - "sharePhotosDesc": "Toggle this on to allow students to share photos in chats.", - "@sharePhotosDesc": { - "type": "text", - "placeholders": {} - }, - "shareFiles": "Share Files", - "@shareFiles": { - "type": "text", - "placeholders": {} - }, - "shareFilesDesc": "Toggle this on to allow students to share files in chats.", - "@shareFilesDesc": { - "type": "text", - "placeholders": {} - }, - "shareLocationDesc": "Toggle this on to allow students to share location in chats.", - "@shareLocationDesc": { - "type": "text", - "placeholders": {} - }, - "selectLanguageLevel": "Select language level", - "@selectLanguageLevel": { - "type": "text", - "placeholders": {} - }, - "noIdenticalLanguages": "Please choose different base and target languages", - "@noIdenticalLanguages": { - "type": "text", - "placeholders": {} - }, - "iWantALanguagePartnerFrom": "Is from:", - "@iWantALanguagePartnerFrom": { - "type": "text", - "placeholders": {} - }, - "worldWide": "Worldwide", - "@worldWide": { - "type": "text", - "placeholders": {} - }, - "noResults": "No results! Try broadening your search.", - "@noResults": { - "type": "text", - "placeholders": {} - }, - "searchBy": "Search by country and languages", - "@searchBy": { - "type": "text", - "placeholders": {} - }, - "iWantAConversationPartner": "I want a conversation partner who", - "@iWantAConversationPartner": { - "type": "text", - "placeholders": {} - }, - "iWantALanguagePartnerWhoSpeaks": "Speaks:", - "@iWantALanguagePartnerWhoSpeaks": { - "type": "text", - "placeholders": {} - }, - "iWantALanguagePartnerWhoIsLearning": "Is learning:", - "@iWantALanguagePartnerWhoIsLearning": { - "type": "text", - "placeholders": {} - }, - "yourBirthdayPlease": "Pangea Chat serves schools and other learning communities, ages 13 and up, around the world.\n\nIn order to protect our young learners, we ask our users to verify their age before connecting to our community.\n\nBefore you can search Pangea Chat for classes, rooms, and new friends, you must verify you are 18 or older.", - "@yourBirthdayPlease": { - "type": "text", - "placeholders": {} - }, - "invalidDob": "Invalid Date of Birth", - "@invalidDob": { - "type": "text", - "placeholders": {} - }, - "enterYourDob": "Enter your Date of Birth", - "@enterYourDob": { - "type": "text", - "placeholders": {} - }, - "getStarted": "Get Started", - "@getStarted": { - "type": "text", - "placeholders": {} - }, - "mustBe13": "User should be 13 years old", - "@mustBe13": { - "type": "text", - "placeholders": {} - }, - "yourBirthdayPleaseShort": "Please selected your age group", - "@yourBirthdayPleaseShort": { - "type": "text", - "placeholders": {} - }, - "joinWithClassCode": "Join class or exchange", - "@joinWithClassCode": { - "type": "text", - "placeholders": {} - }, - "joinWithClassCodeDesc": "Connect to a class or exchange space with the 6-digit invite code provided by the space administrator.", - "@joinWithClassCodeDesc": { - "type": "text", - "placeholders": {} - }, - "joinWithClassCodeHint": "Enter invite code", - "@joinWithClassCodeHint": { - "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": { - "type": "text", - "placeholders": {} - }, - "languageLevelPreA1": "True Beginner (Pre A1)", - "@languageLevelPreA1": { - "type": "text", - "placeholders": {} - }, - "languageLevelA1": "Beginner (A1)", - "@languageLevelA1": { - "type": "text", - "placeholders": {} - }, - "languageLevelA2": "Elementary (A2)", - "@languageLevelA2": { - "type": "text", - "placeholders": {} - }, - "languageLevelB1": "Intermediate (B1)", - "@languageLevelB1": { - "type": "text", - "placeholders": {} - }, - "languageLevelB2": "Upper Intermediate (B2)", - "@languageLevelB2": { - "type": "text", - "placeholders": {} - }, - "languageLevelC1": "Advanced (C1)", - "@languageLevelC1": { - "type": "text", - "placeholders": {} - }, - "languageLevelC2": "Mastery (C2)", - "@languageLevelC2": { - "type": "text", - "placeholders": {} - }, - "changeTheNameOfTheClass": "Change the name", - "@changeTheNameOfTheClass": { - "type": "text", - "placeholders": {} - }, - "changeTheNameOfTheChat": "Change the name of the chat", - "@changeTheNameOfTheChat": { - "type": "text", - "placeholders": {} - }, - "welcomeToYourNewClass": "Welcome! 🙂", - "@welcomeToYourNewClass": { - "type": "text", - "placeholders": {} - }, - "welcomeToClass": "Welcome! 🙂\n- Try joining a chat!\n- Have fun chatting!", - "@welcomeToClass": { - "type": "text", - "placeholders": {} - }, - "welcomeToPangea18Plus": "Welcome to Pangea Chat! 🙂\nWhat's next?\nCreate or join a class!\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": { - "type": "text", - "placeholders": {} - }, - "findALanguagePartner": "Find a conversation partner", - "@findALanguagePartner": { - "type": "text", - "placeholders": {} - }, - "setToPublicSettingsTitle": "Want to find a conversation partner?", - "@setToPublicSettingsTitle": { - "type": "text", - "placeholders": {} - }, - "setToPublicSettingsDesc": "Before you can search for a conversation parter, you must set your profile visibility to public.", - "@setToPublicSettingsDesc": { - "type": "text", - "placeholders": {} - }, - "accountSettings": "Account settings", - "@accountSettings": { - "type": "text", - "placeholders": {} - }, - "unableToFindClassCode": "Unable to find code.", - "@unableToFindClassCode": { - "type": "text", - "placeholders": {} - }, - "askPangeaBot": "Ask Pangea Bot for a contextual definition.", - "sorryNoResults": "Sorry, no results.", - "@sorryNoResults": { - "type": "text", - "placeholders": {} - }, - "ignoreInThisText": "Ignore", - "@ignoreInThisText": { - "type": "text", - "placeholders": {} - }, - "helpMeTranslate": "Help me translate!", - "@helpMeTranslate": { - "type": "text", - "placeholders": {} - }, - "needsItShortMessage": "Try interactive translation!", - "needsIGCShortMessage": "Try interactive grammar assistance!", - "@needsItShortMessage": { - "type": "text", - "placeholders": {} - }, - "needsItMessage": "This message has too many words in your base language.", - "@needsItMessage": { - "type": "text", - "placeholders": {} - }, - "needsIgcMessage": "This message has a grammar error.", - "tokenTranslationTitle": "A word is in your base language.", - "@tokenTranslationTitle": { - "type": "text", - "placeholders": {} - }, - "spanTranslationDesc": "See possible translations below.", - "@spanTranslationDesc": { - "type": "text", - "placeholders": {} - }, - "spanTranslationTitle": "Some words are in your base language.", - "@spanTranslationTitle": { - "type": "text", - "placeholders": {} - }, - "l1SpanAndGrammarTitle": "Outside target language", - "l1SpanAndGrammarDesc": "This could in your base language or it could be a grammar error.", - "otherTitle": "You have an error.", - "@otherTitle": { - "type": "text", - "placeholders": {} - }, - "otherDesc": "See possible corrections below.", - "@otherDesc": { - "type": "text", - "placeholders": {} - }, - "countryInformation": "My country", - "@countryInformation": { - "type": "text", - "placeholders": {} - }, - "myLanguages": "My base and target language", - "@myLanguages": { - "type": "text", - "placeholders": {} - }, - "targetLanguage": "Target Language", - "@targetLanguage": { - "type": "text", - "placeholders": {} - }, - "sourceLanguage": "Base language", - "@sourceLanguage": { - "type": "text", - "placeholders": {} - }, - "languagesISpeak": "Languages I speak", - "@languagesISpeak": { - "type": "text", - "placeholders": {} - }, - "updateLanguage": "My languages", - "@updateLanguage": { - "type": "text", - "placeholders": {} - }, - "whatLanguageYouWantToLearn": "What language do you want to learn?", - "@whatLanguageYouWantToLearn": { - "type": "text", - "placeholders": {} - }, - "whatIsYourBaseLanguage": "What is your base language?", - "@whatIsYourBaseLanguage": { - "type": "text", - "placeholders": {} - }, - "saveChanges": "Save changes", - "@saveChanges": { - "type": "text", - "placeholders": {} - }, - "publicProfileTitle": "Public Profile", - "@publicProfileTitle": { - "type": "text", - "placeholders": {} - }, - "publicProfileDesc": "Your profile must be public in order to search or be found as a conversation partner.", - "@publicProfileDesc": { - "type": "text", - "placeholders": {} - }, - "errorDisableIT": "Translation assistance is turned off.", - "errorDisableIGC": "Grammar assistance is turned off.", - "errorDisableLanguageAssistance": "Translation assistance and grammar assistance are turned off.", - "errorDisableITUserDesc": "Click here to update translation assistance settings", - "errorDisableIGCUserDesc": "Click here to update grammar assistance settings", - "errorDisableLanguageAssistanceUserDesc": "Click here to update translation assistance and grammar assistance settings", - "errorDisableITClassDesc": "Translation assistance is turned off for the space that this chat is in.", - "errorDisableIGCClassDesc": "Grammar assistance is turned off for the space that this chat is in.", - "errorDisableLanguageAssistanceClassDesc": "Translation assistance and grammar assistance are turned off for the space that this chat is in.", - "itIsDisabled": "Interactive Translation is disabled", - "igcIsDisabled": "Interactive Grammar Checking is disabled", - "goToLearningSettings": "Go to My Learning Settings", - "error405Title": "Languages not set", - "error405Desc": "Please set your languages in Main Menu > My Learning Settings.", - "loginOrSignup": "Sign in with", - "@loginOrSignup": { - "type": "text", - "placeholders": {} - }, - "iAgreeToThe": "I agree to the ", - "@iAgreeToThe": { - "type": "text", - "placeholders": {} - }, - "termsAndConditions": "Terms and Conditions", - "@termsAndConditions": { - "type": "text", - "placeholders": {} - }, - "andCertifyIAmAtLeast13YearsOfAge": " and certify I am at least 13 years of age.", - "@andCertifyIAmAtLeast13YearsOfAge": { - "type": "text", - "placeholders": {} - }, - "error502504Title": "Wow, there are a lot of students online!", - "@error502504Title": { - "type": "text", - "placeholders": {} - }, - "error502504Desc": "Translation and grammar tools may be slow or unavailable while the Pangea bots catch up.", - "@error502504Desc": { - "type": "text", - "placeholders": {} - }, - "error404Title": "Translation error!", - "@error404Title": { - "type": "text", - "placeholders": {} - }, - "error404Desc": "Pangea Bot isn't sure how to translate that...", - "@error404Desc": { - "type": "text", - "placeholders": {} - }, - "errorPleaseRefresh": "We're looking into it! Please reload and try again.", - "@errorPleaseRefresh": { - "type": "text", - "placeholders": {} - }, - "findAClass": "Find a class (coming soon)", - "toggleIT": "Interactive Translation", - "@toggleIT": { - "type": "text", - "placeholders": {} - }, - "toggleIGC": "Interactive Grammar Checking", - "@toggleIGC": { - "type": "text", - "placeholders": {} - }, - "toggleToolSettingsDescription": "Here you can toggle your individual language tool settings. For chats within a space, the space settings will take precedence and may override these settings.", - "connectedToStaging": "You are connected to the staging server.", - "@connectedToStaging": { - "type": "text", - "placeholders": {} - }, - "learningSettings": "My Learning Settings", - "classNameRequired": "Please enter a space name", - "@classNameRequired": { - "type": "text", - "placeholders": {} - }, - "sendVoiceNotes": "Send Voice Notes", - "@sendVoiceNotes": { - "type": "text", - "placeholders": {} - }, - "sendVoiceNotesDesc": "Toggle this on to allow students to send voice notes in chats.", - "@sendVoiceNotesDesc": { - "type": "text", - "placeholders": {} - }, - "chatTopic": "Chat topic", - "@chatTopic": { - "type": "text", - "placeholders": {} - }, - "chatTopicDesc": "Set a chat topic", - "@chatTopicDesc": { - "type": "text", - "placeholders": {} - }, - "inviteStudentByUserNameDesc": "If your student already has an account, you can search for them.", - "@inviteStudentByUserNameDesc": { - "type": "text", - "placeholders": {} - }, - "classRoster": "Participants", - "@classRoster": { - "type": "text", - "placeholders": {} - }, - "almostPerfect": "That seems right! Here's what I would have said.", - "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!", - "understandingMessagesTitle": "Definitions and translations!", - "understandingMessagesBody": "Click underlined words for definitions. Translate with message options (upper right).", - "allDone": "All done!", - "vocab": "Vocabulary", - "low": "We have evidence the user does not understand these words.", - "medium": "These words have been used. It is unclear if the words are fully understood or not.", - "high": "We have evidence the user understands these words.", - "unknownProficiency": "These words have not been used in Pangea Chat.", - "changeView": "Switch views.", - "clearAll": "Clear all words?", - "generateVocabulary": "Generate vocabulary from title and description", - "generatePrompts": "Generate prompts", - "subscribe": "Subscribe", - "getAccess": "Unlock learning tools", - "subscriptionDesc": "Messaging is free! Subscribe to unlock interactive translation, grammar checking and learning analytics.", - "subscriptionManagement": "Subscription Management", - "currentSubscription": "Current Subscription", - "changeSubscription": "Change your subscription", - "cancelSubscription": "Cancel your subscription", - "selectYourPlan": "Select Your Plan", - "subsciptionPlatformTooltip": "Please login to your original device to manage your subscription plan", - "subscriptionManagementUnavailable": "Subscription management not available", - "paymentMethod": "Payment Method", - "paymentHistory": "Payment History", - "emptyChatDownloadWarning": "Cannot download empty chat", - "appUpdateAvailable": "Update Available", - "update": "Update", - "updateDesc": "You can now update this app from {localVersion} to {storeVersion}", - "@updateDesc": { - "type": "text", - "placeholders": { - "storeVersion": {}, - "localVersion": {} - } - }, - "maybeLater": "Maybe Later", - "mainMenu": "Main Menu", - "toggleImmersionMode": "Immersion Mode", - "toggleImmersionModeDesc": "When enabled, all messages are displayed in your target language. This setting is most useful in language exchanges.", - "itToggleDescription": "This language learning tool will identify words in your base language and help you translate them to your target language. Though rare, the AI can make translation errors.", - "igcToggleDescription": "This language learning tool will identify common spelling, grammar and punctuation errors in your message and suggest corrections. Though rare, the AI can make correction errors.", - "sendOnEnterDescription": "Turn this off to be able to add line spaces in messages. When the toggle is off on the browser app, you can press Shift + Enter to start a new line. When the toggle is off on mobile apps, just Enter will start a new line.", - "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.", - "originalMessage": "Original Message", - "sentMessage": "Sent Message", - "useType": "Use Type", - "notAvailable": "Not Available", - "taAndGaTooltip": "L2 use with translation assistance and grammar assistance", - "definitionsToolName": "Word Definitions", - "messageTranslationsToolName": "Message Translations", - "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", - "downloadCSVFile": "Download CSV File", - "promotionalSubscriptionDesc": "You currently have a lifetime promotional subscription. Message support@pangea.chat for help changing your subscription.", - "originalSubscriptionPlatform": "Subscription purchased through {purchasePlatform}", - "@originalSubscriptionPlatform": { - "placeholders": { - "purchasePlatform": {} - } - }, - "oneWeekTrial": "One Week Trial", - "creatingSpacePleaseWait": "Creating space. Please wait...", - "downloadXLSXFile": "Download Excel File", - "abDisplayName": "Abkhaz", - "aaDisplayName": "Afar", - "afDisplayName": "Afrikaans", - "akDisplayName": "Akan", - "sqDisplayName": "Albanian", - "amDisplayName": "Amharic", - "arDisplayName": "Arabic", - "anDisplayName": "Aragonese", - "hyDisplayName": "Armenian", - "asDisplayName": "Assamese", - "avDisplayName": "Avaric", - "aeDisplayName": "Avestan", - "ayDisplayName": "Aymara", - "azDisplayName": "Azerbaijani", - "bmDisplayName": "Bambara", - "baDisplayName": "Bashkir", - "euDisplayName": "Basque", - "beDisplayName": "Belarusian", - "bnDisplayName": "Bengali", - "bhDisplayName": "Bihari", - "biDisplayName": "Bislama", - "bsDisplayName": "Bosnian", - "brDisplayName": "Breton", - "bgDisplayName": "Bulgarian", - "myDisplayName": "Burmese", - "caDisplayName": "Catalan, Valencian", - "chDisplayName": "Chamorro", - "ceDisplayName": "Chechen", - "nyDisplayName": "Chichewa, Chewa, Nyanja", - "zhDisplayName": "Chinese", - "cvDisplayName": "Chuvash", - "kwDisplayName": "Cornish", - "coDisplayName": "Corsican", - "crDisplayName": "Cree", - "hrDisplayName": "Croatian", - "csDisplayName": "Czech", - "daDisplayName": "Danish", - "dvDisplayName": "Divehi; Dhivehi; Maldivian;", - "nlDisplayName": "Dutch", - "enDisplayName": "English", - "eoDisplayName": "Esperanto", - "etDisplayName": "Estonian", - "eeDisplayName": "Ewe", - "foDisplayName": "Faroese", - "fjDisplayName": "Fijian", - "fiDisplayName": "Finnish", - "frDisplayName": "French", - "ffDisplayName": "Fula; Fulah; Pulaar; Pular", - "glDisplayName": "Galician", - "kaDisplayName": "Georgian", - "deDisplayName": "German", - "elDisplayName": "Greek, Modern", - "gnDisplayName": "Guaraní", - "guDisplayName": "Gujarati", - "htDisplayName": "Haitian, Haitian Creole", - "haDisplayName": "Hausa", - "heDisplayName": "Hebrew (modern)", - "hzDisplayName": "Herero", - "hiDisplayName": "Hindi", - "hoDisplayName": "Hiri Motu", - "huDisplayName": "Hungarian", - "iaDisplayName": "Interlingua", - "idDisplayName": "Indonesian", - "ieDisplayName": "Interlingue", - "gaDisplayName": "Irish", - "igDisplayName": "Igbo", - "ikDisplayName": "Inupiaq", - "ioDisplayName": "Ido", - "isDisplayName": "Icelandic", - "itDisplayName": "Italian", - "iuDisplayName": "Inuktitut", - "jaDisplayName": "Japanese", - "jvDisplayName": "Javanese", - "klDisplayName": "Kalaallisut, Greenlandic", - "knDisplayName": "Kannada", - "krDisplayName": "Kanuri", - "ksDisplayName": "Kashmiri", - "kkDisplayName": "Kazakh", - "kmDisplayName": "Khmer", - "kiDisplayName": "Kikuyu, Gikuyu", - "rwDisplayName": "Kinyarwanda", - "kyDisplayName": "Kirghiz, Kyrgyz", - "kvDisplayName": "Komi", - "kgDisplayName": "Kongo", - "koDisplayName": "Korean", - "kuDisplayName": "Kurdish", - "kjDisplayName": "Kwanyama, Kuanyama", - "laDisplayName": "Latin", - "lbDisplayName": "Luxembourgish, Letzeburgesch", - "lgDisplayName": "Luganda", - "liDisplayName": "Limburgish, Limburgan, Limburger", - "lnDisplayName": "Lingala", - "loDisplayName": "Lao", - "ltDisplayName": "Lithuanian", - "luDisplayName": "Luba-Katanga", - "lvDisplayName": "Latvian", - "gvDisplayName": "Manx", - "mkDisplayName": "Macedonian", - "mgDisplayName": "Malagasy", - "msDisplayName": "Malay", - "mlDisplayName": "Malayalam", - "mtDisplayName": "Maltese", - "miDisplayName": "Māori", - "mrDisplayName": "Marathi (Marāṭhī)", - "mhDisplayName": "Marshallese", - "mnDisplayName": "Mongolian", - "naDisplayName": "Nauru", - "nvDisplayName": "Navajo, Navaho", - "nbDisplayName": "Norwegian Bokmål", - "ndDisplayName": "North Ndebele", - "neDisplayName": "Nepali", - "ngDisplayName": "Ndonga", - "nnDisplayName": "Norwegian Nynorsk", - "noDisplayName": "Norwegian", - "iiDisplayName": "Nuosu", - "nrDisplayName": "South Ndebele", - "ocDisplayName": "Occitan", - "ojDisplayName": "Ojibwe, Ojibwa", - "cuDisplayName": "Old Church Slavonic, Church Slavic, Church Slavonic, Old Bulgarian, Old Slavonic", - "omDisplayName": "Oromo", - "orDisplayName": "Oriya", - "osDisplayName": "Ossetian, Ossetic", - "paDisplayName": "Panjabi, Punjabi", - "piDisplayName": "Pāli", - "faDisplayName": "Persian", - "plDisplayName": "Polish", - "psDisplayName": "Pashto, Pushto", - "ptDisplayName": "Portuguese", - "quDisplayName": "Quechua", - "rmDisplayName": "Romansh", - "rnDisplayName": "Kirundi", - "roDisplayName": "Romanian, Moldavian, Moldovan", - "ruDisplayName": "Russian", - "saDisplayName": "Sanskrit (Saṁskṛta)", - "scDisplayName": "Sardinian", - "sdDisplayName": "Sindhi", - "seDisplayName": "Northern Sami", - "smDisplayName": "Samoan", - "sgDisplayName": "Sango", - "srDisplayName": "Serbian", - "gdDisplayName": "Scottish Gaelic, Gaelic", - "snDisplayName": "Shona", - "siDisplayName": "Sinhala, Sinhalese", - "skDisplayName": "Slovak", - "slDisplayName": "Slovene", - "soDisplayName": "Somali", - "stDisplayName": "Southern Sotho", - "esDisplayName": "Spanish", - "suDisplayName": "Sundanese", - "swDisplayName": "Swahili", - "ssDisplayName": "Swati", - "svDisplayName": "Swedish", - "taDisplayName": "Tamil", - "teDisplayName": "Telugu", - "tgDisplayName": "Tajik", - "thDisplayName": "Thai", - "tiDisplayName": "Tigrinya", - "boDisplayName": "Tibetan Standard, Tibetan, Central", - "tkDisplayName": "Turkmen", - "tlDisplayName": "Tagalog", - "tnDisplayName": "Tswana", - "toDisplayName": "Tonga (Tonga Islands)", - "trDisplayName": "Turkish", - "tsDisplayName": "Tsonga", - "ttDisplayName": "Tatar", - "twDisplayName": "Twi", - "tyDisplayName": "Tahitian", - "ugDisplayName": "Uighur, Uyghur", - "ukDisplayName": "Ukrainian", - "urDisplayName": "Urdu", - "uzDisplayName": "Uzbek", - "veDisplayName": "Venda", - "viDisplayName": "Vietnamese", - "voDisplayName": "Volapük", - "waDisplayName": "Walloon", - "cyDisplayName": "Welsh", - "woDisplayName": "Wolof", - "fyDisplayName": "Western Frisian", - "xhDisplayName": "Xhosa", - "yiDisplayName": "Yiddish", - "yoDisplayName": "Yoruba", - "zaDisplayName": "Zhuang, Chuang", - "unkDisplayName": "Unknown", - "zuDisplayName": "Zulu", - "hawDisplayName": "Hawaiian", - "hmnDisplayName": "Hmong", - "multiDisplayName": "Multi", - "cebDisplayName": "Cebuano", - "dzDisplayName": "Dzongkha", - "iwDisplayName": "Hebrew", - "jwDisplayName": "Javanese", - "moDisplayName": "Moldavian", - "shDisplayName": "Serbo-Croatian", - "wwCountryDisplayName": "World Wide", - "afCountryDisplayName": "Afghanistan", - "axCountryDisplayName": "Aland Islands", - "alCountryDisplayName": "Albania", - "dzCountryDisplayName": "Algeria", - "asCountryDisplayName": "American Samoa", - "adCountryDisplayName": "Andorra", - "aoCountryDisplayName": "Angola", - "aiCountryDisplayName": "Anguilla", - "agCountryDisplayName": "Antigua and Barbuda", - "arCountryDisplayName": "Argentina", - "amCountryDisplayName": "Armenia", - "awCountryDisplayName": "Aruba", - "acCountryDisplayName": "Ascension Island", - "auCountryDisplayName": "Australia", - "atCountryDisplayName": "Austria", - "azCountryDisplayName": "Azerbaijan", - "bsCountryDisplayName": "Bahamas", - "bhCountryDisplayName": "Bahrain", - "bdCountryDisplayName": "Bangladesh", - "bbCountryDisplayName": "Barbados", - "byCountryDisplayName": "Belarus", - "beCountryDisplayName": "Belgium", - "bzCountryDisplayName": "Belize", - "bjCountryDisplayName": "Benin", - "bmCountryDisplayName": "Bermuda", - "btCountryDisplayName": "Bhutan", - "boCountryDisplayName": "Bolivia", - "baCountryDisplayName": "Bosnia and Herzegovina", - "bwCountryDisplayName": "Botswana", - "brCountryDisplayName": "Brazil", - "ioCountryDisplayName": "British Indian Ocean Territory", - "vgCountryDisplayName": "British Virgin Islands", - "bnCountryDisplayName": "Brunei", - "bgCountryDisplayName": "Bulgaria", - "bfCountryDisplayName": "Burkina Faso", - "biCountryDisplayName": "Burundi", - "khCountryDisplayName": "Cambodia", - "cmCountryDisplayName": "Cameroon", - "caCountryDisplayName": "Canada", - "cvCountryDisplayName": "Cape Verde", - "bqCountryDisplayName": "Caribbean Netherlands", - "kyCountryDisplayName": "Cayman Islands", - "cfCountryDisplayName": "Central African Republic", - "tdCountryDisplayName": "Chad", - "clCountryDisplayName": "Chile", - "cnCountryDisplayName": "China", - "cxCountryDisplayName": "Christmas Island", - "ccCountryDisplayName": "Cocos [Keeling] Islands", - "coCountryDisplayName": "Colombia", - "kmCountryDisplayName": "Comoros", - "cdCountryDisplayName": "Democratic Republic Congo", - "cgCountryDisplayName": "Republic of Congo", - "ckCountryDisplayName": "Cook Islands", - "crCountryDisplayName": "Costa Rica", - "ciCountryDisplayName": "Côte d'Ivoire", - "hrCountryDisplayName": "Croatia", - "cuCountryDisplayName": "Cuba", - "cwCountryDisplayName": "Curaçao", - "cyCountryDisplayName": "Cyprus", - "czCountryDisplayName": "Czech Republic", - "dkCountryDisplayName": "Denmark", - "djCountryDisplayName": "Djibouti", - "dmCountryDisplayName": "Dominica", - "doCountryDisplayName": "Dominican Republic", - "tlCountryDisplayName": "East Timor", - "ecCountryDisplayName": "Ecuador", - "egCountryDisplayName": "Egypt", - "svCountryDisplayName": "El Salvador", - "gqCountryDisplayName": "Equatorial Guinea", - "erCountryDisplayName": "Eritrea", - "eeCountryDisplayName": "Estonia", - "szCountryDisplayName": "Eswatini", - "etCountryDisplayName": "Ethiopia", - "fkCountryDisplayName": "Falkland Islands", - "foCountryDisplayName": "Faroe Islands", - "fjCountryDisplayName": "Fiji", - "fiCountryDisplayName": "Finland", - "frCountryDisplayName": "France", - "gfCountryDisplayName": "French Guiana", - "pfCountryDisplayName": "French Polynesia", - "gaCountryDisplayName": "Gabon", - "gmCountryDisplayName": "Gambia", - "geCountryDisplayName": "Georgia", - "deCountryDisplayName": "Germany", - "ghCountryDisplayName": "Ghana", - "giCountryDisplayName": "Gibraltar", - "grCountryDisplayName": "Greece", - "glCountryDisplayName": "Greenland", - "gdCountryDisplayName": "Grenada", - "gpCountryDisplayName": "Guadeloupe", - "guCountryDisplayName": "Guam", - "gtCountryDisplayName": "Guatemala", - "ggCountryDisplayName": "Guernsey", - "gnCountryDisplayName": "Guinea Conakry", - "gwCountryDisplayName": "Guinea-Bissau", - "gyCountryDisplayName": "Guyana", - "htCountryDisplayName": "Haiti", - "hmCountryDisplayName": "Heard Island and McDonald Islands", - "hnCountryDisplayName": "Honduras", - "hkCountryDisplayName": "Hong Kong", - "huCountryDisplayName": "Hungary", - "isCountryDisplayName": "Iceland", - "inCountryDisplayName": "India", - "idCountryDisplayName": "Indonesia", - "irCountryDisplayName": "Iran", - "iqCountryDisplayName": "Iraq", - "ieCountryDisplayName": "Ireland", - "imCountryDisplayName": "Isle of Man", - "ilCountryDisplayName": "Israel", - "itCountryDisplayName": "Italy", - "jmCountryDisplayName": "Jamaica", - "jpCountryDisplayName": "Japan", - "jeCountryDisplayName": "Jersey", - "joCountryDisplayName": "Jordan", - "kzCountryDisplayName": "Kazakhstan", - "keCountryDisplayName": "Kenya", - "kiCountryDisplayName": "Kiribati", - "xkCountryDisplayName": "Kosovo", - "kwCountryDisplayName": "Kuwait", - "kgCountryDisplayName": "Kyrgyzstan", - "laCountryDisplayName": "Laos", - "lvCountryDisplayName": "Latvia", - "lbCountryDisplayName": "Lebanon", - "lsCountryDisplayName": "Lesotho", - "lrCountryDisplayName": "Liberia", - "lyCountryDisplayName": "Libya", - "liCountryDisplayName": "Liechtenstein", - "ltCountryDisplayName": "Lithuania", - "luCountryDisplayName": "Luxembourg", - "moCountryDisplayName": "Macau", - "mkCountryDisplayName": "Macedonia", - "mgCountryDisplayName": "Madagascar", - "mwCountryDisplayName": "Malawi", - "myCountryDisplayName": "Malaysia", - "mvCountryDisplayName": "Maldives", - "mlCountryDisplayName": "Mali", - "mtCountryDisplayName": "Malta", - "mhCountryDisplayName": "Marshall Islands", - "mqCountryDisplayName": "Martinique", - "mrCountryDisplayName": "Mauritania", - "muCountryDisplayName": "Mauritius", - "ytCountryDisplayName": "Mayotte", - "mxCountryDisplayName": "Mexico", - "fmCountryDisplayName": "Micronesia", - "mdCountryDisplayName": "Moldova", - "mcCountryDisplayName": "Monaco", - "mnCountryDisplayName": "Mongolia", - "meCountryDisplayName": "Montenegro", - "msCountryDisplayName": "Montserrat", - "maCountryDisplayName": "Morocco", - "mzCountryDisplayName": "Mozambique", - "mmCountryDisplayName": "Myanmar (Burma)", - "naCountryDisplayName": "Namibia", - "nrCountryDisplayName": "Nauru", - "npCountryDisplayName": "Nepal", - "nlCountryDisplayName": "Netherlands", - "ncCountryDisplayName": "New Caledonia", - "nzCountryDisplayName": "New Zealand", - "niCountryDisplayName": "Nicaragua", - "neCountryDisplayName": "Niger", - "ngCountryDisplayName": "Nigeria", - "nuCountryDisplayName": "Niue", - "nfCountryDisplayName": "Norfolk Island", - "kpCountryDisplayName": "North Korea", - "mpCountryDisplayName": "Northern Mariana Islands", - "noCountryDisplayName": "Norway", - "omCountryDisplayName": "Oman", - "pkCountryDisplayName": "Pakistan", - "pwCountryDisplayName": "Palau", - "psCountryDisplayName": "Palestinian Territories", - "paCountryDisplayName": "Panama", - "pgCountryDisplayName": "Papua New Guinea", - "pyCountryDisplayName": "Paraguay", - "peCountryDisplayName": "Peru", - "phCountryDisplayName": "Philippines", - "plCountryDisplayName": "Poland", - "ptCountryDisplayName": "Portugal", - "prCountryDisplayName": "Puerto Rico", - "qaCountryDisplayName": "Qatar", - "reCountryDisplayName": "Réunion", - "roCountryDisplayName": "Romania", - "ruCountryDisplayName": "Russia", - "rwCountryDisplayName": "Rwanda", - "blCountryDisplayName": "Saint Barthélemy", - "shCountryDisplayName": "Saint Helena", - "knCountryDisplayName": "St. Kitts", - "lcCountryDisplayName": "St. Lucia", - "mfCountryDisplayName": "Saint Martin", - "pmCountryDisplayName": "Saint Pierre and Miquelon", - "vcCountryDisplayName": "St. Vincent", - "wsCountryDisplayName": "Samoa", - "smCountryDisplayName": "San Marino", - "stCountryDisplayName": "São Tomé and Príncipe", - "saCountryDisplayName": "Saudi Arabia", - "snCountryDisplayName": "Senegal", - "rsCountryDisplayName": "Serbia", - "scCountryDisplayName": "Seychelles", - "slCountryDisplayName": "Sierra Leone", - "sgCountryDisplayName": "Singapore", - "sxCountryDisplayName": "Sint Maarten", - "skCountryDisplayName": "Slovakia", - "siCountryDisplayName": "Slovenia", - "sbCountryDisplayName": "Solomon Islands", - "soCountryDisplayName": "Somalia", - "zaCountryDisplayName": "South Africa", - "gsCountryDisplayName": "South Georgia and the South Sandwich Islands", - "krCountryDisplayName": "South Korea", - "ssCountryDisplayName": "South Sudan", - "esCountryDisplayName": "Spain", - "lkCountryDisplayName": "Sri Lanka", - "sdCountryDisplayName": "Sudan", - "srCountryDisplayName": "Suriname", - "sjCountryDisplayName": "Svalbard and Jan Mayen", - "seCountryDisplayName": "Sweden", - "chCountryDisplayName": "Switzerland", - "syCountryDisplayName": "Syria", - "twCountryDisplayName": "Taiwan", - "tjCountryDisplayName": "Tajikistan", - "tzCountryDisplayName": "Tanzania", - "thCountryDisplayName": "Thailand", - "tgCountryDisplayName": "Togo", - "tkCountryDisplayName": "Tokelau", - "toCountryDisplayName": "Tonga", - "ttCountryDisplayName": "Trinidad/Tobago", - "tnCountryDisplayName": "Tunisia", - "trCountryDisplayName": "Turkey", - "tmCountryDisplayName": "Turkmenistan", - "tcCountryDisplayName": "Turks and Caicos Islands", - "tvCountryDisplayName": "Tuvalu", - "viCountryDisplayName": "U.S. Virgin Islands", - "ugCountryDisplayName": "Uganda", - "uaCountryDisplayName": "Ukraine", - "aeCountryDisplayName": "United Arab Emirates", - "gbCountryDisplayName": "United Kingdom", - "usCountryDisplayName": "United States", - "uyCountryDisplayName": "Uruguay", - "uzCountryDisplayName": "Uzbekistan", - "vuCountryDisplayName": "Vanuatu", - "vaCountryDisplayName": "Vatican City", - "veCountryDisplayName": "Venezuela", - "vnCountryDisplayName": "Vietnam", - "wfCountryDisplayName": "Wallis and Futuna", - "ehCountryDisplayName": "Western Sahara", - "yeCountryDisplayName": "Yemen", - "zmCountryDisplayName": "Zambia", - "zwCountryDisplayName": "Zimbabwe", - "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": { - "placeholders": { - "classOrExchange": {}, - "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.", - "noEligibleSpaces": "There are no eligible spaces to add this to.", - "youAddedToSpace": "You added {child} to {space}", - "@youAddedToSpace": { - "placeholders": { - "child": {}, - "space": {} - } - }, - "youRemovedFromSpace": "You removed {child} from {space}", - "@youRemovedFromSpace": { - "placeholders": { - "child": {}, - "space": {} - } - }, - "invitedToChat": "{user} has invited you to join a chat: {name}! Do you wish to accept?", - "@invitedToChat": { - "placeholders": { - "name": {}, - "user": {} - } - }, - "monthlySubscription": "Monthly", - "yearlySubscription": "Yearly", - "defaultSubscription": "Pangea Chat Subscription", - "freeTrial": "Free Trial", - "grammarAnalytics": "Error Analytics", - "total": "Total: ", - "noDataFound": "No data found", - "promoSubscriptionExpirationDesc": "Your current subscription is promotional and expires on {expiration}. Message support@pangea.chat for help changing your subscription.", - "@promoSubscriptionExpirationDesc": { - "placeholders": { - "expiration": {} - } - }, - "emptyChatNameWarning": "Please enter a name for this chat", - "emptyClassNameWarning": "Please enter a name for this class", - "emptyExchangeNameWarning": "Please enter a name for this exchange", - "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.", - "someErrorTitle": "Hm, something's not right", - "someErrorBody": "It could be an error or something in your base language.", - "bestCorrectionFeedback": "That's correct!", - "distractorFeedback": "That's not quite right.", - "bestAnswerFeedback": "That's correct!", - "definitionDefaultPrompt": "What does this word mean?", - "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", - "lockedChatWarning": "🔒 This chat has been locked", - "lockSpace": "Lock Space", - "lockChat": "Lock Chat", - "archiveSpace": "Archive Space", - "suggestToChat": "Suggest this chat", - "suggestToChatDesc": "Suggested chats will appear in chat lists", - "acceptSelection": "Accept Correction", - "acceptSelectionAnyway": "Use this anyway", - "makingActivity": "Making activity", - "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": { - "placeholders": { - "reportingUserId": {}, - "reportedUserId": {}, - "roomName": {} - } - }, - "reportMessageBody": "Message: {reportedMessage}\nReason: {reason}", - "@reportMessageBody": { - "placeholders": { - "reportedMessage": {}, - "reason": {} - } - }, - "noTeachersFound": "No teachers found to report to", - "pleaseEnterANumber": "Please enter a number greater than 0", - "archiveRoomDescription": "The chat will be moved to the archive for yourself and other non-admin users.", - "roomUpgradeDescription": "The chat will then be recreated with the new room version. All participants will be notified that they need to switch to the new chat. You can find out more about room versions at https://spec.matrix.org/latest/rooms/", - "removeDevicesDescription": "You will be logged out of this device and will no longer be able to receive messages.", - "banUserDescription": "The user will be banned from the chat and will not be able to enter the chat again until they are unbanned.", - "unbanUserDescription": "The user will be able to enter the chat again if they try.", - "kickUserDescription": "The user is kicked out of the chat but not banned. In public chats, the user can rejoin at any time.", - "makeAdminDescription": "Once you make this user admin, you may not be able to undo this as they will then have the same permissions as you.", - "pushNotificationsNotAvailable": "Push notifications not available", - "learnMore": "Learn more", - "yourGlobalUserIdIs": "Your global user-ID is: ", - "noUsersFoundWithQuery": "Unfortunately no user could be found with \"{query}\". Please check whether you made a typo.", - "@noUsersFoundWithQuery": { - "type": "text", - "placeholders": { - "query": {} - } - }, - "searchChatsRooms": "Search for #chats, @users...", - "createClass": "Create class", - "createExchange": "Create exchange", - "viewArchive": "View Archive", - "trialExpiration": "Your free trial expires on {expiration}", - "@trialExpiration": { - "placeholders": { - "expiration": {} - } - }, - "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", - "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.", - "errorGettingAudio": "Error getting audio. Please refresh and try again.", - "nothingFound": "Nothing found...", - "groupName": "Group name", - "createGroupAndInviteUsers": "Create a group and invite users", - "groupCanBeFoundViaSearch": "Group can be found via search", - "wrongRecoveryKey": "Sorry... this does not seem to be the correct recovery key.", - "startConversation": "Start conversation", - "commandHint_sendraw": "Send raw json", - "databaseMigrationTitle": "Database is optimized", - "databaseMigrationBody": "Please wait. This may take a moment.", - "leaveEmptyToClearStatus": "Leave empty to clear your status.", - "select": "Select", - "searchForUsers": "Search for @users...", - "pleaseEnterYourCurrentPassword": "Please enter your current password", - "newPassword": "New password", - "pleaseChooseAStrongPassword": "Please choose a strong password", - "passwordsDoNotMatch": "Passwords do not match", - "passwordIsWrong": "Your entered password is wrong", - "publicLink": "Public link", - "joinSpace": "Join space", - "publicSpaces": "Public spaces", - "addChatOrSubSpace": "Add chat or sub space", - "subspace": "Subspace", - "decline": "Decline", - "thisDevice": "This device:", - "initAppError": "An error occured while init the app", - "databaseBuildErrorBody": "Unable to build the SQlite database. The app tries to use the legacy database for now. Please report this error to the developers at {url}. The error message is: {error}", - "@databaseBuildErrorBody": { - "type": "text", - "placeholders": { - "url": {}, - "error": {} - } - }, - "sessionLostBody": "Your session is lost. Please report this error to the developers at {url}. The error message is: {error}", - "@sessionLostBody": { - "type": "text", - "placeholders": { - "url": {}, - "error": {} - } - }, - "restoreSessionBody": "The app now tries to restore your session from the backup. Please report this error to the developers at {url}. The error message is: {error}", - "@restoreSessionBody": { - "type": "text", - "placeholders": { - "url": {}, - "error": {} - } - }, - "forwardMessageTo": "Forward message to {roomName}?", - "@forwardMessageTo": { - "type": "text", - "placeholders": { - "roomName": {} - } - }, - "signUp": "Sign up", - "pleaseChooseAtLeastChars": "Please choose at least {min} characters.", - "@pleaseChooseAtLeastChars": { - "type": "text", - "placeholders": { - "min": {} - } - }, - "noEmailWarning": "Please enter a valid email address. Otherwise you won't be able to reset your password. If you don't want to, tap again on the button to continue.", - "pleaseEnterValidEmail": "Please enter a valid email address.", - "noAddToSpacePermissions": "You can't add a chat to this space", - "alreadyInSpace": "The chat is already in this space", - "pleaseChooseAUsername": "Please choose a username", - "@pleaseChooseAUsername": { - "type": "text", - "placeholders": {} - }, - "chooseAUsername": "Choose a username", - "@chooseAUsername": { - "type": "text", - "placeholders": {} - }, - "define": "Define", - "listen": "Listen", - "addConversationBot": "Enable Conversation Bot", - "addConversationBotDesc": "Add a bot to this group chat that will ask questions on a specific topic", - "convoBotSettingsTitle": "Conversation Bot Settings", - "convoBotSettingsDescription": "Edit conversation topic and difficulty", - "enterAConversationTopic": "Enter a conversation topic", - "conversationTopic": "Conversation topic", - "enableModeration": "Enable moderation", - "enableModerationDesc": "Enable automatic moderation to review messages before they are sent", - "conversationLanguageLevel": "What is the language level of this conversation?", - "showDefinition": "Show Definition", - "sendReadReceipts": "Send read receipts", - "sendTypingNotificationsDescription": "Other participants in a chat can see when you are typing a new message.", - "sendReadReceiptsDescription": "Other participants in a chat can see when you have read a message.", - "formattedMessages": "Formatted messages", - "formattedMessagesDescription": "Display rich message content like bold text using markdown.", - "verifyOtherUser": "🔐 Verify other user", - "verifyOtherUserDescription": "If you verify another user, you can be sure that you know who you are really writing to. 💪\n\nWhen you start a verification, you and the other user will see a popup in the app. There you will then see a series of emojis or numbers that you have to compare with each other.\n\nThe best way to do this is to meet up or start a video call. 👭", - "verifyOtherDevice": "🔐 Verify other device", - "verifyOtherDeviceDescription": "When you verify another device, those devices can exchange keys, increasing your overall security. 💪 When you start a verification, a popup will appear in the app on both devices. There you will then see a series of emojis or numbers that you have to compare with each other. It's best to have both devices handy before you start the verification. 🤳", - "acceptedKeyVerification": "{sender} accepted key verification", - "@acceptedKeyVerification": { - "type": "text", - "placeholders": { - "sender": {} - } - }, - "canceledKeyVerification": "{sender} canceled key verification", - "@canceledKeyVerification": { - "type": "text", - "placeholders": { - "sender": {} - } - }, - "completedKeyVerification": "{sender} completed key verification", - "@completedKeyVerification": { - "type": "text", - "placeholders": { - "sender": {} - } - }, - "isReadyForKeyVerification": "{sender} is ready for key verification", - "@isReadyForKeyVerification": { - "type": "text", - "placeholders": { - "sender": {} - } - }, - "requestedKeyVerification": "{sender} requested key verification", - "@requestedKeyVerification": { - "type": "text", - "placeholders": { - "sender": {} - } - }, - "startedKeyVerification": "{sender} started key verification", - "@startedKeyVerification": { - "type": "text", - "placeholders": { - "sender": {} - } - }, - "subscriptionPopupTitle": "This sentence could have a grammar mistake...", - "subscriptionPopupDesc": "Subscribe today to unlock translation and grammar correction!", - "seeOptions": "See options", - "continuedWithoutSubscription": "Continue without subscribing", - "trialPeriodExpired": "Your trial period has expired", - "selectToDefine": "Double click a word to see its definition!", - "translations": "translations", - "messageAudio": "message audio", - "definitions": "definitions", - "subscribedToUnlockTools": "Subscribe to unlock language tools, including", - "more": "More", - "translationTooltip": "Translate", - "audioTooltip": "Play Audio", - "speechToTextTooltip": "Transcript", - "certifyAge": "I certify that I am over {age} years of age", - "@certifyAge": { - "type": "text", - "placeholders": { - "age": {} - } - }, - "kickBotWarning": "Kicking Pangea Bot will remove the conversation bot from this chat.", - "joinToView": "Join this room to view details", - "refresh": "Refresh", - "autoPlayTitle": "Auto Play Messages", - "autoPlayDesc": "When enabled, the text-to-speech audio of messages will play automatically when selected.", - "transparent": "Transparent", - "incomingMessages": "Incoming messages", - "stickers": "Stickers", - "discover": "Discover", - "commandHint_ignore": "Ignore the given matrix ID", - "commandHint_unignore": "Unignore the given matrix ID", - "unreadChatsInApp": "{appname}: {unread} unread chats", - "@unreadChatsInApp": { - "type": "text", - "placeholders": { - "appname": {}, - "unread": {} - } - }, - "messageAnalytics": "Message Analytics", - "words": "Words", - "score": "Score", - "accuracy": "Accuracy", - "points": "Points", - "noPaymentInfo": "No payment info necessary!", - "conversationBotModeSelectDescription": "Bot mode", - "conversationBotModeSelectOption_discussion": "Discussion", - "conversationBotModeSelectOption_custom": "Custom", - "conversationBotModeSelectOption_conversation": "Conversation", - "conversationBotModeSelectOption_textAdventure": "Text Adventure", - "conversationBotDiscussionZone_title": "Discussion Settings", - "conversationBotDiscussionZone_discussionTopicLabel": "Discussion Topic", - "conversationBotDiscussionZone_discussionTopicPlaceholder": "Set Discussion Topic", - "conversationBotDiscussionZone_discussionKeywordsLabel": "Discussion Keywords", - "conversationBotDiscussionZone_discussionKeywordsPlaceholder": "Set Discussion Keywords", - "conversationBotDiscussionZone_discussionKeywordsHintText": "Comma separated list of keywords to guide the discussion", - "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel": "Send discussion prompt on a schedule", - "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel": "Hours between discussion prompts", - "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel": "Send discussion prompt when user reacts ⏩ to bot message", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel": "Reaction to send discussion prompt", - "studentAnalyticsNotAvailable": "Student data not currently available", - "roomDataMissing": "Some data may be missing from rooms in which you are not a member.", - "updatePhoneOS": "You may need to update your device's OS version.", - "wordsPerMinute": "Words per minute", - "autoIGCToolName": "Run Language Assistance Automatically", - "autoIGCToolDescription": "Automatically run language assistance after typing messages", - "runGrammarCorrection": "Run grammar correction", - "grammarCorrectionFailed": "Issues to address", - "grammarCorrectionComplete": "Grammar correction complete", - "leaveRoomDescription": "The chat will be moved to the archive. Other users will be able to see that you have left the chat.", - "archiveSpaceDescription": "All chats within this space will be moved to the archive for yourself and other non-admin users.", - "leaveSpaceDescription": "All chats within this space will be moved to the archive. Other users will be able to see that you have left the space.", - "onlyAdminDescription": "Since there are no other admins, all other participants will also be removed.", - "tooltipInstructionsTitle": "Not sure what that does?", - "tooltipInstructionsMobileBody": "Press and hold items to view tooltips.", - "tooltipInstructionsBrowserBody": "Hover over items to view tooltips." + "@@locale": "en", + "@@last_modified": "2021-08-14 12:38:37.885451", + "repeatPassword": "Repeat password", + "@repeatPassword": {}, + "notAnImage": "Not an image file.", + "remove": "Remove", + "importNow": "Import now", + "importEmojis": "Import Emojis", + "importFromZipFile": "Import from .zip file", + "exportEmotePack": "Export Emote pack as .zip", + "replace": "Replace", + "about": "About", + "@about": { + "type": "text", + "placeholders": {} + }, + "accept": "Accept", + "@accept": { + "type": "text", + "placeholders": {} + }, + "acceptedTheInvitation": "👍 {username} accepted the invitation", + "@acceptedTheInvitation": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "account": "Account", + "@account": { + "type": "text", + "placeholders": {} + }, + "accountInformation": "Account information", + "@accountInformation": { + "type": "text", + "placeholders": {} + }, + "activatedEndToEndEncryption": "🔐 {username} activated end to end encryption", + "@activatedEndToEndEncryption": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "addEmail": "Add email", + "@addEmail": { + "type": "text", + "placeholders": {} + }, + "confirmMatrixId": "Please confirm your Matrix ID in order to delete your account.", + "@confirmMatrixId": {}, + "supposedMxid": "This should be {mxid}", + "@supposedMxid": { + "type": "text", + "placeholders": { + "mxid": {} + } + }, + "addGroupDescription": "Add a chat description", + "@addGroupDescription": { + "type": "text", + "placeholders": {} + }, + "addNewFriend": "Add new friend", + "@addNewFriend": { + "type": "text", + "placeholders": {} + }, + "addToSpace": "Add to space", + "@addToSpace": {}, + "admin": "Admin", + "@admin": { + "type": "text", + "placeholders": {} + }, + "alias": "alias", + "@alias": { + "type": "text", + "placeholders": {} + }, + "all": "All", + "@all": { + "type": "text", + "placeholders": {} + }, + "allChats": "All chats", + "@allChats": { + "type": "text", + "placeholders": {} + }, + "alreadyHaveAnAccount": "Already have an account?", + "@alreadyHaveAnAccount": { + "type": "text", + "placeholders": {} + }, + "commandHint_googly": "Send some googly eyes", + "commandHint_cuddle": "Send a cuddle", + "commandHint_hug": "Send a hug", + "googlyEyesContent": "{senderName} sends you googly eyes", + "@googlyEyesContent": { + "type": "text", + "placeholders": { + "senderName": {} + } + }, + "cuddleContent": "{senderName} cuddles you", + "@cuddleContent": { + "type": "text", + "placeholders": { + "senderName": {} + } + }, + "hugContent": "{senderName} hugs you", + "@hugContent": { + "type": "text", + "placeholders": { + "senderName": {} + } + }, + "answeredTheCall": "{senderName} answered the call", + "@answeredTheCall": { + "type": "text", + "placeholders": { + "senderName": {} + } + }, + "anyoneCanJoin": "Anyone can join", + "@anyoneCanJoin": { + "type": "text", + "placeholders": {} + }, + "appLock": "App lock", + "@appLock": { + "type": "text", + "placeholders": {} + }, + "archive": "Archive", + "@archive": { + "type": "text", + "placeholders": {} + }, + "areGuestsAllowedToJoin": "Are guest users allowed to join", + "@areGuestsAllowedToJoin": { + "type": "text", + "placeholders": {} + }, + "areYouSure": "Are you sure?", + "@areYouSure": { + "type": "text", + "placeholders": {} + }, + "areYouSureYouWantToLogout": "Are you sure you want to log out?", + "@areYouSureYouWantToLogout": { + "type": "text", + "placeholders": {} + }, + "askSSSSSign": "To be able to sign the other person, please enter your secure store passphrase or recovery key.", + "@askSSSSSign": { + "type": "text", + "placeholders": {} + }, + "askVerificationRequest": "Accept this verification request from {username}?", + "@askVerificationRequest": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "autoplayImages": "Automatically play animated stickers and emotes", + "@autoplayImages": { + "type": "text", + "placeholder": {} + }, + "badServerLoginTypesException": "The homeserver supports the login types:\n{serverVersions}\nBut this app supports only:\n{supportedVersions}", + "@badServerLoginTypesException": { + "type": "text", + "placeholders": { + "serverVersions": {}, + "supportedVersions": {} + } + }, + "sendTypingNotifications": "Send typing notifications", + "@sendTypingNotifications": {}, + "sendOnEnter": "Always send on enter", + "@sendOnEnter": {}, + "badServerVersionsException": "The homeserver supports the Spec versions:\n{serverVersions}\nBut this app supports only {supportedVersions}", + "@badServerVersionsException": { + "type": "text", + "placeholders": { + "serverVersions": {}, + "supportedVersions": {} + } + }, + "banFromChat": "Ban from chat", + "@banFromChat": { + "type": "text", + "placeholders": {} + }, + "banned": "Banned", + "@banned": { + "type": "text", + "placeholders": {} + }, + "bannedUser": "{username} banned {targetName}", + "@bannedUser": { + "type": "text", + "placeholders": { + "username": {}, + "targetName": {} + } + }, + "blockDevice": "Block Device", + "@blockDevice": { + "type": "text", + "placeholders": {} + }, + "blocked": "Blocked", + "@blocked": { + "type": "text", + "placeholders": {} + }, + "botMessages": "Bot messages", + "@botMessages": { + "type": "text", + "placeholders": {} + }, + "cancel": "Cancel", + "@cancel": { + "type": "text", + "placeholders": {} + }, + "cantOpenUri": "Can't open the URI {uri}", + "@cantOpenUri": { + "type": "text", + "placeholders": { + "uri": {} + } + }, + "changeDeviceName": "Change device name", + "@changeDeviceName": { + "type": "text", + "placeholders": {} + }, + "changedTheChatAvatar": "{username} changed the chat avatar", + "@changedTheChatAvatar": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "changedTheChatDescriptionTo": "{username} changed the chat description to: '{description}'", + "@changedTheChatDescriptionTo": { + "type": "text", + "placeholders": { + "username": {}, + "description": {} + } + }, + "changedTheChatNameTo": "{username} changed the chat name to: '{chatname}'", + "@changedTheChatNameTo": { + "type": "text", + "placeholders": { + "username": {}, + "chatname": {} + } + }, + "changedTheChatPermissions": "{username} changed the chat permissions", + "@changedTheChatPermissions": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "changedTheDisplaynameTo": "{username} changed their displayname to: '{displayname}'", + "@changedTheDisplaynameTo": { + "type": "text", + "placeholders": { + "username": {}, + "displayname": {} + } + }, + "changedTheGuestAccessRules": "{username} changed the guest access rules", + "@changedTheGuestAccessRules": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "changedTheGuestAccessRulesTo": "{username} changed the guest access rules to: {rules}", + "@changedTheGuestAccessRulesTo": { + "type": "text", + "placeholders": { + "username": {}, + "rules": {} + } + }, + "changedTheHistoryVisibility": "{username} changed the history visibility", + "@changedTheHistoryVisibility": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "changedTheHistoryVisibilityTo": "{username} changed the history visibility to: {rules}", + "@changedTheHistoryVisibilityTo": { + "type": "text", + "placeholders": { + "username": {}, + "rules": {} + } + }, + "changedTheJoinRules": "{username} changed the join rules", + "@changedTheJoinRules": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "changedTheJoinRulesTo": "{username} changed the join rules to: {joinRules}", + "@changedTheJoinRulesTo": { + "type": "text", + "placeholders": { + "username": {}, + "joinRules": {} + } + }, + "changedTheProfileAvatar": "{username} changed their avatar", + "@changedTheProfileAvatar": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "changedTheRoomAliases": "{username} changed the room aliases", + "@changedTheRoomAliases": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "changedTheRoomInvitationLink": "{username} changed the invitation link", + "@changedTheRoomInvitationLink": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "changePassword": "Change password", + "@changePassword": { + "type": "text", + "placeholders": {} + }, + "changeTheHomeserver": "Change the homeserver", + "@changeTheHomeserver": { + "type": "text", + "placeholders": {} + }, + "changeTheme": "Change your style", + "@changeTheme": { + "type": "text", + "placeholders": {} + }, + "changeTheNameOfTheGroup": "Change the name of the chat", + "@changeTheNameOfTheGroup": { + "type": "text", + "placeholders": {} + }, + "changeYourAvatar": "Change your avatar", + "@changeYourAvatar": { + "type": "text", + "placeholders": {} + }, + "channelCorruptedDecryptError": "The encryption has been corrupted", + "@channelCorruptedDecryptError": { + "type": "text", + "placeholders": {} + }, + "chat": "Chat", + "@chat": { + "type": "text", + "placeholders": {} + }, + "yourChatBackupHasBeenSetUp": "Your chat backup has been set up.", + "@yourChatBackupHasBeenSetUp": {}, + "chatBackup": "Chat backup", + "@chatBackup": { + "type": "text", + "placeholders": {} + }, + "chatBackupDescription": "Your old messages are secured with a recovery key. Please make sure you don't lose it.", + "@chatBackupDescription": { + "type": "text", + "placeholders": {} + }, + "chatDetails": "Chat details", + "@chatDetails": { + "type": "text", + "placeholders": {} + }, + "chatHasBeenAddedToThisSpace": "Chat has been added to this space", + "@chatHasBeenAddedToThisSpace": {}, + "chats": "Group Chats", + "@chats": { + "type": "text", + "placeholders": {} + }, + "classes": "Classes", + "chooseAStrongPassword": "Choose a strong password", + "@chooseAStrongPassword": { + "type": "text", + "placeholders": {} + }, + "clearArchive": "Clear archive", + "@clearArchive": {}, + "close": "Close", + "@close": { + "type": "text", + "placeholders": {} + }, + "commandHint_markasdm": "Mark as direct message room for the giving Matrix ID", + "@commandHint_markasdm": {}, + "commandHint_markasgroup": "Mark as group", + "@commandHint_markasgroup": {}, + "commandHint_ban": "Ban the given user from this room", + "@commandHint_ban": { + "type": "text", + "description": "Usage hint for the command /ban" + }, + "commandHint_clearcache": "Clear cache", + "@commandHint_clearcache": { + "type": "text", + "description": "Usage hint for the command /clearcache" + }, + "commandHint_create": "Create an empty group chat\nUse --no-encryption to disable encryption", + "@commandHint_create": { + "type": "text", + "description": "Usage hint for the command /create" + }, + "commandHint_discardsession": "Discard session", + "@commandHint_discardsession": { + "type": "text", + "description": "Usage hint for the command /discardsession" + }, + "commandHint_dm": "Start a direct chat\nUse --no-encryption to disable encryption", + "@commandHint_dm": { + "type": "text", + "description": "Usage hint for the command /dm" + }, + "commandHint_html": "Send HTML-formatted text", + "@commandHint_html": { + "type": "text", + "description": "Usage hint for the command /html" + }, + "commandHint_invite": "Invite the given user to this room", + "@commandHint_invite": { + "type": "text", + "description": "Usage hint for the command /invite" + }, + "commandHint_join": "Join the given room", + "@commandHint_join": { + "type": "text", + "description": "Usage hint for the command /join" + }, + "commandHint_kick": "Remove the given user from this room", + "@commandHint_kick": { + "type": "text", + "description": "Usage hint for the command /kick" + }, + "commandHint_leave": "Leave this room", + "@commandHint_leave": { + "type": "text", + "description": "Usage hint for the command /leave" + }, + "commandHint_me": "Describe yourself", + "@commandHint_me": { + "type": "text", + "description": "Usage hint for the command /me" + }, + "commandHint_myroomavatar": "Set your picture for this room (by mxc-uri)", + "@commandHint_myroomavatar": { + "type": "text", + "description": "Usage hint for the command /myroomavatar" + }, + "commandHint_myroomnick": "Set your display name for this room", + "@commandHint_myroomnick": { + "type": "text", + "description": "Usage hint for the command /myroomnick" + }, + "commandHint_op": "Set the given user's power level (default: 50)", + "@commandHint_op": { + "type": "text", + "description": "Usage hint for the command /op" + }, + "commandHint_plain": "Send unformatted text", + "@commandHint_plain": { + "type": "text", + "description": "Usage hint for the command /plain" + }, + "commandHint_react": "Send reply as a reaction", + "@commandHint_react": { + "type": "text", + "description": "Usage hint for the command /react" + }, + "commandHint_send": "Send text", + "@commandHint_send": { + "type": "text", + "description": "Usage hint for the command /send" + }, + "commandHint_unban": "Unban the given user from this room", + "@commandHint_unban": { + "type": "text", + "description": "Usage hint for the command /unban" + }, + "commandInvalid": "Command invalid", + "@commandInvalid": { + "type": "text" + }, + "commandMissing": "{command} is not a command.", + "@commandMissing": { + "type": "text", + "placeholders": { + "command": {} + }, + "description": "State that {command} is not a valid /command." + }, + "compareEmojiMatch": "Please compare the emojis", + "@compareEmojiMatch": { + "type": "text", + "placeholders": {} + }, + "compareNumbersMatch": "Please compare the numbers", + "@compareNumbersMatch": { + "type": "text", + "placeholders": {} + }, + "configureChat": "Configure chat", + "@configureChat": { + "type": "text", + "placeholders": {} + }, + "confirm": "Confirm", + "@confirm": { + "type": "text", + "placeholders": {} + }, + "connect": "Start", + "@connect": { + "type": "text", + "placeholders": {} + }, + "contactHasBeenInvitedToTheGroup": "Contact has been invited to the group", + "@contactHasBeenInvitedToTheGroup": { + "type": "text", + "placeholders": {} + }, + "containsDisplayName": "Contains display name", + "@containsDisplayName": { + "type": "text", + "placeholders": {} + }, + "containsUserName": "Contains username", + "@containsUserName": { + "type": "text", + "placeholders": {} + }, + "contentHasBeenReported": "The content has been reported", + "@contentHasBeenReported": { + "type": "text", + "placeholders": {} + }, + "copiedToClipboard": "Copied to clipboard", + "@copiedToClipboard": { + "type": "text", + "placeholders": {} + }, + "copy": "Copy", + "@copy": { + "type": "text", + "placeholders": {} + }, + "copyToClipboard": "Copy to clipboard", + "@copyToClipboard": { + "type": "text", + "placeholders": {} + }, + "couldNotDecryptMessage": "Could not decrypt message: {error}", + "@couldNotDecryptMessage": { + "type": "text", + "placeholders": { + "error": {} + } + }, + "countParticipants": "{count} participants", + "@countParticipants": { + "type": "text", + "placeholders": { + "count": {} + } + }, + "create": "Create", + "@create": { + "type": "text", + "placeholders": {} + }, + "createdTheChat": "💬 {username} created the chat", + "@createdTheChat": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "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", + "placeholders": {} + }, + "darkTheme": "Dark", + "@darkTheme": { + "type": "text", + "placeholders": {} + }, + "dateAndTimeOfDay": "{date}, {timeOfDay}", + "@dateAndTimeOfDay": { + "type": "text", + "placeholders": { + "date": {}, + "timeOfDay": {} + } + }, + "dateWithoutYear": "{month}-{day}", + "@dateWithoutYear": { + "type": "text", + "placeholders": { + "month": {}, + "day": {} + } + }, + "dateWithYear": "{year}-{month}-{day}", + "@dateWithYear": { + "type": "text", + "placeholders": { + "year": {}, + "month": {}, + "day": {} + } + }, + "deactivateAccountWarning": "This will deactivate your user account. This can not be undone! Are you sure?", + "@deactivateAccountWarning": { + "type": "text", + "placeholders": {} + }, + "defaultPermissionLevel": "Default permission level", + "@defaultPermissionLevel": { + "type": "text", + "placeholders": {} + }, + "delete": "Delete", + "@delete": { + "type": "text", + "placeholders": {} + }, + "deleteAccount": "Delete account", + "@deleteAccount": { + "type": "text", + "placeholders": {} + }, + "deleteMessage": "Delete message", + "@deleteMessage": { + "type": "text", + "placeholders": {} + }, + "device": "Device", + "@device": { + "type": "text", + "placeholders": {} + }, + "deviceId": "Device ID", + "@deviceId": { + "type": "text", + "placeholders": {} + }, + "devices": "Devices", + "@devices": { + "type": "text", + "placeholders": {} + }, + "directChats": "Direct Chats", + "@directChats": { + "type": "text", + "placeholders": {} + }, + "allRooms": "All Group Chats", + "@allRooms": { + "type": "text", + "placeholders": {} + }, + "displaynameHasBeenChanged": "Displayname has been changed", + "@displaynameHasBeenChanged": { + "type": "text", + "placeholders": {} + }, + "downloadFile": "Download file", + "@downloadFile": { + "type": "text", + "placeholders": {} + }, + "edit": "Edit", + "@edit": { + "type": "text", + "placeholders": {} + }, + "editBlockedServers": "Edit blocked servers", + "@editBlockedServers": { + "type": "text", + "placeholders": {} + }, + "chatPermissions": "Chat permissions", + "editChatPermissions": "Edit chat permissions", + "@editChatPermissions": { + "type": "text", + "placeholders": {} + }, + "editDisplayname": "Edit displayname", + "@editDisplayname": { + "type": "text", + "placeholders": {} + }, + "editRoomAliases": "Edit room aliases", + "@editRoomAliases": { + "type": "text", + "placeholders": {} + }, + "editRoomAvatar": "Edit room avatar", + "@editRoomAvatar": { + "type": "text", + "placeholders": {} + }, + "emoteExists": "Emote already exists!", + "@emoteExists": { + "type": "text", + "placeholders": {} + }, + "emoteInvalid": "Invalid emote shortcode!", + "@emoteInvalid": { + "type": "text", + "placeholders": {} + }, + "emoteKeyboardNoRecents": "Recently-used emotes will appear here...", + "@emoteKeyboardNoRecents": { + "type": "text", + "placeholders": {} + }, + "emotePacks": "Emote packs for room", + "@emotePacks": { + "type": "text", + "placeholders": {} + }, + "emoteSettings": "Emote Settings", + "@emoteSettings": { + "type": "text", + "placeholders": {} + }, + "emoteShortcode": "Emote shortcode", + "@emoteShortcode": { + "type": "text", + "placeholders": {} + }, + "emoteWarnNeedToPick": "You need to pick an emote shortcode and an image!", + "@emoteWarnNeedToPick": { + "type": "text", + "placeholders": {} + }, + "emptyChat": "Empty chat", + "@emptyChat": { + "type": "text", + "placeholders": {} + }, + "enableEmotesGlobally": "Enable emote pack globally", + "@enableEmotesGlobally": { + "type": "text", + "placeholders": {} + }, + "enableEncryption": "Enable encryption", + "@enableEncryption": { + "type": "text", + "placeholders": {} + }, + "enableEncryptionWarning": "You won't be able to disable the encryption anymore. Are you sure?", + "@enableEncryptionWarning": { + "type": "text", + "placeholders": {} + }, + "encrypted": "Encrypted", + "@encrypted": { + "type": "text", + "placeholders": {} + }, + "encryption": "Encryption", + "@encryption": { + "type": "text", + "placeholders": {} + }, + "encryptionNotEnabled": "Encryption is not enabled", + "@encryptionNotEnabled": { + "type": "text", + "placeholders": {} + }, + "endedTheCall": "{senderName} ended the call", + "@endedTheCall": { + "type": "text", + "placeholders": { + "senderName": {} + } + }, + "enterAGroupName": "Enter a chat name", + "@enterAGroupName": { + "type": "text", + "placeholders": {} + }, + "enterAnEmailAddress": "Enter an email address", + "@enterAnEmailAddress": { + "type": "text", + "placeholders": {} + }, + "enterASpacepName": "Enter a name", + "@enterASpacepName": {}, + "homeserver": "Homeserver", + "@homeserver": {}, + "enterYourHomeserver": "Enter your homeserver", + "@enterYourHomeserver": { + "type": "text", + "placeholders": {} + }, + "errorObtainingLocation": "Error obtaining location: {error}", + "@errorObtainingLocation": { + "type": "text", + "placeholders": { + "error": {} + } + }, + "everythingReady": "Everything ready!", + "@everythingReady": { + "type": "text", + "placeholders": {} + }, + "extremeOffensive": "Extremely offensive", + "@extremeOffensive": { + "type": "text", + "placeholders": {} + }, + "fileName": "File name", + "@fileName": { + "type": "text", + "placeholders": {} + }, + "fluffychat": "FluffyChat", + "@fluffychat": { + "type": "text", + "placeholders": {} + }, + "fontSize": "Font size", + "@fontSize": { + "type": "text", + "placeholders": {} + }, + "forward": "Forward", + "@forward": { + "type": "text", + "placeholders": {} + }, + "fromJoining": "From joining", + "@fromJoining": { + "type": "text", + "placeholders": {} + }, + "fromTheInvitation": "From the invitation", + "@fromTheInvitation": { + "type": "text", + "placeholders": {} + }, + "goToTheNewRoom": "Go to the new room", + "@goToTheNewRoom": { + "type": "text", + "placeholders": {} + }, + "group": "Chat", + "@group": { + "type": "text", + "placeholders": {} + }, + "chatDescription": "Chat description", + "chatDescriptionHasBeenChanged": "Chat description changed", + "groupIsPublic": "Group is public", + "groupDescription": "Chat description", + "@groupDescription": { + "type": "text", + "placeholders": {} + }, + "groupDescriptionHasBeenChanged": "Chat description changed", + "@groupDescriptionHasBeenChanged": { + "type": "text", + "placeholders": {} + }, + "@groupIsPublic": { + "type": "text", + "placeholders": {} + }, + "groups": "Chats", + "@groups": { + "type": "text", + "placeholders": {} + }, + "groupWith": "Chat with {displayname}", + "@groupWith": { + "type": "text", + "placeholders": { + "displayname": {} + } + }, + "guestsAreForbidden": "Guests are forbidden", + "@guestsAreForbidden": { + "type": "text", + "placeholders": {} + }, + "guestsCanJoin": "Guests can join", + "@guestsCanJoin": { + "type": "text", + "placeholders": {} + }, + "hasWithdrawnTheInvitationFor": "{username} has withdrawn the invitation for {targetName}", + "@hasWithdrawnTheInvitationFor": { + "type": "text", + "placeholders": { + "username": {}, + "targetName": {} + } + }, + "help": "Help", + "@help": { + "type": "text", + "placeholders": {} + }, + "hideRedactedEvents": "Hide redacted events", + "@hideRedactedEvents": { + "type": "text", + "placeholders": {} + }, + "hideUnknownEvents": "Hide unknown events", + "@hideUnknownEvents": { + "type": "text", + "placeholders": {} + }, + "howOffensiveIsThisContent": "How offensive is this content?", + "@howOffensiveIsThisContent": { + "type": "text", + "placeholders": {} + }, + "id": "ID", + "@id": { + "type": "text", + "placeholders": {} + }, + "identity": "Identity", + "@identity": { + "type": "text", + "placeholders": {} + }, + "ignore": "Block", + "@ignore": { + "type": "text", + "placeholders": {} + }, + "ignoredUsers": "Blocked users", + "@ignoredUsers": { + "type": "text", + "placeholders": {} + }, + "ignoreListDescription": "You can block users who are disturbing you. You won't be able to receive any messages or invites from the users on your personal block list.", + "@ignoreListDescription": { + "type": "text", + "placeholders": {} + }, + "ignoreUsername": "Block username", + "@ignoreUsername": { + "type": "text", + "placeholders": {} + }, + "block": "block", + "blockedUsers": "Blocked users", + "blockListDescription": "You can block users who are disturbing you. You won't be able to receive any messages or room invites from the users on your personal block list.", + "blockUsername": "Ignore username", + "iHaveClickedOnLink": "I have clicked on the link", + "@iHaveClickedOnLink": { + "type": "text", + "placeholders": {} + }, + "incorrectPassphraseOrKey": "Incorrect passphrase or recovery key", + "@incorrectPassphraseOrKey": { + "type": "text", + "placeholders": {} + }, + "inoffensive": "Slightly offensive", + "@inoffensive": { + "type": "text", + "placeholders": {} + }, + "inviteContact": "Invite contact", + "@inviteContact": { + "type": "text", + "placeholders": {} + }, + "inviteContactToGroupQuestion": "Do you want to invite {contact} to the chat \"{groupName}\"?", + "@inviteContactToGroup": { + "type": "text", + "placeholders": { + "groupName": {} + } + }, + "inviteContactToGroup": "Invite contact to {groupName}", + "noChatDescriptionYet": "No chat description created yet.", + "tryAgain": "Try again", + "invalidServerName": "Invalid server name", + "invited": "Invited", + "@invited": { + "type": "text", + "placeholders": {} + }, + "redactMessageDescription": "The message will be redacted for all participants in this conversation. This cannot be undone.", + "optionalRedactReason": "(Optional) Reason for redacting this message...", + "invitedUser": "📩 {username} invited {targetName}", + "@invitedUser": { + "type": "text", + "placeholders": { + "username": {}, + "targetName": {} + } + }, + "invitedUsersOnly": "Invited users only", + "@invitedUsersOnly": { + "type": "text", + "placeholders": {} + }, + "inviteForMe": "Invite for me", + "@inviteForMe": { + "type": "text", + "placeholders": {} + }, + "inviteText": "{username} invited you to FluffyChat.\n1. Visit fluffychat.im and install the app \n2. Sign up or sign in \n3. Open the invite link: \n {link}", + "@inviteText": { + "type": "text", + "placeholders": { + "username": {}, + "link": {} + } + }, + "isTyping": "is typing…", + "@isTyping": { + "type": "text", + "placeholders": {} + }, + "joinedTheChat": "👋 {username} joined the chat", + "@joinedTheChat": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "joinRoom": "Join room", + "@joinRoom": { + "type": "text", + "placeholders": {} + }, + "kicked": "👞 {username} kicked {targetName}", + "@kicked": { + "type": "text", + "placeholders": { + "username": {}, + "targetName": {} + } + }, + "kickedAndBanned": "🙅 {username} kicked and banned {targetName}", + "@kickedAndBanned": { + "type": "text", + "placeholders": { + "username": {}, + "targetName": {} + } + }, + "kickFromChat": "Kick from chat", + "@kickFromChat": { + "type": "text", + "placeholders": {} + }, + "lastActiveAgo": "Last active: {localizedTimeShort}", + "@lastActiveAgo": { + "type": "text", + "placeholders": { + "localizedTimeShort": {} + } + }, + "leave": "Leave", + "@leave": { + "type": "text", + "placeholders": {} + }, + "leftTheChat": "Left the chat", + "@leftTheChat": { + "type": "text", + "placeholders": {} + }, + "license": "License", + "@license": { + "type": "text", + "placeholders": {} + }, + "lightTheme": "Light", + "@lightTheme": { + "type": "text", + "placeholders": {} + }, + "loadCountMoreParticipants": "Load {count} more participants", + "@loadCountMoreParticipants": { + "type": "text", + "placeholders": { + "count": {} + } + }, + "dehydrate": "Export session and wipe device", + "@dehydrate": {}, + "dehydrateWarning": "This action cannot be undone. Ensure you safely store the backup file.", + "@dehydrateWarning": {}, + "dehydrateTor": "TOR Users: Export session", + "@dehydrateTor": {}, + "dehydrateTorLong": "For TOR users, it is recommended to export the session before closing the window.", + "@dehydrateTorLong": {}, + "hydrateTor": "TOR Users: Import session export", + "@hydrateTor": {}, + "hydrateTorLong": "Did you export your session last time on TOR? Quickly import it and continue chatting.", + "@hydrateTorLong": {}, + "hydrate": "Restore from backup file", + "@hydrate": {}, + "loadingPleaseWait": "Loading… Please wait.", + "@loadingPleaseWait": { + "type": "text", + "placeholders": {} + }, + "loadMore": "Load more…", + "@loadMore": { + "type": "text", + "placeholders": {} + }, + "locationDisabledNotice": "Location services are disabled. Please enable them to be able to share your location.", + "@locationDisabledNotice": { + "type": "text", + "placeholders": {} + }, + "locationPermissionDeniedNotice": "Location permission denied. Please grant them to be able to share your location.", + "@locationPermissionDeniedNotice": { + "type": "text", + "placeholders": {} + }, + "login": "Login", + "@login": { + "type": "text", + "placeholders": {} + }, + "logInTo": "Log in to {homeserver}", + "@logInTo": { + "type": "text", + "placeholders": { + "homeserver": {} + } + }, + "logout": "Logout", + "@logout": { + "type": "text", + "placeholders": {} + }, + "memberChanges": "Member changes", + "@memberChanges": { + "type": "text", + "placeholders": {} + }, + "mention": "Mention", + "@mention": { + "type": "text", + "placeholders": {} + }, + "messages": "Messages", + "messagesStyle": "Messages:", + "@messages": { + "type": "text", + "placeholders": {} + }, + "moderator": "Moderator", + "@moderator": { + "type": "text", + "placeholders": {} + }, + "muteChat": "Mute chat", + "@muteChat": { + "type": "text", + "placeholders": {} + }, + "needPantalaimonWarning": "Please be aware that you need Pantalaimon to use end-to-end encryption for now.", + "@needPantalaimonWarning": { + "type": "text", + "placeholders": {} + }, + "newChat": "New chat", + "@newChat": { + "type": "text", + "placeholders": {} + }, + "newMessageInFluffyChat": "💬 New message in FluffyChat", + "@newMessageInFluffyChat": { + "type": "text", + "placeholders": {} + }, + "newVerificationRequest": "New verification request!", + "@newVerificationRequest": { + "type": "text", + "placeholders": {} + }, + "next": "Next", + "@next": { + "type": "text", + "placeholders": {} + }, + "no": "No", + "@no": { + "type": "text", + "placeholders": {} + }, + "noConnectionToTheServer": "No connection to the server", + "@noConnectionToTheServer": { + "type": "text", + "placeholders": {} + }, + "noEmotesFound": "No emotes found. 😕", + "@noEmotesFound": { + "type": "text", + "placeholders": {} + }, + "noEncryptionForPublicRooms": "You can only activate encryption as soon as the room is no longer publicly accessible.", + "@noEncryptionForPublicRooms": { + "type": "text", + "placeholders": {} + }, + "noGoogleServicesWarning": "Firebase Cloud Messaging doesn't appear to be available on your device. To still receive push notifications, we recommend installing ntfy. With ntfy or another Unified Push provider you can receive push notifications in a data secure way. You can download ntfy from the PlayStore or from F-Droid.", + "@noGoogleServicesWarning": { + "type": "text", + "placeholders": {} + }, + "noMatrixServer": "{server1} is no matrix server, use {server2} instead?", + "@noMatrixServer": { + "type": "text", + "placeholders": { + "server1": {}, + "server2": {} + } + }, + "shareInviteLink": "Share invite link", + "scanQrCode": "Scan QR code", + "@scanQrCode": {}, + "none": "None", + "@none": { + "type": "text", + "placeholders": {} + }, + "noPasswordRecoveryDescription": "You have not added a way to recover your password yet.", + "@noPasswordRecoveryDescription": { + "type": "text", + "placeholders": {} + }, + "noPermission": "No permission", + "@noPermission": { + "type": "text", + "placeholders": {} + }, + "noRoomsFound": "No rooms found…", + "@noRoomsFound": { + "type": "text", + "placeholders": {} + }, + "notifications": "Notifications", + "@notifications": { + "type": "text", + "placeholders": {} + }, + "notificationsEnabledForThisAccount": "Notifications enabled for this account", + "@notificationsEnabledForThisAccount": { + "type": "text", + "placeholders": {} + }, + "numUsersTyping": "{count} users are typing…", + "@numUsersTyping": { + "type": "text", + "placeholders": { + "count": {} + } + }, + "obtainingLocation": "Obtaining location…", + "@obtainingLocation": { + "type": "text", + "placeholders": {} + }, + "offensive": "Offensive", + "@offensive": { + "type": "text", + "placeholders": {} + }, + "offline": "Offline", + "@offline": { + "type": "text", + "placeholders": {} + }, + "ok": "Ok", + "@ok": { + "type": "text", + "placeholders": {} + }, + "online": "Online", + "@online": { + "type": "text", + "placeholders": {} + }, + "onlineKeyBackupEnabled": "Online Key Backup is enabled", + "@onlineKeyBackupEnabled": { + "type": "text", + "placeholders": {} + }, + "oopsPushError": "Oops! Unfortunately, an error occurred when setting up the push notifications.", + "@oopsPushError": { + "type": "text", + "placeholders": {} + }, + "oopsSomethingWentWrong": "Oops, something went wrong…", + "@oopsSomethingWentWrong": { + "type": "text", + "placeholders": {} + }, + "openAppToReadMessages": "Open app to read messages", + "@openAppToReadMessages": { + "type": "text", + "placeholders": {} + }, + "openCamera": "Open camera", + "@openCamera": { + "type": "text", + "placeholders": {} + }, + "openVideoCamera": "Open camera for a video", + "@openVideoCamera": { + "type": "text", + "placeholders": {} + }, + "oneClientLoggedOut": "One of your clients has been logged out", + "@oneClientLoggedOut": {}, + "addAccount": "Add account", + "@addAccount": {}, + "editBundlesForAccount": "Edit bundles for this account", + "@editBundlesForAccount": {}, + "addToBundle": "Add to bundle", + "@addToBundle": {}, + "removeFromBundle": "Remove from this bundle", + "@removeFromBundle": {}, + "bundleName": "Bundle name", + "@bundleName": {}, + "enableMultiAccounts": "(BETA) Enable multi accounts on this device", + "@enableMultiAccounts": {}, + "openInMaps": "Open in maps", + "@openInMaps": { + "type": "text", + "placeholders": {} + }, + "link": "Link", + "@link": {}, + "serverRequiresEmail": "This server needs to validate your email address for registration.", + "@serverRequiresEmail": {}, + "optionalGroupName": "(Optional) Chat name", + "@optionalGroupName": { + "type": "text", + "placeholders": {} + }, + "or": "Or", + "@or": { + "type": "text", + "placeholders": {} + }, + "participant": "Participant", + "@participant": { + "type": "text", + "placeholders": {} + }, + "passphraseOrKey": "passphrase or recovery key", + "@passphraseOrKey": { + "type": "text", + "placeholders": {} + }, + "password": "Password", + "@password": { + "type": "text", + "placeholders": {} + }, + "passwordForgotten": "Password forgotten", + "@passwordForgotten": { + "type": "text", + "placeholders": {} + }, + "passwordHasBeenChanged": "Password has been changed", + "@passwordHasBeenChanged": { + "type": "text", + "placeholders": {} + }, + "passwordRecovery": "Password recovery", + "@passwordRecovery": { + "type": "text", + "placeholders": {} + }, + "people": "People", + "@people": { + "type": "text", + "placeholders": {} + }, + "pickImage": "Pick an image", + "@pickImage": { + "type": "text", + "placeholders": {} + }, + "pin": "Pin", + "@pin": { + "type": "text", + "placeholders": {} + }, + "play": "Play {fileName}", + "@play": { + "type": "text", + "placeholders": { + "fileName": {} + } + }, + "pleaseChoose": "Please choose", + "@pleaseChoose": { + "type": "text", + "placeholders": {} + }, + "pleaseChooseAPasscode": "Please choose a pass code", + "@pleaseChooseAPasscode": { + "type": "text", + "placeholders": {} + }, + "pleaseClickOnLink": "Please click on the link in the email and then proceed. In rare cases, the email can be sent to spam or take up to 5 minutes to arrive.", + "@pleaseClickOnLink": { + "type": "text", + "placeholders": {} + }, + "pleaseEnter4Digits": "Please enter 4 digits or leave empty to disable app lock.", + "@pleaseEnter4Digits": { + "type": "text", + "placeholders": {} + }, + "pleaseEnterRecoveryKey": "Please enter your recovery key:", + "@pleaseEnterRecoveryKey": {}, + "pleaseEnterYourPassword": "Please enter your password", + "@pleaseEnterYourPassword": { + "type": "text", + "placeholders": {} + }, + "pleaseEnterYourPin": "Please enter your pin", + "@pleaseEnterYourPin": { + "type": "text", + "placeholders": {} + }, + "pleaseEnterYourUsername": "Please enter your username", + "@pleaseEnterYourUsername": { + "type": "text", + "placeholders": {} + }, + "pleaseFollowInstructionsOnWeb": "Please follow the instructions on the website and tap on next.", + "@pleaseFollowInstructionsOnWeb": { + "type": "text", + "placeholders": {} + }, + "privacy": "Privacy", + "@privacy": { + "type": "text", + "placeholders": {} + }, + "publicRooms": "Public Rooms", + "@publicRooms": { + "type": "text", + "placeholders": {} + }, + "pushRules": "Push rules", + "@pushRules": { + "type": "text", + "placeholders": {} + }, + "reason": "Reason", + "@reason": { + "type": "text", + "placeholders": {} + }, + "recording": "Recording", + "@recording": { + "type": "text", + "placeholders": {} + }, + "redactedBy": "Redacted by {username}", + "@redactedBy": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "directChat": "Direct chat", + "redactedByBecause": "Redacted by {username} because: \"{reason}\"", + "@redactedByBecause": { + "type": "text", + "placeholders": { + "username": {}, + "reason": {} + } + }, + "redactedAnEvent": "{username} redacted an event", + "@redactedAnEvent": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "redactMessage": "Redact message", + "@redactMessage": { + "type": "text", + "placeholders": {} + }, + "register": "Register", + "@register": { + "type": "text", + "placeholders": {} + }, + "reject": "Reject", + "@reject": { + "type": "text", + "placeholders": {} + }, + "rejectedTheInvitation": "{username} rejected the invitation", + "@rejectedTheInvitation": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "rejoin": "Rejoin", + "@rejoin": { + "type": "text", + "placeholders": {} + }, + "@remove": { + "type": "text", + "placeholders": {} + }, + "removeAllOtherDevices": "Remove all other devices", + "@removeAllOtherDevices": { + "type": "text", + "placeholders": {} + }, + "removedBy": "Removed by {username}", + "@removedBy": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "removeDevice": "Remove device", + "@removeDevice": { + "type": "text", + "placeholders": {} + }, + "unbanFromChat": "Unban from chat", + "@unbanFromChat": { + "type": "text", + "placeholders": {} + }, + "removeYourAvatar": "Remove your avatar", + "@removeYourAvatar": { + "type": "text", + "placeholders": {} + }, + "replaceRoomWithNewerVersion": "Replace room with newer version", + "@replaceRoomWithNewerVersion": { + "type": "text", + "placeholders": {} + }, + "reply": "Reply", + "@reply": { + "type": "text", + "placeholders": {} + }, + "reportMessage": "Report message", + "@reportMessage": { + "type": "text", + "placeholders": {} + }, + "requestPermission": "Request permission", + "@requestPermission": { + "type": "text", + "placeholders": {} + }, + "roomHasBeenUpgraded": "Room has been upgraded", + "@roomHasBeenUpgraded": { + "type": "text", + "placeholders": {} + }, + "roomVersion": "Room version", + "@roomVersion": { + "type": "text", + "placeholders": {} + }, + "saveFile": "Save file", + "@saveFile": { + "type": "text", + "placeholders": {} + }, + "search": "Search", + "@search": { + "type": "text", + "placeholders": {} + }, + "security": "Security", + "@security": { + "type": "text", + "placeholders": {} + }, + "recoveryKey": "Recovery key", + "@recoveryKey": {}, + "recoveryKeyLost": "Recovery key lost?", + "@recoveryKeyLost": {}, + "seenByUser": "Seen by {username}", + "@seenByUser": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "send": "Send", + "@send": { + "type": "text", + "placeholders": {} + }, + "sendAMessage": "Send a message", + "@sendAMessage": { + "type": "text", + "placeholders": {} + }, + "sendAsText": "Send as text", + "@sendAsText": { + "type": "text" + }, + "sendAudio": "Send audio", + "@sendAudio": { + "type": "text", + "placeholders": {} + }, + "sendFile": "Send file", + "@sendFile": { + "type": "text", + "placeholders": {} + }, + "sendImage": "Send image", + "@sendImage": { + "type": "text", + "placeholders": {} + }, + "sendMessages": "Send messages", + "@sendMessages": { + "type": "text", + "placeholders": {} + }, + "sendOriginal": "Send original", + "@sendOriginal": { + "type": "text", + "placeholders": {} + }, + "sendSticker": "Send sticker", + "@sendSticker": { + "type": "text", + "placeholders": {} + }, + "sendVideo": "Send video", + "@sendVideo": { + "type": "text", + "placeholders": {} + }, + "sentAFile": "📁 {username} sent a file", + "@sentAFile": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "sentAnAudio": "🎤 {username} sent an audio", + "@sentAnAudio": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "sentAPicture": "🖼️ {username} sent a picture", + "@sentAPicture": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "sentASticker": "😊 {username} sent a sticker", + "@sentASticker": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "sentAVideo": "🎥 {username} sent a video", + "@sentAVideo": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "sentCallInformations": "{senderName} sent call information", + "@sentCallInformations": { + "type": "text", + "placeholders": { + "senderName": {} + } + }, + "separateChatTypes": "Separate Direct Chats and Groups", + "@separateChatTypes": { + "type": "text", + "placeholders": {} + }, + "setAsCanonicalAlias": "Set as main alias", + "@setAsCanonicalAlias": { + "type": "text", + "placeholders": {} + }, + "setCustomEmotes": "Set custom emotes", + "@setCustomEmotes": { + "type": "text", + "placeholders": {} + }, + "setChatDescription": "Set chat description", + "setInvitationLink": "Set invitation link", + "@setInvitationLink": { + "type": "text", + "placeholders": {} + }, + "setPermissionsLevel": "Set permissions level", + "@setPermissionsLevel": { + "type": "text", + "placeholders": {} + }, + "setStatus": "Set status", + "@setStatus": { + "type": "text", + "placeholders": {} + }, + "settings": "Settings", + "@settings": { + "type": "text", + "placeholders": {} + }, + "share": "Share", + "@share": { + "type": "text", + "placeholders": {} + }, + "sharedTheLocation": "{username} shared their location", + "@sharedTheLocation": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "shareLocation": "Share location", + "@shareLocation": { + "type": "text", + "placeholders": {} + }, + "showPassword": "Show password", + "@showPassword": { + "type": "text", + "placeholders": {} + }, + "presenceStyle": "Presence:", + "@presenceStyle": { + "type": "text", + "placeholders": {} + }, + "presencesToggle": "Show status messages from other users", + "@presencesToggle": { + "type": "text", + "placeholders": {} + }, + "singlesignon": "Single Sign on", + "@singlesignon": { + "type": "text", + "placeholders": {} + }, + "skip": "Skip", + "@skip": { + "type": "text", + "placeholders": {} + }, + "sourceCode": "Source code", + "@sourceCode": { + "type": "text", + "placeholders": {} + }, + "spaceIsPublic": "Space is public", + "@spaceIsPublic": { + "type": "text", + "placeholders": {} + }, + "spaceName": "Name", + "@spaceName": { + "type": "text", + "placeholders": {} + }, + "startedACall": "{senderName} started a call", + "@startedACall": { + "type": "text", + "placeholders": { + "senderName": {} + } + }, + "startFirstChat": "Start your first chat", + "status": "Status", + "@status": { + "type": "text", + "placeholders": {} + }, + "statusExampleMessage": "How are you today?", + "@statusExampleMessage": { + "type": "text", + "placeholders": {} + }, + "submit": "Submit", + "@submit": { + "type": "text", + "placeholders": {} + }, + "synchronizingPleaseWait": "Synchronizing… Please wait.", + "@synchronizingPleaseWait": { + "type": "text", + "placeholders": {} + }, + "systemTheme": "System", + "@systemTheme": { + "type": "text", + "placeholders": {} + }, + "theyDontMatch": "They Don't Match", + "@theyDontMatch": { + "type": "text", + "placeholders": {} + }, + "theyMatch": "They Match", + "@theyMatch": { + "type": "text", + "placeholders": {} + }, + "title": "FluffyChat", + "@title": { + "description": "Title for the application", + "type": "text", + "placeholders": {} + }, + "toggleFavorite": "Toggle Favorite", + "@toggleFavorite": { + "type": "text", + "placeholders": {} + }, + "toggleMuted": "Toggle Muted", + "@toggleMuted": { + "type": "text", + "placeholders": {} + }, + "toggleUnread": "Mark Read/Unread", + "@toggleUnread": { + "type": "text", + "placeholders": {} + }, + "tooManyRequestsWarning": "Too many requests. Please try again later!", + "@tooManyRequestsWarning": { + "type": "text", + "placeholders": {} + }, + "transferFromAnotherDevice": "Transfer from another device", + "@transferFromAnotherDevice": { + "type": "text", + "placeholders": {} + }, + "tryToSendAgain": "Try to send again", + "@tryToSendAgain": { + "type": "text", + "placeholders": {} + }, + "unavailable": "Unavailable", + "@unavailable": { + "type": "text", + "placeholders": {} + }, + "unbannedUser": "{username} unbanned {targetName}", + "@unbannedUser": { + "type": "text", + "placeholders": { + "username": {}, + "targetName": {} + } + }, + "unblockDevice": "Unblock Device", + "@unblockDevice": { + "type": "text", + "placeholders": {} + }, + "unknownDevice": "Unknown device", + "@unknownDevice": { + "type": "text", + "placeholders": {} + }, + "unknownEncryptionAlgorithm": "Unknown encryption algorithm", + "@unknownEncryptionAlgorithm": { + "type": "text", + "placeholders": {} + }, + "unknownEvent": "Unknown event '{type}'", + "@unknownEvent": { + "type": "text", + "placeholders": { + "type": {} + } + }, + "unmuteChat": "Unmute chat", + "@unmuteChat": { + "type": "text", + "placeholders": {} + }, + "unpin": "Unpin", + "@unpin": { + "type": "text", + "placeholders": {} + }, + "unreadChats": "{unreadCount, plural, =1{1 unread chat} other{{unreadCount} unread chats}}", + "@unreadChats": { + "type": "text", + "placeholders": { + "unreadCount": {} + } + }, + "userAndOthersAreTyping": "{username} and {count} others are typing…", + "@userAndOthersAreTyping": { + "type": "text", + "placeholders": { + "username": {}, + "count": {} + } + }, + "userAndUserAreTyping": "{username} and {username2} are typing…", + "@userAndUserAreTyping": { + "type": "text", + "placeholders": { + "username": {}, + "username2": {} + } + }, + "userIsTyping": "{username} is typing…", + "@userIsTyping": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "userLeftTheChat": "🚪 {username} left the chat", + "@userLeftTheChat": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "username": "Username", + "@username": { + "type": "text", + "placeholders": {} + }, + "userSentUnknownEvent": "{username} sent a {type} event", + "@userSentUnknownEvent": { + "type": "text", + "placeholders": { + "username": {}, + "type": {} + } + }, + "unverified": "Unverified", + "@unverified": {}, + "verified": "Verified", + "@verified": { + "type": "text", + "placeholders": {} + }, + "verify": "Verify", + "@verify": { + "type": "text", + "placeholders": {} + }, + "verifyStart": "Start Verification", + "@verifyStart": { + "type": "text", + "placeholders": {} + }, + "verifySuccess": "You successfully verified!", + "@verifySuccess": { + "type": "text", + "placeholders": {} + }, + "verifyTitle": "Verifying other account", + "@verifyTitle": { + "type": "text", + "placeholders": {} + }, + "videoCall": "Video call", + "@videoCall": { + "type": "text", + "placeholders": {} + }, + "visibilityOfTheChatHistory": "Visibility of the chat history", + "@visibilityOfTheChatHistory": { + "type": "text", + "placeholders": {} + }, + "visibleForAllParticipants": "Visible for all participants", + "@visibleForAllParticipants": { + "type": "text", + "placeholders": {} + }, + "visibleForEveryone": "Visible for everyone", + "@visibleForEveryone": { + "type": "text", + "placeholders": {} + }, + "voiceMessage": "Voice message", + "@voiceMessage": { + "type": "text", + "placeholders": {} + }, + "waitingPartnerAcceptRequest": "Waiting for partner to accept the request…", + "@waitingPartnerAcceptRequest": { + "type": "text", + "placeholders": {} + }, + "waitingPartnerEmoji": "Waiting for partner to accept the emoji…", + "@waitingPartnerEmoji": { + "type": "text", + "placeholders": {} + }, + "waitingPartnerNumbers": "Waiting for partner to accept the numbers…", + "@waitingPartnerNumbers": { + "type": "text", + "placeholders": {} + }, + "wallpaper": "Wallpaper:", + "@wallpaper": { + "type": "text", + "placeholders": {} + }, + "warning": "Warning!", + "@warning": { + "type": "text", + "placeholders": {} + }, + "weSentYouAnEmail": "We sent you an email", + "@weSentYouAnEmail": { + "type": "text", + "placeholders": {} + }, + "whoCanPerformWhichAction": "Who can perform which action", + "@whoCanPerformWhichAction": { + "type": "text", + "placeholders": {} + }, + "whoIsAllowedToJoinThisGroup": "Who is allowed to join this chat", + "@whoIsAllowedToJoinThisGroup": { + "type": "text", + "placeholders": {} + }, + "whyDoYouWantToReportThis": "Why do you want to report this?", + "@whyDoYouWantToReportThis": { + "type": "text", + "placeholders": {} + }, + "wipeChatBackup": "Wipe your chat backup to create a new recovery key?", + "@wipeChatBackup": { + "type": "text", + "placeholders": {} + }, + "withTheseAddressesRecoveryDescription": "With these addresses you can recover your password.", + "@withTheseAddressesRecoveryDescription": { + "type": "text", + "placeholders": {} + }, + "writeAMessage": "Write a message…", + "writeAMessageFlag": "Write a message in {l1flag} or {l2flag}", + "@writeAMessageFlag": { + "type": "text", + "placeholders": { + "l1flag": {}, + "l2flag": {} + } + }, + "yes": "Yes", + "@yes": { + "type": "text", + "placeholders": {} + }, + "you": "You", + "@you": { + "type": "text", + "placeholders": {} + }, + "youAreNoLongerParticipatingInThisChat": "You are no longer participating in this chat", + "@youAreNoLongerParticipatingInThisChat": { + "type": "text", + "placeholders": {} + }, + "youHaveBeenBannedFromThisChat": "You have been banned from this chat", + "@youHaveBeenBannedFromThisChat": { + "type": "text", + "placeholders": {} + }, + "yourPublicKey": "Your public key", + "@yourPublicKey": { + "type": "text", + "placeholders": {} + }, + "messageInfo": "Message info", + "@messageInfo": {}, + "time": "Time", + "@time": {}, + "messageType": "Message Type", + "@messageType": {}, + "sender": "Sender", + "@sender": {}, + "openGallery": "Open gallery", + "@openGallery": {}, + "removeFromSpace": "Remove from space", + "@removeFromSpace": {}, + "addToSpaceDescription": "Select a space to add this chat to it.", + "@addToSpaceDescription": {}, + "start": "Start", + "@start": {}, + "pleaseEnterRecoveryKeyDescription": "To unlock your old messages, please enter your recovery key that has been generated in a previous session. Your recovery key is NOT your password.", + "@pleaseEnterRecoveryKeyDescription": {}, + "publish": "Publish", + "@publish": {}, + "videoWithSize": "Video ({size})", + "@videoWithSize": { + "type": "text", + "placeholders": { + "size": {} + } + }, + "openChat": "Open Chat", + "@openChat": {}, + "markAsRead": "Mark as read", + "@markAsRead": {}, + "reportUser": "Report user", + "@reportUser": {}, + "dismiss": "Dismiss", + "@dismiss": {}, + "reactedWith": "{sender} reacted with {reaction}", + "@reactedWith": { + "type": "text", + "placeholders": { + "sender": {}, + "reaction": {} + } + }, + "pinMessage": "Pin to room", + "@pinMessage": {}, + "confirmEventUnpin": "Are you sure to permanently unpin the event?", + "@confirmEventUnpin": {}, + "emojis": "Emojis", + "@emojis": {}, + "placeCall": "Place call", + "@placeCall": {}, + "voiceCall": "Voice call", + "@voiceCall": {}, + "unsupportedAndroidVersion": "Unsupported Android version", + "@unsupportedAndroidVersion": {}, + "unsupportedAndroidVersionLong": "This feature requires a newer Android version. Please check for updates or Lineage OS support.", + "@unsupportedAndroidVersionLong": {}, + "videoCallsBetaWarning": "Please note that video calls are currently in beta. They might not work as expected or work at all on all platforms.", + "@videoCallsBetaWarning": {}, + "experimentalVideoCalls": "Experimental video calls", + "@experimentalVideoCalls": {}, + "emailOrUsername": "Email or username", + "@emailOrUsername": {}, + "indexedDbErrorTitle": "Private mode issues", + "@indexedDbErrorTitle": {}, + "indexedDbErrorLong": "The message storage is unfortunately not enabled in private mode by default.\nPlease visit\n - about:config\n - set dom.indexedDB.privateBrowsing.enabled to true\nOtherwise, it is not possible to run FluffyChat.", + "@indexedDbErrorLong": {}, + "switchToAccount": "Switch to account {number}", + "@switchToAccount": { + "type": "number", + "placeholders": { + "number": {} + } + }, + "nextAccount": "Next account", + "@nextAccount": {}, + "previousAccount": "Previous account", + "@previousAccount": {}, + "addWidget": "Add widget", + "@addWidget": {}, + "widgetVideo": "Video", + "@widgetVideo": {}, + "widgetEtherpad": "Text note", + "@widgetEtherpad": {}, + "widgetJitsi": "Jitsi Meet", + "@widgetJitsi": {}, + "widgetCustom": "Custom", + "@widgetCustom": {}, + "widgetName": "Name", + "@widgetName": {}, + "widgetUrlError": "This is not a valid URL.", + "@widgetUrlError": {}, + "widgetNameError": "Please provide a display name.", + "@widgetNameError": {}, + "errorAddingWidget": "Error adding the widget.", + "@errorAddingWidget": {}, + "youRejectedTheInvitation": "You rejected the invitation", + "@youRejectedTheInvitation": {}, + "youJoinedTheChat": "You joined the chat", + "@youJoinedTheChat": {}, + "youAcceptedTheInvitation": "👍 You accepted the invitation", + "@youAcceptedTheInvitation": {}, + "youBannedUser": "You banned {user}", + "@youBannedUser": { + "placeholders": { + "user": {} + } + }, + "youHaveWithdrawnTheInvitationFor": "You have withdrawn the invitation for {user}", + "@youHaveWithdrawnTheInvitationFor": { + "placeholders": { + "user": {} + } + }, + "youInvitedToBy": "📩 You have been invited via link to:\n{alias}", + "@youInvitedToBy": { + "placeholders": { + "alias": {} + } + }, + "youInvitedBy": "📩 You have been invited by {user}", + "@youInvitedBy": { + "placeholders": { + "user": {} + } + }, + "youInvitedUser": "📩 You invited {user}", + "@youInvitedUser": { + "placeholders": { + "user": {} + } + }, + "youKicked": "👞 You kicked {user}", + "@youKicked": { + "placeholders": { + "user": {} + } + }, + "youKickedAndBanned": "🙅 You kicked and banned {user}", + "@youKickedAndBanned": { + "placeholders": { + "user": {} + } + }, + "youUnbannedUser": "You unbanned {user}", + "@youUnbannedUser": { + "placeholders": { + "user": {} + } + }, + "hasKnocked": "{user} has knocked", + "@hasKnocked": { + "placeholders": { + "user": {} + } + }, + "users": "Users", + "@users": {}, + "unlockOldMessages": "Unlock old messages", + "@unlockOldMessages": {}, + "storeInSecureStorageDescription": "Store the recovery key in the secure storage of this device.", + "@storeInSecureStorageDescription": {}, + "saveKeyManuallyDescription": "Save this key manually by triggering the system share dialog or clipboard.", + "@saveKeyManuallyDescription": {}, + "storeInAndroidKeystore": "Store in Android KeyStore", + "@storeInAndroidKeystore": {}, + "storeInAppleKeyChain": "Store in Apple KeyChain", + "@storeInAppleKeyChain": {}, + "storeSecurlyOnThisDevice": "Store securely on this device", + "@storeSecurlyOnThisDevice": {}, + "countFiles": "{count} files", + "@countFiles": { + "placeholders": { + "count": {} + } + }, + "user": "User", + "@user": {}, + "custom": "Custom", + "@custom": {}, + "foregroundServiceRunning": "This notification appears when the foreground service is running.", + "@foregroundServiceRunning": {}, + "screenSharingTitle": "screen sharing", + "@screenSharingTitle": {}, + "screenSharingDetail": "You are sharing your screen in FuffyChat", + "@screenSharingDetail": {}, + "callingPermissions": "Calling permissions", + "@callingPermissions": {}, + "callingAccount": "Calling account", + "@callingAccount": {}, + "callingAccountDetails": "Allows FluffyChat to use the native android dialer app.", + "@callingAccountDetails": {}, + "appearOnTop": "Appear on top", + "@appearOnTop": {}, + "appearOnTopDetails": "Allows the app to appear on top (not needed if you already have Fluffychat setup as a calling account)", + "@appearOnTopDetails": {}, + "otherCallingPermissions": "Microphone, camera and other FluffyChat permissions", + "@otherCallingPermissions": {}, + "whyIsThisMessageEncrypted": "Why is this message unreadable?", + "@whyIsThisMessageEncrypted": {}, + "noKeyForThisMessage": "This can happen if the message was sent before you have signed in to your account at this device.\n\nIt is also possible that the sender has blocked your device or something went wrong with the internet connection.\n\nAre you able to read the message on another session? Then you can transfer the message from it! Go to Settings > Devices and make sure that your devices have verified each other. When you open the room the next time and both sessions are in the foreground, the keys will be transmitted automatically.\n\nDo you not want to lose the keys when logging out or switching devices? Make sure that you have enabled the chat backup in the settings.", + "@noKeyForThisMessage": {}, + "newGroup": "New chat", + "@newGroup": {}, + "newSpace": "New class", + "@newSpace": {}, + "enterSpace": "Enter space", + "@enterSpace": {}, + "enterRoom": "Enter room", + "@enterRoom": {}, + "allSpaces": "All spaces", + "@allSpaces": {}, + "numChats": "{number} chats", + "@numChats": { + "type": "number", + "placeholders": { + "number": {} + } + }, + "hideUnimportantStateEvents": "Hide unimportant state events", + "hidePresences": "Hide Status List?", + "doNotShowAgain": "Do not show again", + "wasDirectChatDisplayName": "Empty chat (was {oldDisplayName})", + "@wasDirectChatDisplayName": { + "type": "text", + "placeholders": { + "oldDisplayName": {} + } + }, + "newSpaceDescription": "Spaces allows you to consolidate your chats and build private or public communities.", + "encryptThisChat": "Encrypt this chat", + "disableEncryptionWarning": "For security reasons you can not disable encryption in a chat, where it has been enabled before.", + "sorryThatsNotPossible": "Sorry... that is not possible", + "deviceKeys": "Device keys:", + "reopenChat": "Reopen chat", + "noBackupWarning": "Don't forget your password!", + "noOtherDevicesFound": "No other devices found", + "fileIsTooBigForServer": "The server reports that the file is too large to be sent.", + "fileHasBeenSavedAt": "File has been saved at {path}", + "@fileHasBeenSavedAt": { + "type": "text", + "placeholders": { + "path": {} + } + }, + "jumpToLastReadMessage": "Jump to last read message", + "readUpToHere": "Read up to here", + "jump": "Jump", + "openLinkInBrowser": "Open link in browser", + "reportErrorDescription": "😭 Oh no. Something went wrong. If you want, you can report this bug to the developers.", + "report": "report", + "signInWithPassword": "Sign in with password", + "pleaseTryAgainLaterOrChooseDifferentServer": "Please try again later or choose a different server.", + "signInWith": "Sign in with {provider}", + "@signInWith": { + "type": "text", + "placeholders": { + "provider": {} + } + }, + "profileNotFound": "The user could not be found on the server. Maybe there is a connection problem or the user doesn't exist.", + "setTheme": "Set theme:", + "setColorTheme": "Set color theme:", + "invite": "Invite", + "requests": "Requests", + "inviteGroupChat": "📨 Invite group chat", + "invitePrivateChat": "📨 Invite private chat", + "invalidInput": "Invalid input!", + "wrongPinEntered": "Wrong pin entered! Try again in {seconds} seconds...", + "@wrongPinEntered": { + "type": "text", + "placeholders": { + "seconds": {} + } + }, + "allCorrect": "That's how I would say it! Nice!", + "newWayAllGood": "That's not how I would have said it but it looks good!", + "othersAreBetter": "Hm, there might be a better way to say that.", + "holdForInfo": "Click and hold for word info.", + "greenFeedback": "That's what I would put!", + "yellowFeedback": "Hm, you can try that and see if it works! To use this word, just click it again.", + "redFeedback": "I don't think that's right...", + "customInputFeedbackChoice": "You wrote it in. Nice!", + "itInstructionsTitle": "I can help you translate!", + "itInstructionsBody": "You can click and hold choices for word info.", + "toggleLanguages": "Toggle the language of selected messages.", + "classWelcomeChat": "Welcome Chat", + "@classWelcomeChat": { + "type": "text", + "placeholders": {} + }, + "deleteSpace": "Delete Space", + "deleteGroup": "Delete Group Chat", + "areYouSureDeleteClass": "Are you sure you want to delete this space?", + "areYouSureDeleteGroup": "Are you sure you want to delete this group chat?", + "cannotBeReversed": "This action cannot be reversed", + "enterDeletedClassName": "Enter space name to confirm:", + "incorrectClassName": "Incorrect Space Name", + "oneday": "Last 24 hours", + "@oneday": { + "type": "text", + "placeholders": {} + }, + "oneweek": "Last 7 days", + "@oneweek": { + "type": "text", + "placeholders": {} + }, + "onemonth": "Past month", + "@onemonth": { + "type": "text", + "placeholders": {} + }, + "sixmonth": "Past 6 months", + "@sixmonth": { + "type": "text", + "placeholders": {} + }, + "oneyear": "Past year", + "@oneyear": { + "type": "text", + "placeholders": {} + }, + "gaTooltip": "L2 use with grammar assistance", + "taTooltip": "L2 use with translation assistance", + "unTooltip": "Other", + "interactiveTranslatorSliderHeader": "Interactive Translator", + "interactiveGrammarSliderHeader": "Interactive Grammar Checker", + "interactiveTranslatorNotAllowed": "Disabled", + "@interactiveTranslatorNotAllowed": { + "type": "text", + "placeholders": {} + }, + "interactiveTranslatorAllowed": "Student Choice", + "@interactiveTranslatorAllowed": { + "type": "text", + "placeholders": {} + }, + "interactiveTranslatorRequired": "Required", + "@interactiveTranslatorRequired": { + "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": { + "type": "text", + "placeholders": {} + }, + "interactiveTranslatorAllowedDesc": "Students can choose whether to use translation assistance in space group chats in Main Menu > My Learning Settings.", + "@interactiveTranslatorAllowedDesc": { + "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": { + "type": "text", + "placeholders": {} + }, + "notYetSet": "Not yet set", + "@notYetSet": { + "type": "text", + "placeholders": {} + }, + "multiLingualClass": "Multilingual Class", + "classAnalytics": "Class Analytics", + "@classAnalytics": { + "type": "text", + "placeholders": {} + }, + "allClasses": "All Classes", + "@allClasses": { + "type": "text", + "placeholders": {} + }, + "myLearning": "My Analytics", + "@myLearning": { + "type": "text", + "placeholders": {} + }, + "allChatsAndClasses": "All chats and spaces", + "timeOfLastMessage": "Time of last sent message", + "totalMessages": "Total messages sent", + "waTooltip": "L2 use without assistance", + "changeDateRange": "Change date range", + "numberOfStudents": "Number of Students", + "@numberOfStudents": { + "type": "text", + "placeholders": {} + }, + "classDescription": "Description", + "@classDescription": { + "type": "text", + "placeholders": {} + }, + "classDescriptionDesc": "Set a description", + "@classDescriptionDesc": { + "type": "text", + "placeholders": {} + }, + "requestToEnroll": "Request to Enroll", + "@requestToEnroll": { + "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": { + "type": "text", + "placeholders": {} + }, + "addStudents": "Add students", + "@addStudents": { + "type": "text", + "placeholders": {} + }, + "copyClassLink": "Copy invite link", + "copyClassLinkDesc": "Clicking this link will take students to the app, direct them to make an account and they will automatically join this space.", + "copyClassCode": "Copy invite code", + "inviteStudentByUserName": "Invite student by username", + "@inviteStudentByUserName": { + "type": "text", + "placeholders": {} + }, + "classSettings": "Class Settings", + "@classSettings": { + "type": "text", + "placeholders": {} + }, + "classSettingsDesc": "Edit class languages and proficiency level.", + "@classSettingsDesc": { + "type": "text", + "placeholders": {} + }, + "selectClassRoomDominantLanguage": "What is the base language of your class?", + "@selectClassRoomDominantLanguage": { + "type": "text", + "placeholders": {} + }, + "selectTargetLanguage": "What language are you teaching?", + "@selectTargetLanguage": { + "type": "text", + "placeholders": {} + }, + "whatIsYourClassLanguageLevel": "What is the average language level of your class?", + "@whatIsYourClassLanguageLevel": { + "type": "text", + "placeholders": {} + }, + "studentPermissions": "Student Permissions", + "@studentPermissions": { + "type": "text", + "placeholders": {} + }, + "interactiveTranslator": "Translation assistance", + "@interactiveTranslator": { + "type": "text", + "placeholders": {} + }, + "oneToOneChatsWithinClass": "Private Chats within Space", + "@oneToOneChatsWithinClass": { + "type": "text", + "placeholders": {} + }, + "oneToOneChatsWithinClassDesc": "If you allow private chats, students can initiate and use private chats with other space participants. Otherwise, they can only participate in groups chats.", + "@oneToOneChatsWithinClassDesc": { + "type": "text", + "placeholders": {} + }, + "createGroupChats": "Create Group Chats", + "@createGroupChats": { + "type": "text", + "placeholders": {} + }, + "createGroupChatsDesc": "Toggle this on to allow students to create group chats within the class/exchange space.", + "@createGroupChatsDesc": { + "type": "text", + "placeholders": {} + }, + "shareVideo": "Share Video", + "@shareVideo": { + "type": "text", + "placeholders": {} + }, + "shareVideoDesc": "Toggle this on to allow students to share videos in chats.", + "@shareVideoDesc": { + "type": "text", + "placeholders": {} + }, + "sharePhotos": "Share Photos", + "@sharePhotos": { + "type": "text", + "placeholders": {} + }, + "sharePhotosDesc": "Toggle this on to allow students to share photos in chats.", + "@sharePhotosDesc": { + "type": "text", + "placeholders": {} + }, + "shareFiles": "Share Files", + "@shareFiles": { + "type": "text", + "placeholders": {} + }, + "shareFilesDesc": "Toggle this on to allow students to share files in chats.", + "@shareFilesDesc": { + "type": "text", + "placeholders": {} + }, + "shareLocationDesc": "Toggle this on to allow students to share location in chats.", + "@shareLocationDesc": { + "type": "text", + "placeholders": {} + }, + "selectLanguageLevel": "Select language level", + "@selectLanguageLevel": { + "type": "text", + "placeholders": {} + }, + "noIdenticalLanguages": "Please choose different base and target languages", + "@noIdenticalLanguages": { + "type": "text", + "placeholders": {} + }, + "iWantALanguagePartnerFrom": "Is from:", + "@iWantALanguagePartnerFrom": { + "type": "text", + "placeholders": {} + }, + "worldWide": "Worldwide", + "@worldWide": { + "type": "text", + "placeholders": {} + }, + "noResults": "No results! Try broadening your search.", + "@noResults": { + "type": "text", + "placeholders": {} + }, + "searchBy": "Search by country and languages", + "@searchBy": { + "type": "text", + "placeholders": {} + }, + "iWantAConversationPartner": "I want a conversation partner who", + "@iWantAConversationPartner": { + "type": "text", + "placeholders": {} + }, + "iWantALanguagePartnerWhoSpeaks": "Speaks:", + "@iWantALanguagePartnerWhoSpeaks": { + "type": "text", + "placeholders": {} + }, + "iWantALanguagePartnerWhoIsLearning": "Is learning:", + "@iWantALanguagePartnerWhoIsLearning": { + "type": "text", + "placeholders": {} + }, + "yourBirthdayPlease": "Pangea Chat serves schools and other learning communities, ages 13 and up, around the world.\n\nIn order to protect our young learners, we ask our users to verify their age before connecting to our community.\n\nBefore you can search Pangea Chat for classes, rooms, and new friends, you must verify you are 18 or older.", + "@yourBirthdayPlease": { + "type": "text", + "placeholders": {} + }, + "invalidDob": "Invalid Date of Birth", + "@invalidDob": { + "type": "text", + "placeholders": {} + }, + "enterYourDob": "Enter your Date of Birth", + "@enterYourDob": { + "type": "text", + "placeholders": {} + }, + "getStarted": "Get Started", + "@getStarted": { + "type": "text", + "placeholders": {} + }, + "mustBe13": "User should be 13 years old", + "@mustBe13": { + "type": "text", + "placeholders": {} + }, + "yourBirthdayPleaseShort": "Please selected your age group", + "@yourBirthdayPleaseShort": { + "type": "text", + "placeholders": {} + }, + "joinWithClassCode": "Join class or exchange", + "@joinWithClassCode": { + "type": "text", + "placeholders": {} + }, + "joinWithClassCodeDesc": "Connect to a class or exchange space with the 6-digit invite code provided by the space administrator.", + "@joinWithClassCodeDesc": { + "type": "text", + "placeholders": {} + }, + "joinWithClassCodeHint": "Enter invite code", + "@joinWithClassCodeHint": { + "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": { + "type": "text", + "placeholders": {} + }, + "languageLevelPreA1": "True Beginner (Pre A1)", + "@languageLevelPreA1": { + "type": "text", + "placeholders": {} + }, + "languageLevelA1": "Beginner (A1)", + "@languageLevelA1": { + "type": "text", + "placeholders": {} + }, + "languageLevelA2": "Elementary (A2)", + "@languageLevelA2": { + "type": "text", + "placeholders": {} + }, + "languageLevelB1": "Intermediate (B1)", + "@languageLevelB1": { + "type": "text", + "placeholders": {} + }, + "languageLevelB2": "Upper Intermediate (B2)", + "@languageLevelB2": { + "type": "text", + "placeholders": {} + }, + "languageLevelC1": "Advanced (C1)", + "@languageLevelC1": { + "type": "text", + "placeholders": {} + }, + "languageLevelC2": "Mastery (C2)", + "@languageLevelC2": { + "type": "text", + "placeholders": {} + }, + "changeTheNameOfTheClass": "Change the name", + "@changeTheNameOfTheClass": { + "type": "text", + "placeholders": {} + }, + "changeTheNameOfTheChat": "Change the name of the chat", + "@changeTheNameOfTheChat": { + "type": "text", + "placeholders": {} + }, + "welcomeToYourNewClass": "Welcome! 🙂", + "@welcomeToYourNewClass": { + "type": "text", + "placeholders": {} + }, + "welcomeToClass": "Welcome! 🙂\n- Try joining a chat!\n- Have fun chatting!", + "@welcomeToClass": { + "type": "text", + "placeholders": {} + }, + "welcomeToPangea18Plus": "Welcome to Pangea Chat! 🙂\nWhat's next?\nCreate or join a class!\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": { + "type": "text", + "placeholders": {} + }, + "findALanguagePartner": "Find a conversation partner", + "@findALanguagePartner": { + "type": "text", + "placeholders": {} + }, + "setToPublicSettingsTitle": "Want to find a conversation partner?", + "@setToPublicSettingsTitle": { + "type": "text", + "placeholders": {} + }, + "setToPublicSettingsDesc": "Before you can search for a conversation parter, you must set your profile visibility to public.", + "@setToPublicSettingsDesc": { + "type": "text", + "placeholders": {} + }, + "accountSettings": "Account settings", + "@accountSettings": { + "type": "text", + "placeholders": {} + }, + "unableToFindClassCode": "Unable to find code.", + "@unableToFindClassCode": { + "type": "text", + "placeholders": {} + }, + "askPangeaBot": "Ask Pangea Bot for a contextual definition.", + "sorryNoResults": "Sorry, no results.", + "@sorryNoResults": { + "type": "text", + "placeholders": {} + }, + "ignoreInThisText": "Ignore", + "@ignoreInThisText": { + "type": "text", + "placeholders": {} + }, + "helpMeTranslate": "Help me translate!", + "@helpMeTranslate": { + "type": "text", + "placeholders": {} + }, + "needsItShortMessage": "Try interactive translation!", + "needsIGCShortMessage": "Try interactive grammar assistance!", + "@needsItShortMessage": { + "type": "text", + "placeholders": {} + }, + "needsItMessage": "This message has too many words in your base language.", + "@needsItMessage": { + "type": "text", + "placeholders": {} + }, + "needsIgcMessage": "This message has a grammar error.", + "tokenTranslationTitle": "A word is in your base language.", + "@tokenTranslationTitle": { + "type": "text", + "placeholders": {} + }, + "spanTranslationDesc": "See possible translations below.", + "@spanTranslationDesc": { + "type": "text", + "placeholders": {} + }, + "spanTranslationTitle": "Some words are in your base language.", + "@spanTranslationTitle": { + "type": "text", + "placeholders": {} + }, + "l1SpanAndGrammarTitle": "Outside target language", + "l1SpanAndGrammarDesc": "This could in your base language or it could be a grammar error.", + "otherTitle": "You have an error.", + "@otherTitle": { + "type": "text", + "placeholders": {} + }, + "otherDesc": "See possible corrections below.", + "@otherDesc": { + "type": "text", + "placeholders": {} + }, + "countryInformation": "My country", + "@countryInformation": { + "type": "text", + "placeholders": {} + }, + "myLanguages": "My base and target language", + "@myLanguages": { + "type": "text", + "placeholders": {} + }, + "targetLanguage": "Target Language", + "@targetLanguage": { + "type": "text", + "placeholders": {} + }, + "sourceLanguage": "Base language", + "@sourceLanguage": { + "type": "text", + "placeholders": {} + }, + "languagesISpeak": "Languages I speak", + "@languagesISpeak": { + "type": "text", + "placeholders": {} + }, + "updateLanguage": "My languages", + "@updateLanguage": { + "type": "text", + "placeholders": {} + }, + "whatLanguageYouWantToLearn": "What language do you want to learn?", + "@whatLanguageYouWantToLearn": { + "type": "text", + "placeholders": {} + }, + "whatIsYourBaseLanguage": "What is your base language?", + "@whatIsYourBaseLanguage": { + "type": "text", + "placeholders": {} + }, + "saveChanges": "Save changes", + "@saveChanges": { + "type": "text", + "placeholders": {} + }, + "publicProfileTitle": "Public Profile", + "@publicProfileTitle": { + "type": "text", + "placeholders": {} + }, + "publicProfileDesc": "Your profile must be public in order to search or be found as a conversation partner.", + "@publicProfileDesc": { + "type": "text", + "placeholders": {} + }, + "errorDisableIT": "Translation assistance is turned off.", + "errorDisableIGC": "Grammar assistance is turned off.", + "errorDisableLanguageAssistance": "Translation assistance and grammar assistance are turned off.", + "errorDisableITUserDesc": "Click here to update translation assistance settings", + "errorDisableIGCUserDesc": "Click here to update grammar assistance settings", + "errorDisableLanguageAssistanceUserDesc": "Click here to update translation assistance and grammar assistance settings", + "errorDisableITClassDesc": "Translation assistance is turned off for the space that this chat is in.", + "errorDisableIGCClassDesc": "Grammar assistance is turned off for the space that this chat is in.", + "errorDisableLanguageAssistanceClassDesc": "Translation assistance and grammar assistance are turned off for the space that this chat is in.", + "itIsDisabled": "Interactive Translation is disabled", + "igcIsDisabled": "Interactive Grammar Checking is disabled", + "goToLearningSettings": "Go to My Learning Settings", + "error405Title": "Languages not set", + "error405Desc": "Please set your languages in Main Menu > My Learning Settings.", + "loginOrSignup": "Sign in with", + "@loginOrSignup": { + "type": "text", + "placeholders": {} + }, + "iAgreeToThe": "I agree to the ", + "@iAgreeToThe": { + "type": "text", + "placeholders": {} + }, + "termsAndConditions": "Terms and Conditions", + "@termsAndConditions": { + "type": "text", + "placeholders": {} + }, + "andCertifyIAmAtLeast13YearsOfAge": " and certify I am at least 13 years of age.", + "@andCertifyIAmAtLeast13YearsOfAge": { + "type": "text", + "placeholders": {} + }, + "error502504Title": "Wow, there are a lot of students online!", + "@error502504Title": { + "type": "text", + "placeholders": {} + }, + "error502504Desc": "Translation and grammar tools may be slow or unavailable while the Pangea bots catch up.", + "@error502504Desc": { + "type": "text", + "placeholders": {} + }, + "error404Title": "Translation error!", + "@error404Title": { + "type": "text", + "placeholders": {} + }, + "error404Desc": "Pangea Bot isn't sure how to translate that...", + "@error404Desc": { + "type": "text", + "placeholders": {} + }, + "errorPleaseRefresh": "We're looking into it! Please reload and try again.", + "@errorPleaseRefresh": { + "type": "text", + "placeholders": {} + }, + "findAClass": "Find a class (coming soon)", + "toggleIT": "Interactive Translation", + "@toggleIT": { + "type": "text", + "placeholders": {} + }, + "toggleIGC": "Interactive Grammar Checking", + "@toggleIGC": { + "type": "text", + "placeholders": {} + }, + "toggleToolSettingsDescription": "Here you can toggle your individual language tool settings. For chats within a space, the space settings will take precedence and may override these settings.", + "connectedToStaging": "You are connected to the staging server.", + "@connectedToStaging": { + "type": "text", + "placeholders": {} + }, + "learningSettings": "My Learning Settings", + "classNameRequired": "Please enter a space name", + "@classNameRequired": { + "type": "text", + "placeholders": {} + }, + "sendVoiceNotes": "Send Voice Notes", + "@sendVoiceNotes": { + "type": "text", + "placeholders": {} + }, + "sendVoiceNotesDesc": "Toggle this on to allow students to send voice notes in chats.", + "@sendVoiceNotesDesc": { + "type": "text", + "placeholders": {} + }, + "chatTopic": "Chat topic", + "@chatTopic": { + "type": "text", + "placeholders": {} + }, + "chatTopicDesc": "Set a chat topic", + "@chatTopicDesc": { + "type": "text", + "placeholders": {} + }, + "inviteStudentByUserNameDesc": "If your student already has an account, you can search for them.", + "@inviteStudentByUserNameDesc": { + "type": "text", + "placeholders": {} + }, + "classRoster": "Participants", + "@classRoster": { + "type": "text", + "placeholders": {} + }, + "almostPerfect": "That seems right! Here's what I would have said.", + "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!", + "understandingMessagesTitle": "Definitions and translations!", + "understandingMessagesBody": "Click underlined words for definitions. Translate with message options (upper right).", + "allDone": "All done!", + "vocab": "Vocabulary", + "low": "We have evidence the user does not understand these words.", + "medium": "These words have been used. It is unclear if the words are fully understood or not.", + "high": "We have evidence the user understands these words.", + "unknownProficiency": "These words have not been used in Pangea Chat.", + "changeView": "Switch views.", + "clearAll": "Clear all words?", + "generateVocabulary": "Generate vocabulary from title and description", + "generatePrompts": "Generate prompts", + "subscribe": "Subscribe", + "getAccess": "Unlock learning tools", + "subscriptionDesc": "Messaging is free! Subscribe to unlock interactive translation, grammar checking and learning analytics.", + "subscriptionManagement": "Subscription Management", + "currentSubscription": "Current Subscription", + "changeSubscription": "Change your subscription", + "cancelSubscription": "Cancel your subscription", + "selectYourPlan": "Select Your Plan", + "subsciptionPlatformTooltip": "Please login to your original device to manage your subscription plan", + "subscriptionManagementUnavailable": "Subscription management not available", + "paymentMethod": "Payment Method", + "paymentHistory": "Payment History", + "emptyChatDownloadWarning": "Cannot download empty chat", + "appUpdateAvailable": "Update Available", + "update": "Update", + "updateDesc": "You can now update this app from {localVersion} to {storeVersion}", + "@updateDesc": { + "type": "text", + "placeholders": { + "storeVersion": {}, + "localVersion": {} + } + }, + "maybeLater": "Maybe Later", + "mainMenu": "Main Menu", + "toggleImmersionMode": "Immersion Mode", + "toggleImmersionModeDesc": "When enabled, all messages are displayed in your target language. This setting is most useful in language exchanges.", + "itToggleDescription": "This language learning tool will identify words in your base language and help you translate them to your target language. Though rare, the AI can make translation errors.", + "igcToggleDescription": "This language learning tool will identify common spelling, grammar and punctuation errors in your message and suggest corrections. Though rare, the AI can make correction errors.", + "sendOnEnterDescription": "Turn this off to be able to add line spaces in messages. When the toggle is off on the browser app, you can press Shift + Enter to start a new line. When the toggle is off on mobile apps, just Enter will start a new line.", + "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.", + "originalMessage": "Original Message", + "sentMessage": "Sent Message", + "useType": "Use Type", + "notAvailable": "Not Available", + "taAndGaTooltip": "L2 use with translation assistance and grammar assistance", + "definitionsToolName": "Word Definitions", + "messageTranslationsToolName": "Message Translations", + "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", + "downloadCSVFile": "Download CSV File", + "promotionalSubscriptionDesc": "You currently have a lifetime promotional subscription. Message support@pangea.chat for help changing your subscription.", + "originalSubscriptionPlatform": "Subscription purchased through {purchasePlatform}", + "@originalSubscriptionPlatform": { + "placeholders": { + "purchasePlatform": {} + } + }, + "oneWeekTrial": "One Week Trial", + "creatingSpacePleaseWait": "Creating space. Please wait...", + "downloadXLSXFile": "Download Excel File", + "abDisplayName": "Abkhaz", + "aaDisplayName": "Afar", + "afDisplayName": "Afrikaans", + "akDisplayName": "Akan", + "sqDisplayName": "Albanian", + "amDisplayName": "Amharic", + "arDisplayName": "Arabic", + "anDisplayName": "Aragonese", + "hyDisplayName": "Armenian", + "asDisplayName": "Assamese", + "avDisplayName": "Avaric", + "aeDisplayName": "Avestan", + "ayDisplayName": "Aymara", + "azDisplayName": "Azerbaijani", + "bmDisplayName": "Bambara", + "baDisplayName": "Bashkir", + "euDisplayName": "Basque", + "beDisplayName": "Belarusian", + "bnDisplayName": "Bengali", + "bhDisplayName": "Bihari", + "biDisplayName": "Bislama", + "bsDisplayName": "Bosnian", + "brDisplayName": "Breton", + "bgDisplayName": "Bulgarian", + "myDisplayName": "Burmese", + "caDisplayName": "Catalan, Valencian", + "chDisplayName": "Chamorro", + "ceDisplayName": "Chechen", + "nyDisplayName": "Chichewa, Chewa, Nyanja", + "zhDisplayName": "Chinese", + "cvDisplayName": "Chuvash", + "kwDisplayName": "Cornish", + "coDisplayName": "Corsican", + "crDisplayName": "Cree", + "hrDisplayName": "Croatian", + "csDisplayName": "Czech", + "daDisplayName": "Danish", + "dvDisplayName": "Divehi; Dhivehi; Maldivian;", + "nlDisplayName": "Dutch", + "enDisplayName": "English", + "eoDisplayName": "Esperanto", + "etDisplayName": "Estonian", + "eeDisplayName": "Ewe", + "foDisplayName": "Faroese", + "fjDisplayName": "Fijian", + "fiDisplayName": "Finnish", + "frDisplayName": "French", + "ffDisplayName": "Fula; Fulah; Pulaar; Pular", + "glDisplayName": "Galician", + "kaDisplayName": "Georgian", + "deDisplayName": "German", + "elDisplayName": "Greek, Modern", + "gnDisplayName": "Guaraní", + "guDisplayName": "Gujarati", + "htDisplayName": "Haitian, Haitian Creole", + "haDisplayName": "Hausa", + "heDisplayName": "Hebrew (modern)", + "hzDisplayName": "Herero", + "hiDisplayName": "Hindi", + "hoDisplayName": "Hiri Motu", + "huDisplayName": "Hungarian", + "iaDisplayName": "Interlingua", + "idDisplayName": "Indonesian", + "ieDisplayName": "Interlingue", + "gaDisplayName": "Irish", + "igDisplayName": "Igbo", + "ikDisplayName": "Inupiaq", + "ioDisplayName": "Ido", + "isDisplayName": "Icelandic", + "itDisplayName": "Italian", + "iuDisplayName": "Inuktitut", + "jaDisplayName": "Japanese", + "jvDisplayName": "Javanese", + "klDisplayName": "Kalaallisut, Greenlandic", + "knDisplayName": "Kannada", + "krDisplayName": "Kanuri", + "ksDisplayName": "Kashmiri", + "kkDisplayName": "Kazakh", + "kmDisplayName": "Khmer", + "kiDisplayName": "Kikuyu, Gikuyu", + "rwDisplayName": "Kinyarwanda", + "kyDisplayName": "Kirghiz, Kyrgyz", + "kvDisplayName": "Komi", + "kgDisplayName": "Kongo", + "koDisplayName": "Korean", + "kuDisplayName": "Kurdish", + "kjDisplayName": "Kwanyama, Kuanyama", + "laDisplayName": "Latin", + "lbDisplayName": "Luxembourgish, Letzeburgesch", + "lgDisplayName": "Luganda", + "liDisplayName": "Limburgish, Limburgan, Limburger", + "lnDisplayName": "Lingala", + "loDisplayName": "Lao", + "ltDisplayName": "Lithuanian", + "luDisplayName": "Luba-Katanga", + "lvDisplayName": "Latvian", + "gvDisplayName": "Manx", + "mkDisplayName": "Macedonian", + "mgDisplayName": "Malagasy", + "msDisplayName": "Malay", + "mlDisplayName": "Malayalam", + "mtDisplayName": "Maltese", + "miDisplayName": "Māori", + "mrDisplayName": "Marathi (Marāṭhī)", + "mhDisplayName": "Marshallese", + "mnDisplayName": "Mongolian", + "naDisplayName": "Nauru", + "nvDisplayName": "Navajo, Navaho", + "nbDisplayName": "Norwegian Bokmål", + "ndDisplayName": "North Ndebele", + "neDisplayName": "Nepali", + "ngDisplayName": "Ndonga", + "nnDisplayName": "Norwegian Nynorsk", + "noDisplayName": "Norwegian", + "iiDisplayName": "Nuosu", + "nrDisplayName": "South Ndebele", + "ocDisplayName": "Occitan", + "ojDisplayName": "Ojibwe, Ojibwa", + "cuDisplayName": "Old Church Slavonic, Church Slavic, Church Slavonic, Old Bulgarian, Old Slavonic", + "omDisplayName": "Oromo", + "orDisplayName": "Oriya", + "osDisplayName": "Ossetian, Ossetic", + "paDisplayName": "Panjabi, Punjabi", + "piDisplayName": "Pāli", + "faDisplayName": "Persian", + "plDisplayName": "Polish", + "psDisplayName": "Pashto, Pushto", + "ptDisplayName": "Portuguese", + "quDisplayName": "Quechua", + "rmDisplayName": "Romansh", + "rnDisplayName": "Kirundi", + "roDisplayName": "Romanian, Moldavian, Moldovan", + "ruDisplayName": "Russian", + "saDisplayName": "Sanskrit (Saṁskṛta)", + "scDisplayName": "Sardinian", + "sdDisplayName": "Sindhi", + "seDisplayName": "Northern Sami", + "smDisplayName": "Samoan", + "sgDisplayName": "Sango", + "srDisplayName": "Serbian", + "gdDisplayName": "Scottish Gaelic, Gaelic", + "snDisplayName": "Shona", + "siDisplayName": "Sinhala, Sinhalese", + "skDisplayName": "Slovak", + "slDisplayName": "Slovene", + "soDisplayName": "Somali", + "stDisplayName": "Southern Sotho", + "esDisplayName": "Spanish", + "suDisplayName": "Sundanese", + "swDisplayName": "Swahili", + "ssDisplayName": "Swati", + "svDisplayName": "Swedish", + "taDisplayName": "Tamil", + "teDisplayName": "Telugu", + "tgDisplayName": "Tajik", + "thDisplayName": "Thai", + "tiDisplayName": "Tigrinya", + "boDisplayName": "Tibetan Standard, Tibetan, Central", + "tkDisplayName": "Turkmen", + "tlDisplayName": "Tagalog", + "tnDisplayName": "Tswana", + "toDisplayName": "Tonga (Tonga Islands)", + "trDisplayName": "Turkish", + "tsDisplayName": "Tsonga", + "ttDisplayName": "Tatar", + "twDisplayName": "Twi", + "tyDisplayName": "Tahitian", + "ugDisplayName": "Uighur, Uyghur", + "ukDisplayName": "Ukrainian", + "urDisplayName": "Urdu", + "uzDisplayName": "Uzbek", + "veDisplayName": "Venda", + "viDisplayName": "Vietnamese", + "voDisplayName": "Volapük", + "waDisplayName": "Walloon", + "cyDisplayName": "Welsh", + "woDisplayName": "Wolof", + "fyDisplayName": "Western Frisian", + "xhDisplayName": "Xhosa", + "yiDisplayName": "Yiddish", + "yoDisplayName": "Yoruba", + "zaDisplayName": "Zhuang, Chuang", + "unkDisplayName": "Unknown", + "zuDisplayName": "Zulu", + "hawDisplayName": "Hawaiian", + "hmnDisplayName": "Hmong", + "multiDisplayName": "Multi", + "cebDisplayName": "Cebuano", + "dzDisplayName": "Dzongkha", + "iwDisplayName": "Hebrew", + "jwDisplayName": "Javanese", + "moDisplayName": "Moldavian", + "shDisplayName": "Serbo-Croatian", + "wwCountryDisplayName": "World Wide", + "afCountryDisplayName": "Afghanistan", + "axCountryDisplayName": "Aland Islands", + "alCountryDisplayName": "Albania", + "dzCountryDisplayName": "Algeria", + "asCountryDisplayName": "American Samoa", + "adCountryDisplayName": "Andorra", + "aoCountryDisplayName": "Angola", + "aiCountryDisplayName": "Anguilla", + "agCountryDisplayName": "Antigua and Barbuda", + "arCountryDisplayName": "Argentina", + "amCountryDisplayName": "Armenia", + "awCountryDisplayName": "Aruba", + "acCountryDisplayName": "Ascension Island", + "auCountryDisplayName": "Australia", + "atCountryDisplayName": "Austria", + "azCountryDisplayName": "Azerbaijan", + "bsCountryDisplayName": "Bahamas", + "bhCountryDisplayName": "Bahrain", + "bdCountryDisplayName": "Bangladesh", + "bbCountryDisplayName": "Barbados", + "byCountryDisplayName": "Belarus", + "beCountryDisplayName": "Belgium", + "bzCountryDisplayName": "Belize", + "bjCountryDisplayName": "Benin", + "bmCountryDisplayName": "Bermuda", + "btCountryDisplayName": "Bhutan", + "boCountryDisplayName": "Bolivia", + "baCountryDisplayName": "Bosnia and Herzegovina", + "bwCountryDisplayName": "Botswana", + "brCountryDisplayName": "Brazil", + "ioCountryDisplayName": "British Indian Ocean Territory", + "vgCountryDisplayName": "British Virgin Islands", + "bnCountryDisplayName": "Brunei", + "bgCountryDisplayName": "Bulgaria", + "bfCountryDisplayName": "Burkina Faso", + "biCountryDisplayName": "Burundi", + "khCountryDisplayName": "Cambodia", + "cmCountryDisplayName": "Cameroon", + "caCountryDisplayName": "Canada", + "cvCountryDisplayName": "Cape Verde", + "bqCountryDisplayName": "Caribbean Netherlands", + "kyCountryDisplayName": "Cayman Islands", + "cfCountryDisplayName": "Central African Republic", + "tdCountryDisplayName": "Chad", + "clCountryDisplayName": "Chile", + "cnCountryDisplayName": "China", + "cxCountryDisplayName": "Christmas Island", + "ccCountryDisplayName": "Cocos [Keeling] Islands", + "coCountryDisplayName": "Colombia", + "kmCountryDisplayName": "Comoros", + "cdCountryDisplayName": "Democratic Republic Congo", + "cgCountryDisplayName": "Republic of Congo", + "ckCountryDisplayName": "Cook Islands", + "crCountryDisplayName": "Costa Rica", + "ciCountryDisplayName": "Côte d'Ivoire", + "hrCountryDisplayName": "Croatia", + "cuCountryDisplayName": "Cuba", + "cwCountryDisplayName": "Curaçao", + "cyCountryDisplayName": "Cyprus", + "czCountryDisplayName": "Czech Republic", + "dkCountryDisplayName": "Denmark", + "djCountryDisplayName": "Djibouti", + "dmCountryDisplayName": "Dominica", + "doCountryDisplayName": "Dominican Republic", + "tlCountryDisplayName": "East Timor", + "ecCountryDisplayName": "Ecuador", + "egCountryDisplayName": "Egypt", + "svCountryDisplayName": "El Salvador", + "gqCountryDisplayName": "Equatorial Guinea", + "erCountryDisplayName": "Eritrea", + "eeCountryDisplayName": "Estonia", + "szCountryDisplayName": "Eswatini", + "etCountryDisplayName": "Ethiopia", + "fkCountryDisplayName": "Falkland Islands", + "foCountryDisplayName": "Faroe Islands", + "fjCountryDisplayName": "Fiji", + "fiCountryDisplayName": "Finland", + "frCountryDisplayName": "France", + "gfCountryDisplayName": "French Guiana", + "pfCountryDisplayName": "French Polynesia", + "gaCountryDisplayName": "Gabon", + "gmCountryDisplayName": "Gambia", + "geCountryDisplayName": "Georgia", + "deCountryDisplayName": "Germany", + "ghCountryDisplayName": "Ghana", + "giCountryDisplayName": "Gibraltar", + "grCountryDisplayName": "Greece", + "glCountryDisplayName": "Greenland", + "gdCountryDisplayName": "Grenada", + "gpCountryDisplayName": "Guadeloupe", + "guCountryDisplayName": "Guam", + "gtCountryDisplayName": "Guatemala", + "ggCountryDisplayName": "Guernsey", + "gnCountryDisplayName": "Guinea Conakry", + "gwCountryDisplayName": "Guinea-Bissau", + "gyCountryDisplayName": "Guyana", + "htCountryDisplayName": "Haiti", + "hmCountryDisplayName": "Heard Island and McDonald Islands", + "hnCountryDisplayName": "Honduras", + "hkCountryDisplayName": "Hong Kong", + "huCountryDisplayName": "Hungary", + "isCountryDisplayName": "Iceland", + "inCountryDisplayName": "India", + "idCountryDisplayName": "Indonesia", + "irCountryDisplayName": "Iran", + "iqCountryDisplayName": "Iraq", + "ieCountryDisplayName": "Ireland", + "imCountryDisplayName": "Isle of Man", + "ilCountryDisplayName": "Israel", + "itCountryDisplayName": "Italy", + "jmCountryDisplayName": "Jamaica", + "jpCountryDisplayName": "Japan", + "jeCountryDisplayName": "Jersey", + "joCountryDisplayName": "Jordan", + "kzCountryDisplayName": "Kazakhstan", + "keCountryDisplayName": "Kenya", + "kiCountryDisplayName": "Kiribati", + "xkCountryDisplayName": "Kosovo", + "kwCountryDisplayName": "Kuwait", + "kgCountryDisplayName": "Kyrgyzstan", + "laCountryDisplayName": "Laos", + "lvCountryDisplayName": "Latvia", + "lbCountryDisplayName": "Lebanon", + "lsCountryDisplayName": "Lesotho", + "lrCountryDisplayName": "Liberia", + "lyCountryDisplayName": "Libya", + "liCountryDisplayName": "Liechtenstein", + "ltCountryDisplayName": "Lithuania", + "luCountryDisplayName": "Luxembourg", + "moCountryDisplayName": "Macau", + "mkCountryDisplayName": "Macedonia", + "mgCountryDisplayName": "Madagascar", + "mwCountryDisplayName": "Malawi", + "myCountryDisplayName": "Malaysia", + "mvCountryDisplayName": "Maldives", + "mlCountryDisplayName": "Mali", + "mtCountryDisplayName": "Malta", + "mhCountryDisplayName": "Marshall Islands", + "mqCountryDisplayName": "Martinique", + "mrCountryDisplayName": "Mauritania", + "muCountryDisplayName": "Mauritius", + "ytCountryDisplayName": "Mayotte", + "mxCountryDisplayName": "Mexico", + "fmCountryDisplayName": "Micronesia", + "mdCountryDisplayName": "Moldova", + "mcCountryDisplayName": "Monaco", + "mnCountryDisplayName": "Mongolia", + "meCountryDisplayName": "Montenegro", + "msCountryDisplayName": "Montserrat", + "maCountryDisplayName": "Morocco", + "mzCountryDisplayName": "Mozambique", + "mmCountryDisplayName": "Myanmar (Burma)", + "naCountryDisplayName": "Namibia", + "nrCountryDisplayName": "Nauru", + "npCountryDisplayName": "Nepal", + "nlCountryDisplayName": "Netherlands", + "ncCountryDisplayName": "New Caledonia", + "nzCountryDisplayName": "New Zealand", + "niCountryDisplayName": "Nicaragua", + "neCountryDisplayName": "Niger", + "ngCountryDisplayName": "Nigeria", + "nuCountryDisplayName": "Niue", + "nfCountryDisplayName": "Norfolk Island", + "kpCountryDisplayName": "North Korea", + "mpCountryDisplayName": "Northern Mariana Islands", + "noCountryDisplayName": "Norway", + "omCountryDisplayName": "Oman", + "pkCountryDisplayName": "Pakistan", + "pwCountryDisplayName": "Palau", + "psCountryDisplayName": "Palestinian Territories", + "paCountryDisplayName": "Panama", + "pgCountryDisplayName": "Papua New Guinea", + "pyCountryDisplayName": "Paraguay", + "peCountryDisplayName": "Peru", + "phCountryDisplayName": "Philippines", + "plCountryDisplayName": "Poland", + "ptCountryDisplayName": "Portugal", + "prCountryDisplayName": "Puerto Rico", + "qaCountryDisplayName": "Qatar", + "reCountryDisplayName": "Réunion", + "roCountryDisplayName": "Romania", + "ruCountryDisplayName": "Russia", + "rwCountryDisplayName": "Rwanda", + "blCountryDisplayName": "Saint Barthélemy", + "shCountryDisplayName": "Saint Helena", + "knCountryDisplayName": "St. Kitts", + "lcCountryDisplayName": "St. Lucia", + "mfCountryDisplayName": "Saint Martin", + "pmCountryDisplayName": "Saint Pierre and Miquelon", + "vcCountryDisplayName": "St. Vincent", + "wsCountryDisplayName": "Samoa", + "smCountryDisplayName": "San Marino", + "stCountryDisplayName": "São Tomé and Príncipe", + "saCountryDisplayName": "Saudi Arabia", + "snCountryDisplayName": "Senegal", + "rsCountryDisplayName": "Serbia", + "scCountryDisplayName": "Seychelles", + "slCountryDisplayName": "Sierra Leone", + "sgCountryDisplayName": "Singapore", + "sxCountryDisplayName": "Sint Maarten", + "skCountryDisplayName": "Slovakia", + "siCountryDisplayName": "Slovenia", + "sbCountryDisplayName": "Solomon Islands", + "soCountryDisplayName": "Somalia", + "zaCountryDisplayName": "South Africa", + "gsCountryDisplayName": "South Georgia and the South Sandwich Islands", + "krCountryDisplayName": "South Korea", + "ssCountryDisplayName": "South Sudan", + "esCountryDisplayName": "Spain", + "lkCountryDisplayName": "Sri Lanka", + "sdCountryDisplayName": "Sudan", + "srCountryDisplayName": "Suriname", + "sjCountryDisplayName": "Svalbard and Jan Mayen", + "seCountryDisplayName": "Sweden", + "chCountryDisplayName": "Switzerland", + "syCountryDisplayName": "Syria", + "twCountryDisplayName": "Taiwan", + "tjCountryDisplayName": "Tajikistan", + "tzCountryDisplayName": "Tanzania", + "thCountryDisplayName": "Thailand", + "tgCountryDisplayName": "Togo", + "tkCountryDisplayName": "Tokelau", + "toCountryDisplayName": "Tonga", + "ttCountryDisplayName": "Trinidad/Tobago", + "tnCountryDisplayName": "Tunisia", + "trCountryDisplayName": "Turkey", + "tmCountryDisplayName": "Turkmenistan", + "tcCountryDisplayName": "Turks and Caicos Islands", + "tvCountryDisplayName": "Tuvalu", + "viCountryDisplayName": "U.S. Virgin Islands", + "ugCountryDisplayName": "Uganda", + "uaCountryDisplayName": "Ukraine", + "aeCountryDisplayName": "United Arab Emirates", + "gbCountryDisplayName": "United Kingdom", + "usCountryDisplayName": "United States", + "uyCountryDisplayName": "Uruguay", + "uzCountryDisplayName": "Uzbekistan", + "vuCountryDisplayName": "Vanuatu", + "vaCountryDisplayName": "Vatican City", + "veCountryDisplayName": "Venezuela", + "vnCountryDisplayName": "Vietnam", + "wfCountryDisplayName": "Wallis and Futuna", + "ehCountryDisplayName": "Western Sahara", + "yeCountryDisplayName": "Yemen", + "zmCountryDisplayName": "Zambia", + "zwCountryDisplayName": "Zimbabwe", + "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": { + "placeholders": { + "classOrExchange": {}, + "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.", + "noEligibleSpaces": "There are no eligible spaces to add this to.", + "youAddedToSpace": "You added {child} to {space}", + "@youAddedToSpace": { + "placeholders": { + "child": {}, + "space": {} + } + }, + "youRemovedFromSpace": "You removed {child} from {space}", + "@youRemovedFromSpace": { + "placeholders": { + "child": {}, + "space": {} + } + }, + "invitedToChat": "{user} has invited you to join a chat: {name}! Do you wish to accept?", + "@invitedToChat": { + "placeholders": { + "name": {}, + "user": {} + } + }, + "monthlySubscription": "Monthly", + "yearlySubscription": "Yearly", + "defaultSubscription": "Pangea Chat Subscription", + "freeTrial": "Free Trial", + "grammarAnalytics": "Error Analytics", + "total": "Total: ", + "noDataFound": "No data found", + "promoSubscriptionExpirationDesc": "Your current subscription is promotional and expires on {expiration}. Message support@pangea.chat for help changing your subscription.", + "@promoSubscriptionExpirationDesc": { + "placeholders": { + "expiration": {} + } + }, + "emptyChatNameWarning": "Please enter a name for this chat", + "emptyClassNameWarning": "Please enter a name for this class", + "emptyExchangeNameWarning": "Please enter a name for this exchange", + "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.", + "someErrorTitle": "Hm, something's not right", + "someErrorBody": "It could be an error or something in your base language.", + "bestCorrectionFeedback": "That's correct!", + "distractorFeedback": "That's not quite right.", + "bestAnswerFeedback": "That's correct!", + "definitionDefaultPrompt": "What does this word mean?", + "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", + "lockedChatWarning": "🔒 This chat has been locked", + "lockSpace": "Lock Space", + "lockChat": "Lock Chat", + "archiveSpace": "Archive Space", + "suggestToChat": "Suggest this chat", + "suggestToChatDesc": "Suggested chats will appear in chat lists", + "acceptSelection": "Accept Correction", + "acceptSelectionAnyway": "Use this anyway", + "makingActivity": "Making activity", + "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": { + "placeholders": { + "reportingUserId": {}, + "reportedUserId": {}, + "roomName": {} + } + }, + "reportMessageBody": "Message: {reportedMessage}\nReason: {reason}", + "@reportMessageBody": { + "placeholders": { + "reportedMessage": {}, + "reason": {} + } + }, + "noTeachersFound": "No teachers found to report to", + "pleaseEnterANumber": "Please enter a number greater than 0", + "archiveRoomDescription": "The chat will be moved to the archive for yourself and other non-admin users.", + "roomUpgradeDescription": "The chat will then be recreated with the new room version. All participants will be notified that they need to switch to the new chat. You can find out more about room versions at https://spec.matrix.org/latest/rooms/", + "removeDevicesDescription": "You will be logged out of this device and will no longer be able to receive messages.", + "banUserDescription": "The user will be banned from the chat and will not be able to enter the chat again until they are unbanned.", + "unbanUserDescription": "The user will be able to enter the chat again if they try.", + "kickUserDescription": "The user is kicked out of the chat but not banned. In public chats, the user can rejoin at any time.", + "makeAdminDescription": "Once you make this user admin, you may not be able to undo this as they will then have the same permissions as you.", + "pushNotificationsNotAvailable": "Push notifications not available", + "learnMore": "Learn more", + "yourGlobalUserIdIs": "Your global user-ID is: ", + "noUsersFoundWithQuery": "Unfortunately no user could be found with \"{query}\". Please check whether you made a typo.", + "@noUsersFoundWithQuery": { + "type": "text", + "placeholders": { + "query": {} + } + }, + "searchChatsRooms": "Search for #chats, @users...", + "createClass": "Create class", + "createExchange": "Create exchange", + "viewArchive": "View Archive", + "trialExpiration": "Your free trial expires on {expiration}", + "@trialExpiration": { + "placeholders": { + "expiration": {} + } + }, + "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", + "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.", + "errorGettingAudio": "Error getting audio. Please refresh and try again.", + "nothingFound": "Nothing found...", + "groupName": "Group name", + "createGroupAndInviteUsers": "Create a group and invite users", + "groupCanBeFoundViaSearch": "Group can be found via search", + "wrongRecoveryKey": "Sorry... this does not seem to be the correct recovery key.", + "startConversation": "Start conversation", + "commandHint_sendraw": "Send raw json", + "databaseMigrationTitle": "Database is optimized", + "databaseMigrationBody": "Please wait. This may take a moment.", + "leaveEmptyToClearStatus": "Leave empty to clear your status.", + "select": "Select", + "searchForUsers": "Search for @users...", + "pleaseEnterYourCurrentPassword": "Please enter your current password", + "newPassword": "New password", + "pleaseChooseAStrongPassword": "Please choose a strong password", + "passwordsDoNotMatch": "Passwords do not match", + "passwordIsWrong": "Your entered password is wrong", + "publicLink": "Public link", + "joinSpace": "Join space", + "publicSpaces": "Public spaces", + "addChatOrSubSpace": "Add chat or sub space", + "subspace": "Subspace", + "decline": "Decline", + "thisDevice": "This device:", + "initAppError": "An error occured while init the app", + "databaseBuildErrorBody": "Unable to build the SQlite database. The app tries to use the legacy database for now. Please report this error to the developers at {url}. The error message is: {error}", + "@databaseBuildErrorBody": { + "type": "text", + "placeholders": { + "url": {}, + "error": {} + } + }, + "sessionLostBody": "Your session is lost. Please report this error to the developers at {url}. The error message is: {error}", + "@sessionLostBody": { + "type": "text", + "placeholders": { + "url": {}, + "error": {} + } + }, + "restoreSessionBody": "The app now tries to restore your session from the backup. Please report this error to the developers at {url}. The error message is: {error}", + "@restoreSessionBody": { + "type": "text", + "placeholders": { + "url": {}, + "error": {} + } + }, + "forwardMessageTo": "Forward message to {roomName}?", + "@forwardMessageTo": { + "type": "text", + "placeholders": { + "roomName": {} + } + }, + "signUp": "Sign up", + "pleaseChooseAtLeastChars": "Please choose at least {min} characters.", + "@pleaseChooseAtLeastChars": { + "type": "text", + "placeholders": { + "min": {} + } + }, + "noEmailWarning": "Please enter a valid email address. Otherwise you won't be able to reset your password. If you don't want to, tap again on the button to continue.", + "pleaseEnterValidEmail": "Please enter a valid email address.", + "noAddToSpacePermissions": "You can't add a chat to this space", + "alreadyInSpace": "The chat is already in this space", + "pleaseChooseAUsername": "Please choose a username", + "@pleaseChooseAUsername": { + "type": "text", + "placeholders": {} + }, + "chooseAUsername": "Choose a username", + "@chooseAUsername": { + "type": "text", + "placeholders": {} + }, + "define": "Define", + "listen": "Listen", + "addConversationBot": "Enable Conversation Bot", + "addConversationBotDesc": "Add a bot to this group chat that will ask questions on a specific topic", + "convoBotSettingsTitle": "Conversation Bot Settings", + "convoBotSettingsDescription": "Edit conversation topic and difficulty", + "enterAConversationTopic": "Enter a conversation topic", + "conversationTopic": "Conversation topic", + "enableModeration": "Enable moderation", + "enableModerationDesc": "Enable automatic moderation to review messages before they are sent", + "conversationLanguageLevel": "What is the language level of this conversation?", + "showDefinition": "Show Definition", + "sendReadReceipts": "Send read receipts", + "sendTypingNotificationsDescription": "Other participants in a chat can see when you are typing a new message.", + "sendReadReceiptsDescription": "Other participants in a chat can see when you have read a message.", + "formattedMessages": "Formatted messages", + "formattedMessagesDescription": "Display rich message content like bold text using markdown.", + "verifyOtherUser": "🔐 Verify other user", + "verifyOtherUserDescription": "If you verify another user, you can be sure that you know who you are really writing to. 💪\n\nWhen you start a verification, you and the other user will see a popup in the app. There you will then see a series of emojis or numbers that you have to compare with each other.\n\nThe best way to do this is to meet up or start a video call. 👭", + "verifyOtherDevice": "🔐 Verify other device", + "verifyOtherDeviceDescription": "When you verify another device, those devices can exchange keys, increasing your overall security. 💪 When you start a verification, a popup will appear in the app on both devices. There you will then see a series of emojis or numbers that you have to compare with each other. It's best to have both devices handy before you start the verification. 🤳", + "acceptedKeyVerification": "{sender} accepted key verification", + "@acceptedKeyVerification": { + "type": "text", + "placeholders": { + "sender": {} + } + }, + "canceledKeyVerification": "{sender} canceled key verification", + "@canceledKeyVerification": { + "type": "text", + "placeholders": { + "sender": {} + } + }, + "completedKeyVerification": "{sender} completed key verification", + "@completedKeyVerification": { + "type": "text", + "placeholders": { + "sender": {} + } + }, + "isReadyForKeyVerification": "{sender} is ready for key verification", + "@isReadyForKeyVerification": { + "type": "text", + "placeholders": { + "sender": {} + } + }, + "requestedKeyVerification": "{sender} requested key verification", + "@requestedKeyVerification": { + "type": "text", + "placeholders": { + "sender": {} + } + }, + "startedKeyVerification": "{sender} started key verification", + "@startedKeyVerification": { + "type": "text", + "placeholders": { + "sender": {} + } + }, + "subscriptionPopupTitle": "This sentence could have a grammar mistake...", + "subscriptionPopupDesc": "Subscribe today to unlock translation and grammar correction!", + "seeOptions": "See options", + "continuedWithoutSubscription": "Continue without subscribing", + "trialPeriodExpired": "Your trial period has expired", + "selectToDefine": "Double click a word to see its definition!", + "translations": "translations", + "messageAudio": "message audio", + "definitions": "definitions", + "subscribedToUnlockTools": "Subscribe to unlock language tools, including", + "more": "More", + "translationTooltip": "Translate", + "audioTooltip": "Play Audio", + "speechToTextTooltip": "Transcript", + "certifyAge": "I certify that I am over {age} years of age", + "@certifyAge": { + "type": "text", + "placeholders": { + "age": {} + } + }, + "kickBotWarning": "Kicking Pangea Bot will remove the conversation bot from this chat.", + "joinToView": "Join this room to view details", + "refresh": "Refresh", + "autoPlayTitle": "Auto Play Messages", + "autoPlayDesc": "When enabled, the text-to-speech audio of messages will play automatically when selected.", + "transparent": "Transparent", + "incomingMessages": "Incoming messages", + "stickers": "Stickers", + "discover": "Discover", + "commandHint_ignore": "Ignore the given matrix ID", + "commandHint_unignore": "Unignore the given matrix ID", + "unreadChatsInApp": "{appname}: {unread} unread chats", + "@unreadChatsInApp": { + "type": "text", + "placeholders": { + "appname": {}, + "unread": {} + } + }, + "messageAnalytics": "Message Analytics", + "words": "Words", + "score": "Score", + "accuracy": "Accuracy", + "points": "Points", + "noPaymentInfo": "No payment info necessary!", + "conversationBotModeSelectDescription": "Bot mode", + "conversationBotModeSelectOption_discussion": "Discussion", + "conversationBotModeSelectOption_custom": "Custom", + "conversationBotModeSelectOption_conversation": "Conversation", + "conversationBotModeSelectOption_textAdventure": "Text Adventure", + "conversationBotDiscussionZone_title": "Discussion Settings", + "conversationBotDiscussionZone_discussionTopicLabel": "Discussion Topic", + "conversationBotDiscussionZone_discussionTopicPlaceholder": "Set Discussion Topic", + "conversationBotDiscussionZone_discussionKeywordsLabel": "Discussion Keywords", + "conversationBotDiscussionZone_discussionKeywordsPlaceholder": "Set Discussion Keywords", + "conversationBotDiscussionZone_discussionKeywordsHintText": "Comma separated list of keywords to guide the discussion", + "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel": "Send discussion prompt on a schedule", + "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel": "Hours between discussion prompts", + "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel": "Send discussion prompt when user reacts ⏩ to bot message", + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel": "Reaction to send discussion prompt", + "studentAnalyticsNotAvailable": "Student data not currently available", + "roomDataMissing": "Some data may be missing from rooms in which you are not a member.", + "updatePhoneOS": "You may need to update your device's OS version.", + "wordsPerMinute": "Words per minute", + "autoIGCToolName": "Run Language Assistance Automatically", + "autoIGCToolDescription": "Automatically run language assistance after typing messages", + "runGrammarCorrection": "Run grammar correction", + "grammarCorrectionFailed": "Issues to address", + "grammarCorrectionComplete": "Grammar correction complete", + "leaveRoomDescription": "The chat will be moved to the archive. Other users will be able to see that you have left the chat.", + "archiveSpaceDescription": "All chats within this space will be moved to the archive for yourself and other non-admin users.", + "leaveSpaceDescription": "All chats within this space will be moved to the archive. Other users will be able to see that you have left the space.", + "onlyAdminDescription": "Since there are no other admins, all other participants will also be removed.", + "tooltipInstructionsTitle": "Not sure what that does?", + "tooltipInstructionsMobileBody": "Press and hold items to view tooltips.", + "tooltipInstructionsBrowserBody": "Hover over items to view tooltips.", + "addSpaceToSpaceDescription": "Select a space to add as a parent", + "roomCapacity": "Room Capacity", + "roomFull": "This room is already at capacity.", + "topicNotSet": "The topic has not been set.", + "capacityNotSet": "This room has no capacity limit.", + "roomCapacityHasBeenChanged": "Room capacity changed", + "roomExceedsCapacity": "Room exceeds capacity. Consider removing students from the room, or raising the capacity.", + "capacitySetTooLow": "Room capacity cannot be set below the current number of non-admins.", + "roomCapacityExplanation": "Room capacity limits the number of non-admins allowed in a room.", + "enterNumber": "Please enter a whole number value.", + "buildTranslation": "Build your translation from the choices above" } \ No newline at end of file diff --git a/assets/l10n/intl_es.arb b/assets/l10n/intl_es.arb index fa097d393..4ad1fbd3e 100644 --- a/assets/l10n/intl_es.arb +++ b/assets/l10n/intl_es.arb @@ -4651,5 +4651,29 @@ "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel": "Enviar aviso de discusión cuando el usuario reacciona ⏩ al mensaje del bot.", "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel": "Reacción al envío del aviso de debate", "studentAnalyticsNotAvailable": "Datos de los estudiantes no disponibles actualmente", - "roomDataMissing": "Es posible que falten algunos datos de las salas de las que no es miembro." -} \ No newline at end of file + "roomDataMissing": "Es posible que falten algunos datos de las salas de las que no es miembro.", + "suggestToChat": "Sugerir este chat", + "suggestToChatDesc": "Los chats sugeridos aparecerán en las listas de chats", + "roomCapacity": "Capacidad de la sala", + "roomFull": "Esta sala ya está al límite de su capacidad.", + "topicNotSet": "El tema no se ha fijado.", + "capacityNotSet": "Esta sala no tiene límite de capacidad.", + "roomCapacityHasBeenChanged": "Capacidad de la sala modificada", + "roomExceedsCapacity": "La sala supera su capacidad. Considere la posibilidad de retirar a los alumnos de la sala o de aumentar la capacidad.", + "capacitySetTooLow": "La capacidad de la sala no puede fijarse por debajo del número actual de no administradores.", + "roomCapacityExplanation": "La capacidad de la sala limita el número de personas que pueden entrar en ella.", + "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", + "grammarCorrectionFailed": "Cuestiones a tratar", + "grammarCorrectionComplete": "Corrección gramatical completa", + "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.", + "onlyAdminDescription": "Como no hay más administradores, todos los demás participantes también serán eliminados.", + "tooltipInstructionsTitle": "¿No sabes para qué sirve?", + "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" +} diff --git a/lib/config/routes.dart b/lib/config/routes.dart index e6ab37967..8bf162372 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -24,6 +24,7 @@ import 'package:fluffychat/pages/settings_notifications/settings_notifications.d import 'package:fluffychat/pages/settings_password/settings_password.dart'; import 'package:fluffychat/pages/settings_security/settings_security.dart'; 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'; @@ -171,6 +172,28 @@ abstract class AppRoutes { const StudentAnalyticsPage(), ), 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', @@ -189,6 +212,36 @@ abstract class AppRoutes { state, const ClassAnalyticsPage(), ), + 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, + ), + ), + ), + ], ), ], ), diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 73a0447d5..69afab2aa 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -22,7 +22,6 @@ import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dar 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/student_analytics_summary_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'; @@ -68,7 +67,10 @@ class ChatPage extends StatelessWidget { @override Widget build(BuildContext context) { final room = Matrix.of(context).client.getRoomById(roomId); - if (room == null) { + // #Pangea + if (room == null || room.membership == Membership.leave) { + // if (room == null) { + // Pangea# return Scaffold( appBar: AppBar(title: Text(L10n.of(context)!.oopsSomethingWentWrong)), body: Center( @@ -654,34 +656,8 @@ class ChatController extends State ); return; } - // ensure that analytics room exists / is created for the active langCode await room.ensureAnalyticsRoomExists(); - pangeaController.myAnalytics.handleMessage( - room, - RecentMessageRecord( - eventId: msgEventId, - chatId: room.id, - useType: useType ?? UseType.un, - time: DateTime.now(), - ), - isEdit: previousEdit != null, - ); - - if (choreo != null && - tokensSent != null && - originalSent?.langCode == - pangeaController.languageController - .activeL2Code(roomID: room.id)) { - pangeaController.myAnalytics.saveConstructsMixed( - [ - // ...choreo.toVocabUse(tokensSent.tokens, room.id, msgEventId), - ...choreo.toGrammarConstructUse(msgEventId, room.id), - ], - originalSent!.langCode, - isEdit: previousEdit != null, - ); - } }, onError: (err, stack) => ErrorHandler.logError(e: err, s: stack), ); diff --git a/lib/pages/chat/chat_input_row.dart b/lib/pages/chat/chat_input_row.dart index fd8d95b5b..6bf19a850 100644 --- a/lib/pages/chat/chat_input_row.dart +++ b/lib/pages/chat/chat_input_row.dart @@ -1,5 +1,6 @@ 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'; @@ -33,6 +34,25 @@ class ChatInputRow extends StatelessWidget { final activel2 = controller.pangeaController.languageController.activeL2Model(); + String hintText() { + if (controller.choreographer.choreoMode == ChoreoMode.it) { + return L10n.of(context)!.buildTranslation; + } + return activel1 != null && + activel2 != null && + activel1.langCode != LanguageKeys.unknownLanguage && + activel2.langCode != LanguageKeys.unknownLanguage + ? L10n.of(context)!.writeAMessageFlag( + activel1.languageEmoji ?? + activel1.getDisplayName(context) ?? + activel1.langCode, + activel2.languageEmoji ?? + activel2.getDisplayName(context) ?? + activel2.langCode, + ) + : L10n.of(context)!.writeAMessage; + } + return Column( children: [ ITBar( @@ -331,21 +351,10 @@ class ChatInputRow extends StatelessWidget { bottom: 6.0, top: 3.0, ), - hintText: activel1 != null && - activel2 != null && - activel1.langCode != - LanguageKeys.unknownLanguage && - activel2.langCode != - LanguageKeys.unknownLanguage - ? L10n.of(context)!.writeAMessageFlag( - activel1.languageEmoji ?? - activel1.getDisplayName(context) ?? - activel1.langCode, - activel2.languageEmoji ?? - activel2.getDisplayName(context) ?? - activel2.langCode, - ) - : L10n.of(context)!.writeAMessage, + // #Pangea + // hintText: L10n.of(context)!.writeAMessage, + hintText: hintText(), + // Pangea# hintMaxLines: 1, border: InputBorder.none, enabledBorder: InputBorder.none, diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 317d81158..ff6da5099 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -21,6 +21,7 @@ import 'package:fluffychat/widgets/unread_rooms_badge.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; +import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; import '../../utils/stream_extension.dart'; @@ -152,6 +153,11 @@ class ChatView extends StatelessWidget { context: context, future: () => controller.room.join(), ); + // #Pangea + controller.room.leaveIfFull().then( + (full) => full ? context.go('/rooms') : null, + ); + // Pangea# } final bottomSheetPadding = FluffyThemes.isColumnMode(context) ? 16.0 : 8.0; final scrollUpBannerEventId = controller.scrollUpBannerEventId; diff --git a/lib/pages/chat_details/chat_details.dart b/lib/pages/chat_details/chat_details.dart index 07a12876e..e64b01bf0 100644 --- a/lib/pages/chat_details/chat_details.dart +++ b/lib/pages/chat_details/chat_details.dart @@ -3,8 +3,8 @@ import 'package:collection/collection.dart'; import 'package:file_picker/file_picker.dart'; import 'package:fluffychat/pages/chat_details/chat_details_view.dart'; import 'package:fluffychat/pages/settings/settings.dart'; +import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_description_button.dart'; import 'package:fluffychat/pangea/utils/set_class_name.dart'; -import 'package:fluffychat/pangea/utils/set_class_topic.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/matrix_sdk_extensions/matrix_locals.dart'; diff --git a/lib/pages/chat_details/chat_details_view.dart b/lib/pages/chat_details/chat_details_view.dart index 57e2a43e8..3159af329 100644 --- a/lib/pages/chat_details/chat_details_view.dart +++ b/lib/pages/chat_details/chat_details_view.dart @@ -8,6 +8,7 @@ import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_des import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_details_toggle_add_students_tile.dart'; import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_invitation_buttons.dart'; import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_name_button.dart'; +import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_capacity_button.dart'; import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_rules_editor.dart'; import 'package:fluffychat/pangea/utils/lock_room.dart'; import 'package:fluffychat/pangea/widgets/class/add_class_and_invite.dart'; @@ -34,7 +35,10 @@ class ChatDetailsView extends StatelessWidget { @override Widget build(BuildContext context) { final room = Matrix.of(context).client.getRoomById(controller.roomId!); - if (room == null) { + // #Pangea + if (room == null || room.membership == Membership.leave) { + // if (room == null) { + // Pangea# return Scaffold( appBar: AppBar( title: Text(L10n.of(context)!.oopsSomethingWentWrong), @@ -236,8 +240,9 @@ class ChatDetailsView extends StatelessWidget { height: 1, color: Theme.of(context).dividerColor, ), - // #Pangea - if (room.canSendEvent('m.room.name')) + // if (room.canSendEvent('m.room.name')) + if (room.isRoomAdmin) + // #Pangea ClassNameButton( room: room, controller: controller, @@ -247,6 +252,12 @@ class ChatDetailsView extends StatelessWidget { room: room, controller: controller, ), + // #Pangea + RoomCapacityButton( + room: room, + controller: controller, + ), + // Pangea# if ((room.isPangeaClass || room.isExchange) && room.isRoomAdmin) ListTile( @@ -435,7 +446,9 @@ class ChatDetailsView extends StatelessWidget { // : null, // ), // if (!room.isDirectChat) - if (!room.isDirectChat && !room.isSpace) + if (!room.isDirectChat && + !room.isSpace && + room.isRoomAdmin) // Pangea# ListTile( // #Pangea @@ -510,7 +523,9 @@ class ChatDetailsView extends StatelessWidget { room: room, ), const Divider(height: 1), - if (!room.isPangeaClass && !room.isDirectChat) + if (!room.isPangeaClass && + !room.isDirectChat && + room.isRoomAdmin) AddToSpaceToggles( roomId: room.id, key: controller.addToSpaceKey, diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index f7294e571..47fb51235 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -16,7 +16,6 @@ import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; import 'package:fluffychat/pangea/widgets/subscription/subscription_snackbar.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/tor_stub.dart' if (dart.library.html) 'package:tor_detector_web/tor_detector_web.dart'; @@ -801,7 +800,10 @@ class ChatListController extends State final selectedSpace = await showConfirmationDialog( context: context, title: L10n.of(context)!.addToSpace, - message: L10n.of(context)!.addToSpaceDescription, + // #Pangea + // message: L10n.of(context)!.addToSpaceDescription, + message: L10n.of(context)!.addSpaceToSpaceDescription, + // Pangea# fullyCapitalizedForMaterial: false, actions: Matrix.of(context) .client @@ -820,8 +822,11 @@ class ChatListController extends State .map( (space) => AlertDialogAction( key: space.id, - label: space - .getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)), + // #Pangea + // label: space + // .getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)), + label: space.nameIncludingParents(context), + // Pangea# ), ) .toList(), @@ -902,6 +907,7 @@ class ChatListController extends State if (mounted) { GoogleAnalytics.analyticsUserUpdate(client.userID); await pangeaController.subscriptionController.initialize(); + await pangeaController.myAnalytics.addEventsListener(); pangeaController.afterSyncAndFirstLoginInitialization(context); await pangeaController.inviteBotToExistingSpaces(); await pangeaController.setPangeaPushRules(); diff --git a/lib/pages/chat_list/chat_list_view.dart b/lib/pages/chat_list/chat_list_view.dart index 5736cad77..10ed00edd 100644 --- a/lib/pages/chat_list/chat_list_view.dart +++ b/lib/pages/chat_list/chat_list_view.dart @@ -138,17 +138,14 @@ class ChatListView extends StatelessWidget { builder: (context) { final allSpaces = client.rooms.where((room) => room.isSpace); - // #Pangea - // final rootSpaces = allSpaces - // .where( - // (space) => !allSpaces.any( - // (parentSpace) => parentSpace.spaceChildren - // .any((child) => child.roomId == space.id), - // ), - // ) - // .toList(); - final rootSpaces = allSpaces.toList(); - // Pangea# + final rootSpaces = allSpaces + .where( + (space) => !allSpaces.any( + (parentSpace) => parentSpace.spaceChildren + .any((child) => child.roomId == space.id), + ), + ) + .toList(); final destinations = getNavigationDestinations(context); return SizedBox( @@ -228,9 +225,9 @@ class ChatListView extends StatelessWidget { NavigationDestinationLabelBehavior.alwaysHide, height: 64, shadowColor: - Theme.of(context).colorScheme.onBackground, + Theme.of(context).colorScheme.onSurface, surfaceTintColor: - Theme.of(context).colorScheme.background, + Theme.of(context).colorScheme.surface, selectedIndex: controller.selectedIndex, onDestinationSelected: controller.onDestinationSelected, diff --git a/lib/pages/chat_list/client_chooser_button.dart b/lib/pages/chat_list/client_chooser_button.dart index 58b982cab..602be9192 100644 --- a/lib/pages/chat_list/client_chooser_button.dart +++ b/lib/pages/chat_list/client_chooser_button.dart @@ -1,6 +1,5 @@ import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:fluffychat/pangea/constants/class_default_values.dart'; -import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart'; import 'package:fluffychat/pangea/utils/class_code.dart'; import 'package:fluffychat/pangea/utils/find_conversation_partner_dialog.dart'; import 'package:fluffychat/pangea/utils/logout.dart'; @@ -69,7 +68,7 @@ class ClientChooserButton extends StatelessWidget { ), ), PopupMenuItem( - enabled: matrix.client.allMyAnalyticsRooms.isNotEmpty, + enabled: matrix.client.rooms.isNotEmpty, value: SettingsAction.myAnalytics, child: Row( children: [ diff --git a/lib/pages/chat_list/space_view.dart b/lib/pages/chat_list/space_view.dart index 0757354c5..e7237ccba 100644 --- a/lib/pages/chat_list/space_view.dart +++ b/lib/pages/chat_list/space_view.dart @@ -179,6 +179,12 @@ class _SpaceViewState extends State { // Wait for room actually appears in sync await client.waitForRoomInSync(spaceChild.roomId, join: true); } + // #Pangea + final room = client.getRoomById(spaceChild.roomId); + if (room != null && (await room.leaveIfFull())) { + throw L10n.of(context)!.roomFull; + } + // Pangea# }, ); if (result.error != null) return; @@ -197,6 +203,9 @@ class _SpaceViewState extends State { ); await room.join(); await waitForRoom; + if (await room.leaveIfFull()) { + throw L10n.of(context)!.roomFull; + } }, ); if (joinResult.error != null) return; @@ -271,9 +280,8 @@ class _SpaceViewState extends State { icon: Icons.architecture_outlined, isDestructiveAction: true, ), - + // if (room != null) if (room != null && room.membership != Membership.leave) - // if (room != null) // Pangea# SheetAction( key: SpaceChildContextAction.leave, @@ -329,34 +337,14 @@ class _SpaceViewState extends State { case SpaceChildContextAction.archive: widget.controller.cancelAction(); // #Pangea - if (room == null) return; - // room.isSpace - // ? await showFutureLoadingDialog( - // context: context, - // future: () async { - // await room.archiveSpace( - // Matrix.of(context).client, - // ); - // widget.controller.selectedRoomIds.clear(); - // }, - // ) - // : await widget.controller.archiveAction(); - if (room.isSpace) { - await room.archiveSpace( - context, - Matrix.of(context).client, - ); - } else { - widget.controller.toggleSelection(room.id); - await widget.controller.archiveAction(); - } + if (room == null || room.membership == Membership.leave) return; // Pangea# _refresh(); break; case SpaceChildContextAction.addToSpace: widget.controller.cancelAction(); // #Pangea - if (room == null) return; + if (room == null || room.membership == Membership.leave) return; // Pangea# widget.controller.toggleSelection(room.id); await widget.controller.addToSpace(); @@ -584,21 +572,19 @@ class _SpaceViewState extends State { final allSpaces = client.rooms.where((room) => room.isSpace); if (activeSpaceId == null) { final rootSpaces = allSpaces - // #Pangea - // .where( - // (space) => - // !allSpaces.any( - // (parentSpace) => parentSpace.spaceChildren - // .any((child) => child.roomId == space.id), - // ) && - // space - // .getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)) - // .toLowerCase() - // .contains( - // widget.controller.searchController.text.toLowerCase(), - // ), - //) - // Pangea# + .where( + (space) => + !allSpaces.any( + (parentSpace) => parentSpace.spaceChildren + .any((child) => child.roomId == space.id), + ) && + space + .getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)) + .toLowerCase() + .contains( + widget.controller.searchController.text.toLowerCase(), + ), + ) .toList(); return SafeArea( @@ -614,7 +600,7 @@ class _SpaceViewState extends State { MatrixLocals(L10n.of(context)!), ); return Material( - color: Theme.of(context).colorScheme.background, + color: Theme.of(context).colorScheme.surface, child: ListTile( leading: Avatar( mxContent: rootSpace.avatar, @@ -949,7 +935,7 @@ class _SpaceViewState extends State { : L10n.of(context)!.enterRoom), maxLines: 1, style: TextStyle( - color: Theme.of(context).colorScheme.onBackground, + color: Theme.of(context).colorScheme.onSurface, ), ), trailing: isSpace diff --git a/lib/pages/chat_list/utils/on_chat_tap.dart b/lib/pages/chat_list/utils/on_chat_tap.dart index b272d424e..f7daa248e 100644 --- a/lib/pages/chat_list/utils/on_chat_tap.dart +++ b/lib/pages/chat_list/utils/on_chat_tap.dart @@ -1,5 +1,6 @@ import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:fluffychat/pages/chat/send_file_dialog.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; @@ -65,6 +66,9 @@ void onChatTap(Room room, BuildContext context) async { room.id, join: true, ); + if (await room.leaveIfFull()) { + throw L10n.of(context)!.roomFull; + } await room.join(); await waitForRoom; }, diff --git a/lib/pages/new_group/new_group.dart b/lib/pages/new_group/new_group.dart index 2aec182fb..b3a1703af 100644 --- a/lib/pages/new_group/new_group.dart +++ b/lib/pages/new_group/new_group.dart @@ -3,8 +3,10 @@ import 'dart:typed_data'; import 'package:file_picker/file_picker.dart'; import 'package:fluffychat/pages/new_group/new_group_view.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/chat_topic_model.dart'; import 'package:fluffychat/pangea/models/lemma.dart'; +import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_capacity_button.dart'; import 'package:fluffychat/pangea/utils/bot_name.dart'; import 'package:fluffychat/pangea/utils/class_chat_power_levels.dart'; import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; @@ -51,6 +53,8 @@ class NewGroupController extends State { final GlobalKey addToSpaceKey = GlobalKey(); final GlobalKey addConversationBotKey = GlobalKey(); + final GlobalKey addCapacityKey = + GlobalKey(); ChatTopic chatTopic = ChatTopic.empty; @@ -145,10 +149,16 @@ class NewGroupController extends State { visibility: sdk.Visibility.public, ); } - //#Pangea + // #Pangea GoogleAnalytics.createChat(roomId); await addToSpaceKey.currentState!.addSpaces(roomId); - //Pangea# + + final capacity = addCapacityKey.currentState?.capacity; + final room = client.getRoomById(roomId); + if (capacity != null && room != null) { + room.updateRoomCapacity(capacity); + } + // Pangea# context.go('/rooms/$roomId/invite'); } catch (e, s) { sdk.Logs().d('Unable to create group', e, s); diff --git a/lib/pages/new_group/new_group_view.dart b/lib/pages/new_group/new_group_view.dart index ced178038..ddf55dfbd 100644 --- a/lib/pages/new_group/new_group_view.dart +++ b/lib/pages/new_group/new_group_view.dart @@ -1,5 +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'; @@ -87,6 +88,9 @@ class NewGroupView extends StatelessWidget { // ), // ), // ), + RoomCapacityButton( + key: controller.addCapacityKey, + ), ConversationBotSettings( key: controller.addConversationBotKey, activeSpaceId: controller.activeSpaceId, diff --git a/lib/pages/new_space/new_space.dart b/lib/pages/new_space/new_space.dart index e8145080b..fcb9f136b 100644 --- a/lib/pages/new_space/new_space.dart +++ b/lib/pages/new_space/new_space.dart @@ -3,8 +3,8 @@ import 'dart:developer'; import 'package:file_picker/file_picker.dart'; import 'package:fluffychat/pages/new_space/new_space_view.dart'; import 'package:fluffychat/pangea/constants/class_default_values.dart'; -import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/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/utils/bot_name.dart'; import 'package:fluffychat/pangea/utils/class_chat_power_levels.dart'; @@ -38,6 +38,8 @@ class NewSpaceController extends State { final GlobalKey addToSpaceKey = GlobalKey(); final GlobalKey classSettingsKey = GlobalKey(); + final GlobalKey addCapacityKey = + GlobalKey(); //Pangea# bool loading = false; @@ -77,7 +79,6 @@ class NewSpaceController extends State { stateKey: '', content: { 'events': { - PangeaEventTypes.studentAnalyticsSummary: 0, EventTypes.spaceChild: 0, }, 'users_default': 0, @@ -198,6 +199,11 @@ class NewSpaceController extends State { } await Future.wait(futures); + final capacity = addCapacityKey.currentState?.capacity; + final space = client.getRoomById(spaceId); + if (capacity != null && space != null) { + space.updateRoomCapacity(capacity); + } final newChatRoomId = await Matrix.of(context).client.createGroupChat( enableEncryption: false, preset: sdk.CreateRoomPreset.publicChat, diff --git a/lib/pages/new_space/new_space_view.dart b/lib/pages/new_space/new_space_view.dart index a0a4d0927..09abb7066 100644 --- a/lib/pages/new_space/new_space_view.dart +++ b/lib/pages/new_space/new_space_view.dart @@ -1,6 +1,7 @@ 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/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'; @@ -130,6 +131,10 @@ class NewSpaceView extends StatelessWidget { // ), // ), // const SizedBox(height: 16), + + RoomCapacityButton( + key: controller.addCapacityKey, + ), if (controller.newClassMode) ClassSettings( key: controller.classSettingsKey, diff --git a/lib/pangea/choreographer/controllers/igc_controller.dart b/lib/pangea/choreographer/controllers/igc_controller.dart index b0c8dd462..d5fd0de95 100644 --- a/lib/pangea/choreographer/controllers/igc_controller.dart +++ b/lib/pangea/choreographer/controllers/igc_controller.dart @@ -55,6 +55,9 @@ class IgcController { try { if (choreographer.currentText.isEmpty) return clear(); + // the error spans are going to be reloaded, so clear the cache + _clearCache(); + debugPrint('getIGCTextData called with ${choreographer.currentText}'); debugPrint('getIGCTextData called with tokensOnly = $tokensOnly'); @@ -287,6 +290,7 @@ class IgcController { clear() { igcTextData = null; + _clearCache(); // Not sure why this is here // MatrixState.pAnyState.closeOverlay(); } diff --git a/lib/pangea/constants/local.key.dart b/lib/pangea/constants/local.key.dart index c0390c2ba..743fe1143 100644 --- a/lib/pangea/constants/local.key.dart +++ b/lib/pangea/constants/local.key.dart @@ -11,4 +11,5 @@ class PLocalKey { static const String dismissedPaywall = 'dismissedPaywall'; static const String paywallBackoff = 'paywallBackoff'; static const String autoPlayMessages = 'autoPlayMessages'; + static const String messagesSinceUpdate = 'messagesSinceLastUpdate'; } diff --git a/lib/pangea/constants/model_keys.dart b/lib/pangea/constants/model_keys.dart index ef84a7064..0cd14e5a4 100644 --- a/lib/pangea/constants/model_keys.dart +++ b/lib/pangea/constants/model_keys.dart @@ -110,4 +110,7 @@ class ModelKey { "discussion_trigger_reaction_enabled"; static const String discussionTriggerReactionKey = "discussion_trigger_reaction_key"; + + static const String prevEventId = "prev_event_id"; + static const String prevLastUpdated = "prev_last_updated"; } diff --git a/lib/pangea/constants/pangea_event_types.dart b/lib/pangea/constants/pangea_event_types.dart index cfdb7f0d7..a182728dd 100644 --- a/lib/pangea/constants/pangea_event_types.dart +++ b/lib/pangea/constants/pangea_event_types.dart @@ -6,18 +6,21 @@ class PangeaEventTypes { static const rules = "p.rules"; - static const studentAnalyticsSummary = "pangea.usranalytics"; + // static const studentAnalyticsSummary = "pangea.usranalytics"; + static const summaryAnalytics = "pangea.summaryAnalytics"; + static const construct = "pangea.construct"; static const translation = "pangea.translation"; static const tokens = "pangea.tokens"; static const choreoRecord = "pangea.record"; static const representation = "pangea.representation"; - static const vocab = "p.vocab"; + // static const vocab = "p.vocab"; static const roomInfo = "pangea.roomtopic"; static const audio = "p.audio"; static const botOptions = "pangea.bot_options"; + static const capacity = "pangea.capacity"; static const userAge = "pangea.user_age"; diff --git a/lib/pangea/controllers/class_controller.dart b/lib/pangea/controllers/class_controller.dart index aed5574d2..1e2febf21 100644 --- a/lib/pangea/controllers/class_controller.dart +++ b/lib/pangea/controllers/class_controller.dart @@ -13,6 +13,7 @@ import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:matrix/matrix.dart'; import '../../widgets/matrix.dart'; @@ -133,8 +134,9 @@ class ClassController extends BaseController { ClassCodeUtil.messageSnack(context, L10n.of(context)!.alreadyInClass); return; } + await _pangeaController.matrixState.client.joinRoom(classChunk.roomId); - setActiveSpaceIdInChatListController(classChunk.roomId); + if (_pangeaController.matrixState.client.getRoomById(classChunk.roomId) == null) { await _pangeaController.matrixState.client.waitForRoomInSync( @@ -143,6 +145,26 @@ class ClassController extends BaseController { ); } + // If the room is full, leave + final room = + _pangeaController.matrixState.client.getRoomById(classChunk.roomId); + if (room == null) { + return; + } + final joinResult = await showFutureLoadingDialog( + context: context, + future: () async { + if (await room.leaveIfFull()) { + throw L10n.of(context)!.roomFull; + } + }, + ); + if (joinResult.error != null) { + return; + } + + setActiveSpaceIdInChatListController(classChunk.roomId); + // add the user's analytics room to this joined space // so their teachers can join them via the space hierarchy final Room? joinedSpace = diff --git a/lib/pangea/controllers/message_analytics_controller.dart b/lib/pangea/controllers/message_analytics_controller.dart index 36d0bdad5..c0c8ecd1a 100644 --- a/lib/pangea/controllers/message_analytics_controller.dart +++ b/lib/pangea/controllers/message_analytics_controller.dart @@ -1,34 +1,39 @@ +import 'dart:async'; 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/enum/construct_type_enum.dart'; import 'package:fluffychat/pangea/enum/time_span.dart'; -import 'package:fluffychat/pangea/models/student_analytics_summary_model.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/pages/analytics/base_analytics.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:flutter/foundation.dart'; import 'package:matrix/matrix.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; import '../constants/class_default_values.dart'; import '../extensions/client_extension/client_extension.dart'; import '../extensions/pangea_room_extension/pangea_room_extension.dart'; -import '../matrix_event_wrappers/construct_analytics_event.dart'; -import '../models/chart_analytics_model.dart'; -import '../models/student_analytics_event.dart'; +import '../models/analytics/chart_analytics_model.dart'; import 'base_controller.dart'; import 'pangea_controller.dart'; +// controls the fetching of analytics data class AnalyticsController extends BaseController { late PangeaController _pangeaController; - final List _cachedModels = []; + final List _cachedAnalyticsModels = []; final List _cachedConstructs = []; AnalyticsController(PangeaController pangeaController) : super() { _pangeaController = pangeaController; } + ///////// TIME SPANS ////////// String get _analyticsTimeSpanKey => "ANALYTICS_TIME_SPAN_KEY"; TimeSpan get currentAnalyticsTimeSpan { @@ -57,232 +62,480 @@ class AnalyticsController extends BaseController { ); } - Future> allClassAnalytics() async { - final List> classAnalyticFutures = []; - for (final classRoom in (await _pangeaController - .matrixState.client.classesAndExchangesImTeaching)) { - classAnalyticFutures.add( - getAnalytics(classRoom: classRoom), + Future 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 analyticsRooms = _pangeaController + .matrixState.client.allMyAnalyticsRooms + .where((room) => room.isAnalyticsRoom) + .toList(); + + final List lastUpdates = []; + for (final Room analyticsRoom in analyticsRooms) { + final DateTime? lastUpdated = await analyticsRoom.analyticsLastUpdated( + type, + _pangeaController.matrixState.client.userID!, + ); + if (lastUpdated != null) { + lastUpdates.add(lastUpdated); + } + } + + if (lastUpdates.isEmpty) return null; + return lastUpdates.reduce( + (check, mostRecent) => check.isAfter(mostRecent) ? check : mostRecent, + ); + } + + Future 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 + // TODO - figure out how to do this on a per-student basis + await space.requestParticipants(); + + final List> lastUpdatedFutures = []; + for (final student in space.students) { + final Room? analyticsRoom = _pangeaController.matrixState.client + .analyticsRoomLocal(langCode, student.id); + if (analyticsRoom == null) continue; + lastUpdatedFutures.add( + analyticsRoom.analyticsLastUpdated( + type, + student.id, + ), ); } - return Future.wait(classAnalyticFutures); + final List lastUpdatedWithNulls = + await Future.wait(lastUpdatedFutures); + final List lastUpdates = + lastUpdatedWithNulls.where((e) => e != null).cast().toList(); + if (lastUpdates.isNotEmpty) { + return lastUpdates.reduce( + (check, mostRecent) => check.isAfter(mostRecent) ? check : mostRecent, + ); + } + return null; + } + + // Map of space ids to the last fetched hierarchy. Used when filtering + // private chat analytics to determine which children are already visible + // in the chat list + final Map> _lastFetchedHierarchies = {}; + void setLatestHierarchy(String spaceId, GetSpaceHierarchyResponse resp) { + final List roomIds = resp.rooms.map((room) => room.roomId).toList(); + _lastFetchedHierarchies[spaceId] = roomIds; + } + + //////////////////////////// MESSAGE SUMMARY ANALYTICS //////////////////////////// + + Future> 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 List allEvents = []; + + // TODO switch to using list of futures + for (final Room analyticsRoom in analyticsRooms) { + final List? roomEvents = + await analyticsRoom.getAnalyticsEvents( + type: PangeaEventTypes.summaryAnalytics, + since: currentAnalyticsTimeSpan.cutOffDate, + userId: _pangeaController.matrixState.client.userID!, + ); + + allEvents.addAll( + roomEvents?.cast() ?? [], + ); + } + return allEvents; + } + + Future> spaceMemberAnalytics( + Room space, + ) 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 + await space.postLoad(); + await space.requestParticipants(); + + // TODO switch to using list of futures + final List analyticsEvents = []; + for (final student in space.students) { + final Room? analyticsRoom = _pangeaController.matrixState.client + .analyticsRoomLocal(langCode, student.id); + + if (analyticsRoom != null) { + final List? roomEvents = + await analyticsRoom.getAnalyticsEvents( + type: PangeaEventTypes.summaryAnalytics, + since: currentAnalyticsTimeSpan.cutOffDate, + userId: student.id, + ); + analyticsEvents.addAll( + roomEvents?.cast() ?? [], + ); + } + } + + final List spaceChildrenIds = space.allSpaceChildRoomIds; + + // filter out the analyics events that don't belong to the space's children + final List allAnalyticsEvents = []; + for (final analyticsEvent in analyticsEvents) { + analyticsEvent.content.messages.removeWhere( + (msg) => !spaceChildrenIds.contains(msg.chatId), + ); + allAnalyticsEvents.add(analyticsEvent); + } + + return allAnalyticsEvents; } ChartAnalyticsModel? getAnalyticsLocal({ TimeSpan? timeSpan, - String? classId, - String? studentId, - String? chatId, + required AnalyticsSelected defaultSelected, + AnalyticsSelected? selected, bool forceUpdate = false, bool updateExpired = false, + DateTime? lastUpdated, }) { timeSpan ??= currentAnalyticsTimeSpan; - final int index = _cachedModels.indexWhere( + final int index = _cachedAnalyticsModels.indexWhere( (e) => (e.timeSpan == timeSpan) && - (e.classId == classId) && - (e.studentId == studentId) && - (e.chatId == chatId), + (e.defaultSelected.id == defaultSelected.id) && + (e.defaultSelected.type == defaultSelected.type) && + (e.selected?.id == selected?.id) && + (e.selected?.type == selected?.type), ); if (index != -1) { - if ((updateExpired && _cachedModels[index].isExpired) || forceUpdate) { - _cachedModels.removeAt(index); + if ((updateExpired && _cachedAnalyticsModels[index].isExpired) || + forceUpdate || + _cachedAnalyticsModels[index].needsUpdate(lastUpdated)) { + _cachedAnalyticsModels.removeAt(index); } else { - return _cachedModels[index].chartAnalyticsModel; + return _cachedAnalyticsModels[index].chartAnalyticsModel; } } return null; } - Future getAnalytics({ + void cacheAnalytics({ + required ChartAnalyticsModel chartAnalyticsModel, + required AnalyticsSelected defaultSelected, + AnalyticsSelected? selected, TimeSpan? timeSpan, - Room? classRoom, - String? studentId, - String? chatId, - bool forceUpdate = false, - }) async { - timeSpan ??= currentAnalyticsTimeSpan; - try { - final cachedModel = getAnalyticsLocal( - classId: classRoom?.id, - studentId: studentId, - chatId: chatId, - updateExpired: true, - forceUpdate: forceUpdate, - ); - if (cachedModel != null) return cachedModel; - // debugger(when: classRoom?.displayname.contains('clizass') ?? false); - late List studentAnalyticsSummaryEvents; - if (classRoom == null) { - if (studentId == null) { - debugger(when: kDebugMode); - ErrorHandler.logError( - m: "studentId should have been defined", - s: StackTrace.current, - ); - } else { - studentAnalyticsSummaryEvents = - await _pangeaController.myAnalytics.allMyAnalyticsEvents(); - } - } else { - if (studentId != null) { - studentAnalyticsSummaryEvents = [ - await classRoom.getStudentAnalytics(studentId), - ]; - } else { - studentAnalyticsSummaryEvents = await classRoom.getClassAnalytics(); - } - if (studentId != null && chatId != null) { - debugger(when: kDebugMode); - ErrorHandler.logError( - m: "studentId and chatId should have both been defined", - s: StackTrace.current, - ); - studentAnalyticsSummaryEvents = []; - } - } - - final List msgs = []; - for (final event in studentAnalyticsSummaryEvents) { - if (event != null) { - msgs.addAll(event.content.messages); - } else { - debugPrint("studentAnalyticsSummaryEvent is null"); - } - } - - final newModel = ChartAnalyticsModel( - timeSpan: timeSpan, - msgs: msgs, - chatId: chatId, - ); - - _cachedModels.add( - CacheModel( - timeSpan: timeSpan, - classId: classRoom?.id, - studentId: studentId, - chatId: chatId, - chartAnalyticsModel: newModel, - ), - ); - - return newModel; - } catch (err, s) { - debugger(when: kDebugMode); - ErrorHandler.logError(e: err, s: s); - return ChartAnalyticsModel(msgs: [], timeSpan: timeSpan, chatId: chatId); - } - } - - Future getAnalyticsForPrivateChats({ - TimeSpan? timeSpan, - required Room? classRoom, - bool forceUpdate = false, - }) async { - timeSpan ??= currentAnalyticsTimeSpan; - - try { - if (classRoom == null) { - return ChartAnalyticsModel(msgs: [], timeSpan: timeSpan); - } - - final cachedModel = getAnalyticsLocal( - classId: classRoom.id, - studentId: null, - chatId: AnalyticsEntryType.privateChats.toString(), - updateExpired: true, - forceUpdate: forceUpdate, - ); - if (cachedModel != null) return cachedModel; - final List studentAnalyticsSummaryEvents = - await classRoom.getClassAnalytics(); - final List directChatIds = - classRoom.childrenAndGrandChildrenDirectChatIds; - - final List msgs = []; - for (final event in studentAnalyticsSummaryEvents) { - if (event != null) { - msgs.addAll( - event.content.messages - .where((m) => directChatIds.contains(m.chatId)), - ); - } else { - debugPrint("studentAnalyticsSummaryEvent is null"); - } - } - final newModel = ChartAnalyticsModel( - timeSpan: timeSpan, - msgs: msgs, - chatId: null, - ); - - _cachedModels.add( - CacheModel( - timeSpan: timeSpan, - classId: classRoom.id, - studentId: null, - chatId: AnalyticsEntryType.privateChats.toString(), - chartAnalyticsModel: newModel, - ), - ); - - return newModel; - } catch (err, s) { - debugger(when: kDebugMode); - ErrorHandler.logError(e: err, s: s); - return ChartAnalyticsModel(msgs: [], timeSpan: timeSpan); - } - } - - List? _constructs; - bool settingConstructs = false; - - List? get constructs => _constructs; - - String? getLangCode({ - Room? space, - String? roomID, }) { - final String? targetRoomID = space?.id ?? roomID; - final String? roomLangCode = - _pangeaController.languageController.activeL2Code(roomID: targetRoomID); - final String? userLangCode = - _pangeaController.languageController.userL2?.langCode; - - return roomLangCode ?? userLangCode; + _cachedAnalyticsModels.add( + AnalyticsCacheModel( + timeSpan: timeSpan ?? currentAnalyticsTimeSpan, + chartAnalyticsModel: chartAnalyticsModel, + defaultSelected: defaultSelected, + selected: selected, + ), + ); } - Future myAnalyticsRoom(String langCode) => - _pangeaController.matrixState.client.getMyAnalyticsRoom(langCode); + List filterStudentAnalytics( + List unfiltered, + String? studentId, + ) { + final List filtered = + List.from(unfiltered); + filtered.removeWhere((e) => e.event.senderId != studentId); + return filtered; + } - Room? studentAnalyticsRoom(String studentId, String langCode) => - _pangeaController.matrixState.client.analyticsRoomLocal( - langCode, - studentId, + List filterRoomAnalytics( + List unfiltered, + String? roomID, + ) { + List filtered = [...unfiltered]; + Room? room; + if (roomID != null) { + room = _pangeaController.matrixState.client.getRoomById(roomID); + if (room?.isSpace == true) { + return filterSpaceAnalytics(unfiltered, roomID); + } + } + + filtered = filtered + .where( + (e) => (e.content).messages.any((u) => u.chatId == roomID), + ) + .toList(); + filtered.forEachIndexed( + (i, _) => (filtered[i].content).messages.removeWhere( + (u) => u.chatId != roomID, + ), + ); + return filtered; + } + + Future> filterPrivateChatAnalytics( + List unfiltered, + 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 privateChatIds = space?.allSpaceChildRoomIds ?? []; + final List lastFetched = _lastFetchedHierarchies[space!.id] ?? []; + for (final id in lastFetched) { + privateChatIds.removeWhere((e) => e == id); + } + + List filtered = + List.from(unfiltered); + filtered = filtered.where((e) { + return (e.content).messages.any( + (u) => privateChatIds.contains(u.chatId), + ); + }).toList(); + filtered.forEachIndexed( + (i, _) => (filtered[i].content).messages.removeWhere( + (u) => !privateChatIds.contains(u.chatId), + ), + ); + return filtered; + } + + List filterSpaceAnalytics( + List unfiltered, + String spaceId, + ) { + final selectedSpace = + _pangeaController.matrixState.client.getRoomById(spaceId); + final List chatIds = selectedSpace?.spaceChildren + .map((e) => e.roomId) + .where((e) => e != null) + .cast() + .toList() ?? + []; + + List filtered = + List.from(unfiltered); + + filtered = filtered + .where( + (e) => e.content.messages.any((u) => chatIds.contains(u.chatId)), + ) + .toList(); + + filtered.forEachIndexed( + (i, _) => (filtered[i].content).messages.removeWhere( + (u) => !chatIds.contains(u.chatId), + ), + ); + return filtered; + } + + Future> filterAnalytics({ + required List unfilteredAnalytics, + required AnalyticsSelected defaultSelected, + Room? space, + AnalyticsSelected? selected, + }) async { + for (int i = 0; i < unfilteredAnalytics.length; i++) { + unfilteredAnalytics[i].content.messages.removeWhere( + (record) => record.time.isBefore( + currentAnalyticsTimeSpan.cutOffDate, + ), + ); + } + + switch (selected?.type) { + case null: + return unfilteredAnalytics; + case AnalyticsEntryType.student: + if (defaultSelected.type != AnalyticsEntryType.space) { + throw Exception( + "student filtering not available for default filter ${defaultSelected.type}", + ); + } + return filterStudentAnalytics(unfilteredAnalytics, selected?.id); + case AnalyticsEntryType.room: + return filterRoomAnalytics(unfilteredAnalytics, selected?.id); + case AnalyticsEntryType.privateChats: + if (defaultSelected.type == AnalyticsEntryType.student) { + throw "private chat filtering not available for my analytics"; + } + return await filterPrivateChatAnalytics( + unfilteredAnalytics, + space, + ); + case AnalyticsEntryType.space: + return filterSpaceAnalytics(unfilteredAnalytics, selected!.id); + default: + throw Exception("invalid filter type - ${selected?.type}"); + } + } + + Future getAnalytics({ + required AnalyticsSelected defaultSelected, + AnalyticsSelected? selected, + bool forceUpdate = false, + }) async { + try { + await _pangeaController.matrixState.client.roomsLoading; + + // 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, + ); + if (space == null) { + ErrorHandler.logError( + m: "space not found in getAnalytics", + data: { + "defaultSelected": defaultSelected, + "selected": selected, + }, + ); + return ChartAnalyticsModel( + msgs: [], + 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; + if (defaultSelected.type != AnalyticsEntryType.space) { + // if default selected view is my analytics, check for the last + // time the logged in user updated their analytics events + // this gets passed to getAnalyticsLocal to determine if the cached + // entry is out-of-date + lastUpdated = await myAnalyticsLastUpdated( + PangeaEventTypes.summaryAnalytics, + ); + } else { + // else, get the last time a student in the space updated their analytics + lastUpdated = await spaceAnalyticsLastUpdated( + PangeaEventTypes.summaryAnalytics, + space!, + langCode!, + ); + } + + final ChartAnalyticsModel? local = getAnalyticsLocal( + defaultSelected: defaultSelected, + selected: selected, + forceUpdate: forceUpdate, + lastUpdated: lastUpdated, + ); + if (local != null && !forceUpdate) { + return local; + } + + // get all the relevant summary analytics events for the current timespan + final List summaryEvents = + defaultSelected.type == AnalyticsEntryType.space + ? await spaceMemberAnalytics(space!) + : await mySummaryAnalytics(); + + // filter out the analytics events based on filters the user has chosen + final List filteredAnalytics = + await filterAnalytics( + unfilteredAnalytics: summaryEvents, + defaultSelected: defaultSelected, + space: space, + selected: selected, ); - Future> allMyConstructs( - String langCode, { - ConstructType? type, - }) async { - final Room analyticsRoom = await myAnalyticsRoom(langCode); + // then create and return the model to be displayed + final ChartAnalyticsModel newModel = ChartAnalyticsModel( + timeSpan: currentAnalyticsTimeSpan, + msgs: filteredAnalytics + .map((event) => event.content.messages) + .expand((msgs) => msgs) + .toList(), + ); + + cacheAnalytics( + chartAnalyticsModel: newModel, + defaultSelected: defaultSelected, + selected: selected, + timeSpan: currentAnalyticsTimeSpan, + ); + + return newModel; + } catch (err, s) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: err, s: s); + return ChartAnalyticsModel( + msgs: [], + timeSpan: currentAnalyticsTimeSpan, + ); + } + } + + //////////////////////////// CONSTRUCTS //////////////////////////// + + Future> allMyConstructs() async { + final List analyticsRooms = + _pangeaController.matrixState.client.allMyAnalyticsRooms; + + final List allConstructs = []; + for (final Room analyticsRoom in analyticsRooms) { + final List? roomEvents = + (await analyticsRoom.getAnalyticsEvents( + type: PangeaEventTypes.construct, + since: currentAnalyticsTimeSpan.cutOffDate, + userId: _pangeaController.matrixState.client.userID!, + )) + ?.cast(); + allConstructs.addAll(roomEvents ?? []); + } + final List adminSpaceRooms = await _pangeaController.matrixState.client.teacherRoomIds; - - final allConstructs = type == null - ? await analyticsRoom.allConstructEvents - : (await analyticsRoom.allConstructEvents) - .where((e) => e.content.type == type) - .toList(); - - for (int i = 0; i < allConstructs.length; i++) { - final construct = allConstructs[i]; - final uses = construct.content.uses; - uses.removeWhere((u) => adminSpaceRooms.contains(u.chatId)); + for (final construct in allConstructs) { + construct.content.uses.removeWhere( + (use) => adminSpaceRooms.contains(use.chatId), + ); } return allConstructs @@ -290,91 +543,94 @@ class AnalyticsController extends BaseController { .toList(); } - Future> allSpaceMemberConstructs( + Future> allSpaceMemberConstructs( Room space, - String langCode, { - ConstructType? type, - }) async { - final List>> constructEventFutures = []; + ) async { await space.postLoad(); await space.requestParticipants(); - for (final student in space.students) { - final Room? room = _pangeaController.matrixState.client - .analyticsRoomLocal(langCode, student.id); - if (room != null) constructEventFutures.add(room.allConstructEvents); - } + final String? langCode = _pangeaController.languageController.activeL2Code( + roomID: space.id, + ); - final List> constructLists = - await Future.wait(constructEventFutures); - - final List spaceChildrenIds = space.spaceChildren - .map((e) => e.roomId) - .where((e) => e != null) - .cast() - .toList(); - - final List allConstructs = []; - for (final constructList in constructLists) { - for (int i = 0; i < constructList.length; i++) { - final construct = constructList[i]; - final uses = construct.content.uses; - uses.removeWhere((u) => !spaceChildrenIds.contains(u.chatId)); - } - allConstructs.addAll( - constructList.where((e) => e.content.uses.isNotEmpty), + if (langCode == null) { + ErrorHandler.logError( + m: "langCode missing in allSpaceMemberConstructs", + data: { + "space": space, + }, ); + return []; } - return type == null - ? allConstructs - : allConstructs.where((e) => e.content.type == type).toList(); + final List constructEvents = []; + for (final student in space.students) { + final Room? analyticsRoom = _pangeaController.matrixState.client + .analyticsRoomLocal(langCode, student.id); + if (analyticsRoom != null) { + final List? roomEvents = + (await analyticsRoom.getAnalyticsEvents( + type: PangeaEventTypes.construct, + since: currentAnalyticsTimeSpan.cutOffDate, + userId: student.id, + )) + ?.cast(); + constructEvents.addAll(roomEvents ?? []); + } + } + + final List spaceChildrenIds = space.allSpaceChildRoomIds; + final List allConstructs = []; + for (final constructEvent in constructEvents) { + constructEvent.content.uses.removeWhere( + (use) => !spaceChildrenIds.contains(use.chatId), + ); + + if (constructEvent.content.uses.isNotEmpty) { + allConstructs.add(constructEvent); + } + } + + return allConstructs; } - List filterStudentConstructs( - List unfilteredConstructs, + List filterStudentConstructs( + List unfilteredConstructs, String? studentId, ) { - final List filtered = - List.from(unfilteredConstructs); - filtered.removeWhere((e) => e.event.senderId != studentId); + final List filtered = + List.from(unfilteredConstructs); + filtered.removeWhere((element) => element.event.senderId != studentId); return filtered; } - List filterRoomConstructs( - List unfilteredConstructs, + List filterRoomConstructs( + List unfilteredConstructs, String? roomID, ) { - List filtered = [...unfilteredConstructs]; - filtered = unfilteredConstructs - .where((e) => e.content.uses.any((u) => u.chatId == roomID)) - .toList(); - filtered.forEachIndexed( - (i, _) => filtered[i].content.uses.removeWhere((u) => u.chatId != roomID), - ); + final List filtered = [...unfilteredConstructs]; + for (final construct in filtered) { + construct.content.uses.removeWhere((u) => u.chatId != roomID); + } return filtered; } - List filterPrivateChatConstructs( - List unfilteredConstructs, + List filterPrivateChatConstructs( + List unfilteredConstructs, Room parentSpace, ) { - final List directChatIds = - parentSpace.childrenAndGrandChildrenDirectChatIds; - List filtered = - List.from(unfilteredConstructs); - filtered = filtered.where((e) { - return e.content.uses.any((u) => directChatIds.contains(u.chatId)); - }).toList(); - filtered.forEachIndexed( - (i, _) => filtered[i].content.uses.removeWhere( - (u) => !directChatIds.contains(u.chatId), - ), - ); + final List directChatIds = []; + final List filtered = + List.from(unfilteredConstructs); + for (final construct in filtered) { + construct.content.uses.removeWhere( + (use) => !directChatIds.contains(use.chatId), + ); + } return filtered; } - List filterSpaceConstructs( - List unfilteredConstructs, + List filterSpaceConstructs( + List unfilteredConstructs, Room space, ) { final List chatIds = space.spaceChildren @@ -383,67 +639,69 @@ class AnalyticsController extends BaseController { .cast() .toList(); - List filtered = - List.from(unfilteredConstructs); - filtered = filtered - .where((e) => e.content.uses.any((u) => chatIds.contains(u.chatId))) - .toList(); + final List filtered = + List.from(unfilteredConstructs); + + for (final construct in filtered) { + construct.content.uses.removeWhere( + (use) => !chatIds.contains(use.chatId), + ); + } - filtered.forEachIndexed( - (i, _) => filtered[i].content.uses.removeWhere( - (u) => !chatIds.contains(u.chatId), - ), - ); return filtered; } - List? getConstructsLocal({ + List? getConstructsLocal({ required TimeSpan timeSpan, required ConstructType constructType, required AnalyticsSelected defaultSelected, AnalyticsSelected? selected, + DateTime? lastUpdated, }) { - final cachedEntry = _cachedConstructs - .firstWhereOrNull( - (e) => - e.timeSpan == timeSpan && - e.type == constructType && - e.defaultSelected.id == defaultSelected.id && - e.defaultSelected.type == defaultSelected.type && - e.selected?.id == selected?.id && - e.selected?.type == selected?.type, - ) - ?.events; - return cachedEntry; + final index = _cachedConstructs.indexWhere( + (e) => + e.timeSpan == timeSpan && + e.type == constructType && + e.defaultSelected.id == defaultSelected.id && + e.defaultSelected.type == defaultSelected.type && + e.selected?.id == selected?.id && + e.selected?.type == selected?.type, + ); + + if (index > -1) { + if (_cachedConstructs[index].needsUpdate(lastUpdated)) { + _cachedConstructs.removeAt(index); + return null; + } + return _cachedConstructs[index].events; + } + + return null; } void cacheConstructs({ required ConstructType constructType, - required List events, + required List events, required AnalyticsSelected defaultSelected, AnalyticsSelected? selected, }) { - _cachedConstructs.add( - ConstructCacheEntry( - timeSpan: currentAnalyticsTimeSpan, - type: constructType, - events: events, - defaultSelected: defaultSelected, - selected: selected, - ), + final entry = ConstructCacheEntry( + timeSpan: currentAnalyticsTimeSpan, + type: constructType, + events: List.from(events), + defaultSelected: defaultSelected, + selected: selected, ); + _cachedConstructs.add(entry); } - Future> getMyConstructs({ + Future> getMyConstructs({ required AnalyticsSelected defaultSelected, required ConstructType constructType, - required String langCode, AnalyticsSelected? selected, }) async { - final List unfilteredConstructs = await allMyConstructs( - langCode, - type: constructType, - ); + final List unfilteredConstructs = + await allMyConstructs(); final Room? space = selected?.type == AnalyticsEntryType.space ? _pangeaController.matrixState.client.getRoomById(selected!.id) @@ -451,39 +709,33 @@ class AnalyticsController extends BaseController { return filterConstructs( unfilteredConstructs: unfilteredConstructs, - langCode: langCode, space: space, defaultSelected: defaultSelected, selected: selected, ); } - Future> getSpaceConstructs({ + Future> getSpaceConstructs({ required ConstructType constructType, required Room space, required AnalyticsSelected defaultSelected, - required String langCode, AnalyticsSelected? selected, }) async { - final List unfilteredConstructs = + final List unfilteredConstructs = await allSpaceMemberConstructs( space, - langCode, - type: constructType, ); return filterConstructs( unfilteredConstructs: unfilteredConstructs, - langCode: langCode, space: space, defaultSelected: defaultSelected, selected: selected, ); } - Future> filterConstructs({ - required List unfilteredConstructs, - required String langCode, + Future> filterConstructs({ + required List unfilteredConstructs, required AnalyticsSelected defaultSelected, Room? space, AnalyticsSelected? selected, @@ -495,9 +747,8 @@ class AnalyticsController extends BaseController { for (int i = 0; i < unfilteredConstructs.length; i++) { final construct = unfilteredConstructs[i]; - final uses = construct.content.uses; - uses.removeWhere( - (u) => u.timeStamp.isBefore(currentAnalyticsTimeSpan.cutOffDate), + construct.content.uses.removeWhere( + (use) => use.timeStamp.isBefore(currentAnalyticsTimeSpan.cutOffDate), ); } @@ -512,12 +763,7 @@ class AnalyticsController extends BaseController { "student filtering not available for default filter ${defaultSelected.type}", ); } - final Room? analyticsRoom = - studentAnalyticsRoom(selected!.id, langCode); - if (analyticsRoom == null) { - throw Exception("analyticsRoom missing in filterConstructs"); - } - return filterStudentConstructs(unfilteredConstructs, selected.id); + return filterStudentConstructs(unfilteredConstructs, selected!.id); case AnalyticsEntryType.room: return filterRoomConstructs(unfilteredConstructs, selected?.id); case AnalyticsEntryType.privateChats: @@ -531,123 +777,122 @@ class AnalyticsController extends BaseController { } } - Future?> setConstructs({ + Future?> getConstructs({ required ConstructType constructType, required AnalyticsSelected defaultSelected, AnalyticsSelected? selected, - bool removeIT = false, + bool removeIT = true, bool forceUpdate = false, }) async { - final List? local = getConstructsLocal( - timeSpan: currentAnalyticsTimeSpan, - constructType: constructType, - defaultSelected: defaultSelected, - selected: selected, - ); - if (local != null && !forceUpdate) { - _constructs = local; - return _constructs; - } - - if (settingConstructs) return _constructs; - settingConstructs = true; await _pangeaController.matrixState.client.roomsLoading; + Room? space; + String? langCode; if (defaultSelected.type == AnalyticsEntryType.space) { space = _pangeaController.matrixState.client.getRoomById( defaultSelected.id, ); + if (space == null) { + ErrorHandler.logError( + m: "space not found in setConstructs", + data: { + "defaultSelected": defaultSelected, + "selected": selected, + }, + ); + 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 []; + } } - final String? roomID = space?.id ?? selected?.id; - final String? langCode = getLangCode( - space: space, - roomID: roomID, - ); - - if (langCode == null) { - ErrorHandler.logError( - m: "langCode missing in getConstructs", - data: { - "constructType": constructType, - "AnalyticsEntryType": defaultSelected.type, - "AnalyticsEntryId": defaultSelected.id, - "space": space, - }, + DateTime? lastUpdated; + if (defaultSelected.type != AnalyticsEntryType.space) { + // if default selected view is my analytics, check for the last + // time the logged in user updated their analytics events + // this gets passed to getAnalyticsLocal to determine if the cached + // entry is out-of-date + lastUpdated = await myAnalyticsLastUpdated( + PangeaEventTypes.construct, ); - throw "langCode missing in getConstructs"; + } else { + // else, get the last time a student in the space updated their analytics + lastUpdated = await spaceAnalyticsLastUpdated( + PangeaEventTypes.construct, + space!, + langCode!, + ); + } + + final List? local = getConstructsLocal( + timeSpan: currentAnalyticsTimeSpan, + constructType: constructType, + defaultSelected: defaultSelected, + selected: selected, + lastUpdated: lastUpdated, + ); + if (local != null && !forceUpdate) { + return local; } final filteredConstructs = space == null ? await getMyConstructs( constructType: constructType, - langCode: langCode, defaultSelected: defaultSelected, selected: selected, ) : await getSpaceConstructs( constructType: constructType, space: space, - langCode: langCode, defaultSelected: defaultSelected, selected: selected, ); - _constructs = removeIT - ? filteredConstructs - .where( - (element) => - element.content.lemma != "Try interactive translation" && - element.content.lemma != "itStart" && - element.content.lemma != MatchRuleIds.interactiveTranslation, - ) - .toList() - : filteredConstructs; + if (removeIT) { + for (final construct in filteredConstructs) { + construct.content.uses.removeWhere( + (element) => + element.lemma == "Try interactive translation" || + element.lemma == "itStart" || + element.lemma == MatchRuleIds.interactiveTranslation, + ); + } + } if (local == null) { cacheConstructs( constructType: constructType, - events: _constructs!, + events: filteredConstructs, defaultSelected: defaultSelected, selected: selected, ); } - settingConstructs = false; - return _constructs; + return filteredConstructs; } } -class ConstructCacheEntry { +abstract class CacheEntry { final TimeSpan timeSpan; - final ConstructType type; - final List events; final AnalyticsSelected defaultSelected; AnalyticsSelected? selected; + late final DateTime _createdAt; - ConstructCacheEntry({ + CacheEntry({ required this.timeSpan, - required this.type, - required this.events, required this.defaultSelected, this.selected, - }); -} - -class CacheModel { - TimeSpan timeSpan; - ChartAnalyticsModel chartAnalyticsModel; - String? classId; - String? chatId; - String? studentId; - late DateTime _createdAt; - - CacheModel({ - required this.timeSpan, - required this.classId, - required this.chartAnalyticsModel, - required this.chatId, - required this.studentId, }) { _createdAt = DateTime.now(); } @@ -655,11 +900,47 @@ class CacheModel { bool get isExpired => DateTime.now().difference(_createdAt).inMinutes > ClassDefaultValues.minutesDelayToMakeNewChartAnalytics; + + bool needsUpdate(DateTime? lastEventUpdated) { + // cache entry is invalid if it's older than the last event update + // if lastEventUpdated is null, that would indicate that no events + // of this type have been sent to the room. In this case, there + // shouldn't be any cached data. + if (lastEventUpdated == null) { + Sentry.addBreadcrumb( + Breadcrumb(message: "lastEventUpdated is null in needsUpdate"), + ); + return false; + } + return _createdAt.isBefore(lastEventUpdated); + } } -// class ListTotals { -// String listName; -// ConstructUses vocabUse; +class ConstructCacheEntry extends CacheEntry { + final ConstructType type; + final List events; -// ListTotals({required this.listName, required this.vocabUse}); -// } + ConstructCacheEntry({ + required this.type, + required this.events, + required super.timeSpan, + required super.defaultSelected, + super.selected, + }); +} + +class AnalyticsCacheModel extends CacheEntry { + ChartAnalyticsModel chartAnalyticsModel; + + AnalyticsCacheModel({ + required this.chartAnalyticsModel, + required super.timeSpan, + required super.defaultSelected, + super.selected, + }); + + @override + bool get isExpired => + DateTime.now().difference(_createdAt).inMinutes > + ClassDefaultValues.minutesDelayToMakeNewChartAnalytics; +} diff --git a/lib/pangea/controllers/my_analytics_controller.dart b/lib/pangea/controllers/my_analytics_controller.dart index 0f17eaa20..cd95dd002 100644 --- a/lib/pangea/controllers/my_analytics_controller.dart +++ b/lib/pangea/controllers/my_analytics_controller.dart @@ -1,159 +1,313 @@ import 'dart:async'; 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/enum/construct_type_enum.dart'; -import 'package:fluffychat/pangea/matrix_event_wrappers/construct_analytics_event.dart'; -import 'package:fluffychat/pangea/models/student_analytics_summary_model.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/utils/error_handler.dart'; import 'package:flutter/foundation.dart'; import 'package:matrix/matrix.dart'; import '../extensions/client_extension/client_extension.dart'; import '../extensions/pangea_room_extension/pangea_room_extension.dart'; -import '../models/constructs_analytics_model.dart'; -import '../models/student_analytics_event.dart'; -class MyAnalyticsController { +// controls the sending of analytics events +class MyAnalyticsController extends BaseController { late PangeaController _pangeaController; + Timer? _updateTimer; + final int _maxMessagesCached = 10; + final int _minutesBeforeUpdate = 5; MyAnalyticsController(PangeaController pangeaController) { _pangeaController = pangeaController; } - String? get _userId => _pangeaController.matrixState.client.userID; + // 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 addEventsListener() async { + final Client client = _pangeaController.matrixState.client; - //PTODO - locally cache and update periodically - Future handleMessage( - Room room, - RecentMessageRecord messageRecord, { - bool isEdit = false, - }) async { - try { - debugPrint("in handle message with type ${messageRecord.useType}"); - if (_userId == null) { - debugger(when: kDebugMode); - ErrorHandler.logError( - m: "null userId in updateAnalytics", - s: StackTrace.current, - ); - return; - } + // 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); + } - await _pangeaController.classController.addDirectChatsToClasses(room); - //expanding this to all parents of the room - // final List spaces = room.immediateClassParents; - final List spaces = room.pangeaSpaceParents; - // janky but probably stays until we have a class analytics bot added by - // default to all chats + client.onSync.stream + .where((SyncUpdate update) => update.rooms?.join != null) + .listen((update) { + updateAnalyticsTimer(update, lastUpdated); + }); + } - final List events = await analyticsEvents(spaces); + // 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)!; + // get the new events in this sync that are messages + final List? events = entry.value.timeline?.events + ?.map((event) => Event.fromMatrixEvent(event, room)) + .where((event) => eventHasAnalytics(event, lastUpdated)) + .toList(); + + // add their event IDs to the cache of un-added event IDs + if (events == null || events.isEmpty) continue; for (final event in events) { - if (event != null) { - event.handleNewMessage(messageRecord, isEdit: isEdit); - } + addMessageSinceUpdate(event.eventId); } + + // cancel the last timer that was set on message event and + // reset it to fire after _minutesBeforeUpdate minutes + _updateTimer?.cancel(); + _updateTimer = Timer(Duration(minutes: _minutesBeforeUpdate), () { + debugPrint("timer fired, updating analytics"); + updateAnalytics(); + }); + } + } + + // 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)) && + event.type == EventTypes.Message && + event.messageType == MessageTypes.Text && + !(event.eventId.contains("web") && + !(event.eventId.contains("android")) && + !(event.eventId.contains("iOS"))); + } + + // adds an event ID to the cache of un-added event IDs + // if the event IDs isn't already added + void addMessageSinceUpdate(String eventId) { + final List currentCache = messagesSinceUpdate; + if (!currentCache.contains(eventId)) { + currentCache.add(eventId); + _pangeaController.pStoreService.save( + PLocalKey.messagesSinceUpdate, + currentCache, + local: true, + ); + } + + // if the cached has reached if max-length, update analytics + if (messagesSinceUpdate.length > _maxMessagesCached) { + debugPrint("reached max messages, updating"); + updateAnalytics(); + } + } + + // called before updating analytics + void clearMessagesSinceUpdate() { + _pangeaController.pStoreService.save( + PLocalKey.messagesSinceUpdate, + [], + local: true, + ); + } + + // a local cache of eventIds for messages sent since the last update + // it's possible for this cache to be invalid or deleted + // It's a proxy measure for messages sent since last update + List get messagesSinceUpdate { + final dynamic locallySaved = _pangeaController.pStoreService.read( + PLocalKey.messagesSinceUpdate, + local: true, + ); + if (locallySaved == null) { + _pangeaController.pStoreService.save( + PLocalKey.messagesSinceUpdate, + [], + local: true, + ); + return []; + } + + try { + return locallySaved as List; + } catch (err) { + _pangeaController.pStoreService.save( + PLocalKey.messagesSinceUpdate, + [], + local: true, + ); + return []; + } + } + + Future 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 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 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, + ); + 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!, + ); + + // 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 analyticsContent = []; + + 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 messages the logged in user has sent in all chats + // since the last analytics event was sent + List? recentMsgs; + try { + recentMsgs = await chat.myMessageEventsInChat( + since: lastUpdated, + ); + } catch (err) { + debugPrint("failed to fetch messages for chat ${chat.id}"); + continue; + } + + 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), + ); + } + + // send the analytics data to the analytics room + if (analyticsContent.isEmpty) return; + await AnalyticsEvent.sendEvent( + analyticsRoom, + type, + analyticsContent, + ); + } + + // 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> createMissingAnalyticsRooms() async { + List targetLangs = []; + final String? userL2 = _pangeaController.languageController.activeL2Code(); + if (userL2 != null) targetLangs.add(userL2); + final List spaceL2s = studentSpaces + .map( + (space) => _pangeaController.languageController.activeL2Code( + roomID: space.id, + ), + ) + .toList(); + targetLangs.addAll(spaceL2s.where((l2) => l2 != null).cast()); + targetLangs = targetLangs.toSet().toList(); + for (final String langCode in targetLangs) { + await _pangeaController.matrixState.client.getMyAnalyticsRoom(langCode); + } + return _pangeaController.matrixState.client.allMyAnalyticsRooms; + } + + List _studentChats = []; + + Future setStudentChats() async { + final List 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 get studentChats { + try { + if (_studentChats.isNotEmpty) return _studentChats; + setStudentChats(); + return _studentChats; } catch (err) { debugger(when: kDebugMode); + return []; } } - Future> analyticsEvents( - List spaces, - ) async { - final List> events = []; - if (_userId != null) { - for (final space in spaces) { - events.add(space.getStudentAnalytics(_userId!)); - } - } - return Future.wait(events); + List _studentSpaces = []; + + Future setStudentSpaces() async { + _studentSpaces = await _pangeaController + .matrixState.client.classesAndExchangesImStudyingIn; } - Future> allMyAnalyticsEvents() async => - analyticsEvents( - await _pangeaController - .matrixState.client.classesAndExchangesImStudyingIn, - ); - - Future saveConstructsMixed( - List allUses, - String langCode, { - bool isEdit = false, - }) async { + List get studentSpaces { try { - final Map> aggregatedVocabUse = {}; - for (final use in allUses) { - if (use.lemma == null) continue; - aggregatedVocabUse[use.lemma!] ??= []; - aggregatedVocabUse[use.lemma]!.add(use); - } - final Room analyticsRoom = await _pangeaController.matrixState.client - .getMyAnalyticsRoom(langCode); - - final List> saveFutures = []; - for (final uses in aggregatedVocabUse.entries) { - debugPrint("saving of type ${uses.value.first.constructType}"); - saveFutures.add( - analyticsRoom.saveConstructUsesSameLemma( - uses.key, - uses.value.first.constructType ?? ConstructType.grammar, - uses.value, - isEdit: isEdit, - ), - ); - } - - await Future.wait(saveFutures); - } catch (err, s) { + if (_studentSpaces.isNotEmpty) return _studentSpaces; + setStudentSpaces(); + return _studentSpaces; + } catch (err) { debugger(when: kDebugMode); - if (!kDebugMode) rethrow; - ErrorHandler.logError(e: err, s: s); + return []; } } - - // used to aggregate ConstructEvents, from multiple senders (students) with the same lemma - List aggregateConstructData( - List constructs, - ) { - final Map> lemmasToConstructs = {}; - for (final construct in constructs) { - lemmasToConstructs[construct.content.lemma] ??= []; - lemmasToConstructs[construct.content.lemma]!.add(construct); - } - - final List aggregatedConstructs = []; - for (final lemmaToConstructs in lemmasToConstructs.entries) { - final List lemmaConstructs = lemmaToConstructs.value; - final AggregateConstructUses aggregatedData = AggregateConstructUses( - constructs: lemmaConstructs, - ); - aggregatedConstructs.add(aggregatedData); - } - return aggregatedConstructs; - } -} - -class AggregateConstructUses { - final List _constructs; - - AggregateConstructUses({required List constructs}) - : _constructs = constructs; - - String get lemma { - assert( - _constructs.isNotEmpty && - _constructs.every( - (construct) => - construct.content.lemma == _constructs.first.content.lemma, - ), - ); - return _constructs.first.content.lemma; - } - - List get uses => _constructs - .map((construct) => construct.content.uses) - .expand((element) => element) - .toList(); } diff --git a/lib/pangea/controllers/pangea_controller.dart b/lib/pangea/controllers/pangea_controller.dart index ad2a27145..68cfc59fc 100644 --- a/lib/pangea/controllers/pangea_controller.dart +++ b/lib/pangea/controllers/pangea_controller.dart @@ -248,29 +248,11 @@ class PangeaController { if (!userIds.contains(BotName.byEnvironment)) { try { await space.invite(BotName.byEnvironment); - await space.postLoad(); - await space.setPower( - BotName.byEnvironment, - ClassDefaultValues.powerLevelOfAdmin, - ); } catch (err) { ErrorHandler.logError( e: "Failed to invite pangea bot to space ${space.id}", ); } - } else if (space.getPowerLevelByUserId(BotName.byEnvironment) < - ClassDefaultValues.powerLevelOfAdmin) { - try { - await space.postLoad(); - await space.setPower( - BotName.byEnvironment, - ClassDefaultValues.powerLevelOfAdmin, - ); - } catch (err) { - ErrorHandler.logError( - e: "Failed to reset power level for pangea bot in space ${space.id}", - ); - } } } } diff --git a/lib/pangea/enum/bar_chart_view_enum.dart b/lib/pangea/enum/bar_chart_view_enum.dart index d59f34d9a..3fe812634 100644 --- a/lib/pangea/enum/bar_chart_view_enum.dart +++ b/lib/pangea/enum/bar_chart_view_enum.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; - import 'package:flutter_gen/gen_l10n/l10n.dart'; enum BarChartViewSelection { @@ -30,4 +29,15 @@ 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'; + } + } } diff --git a/lib/pangea/enum/time_span.dart b/lib/pangea/enum/time_span.dart index 23a54e4ea..ddc9ce32b 100644 --- a/lib/pangea/enum/time_span.dart +++ b/lib/pangea/enum/time_span.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; - import 'package:flutter_gen/gen_l10n/l10n.dart'; -import '../models/chart_analytics_model.dart'; +import '../models/analytics/chart_analytics_model.dart'; enum TimeSpan { day, week, month, sixmonths, year } diff --git a/lib/pangea/extensions/client_extension/classes_and_exchanges_extension.dart b/lib/pangea/extensions/client_extension/classes_and_exchanges_extension.dart index 3108a90f5..af1df62a0 100644 --- a/lib/pangea/extensions/client_extension/classes_and_exchanges_extension.dart +++ b/lib/pangea/extensions/client_extension/classes_and_exchanges_extension.dart @@ -38,7 +38,13 @@ extension ClassesAndExchangesClientExtension on Client { .toList(); Future> get _classesAndExchangesImStudyingIn async { - for (final Room space in rooms.where((room) => room.isSpace)) { + final List 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(); } diff --git a/lib/pangea/extensions/client_extension/client_analytics_extension.dart b/lib/pangea/extensions/client_extension/client_analytics_extension.dart index 9cd00df23..6057b5a87 100644 --- a/lib/pangea/extensions/client_extension/client_analytics_extension.dart +++ b/lib/pangea/extensions/client_extension/client_analytics_extension.dart @@ -96,23 +96,6 @@ extension AnalyticsClientExtension on Client { await Future.wait(makePublicFutures); } - Future _updateMyLearningAnalyticsForAllClassesImIn([ - PLocalStore? storageService, - ]) async { - try { - final List> updateFutures = []; - for (final classRoom in classesAndExchangesImIn) { - updateFutures - .add(classRoom.updateMyLearningAnalyticsForClass(storageService)); - } - await Future.wait(updateFutures); - } catch (err, s) { - if (kDebugMode) rethrow; - // debugger(when: kDebugMode); - ErrorHandler.logError(e: err, s: s); - } - } - // Add all the users' analytics room to all the spaces the student studies in // So teachers can join them via space hierarchy // Will not always work, as there may be spaces where students don't have permission to add chats diff --git a/lib/pangea/extensions/client_extension/client_extension.dart b/lib/pangea/extensions/client_extension/client_extension.dart index c2c673dff..d23caa5de 100644 --- a/lib/pangea/extensions/client_extension/client_extension.dart +++ b/lib/pangea/extensions/client_extension/client_extension.dart @@ -11,8 +11,6 @@ import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:flutter/foundation.dart'; import 'package:matrix/matrix.dart'; -import '../../utils/p_store.dart'; - part "classes_and_exchanges_extension.dart"; part "client_analytics_extension.dart"; part "general_info_extension.dart"; @@ -31,11 +29,6 @@ extension PangeaClient on Client { Future updateAnalyticsRoomVisibility() async => await _updateAnalyticsRoomVisibility(); - Future updateMyLearningAnalyticsForAllClassesImIn([ - PLocalStore? storageService, - ]) async => - await _updateMyLearningAnalyticsForAllClassesImIn(storageService); - Future addAnalyticsRoomsToAllSpaces() async => await _addAnalyticsRoomsToAllSpaces(); diff --git a/lib/pangea/extensions/client_extension/general_info_extension.dart b/lib/pangea/extensions/client_extension/general_info_extension.dart index af9700cf6..058b6f695 100644 --- a/lib/pangea/extensions/client_extension/general_info_extension.dart +++ b/lib/pangea/extensions/client_extension/general_info_extension.dart @@ -5,11 +5,7 @@ extension GeneralInfoClientExtension on Client { final List adminRoomIds = []; for (final Room adminSpace in (await _classesAndExchangesImTeaching)) { adminRoomIds.add(adminSpace.id); - final children = adminSpace.childrenAndGrandChildren; - final List adminSpaceRooms = children - .where((e) => e.roomId != null) - .map((e) => e.roomId!) - .toList(); + final List adminSpaceRooms = adminSpace.allSpaceChildRoomIds; adminRoomIds.addAll(adminSpaceRooms); } return adminRoomIds; diff --git a/lib/pangea/extensions/pangea_room_extension/children_and_parents_extension.dart b/lib/pangea/extensions/pangea_room_extension/children_and_parents_extension.dart index 4362c17d8..78ab91cc8 100644 --- a/lib/pangea/extensions/pangea_room_extension/children_and_parents_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/children_and_parents_extension.dart @@ -20,57 +20,6 @@ extension ChildrenAndParentsRoomExtension on Room { List get _joinedChildrenRoomIds => joinedChildren.map((child) => child.id).toList(); - List get _childrenAndGrandChildren { - if (!isSpace) return []; - final List kids = []; - for (final child in spaceChildren) { - kids.add(child); - if (child.roomId != null) { - final Room? childRoom = client.getRoomById(child.roomId!); - if (childRoom != null && childRoom.isSpace) { - kids.addAll(childRoom.spaceChildren); - } - } - } - return kids.where((element) => element.roomId != null).toList(); - } - - //this assumes that a user has been invited to all group chats in a space - //it is a janky workaround for determining whether a spacechild is a direct chat - //since the spaceChild object doesn't contain this info. this info is only accessible - //when the user has joined or been invited to the room. direct chats included in - //a space show up in spaceChildren but the user has not been invited to them. - List get _childrenAndGrandChildrenDirectChatIds { - final List nonDirectChatRoomIds = childrenAndGrandChildren - .where((child) => child.roomId != null) - .map((e) => client.getRoomById(e.roomId!)) - .where((r) => r != null && !r.isDirectChat) - .map((e) => e!.id) - .toList(); - - return childrenAndGrandChildren - .where( - (child) => - child.roomId != null && - !nonDirectChatRoomIds.contains(child.roomId), - ) - .map((e) => e.roomId) - .cast() - .toList(); - - // return childrenAndGrandChildren - // .where((element) => element.roomId != null) - // .where( - // (child) { - // final room = client.getRoomById(child.roomId!); - // return room == null || room.isDirectChat; - // }, - // ) - // .map((e) => e.roomId) - // .cast() - // .toList(); - } - Future> _getChildRooms() async { final List children = []; for (final child in spaceChildren) { @@ -145,4 +94,37 @@ extension ChildrenAndParentsRoomExtension on Room { ), ) .toList(); + + String _nameIncludingParents(BuildContext context) { + String nameSoFar = getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)); + Room currentRoom = this; + if (currentRoom.immediateClassParents.isEmpty) { + return nameSoFar; + } + currentRoom = currentRoom.immediateClassParents.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) { + return nameSoFar; + } + return "... > $nameSoFar"; + } + + // gets all space children of a given space, down the + // space tree. + List get _allSpaceChildRoomIds { + final List childIds = []; + for (final child in spaceChildren) { + if (child.roomId == null) continue; + childIds.add(child.roomId!); + final Room? room = client.getRoomById(child.roomId!); + if (room != null && room.isSpace) { + childIds.addAll(room._allSpaceChildRoomIds); + } + } + return childIds; + } } diff --git a/lib/pangea/extensions/pangea_room_extension/class_and_exchange_settings_extension.dart b/lib/pangea/extensions/pangea_room_extension/class_and_exchange_settings_extension.dart index e71ed8f4f..04fc472f4 100644 --- a/lib/pangea/extensions/pangea_room_extension/class_and_exchange_settings_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/class_and_exchange_settings_extension.dart @@ -62,22 +62,11 @@ extension ClassAndExchangeSettingsRoomExtension on Room { } final Event? currentPower = getState(EventTypes.RoomPowerLevels); final Map? currentPowerContent = - currentPower?.content as Map?; - if (currentPowerContent == null) { - return; - } - if (!(currentPowerContent.containsKey("events"))) { - currentPowerContent["events"] = {}; - } - final spaceChildPower = - currentPowerContent["events"][EventTypes.spaceChild]; - final studentAnalyticsPower = currentPowerContent["events"] - [PangeaEventTypes.studentAnalyticsSummary]; + currentPower?.content["events"] as Map?; + final spaceChildPower = currentPowerContent?[EventTypes.spaceChild]; - if ((spaceChildPower == null || studentAnalyticsPower == null)) { + if (spaceChildPower == null && currentPowerContent != null) { currentPowerContent["events"][EventTypes.spaceChild] = 0; - currentPowerContent["events"] - [PangeaEventTypes.studentAnalyticsSummary] = 0; await client.setRoomStateWithKey( id, diff --git a/lib/pangea/extensions/pangea_room_extension/events_extension.dart b/lib/pangea/extensions/pangea_room_extension/events_extension.dart index 9b5922041..631d21478 100644 --- a/lib/pangea/extensions/pangea_room_extension/events_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/events_extension.dart @@ -1,6 +1,20 @@ part of "pangea_room_extension.dart"; extension EventsRoomExtension on Room { + Future _leaveIfFull() async { + await postLoad(); + if (!isRoomAdmin && + (_capacity != null) && + (await _numNonAdmins) > (_capacity!)) { + if (!isSpace) { + markUnread(false); + } + await leave(); + return true; + } + return false; + } + Future _archive() async { final students = (await requestParticipants()) .where( @@ -103,7 +117,6 @@ extension EventsRoomExtension on Room { required String type, }) async { try { - debugPrint("creating $type child for $parentEventId"); Sentry.addBreadcrumb(Breadcrumb.fromJson(content)); if (parentEventId.contains("web")) { debugger(when: kDebugMode); @@ -298,122 +311,153 @@ extension EventsRoomExtension on Room { } } - ConstructEvent? _vocabEventLocal(String lemma) { - if (!isAnalyticsRoom) throw Exception("not an analytics room"); + // ConstructEvent? _vocabEventLocal(String lemma) { + // if (!isAnalyticsRoom) throw Exception("not an analytics room"); - final Event? matrixEvent = getState(PangeaEventTypes.vocab, lemma); + // final Event? matrixEvent = getState(PangeaEventTypes.vocab, lemma); - return matrixEvent != null ? ConstructEvent(event: matrixEvent) : null; - } + // return matrixEvent != null ? ConstructEvent(event: matrixEvent) : null; + // } - Future _vocabEvent( - String lemma, - ConstructType type, [ - bool makeIfNull = false, - ]) async { - try { - if (!isAnalyticsRoom) throw Exception("not an analytics room"); + // Future _vocabEvent( + // String lemma, + // ConstructType type, [ + // bool makeIfNull = false, + // ]) async { + // try { + // if (!isAnalyticsRoom) throw Exception("not an analytics room"); - ConstructEvent? localEvent = _vocabEventLocal(lemma); + // ConstructEvent? localEvent = _vocabEventLocal(lemma); - if (localEvent != null) return localEvent; + // if (localEvent != null) return localEvent; - await postLoad(); - localEvent = _vocabEventLocal(lemma); + // await postLoad(); + // localEvent = _vocabEventLocal(lemma); - if (localEvent == null && isRoomOwner && makeIfNull) { - final Event matrixEvent = await _createVocabEvent(lemma, type); - localEvent = ConstructEvent(event: matrixEvent); - } + // if (localEvent == null && isRoomOwner && makeIfNull) { + // final Event matrixEvent = await _createVocabEvent(lemma, type); + // localEvent = ConstructEvent(event: matrixEvent); + // } - return localEvent!; - } catch (err) { - debugger(when: kDebugMode); - rethrow; - } - } + // return localEvent!; + // } catch (err) { + // debugger(when: kDebugMode); + // rethrow; + // } + // } - Future> _removeEditedLemmas( - List lemmaUses, - ) async { - final List removeUses = []; - for (final use in lemmaUses) { - if (use.msgId == null) continue; - final List removeIds = await client.getEditHistory( - use.chatId, - use.msgId!, - ); - removeUses.addAll(removeIds); - } - lemmaUses.removeWhere((use) => removeUses.contains(use.msgId)); - final allEvents = await allConstructEvents; - for (final constructEvent in allEvents) { - await constructEvent.removeEdittedUses(removeUses, client); - } - return lemmaUses; - } + // Future _createVocabEvent(String lemma, ConstructType type) async { + // try { + // if (!isRoomOwner) { + // throw Exception( + // "Tried to create vocab event in room where user is not owner", + // ); + // } + // final String eventId = await client.setRoomStateWithKey( + // id, + // PangeaEventTypes.vocab, + // lemma, + // ConstructUses(lemma: lemma, type: type).toJson(), + // ); + // final Event? event = await getEventById(eventId); - Future _saveConstructUsesSameLemma( - String lemma, - ConstructType type, - List lemmaUses, { - bool isEdit = false, + // if (event == null) { + // debugger(when: kDebugMode); + // throw Exception( + // "null event after creation with eventId $eventId in _createVocabEvent", + // ); + // } + // return event; + // } catch (err, stack) { + // debugger(when: kDebugMode); + // ErrorHandler.logError(e: err, s: stack, data: powerLevels); + // rethrow; + // } + // } + + Future> myMessageEventsInChat({ + DateTime? since, }) async { - final ConstructEvent? localEvent = _vocabEventLocal(lemma); - - if (isEdit) { - lemmaUses = await removeEditedLemmas(lemmaUses); - } - - if (localEvent == null) { - await client.setRoomStateWithKey( - id, - PangeaEventTypes.vocab, - lemma, - ConstructUses(lemma: lemma, type: type, uses: lemmaUses).toJson(), + final List 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, ); - } else { - localEvent.addAll(lemmaUses); - await updateStateEvent(localEvent.event); - } + }).toList(); } - Future> get _allConstructEvents async { - await postLoad(); - return states[PangeaEventTypes.vocab] - ?.values - .map((Event event) => ConstructEvent(event: event)) - .toList() - .cast() ?? - []; - } - - Future _createVocabEvent(String lemma, ConstructType type) async { + // fetch event of a certain type by a certain sender + // since a certain time or up to a certain amount + Future> getEventsBySender({ + required String type, + required String sender, + DateTime? since, + int? count, + }) async { try { - if (!isRoomOwner) { - throw Exception( - "Tried to create vocab event in room where user is not owner", - ); - } - final String eventId = await client.setRoomStateWithKey( - id, - PangeaEventTypes.vocab, - lemma, - ConstructUses(lemma: lemma, type: type).toJson(), - ); - final Event? event = await getEventById(eventId); + int numberOfSearches = 0; + final Timeline timeline = await getTimeline(); - if (event == null) { - debugger(when: kDebugMode); - throw Exception( - "null event after creation with eventId $eventId in _createVocabEvent", + List relevantEvents() => timeline.events + .where((event) => event.senderId == sender && event.type == type) + .toList(); + + bool reachedEnd() { + if (since != null) { + return relevantEvents().any( + (event) => event.originServerTs.isBefore(since), + ); + } + if (count != null) { + return relevantEvents().length >= count; + } + return false; + } + + while (timeline.canRequestHistory && + !reachedEnd() && + numberOfSearches < 10) { + await timeline.requestHistory(historyCount: 100); + numberOfSearches += 1; + if (reachedEnd()) { + break; + } + } + + final List fetchedEvents = timeline.events + .where((event) => event.senderId == sender && event.type == type) + .toList(); + + if (since != null) { + fetchedEvents.removeWhere( + (event) => event.originServerTs.isBefore(since), ); } - return event; - } catch (err, stack) { + + final List events = []; + for (Event event in fetchedEvents) { + if (event.relationshipType == RelationshipTypes.edit) continue; + if (event.hasAggregatedEvents(timeline, RelationshipTypes.edit)) { + event = event.getDisplayEvent(timeline); + } + events.add(event); + } + + return events; + } catch (err, s) { + if (kDebugMode) rethrow; debugger(when: kDebugMode); - ErrorHandler.logError(e: err, s: stack, data: powerLevels); - rethrow; + ErrorHandler.logError(e: err, s: s); + return []; } } } diff --git a/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart index 25790f476..edcd80b04 100644 --- a/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart @@ -7,11 +7,16 @@ import 'package:fluffychat/pangea/constants/class_default_values.dart'; import 'package:fluffychat/pangea/constants/model_keys.dart'; import 'package:fluffychat/pangea/constants/pangea_room_types.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/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/tokens_event_content_model.dart'; import 'package:fluffychat/pangea/utils/bot_name.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -21,20 +26,13 @@ import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:html_unescape/html_unescape.dart'; import 'package:matrix/matrix.dart'; import 'package:matrix/src/utils/markdown.dart'; -import 'package:matrix/src/utils/space_child.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import '../../../config/app_config.dart'; import '../../constants/pangea_event_types.dart'; -import '../../enum/construct_type_enum.dart'; import '../../enum/use_type.dart'; -import '../../matrix_event_wrappers/construct_analytics_event.dart'; import '../../models/choreo_record.dart'; -import '../../models/constructs_analytics_model.dart'; import '../../models/representation_content_model.dart'; -import '../../models/student_analytics_event.dart'; -import '../../models/student_analytics_summary_model.dart'; -import '../../utils/p_store.dart'; import '../client_extension/client_extension.dart'; part "children_and_parents_extension.dart"; @@ -63,47 +61,42 @@ extension PangeaRoom on Room { Future addAnalyticsRoomsToSpace() async => await _addAnalyticsRoomsToSpace(); - Future getStudentAnalytics( - String studentId, { - bool forcedUpdate = false, - }) async => - await _getStudentAnalytics(studentId, forcedUpdate: forcedUpdate); - - Future> getClassAnalytics([ - List? studentIds, - ]) async => - await _getClassAnalytics( - studentIds, - ); - - Future updateMyLearningAnalyticsForClass([ - PLocalStore? storageService, - ]) async => - await _updateMyLearningAnalyticsForClass( - storageService, - ); - Future inviteSpaceTeachersToAnalyticsRoom(Room analyticsRoom) async => await _inviteSpaceTeachersToAnalyticsRoom(analyticsRoom); Future inviteTeachersToAnalyticsRoom() async => await _inviteTeachersToAnalyticsRoom(); - // Invite teachers of 1 space to all users' analytics rooms Future inviteSpaceTeachersToAnalyticsRooms() async => await _inviteSpaceTeachersToAnalyticsRooms(); + Future getLastAnalyticsEvent( + String type, + String userId, + ) async => + await _getLastAnalyticsEvent(type, userId); + + Future analyticsLastUpdated(String type, String userId) async { + return await _analyticsLastUpdated(type, userId); + } + + Future?> getAnalyticsEvents({ + required String type, + required String userId, + DateTime? since, + }) async => + await _getAnalyticsEvents(type: type, since: since, userId: userId); + + String? get madeForLang => _madeForLang; + + bool isMadeForLang(String langCode) => _isMadeForLang(langCode); + // children_and_parents List get joinedChildren => _joinedChildren; List get joinedChildrenRoomIds => _joinedChildrenRoomIds; - List get childrenAndGrandChildren => _childrenAndGrandChildren; - - List get childrenAndGrandChildrenDirectChatIds => - _childrenAndGrandChildrenDirectChatIds; - Future> getChildRooms() async => await _getChildRooms(); Future joinSpaceChild(String roomID) async => @@ -116,6 +109,11 @@ extension PangeaRoom on Room { List get pangeaSpaceParents => _pangeaSpaceParents; + String nameIncludingParents(BuildContext context) => + _nameIncludingParents(context); + + List get allSpaceChildRoomIds => _allSpaceChildRoomIds; + // class_and_exchange_settings DateTime? get rulesUpdatedAt => _rulesUpdatedAt; @@ -142,6 +140,7 @@ extension PangeaRoom on Room { // events + Future leaveIfFull() async => await _leaveIfFull(); Future archive() async => await _archive(); Future archiveSpace( @@ -203,31 +202,10 @@ extension PangeaRoom on Room { Future updateStateEvent(Event stateEvent) => _updateStateEvent(stateEvent); - Future vocabEvent( - String lemma, - ConstructType type, [ - bool makeIfNull = false, - ]) => - _vocabEvent(lemma, type, makeIfNull); - - Future> removeEditedLemmas( - List lemmaUses, - ) async => - await _removeEditedLemmas(lemmaUses); - - Future saveConstructUsesSameLemma( - String lemma, - ConstructType type, - List lemmaUses, { - bool isEdit = false, - }) async => - await _saveConstructUsesSameLemma(lemma, type, lemmaUses, isEdit: isEdit); - - Future> get allConstructEvents async => - await _allConstructEvents; - // room_information + Future get numNonAdmins async => await _numNonAdmins; + DateTime? get creationTime => _creationTime; String? get creatorId => _creatorId; @@ -242,7 +220,7 @@ extension PangeaRoom on Room { bool get isDirectChatWithoutMe => _isDirectChatWithoutMe; - bool isMadeForLang(String langCode) => _isMadeForLang(langCode); + // bool isMadeForLang(String langCode) => _isMadeForLang(langCode); Future get isBotRoom async => await _isBotRoom; @@ -258,6 +236,11 @@ extension PangeaRoom on Room { // room_settings + Future updateRoomCapacity(int newCapacity) => + _updateRoomCapacity(newCapacity); + + int? get capacity => _capacity; + PangeaRoomRules? get pangeaRoomRules => _pangeaRoomRules; PangeaRoomRules? get firstRules => _firstRules; diff --git a/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart b/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart index b6c58a40d..756f83adf 100644 --- a/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart @@ -123,185 +123,6 @@ extension AnalyticsRoomExtension on Room { } } - StudentAnalyticsEvent? _getStudentAnalyticsLocal(String studentId) { - if (!isSpace) { - debugger(when: kDebugMode); - ErrorHandler.logError( - m: "calling getStudentAnalyticsLocal on non-space room", - s: StackTrace.current, - ); - return null; - } - - final Event? matrixEvent = getState( - PangeaEventTypes.studentAnalyticsSummary, - studentId, - ); - - return matrixEvent != null - ? StudentAnalyticsEvent(event: matrixEvent) - : null; - } - - Future _getStudentAnalytics( - String studentId, { - bool forcedUpdate = false, - }) async { - try { - if (!isSpace) { - debugger(when: kDebugMode); - throw Exception("calling getStudentAnalyticsLocal on non-space room"); - } - StudentAnalyticsEvent? localEvent = _getStudentAnalyticsLocal(studentId); - - if (localEvent == null) { - await postLoad(); - localEvent = _getStudentAnalyticsLocal(studentId); - } - - if (studentId == client.userID && localEvent == null) { - final Event? matrixEvent = await _createStudentAnalyticsEvent(); - if (matrixEvent != null) { - localEvent = StudentAnalyticsEvent(event: matrixEvent); - } - } - - return localEvent; - } catch (err) { - debugger(when: kDebugMode); - rethrow; - } - } - - /// if [studentIds] is null, returns all students - Future> _getClassAnalytics([ - List? studentIds, - ]) async { - await postLoad(); - await requestParticipants(); - final List> sassFutures = []; - final List filteredIds = students - .where( - (element) => studentIds == null || studentIds.contains(element.id), - ) - .map((e) => e.id) - .toList(); - for (final id in filteredIds) { - sassFutures.add( - getStudentAnalytics( - id, - ), - ); - } - return Future.wait(sassFutures); - } - - /// if [isSpace] - /// for all child chats, call _getChatAnalyticsGlobal and merge results - /// else - /// get analytics from pangea chat server - /// do any needed conversion work - /// save RoomAnalytics object to PangeaEventTypes.analyticsSummary event - Future _createStudentAnalyticsEvent() async { - try { - await postLoad(); - if (!pangeaCanSendEvent(PangeaEventTypes.studentAnalyticsSummary)) { - ErrorHandler.logError( - m: "null powerLevels in createStudentAnalytics", - s: StackTrace.current, - ); - return null; - } - if (client.userID == null) { - debugger(when: kDebugMode); - throw Exception("null userId in createStudentAnalytics"); - } - - final String eventId = await client.setRoomStateWithKey( - id, - PangeaEventTypes.studentAnalyticsSummary, - client.userID!, - StudentAnalyticsSummary( - // studentId: client.userID!, - lastUpdated: DateTime.now(), - messages: [], - ).toJson(), - ); - final Event? event = await getEventById(eventId); - - if (event == null) { - debugger(when: kDebugMode); - throw Exception( - "null event after creation with eventId $eventId in createStudentAnalytics", - ); - } - return event; - } catch (err, stack) { - ErrorHandler.logError(e: err, s: stack, data: powerLevels); - return null; - } - } - - /// for each chat in class - /// get timeline back to january 15 - /// get messages - /// discard timeline - /// save messages to StudentAnalyticsSummary - Future _updateMyLearningAnalyticsForClass([ - PLocalStore? storageService, - ]) async { - try { - final String migratedAnalyticsKey = - "MIGRATED_ANALYTICS_KEY${id.localpart}"; - - if (storageService?.read( - migratedAnalyticsKey, - local: true, - ) ?? - false) return; - - if (!isPangeaClass && !isExchange) { - throw Exception( - "In updateMyLearningAnalyticsForClass with room that is not not a class", - ); - } - - if (client.userID == null) { - debugger(when: kDebugMode); - return; - } - - final StudentAnalyticsEvent? myAnalEvent = - await getStudentAnalytics(client.userID!); - - if (myAnalEvent == null) { - debugPrint("null analytcs event for $id"); - if (pangeaCanSendEvent(PangeaEventTypes.studentAnalyticsSummary)) { - // debugger(when: kDebugMode); - } - return; - } - - final updateMessages = await _messageListForAllChildChats; - updateMessages.removeWhere( - (element) => myAnalEvent.content.messages.any( - (e) => e.eventId == element.eventId, - ), - ); - myAnalEvent.bulkUpdate(updateMessages); - - await storageService?.save( - migratedAnalyticsKey, - true, - local: true, - ); - } catch (err, s) { - if (kDebugMode) rethrow; - // debugger(when: kDebugMode); - ErrorHandler.logError(e: err, s: s); - } - } - // invite teachers of 1 space to 1 analytics room Future _inviteSpaceTeachersToAnalyticsRoom(Room analyticsRoom) async { if (!isSpace) { @@ -373,4 +194,67 @@ extension AnalyticsRoomExtension on Room { await inviteSpaceTeachersToAnalyticsRoom(analyticsRoom); } } + + Future _getLastAnalyticsEvent( + String type, + String userId, + ) async { + final List events = await getEventsBySender( + type: type, + sender: userId, + count: 1, + ); + if (events.isEmpty) return null; + final Event event = events.first; + AnalyticsEvent? analyticsEvent; + switch (type) { + case PangeaEventTypes.summaryAnalytics: + analyticsEvent = SummaryAnalyticsEvent(event: event); + case PangeaEventTypes.construct: + analyticsEvent = ConstructAnalyticsEvent(event: event); + } + return analyticsEvent; + } + + Future _analyticsLastUpdated(String type, String userId) async { + final lastEvent = await _getLastAnalyticsEvent(type, userId); + return lastEvent?.event.originServerTs; + } + + Future?> _getAnalyticsEvents({ + required String type, + required String userId, + DateTime? since, + }) async { + final List events = await getEventsBySender( + type: type, + sender: userId, + since: since, + ); + final List analyticsEvents = []; + for (final Event event in events) { + switch (type) { + case PangeaEventTypes.summaryAnalytics: + analyticsEvents.add(SummaryAnalyticsEvent(event: event)); + break; + case PangeaEventTypes.construct: + analyticsEvents.add(ConstructAnalyticsEvent(event: event)); + break; + } + } + + return analyticsEvents; + } + + String? get _madeForLang { + final creationContent = getState(EventTypes.RoomCreate)?.content; + return creationContent?.tryGet(ModelKey.langCode) ?? + creationContent?.tryGet(ModelKey.oldLangCode); + } + + bool _isMadeForLang(String langCode) { + final creationContent = getState(EventTypes.RoomCreate)?.content; + return creationContent?.tryGet(ModelKey.langCode) == langCode || + creationContent?.tryGet(ModelKey.oldLangCode) == langCode; + } } diff --git a/lib/pangea/extensions/pangea_room_extension/room_information_extension.dart b/lib/pangea/extensions/pangea_room_extension/room_information_extension.dart index 0c34a8aaf..213d3c4ce 100644 --- a/lib/pangea/extensions/pangea_room_extension/room_information_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/room_information_extension.dart @@ -1,6 +1,17 @@ part of "pangea_room_extension.dart"; extension RoomInformationRoomExtension on Room { + Future get _numNonAdmins async { + return (await requestParticipants()) + .where( + (e) => + e.powerLevel < ClassDefaultValues.powerLevelOfAdmin && + e.id != BotName.byEnvironment, + ) + .toList() + .length; + } + DateTime? get _creationTime => getState(EventTypes.RoomCreate)?.originServerTs; @@ -34,11 +45,11 @@ extension RoomInformationRoomExtension on Room { bool get _isDirectChatWithoutMe => isDirectChat && !getParticipants().any((e) => e.id == client.userID); - bool _isMadeForLang(String langCode) { - final creationContent = getState(EventTypes.RoomCreate)?.content; - return creationContent?.tryGet(ModelKey.langCode) == langCode || - creationContent?.tryGet(ModelKey.oldLangCode) == langCode; - } + // bool _isMadeForLang(String langCode) { + // final creationContent = getState(EventTypes.RoomCreate)?.content; + // return creationContent?.tryGet(ModelKey.langCode) == langCode || + // creationContent?.tryGet(ModelKey.oldLangCode) == langCode; + // } Future get _isBotRoom async { final List participants = await requestParticipants(); diff --git a/lib/pangea/extensions/pangea_room_extension/room_settings_extension.dart b/lib/pangea/extensions/pangea_room_extension/room_settings_extension.dart index 9746a5680..0eecb691d 100644 --- a/lib/pangea/extensions/pangea_room_extension/room_settings_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/room_settings_extension.dart @@ -1,6 +1,19 @@ part of "pangea_room_extension.dart"; extension RoomSettingsRoomExtension on Room { + Future _updateRoomCapacity(int newCapacity) => + client.setRoomStateWithKey( + id, + PangeaEventTypes.capacity, + '', + {'capacity': newCapacity}, + ); + + int? get _capacity { + final t = getState(PangeaEventTypes.capacity)?.content['capacity']; + return t is int ? t : null; + } + PangeaRoomRules? get _pangeaRoomRules { try { final Map? content = pangeaRoomRulesStateEvent?.content; diff --git a/lib/pangea/matrix_event_wrappers/construct_analytics_event.dart b/lib/pangea/matrix_event_wrappers/construct_analytics_event.dart index 176d84bbd..0dd820676 100644 --- a/lib/pangea/matrix_event_wrappers/construct_analytics_event.dart +++ b/lib/pangea/matrix_event_wrappers/construct_analytics_event.dart @@ -1,53 +1,60 @@ -import 'package:fluffychat/pangea/models/constructs_analytics_model.dart'; -import 'package:matrix/matrix.dart'; +// import 'package:fluffychat/pangea/models/constructs_analytics_model.dart'; +// import 'package:flutter/material.dart'; +// import 'package:matrix/matrix.dart'; -import '../constants/pangea_event_types.dart'; +// import '../constants/pangea_event_types.dart'; -class ConstructEvent { - late Event _event; - ConstructUses? _contentCache; +// class ConstructEvent { +// late Event _event; +// ConstructUses? _contentCache; - ConstructEvent({required Event event}) { - if (event.type != PangeaEventTypes.vocab) { - throw Exception( - "${event.type} should not be used to make a StudentAnalyticsEvent", - ); - } - _event = event; - } +// ConstructEvent({required Event event}) { +// if (event.type != PangeaEventTypes.vocab) { +// throw Exception( +// "${event.type} should not be used to make a StudentAnalyticsEvent", +// ); +// } +// _event = event; +// } - Event get event => _event; +// Event get event => _event; - ConstructUses get content { - _contentCache ??= ConstructUses.fromJson(event.content); - if (_contentCache!.lemma.isEmpty) { - _contentCache!.lemma = event.stateKey!; - } - return _contentCache!; - } +// ConstructUses get content { +// _contentCache ??= ConstructUses.fromJson(event.content); +// if (_contentCache!.lemma.isEmpty) { +// _contentCache!.lemma = event.stateKey!; +// } +// return _contentCache!; +// } - void addAll(List uses) { - content.uses.addAll(uses); - event.content = content.toJson(); - } +// void addAll(List uses) { +// for (final use in uses) { +// if (content.uses.any((element) => element.id == use.id)) { +// continue; +// } +// debugPrint("${use.toJson()}"); +// content.uses.add(use); +// } +// event.content = content.toJson(); +// } - Future removeEdittedUses( - List removeIds, - Client client, - ) async { - _contentCache ??= ConstructUses.fromJson(event.content); - if (_contentCache == null || _event.stateKey == null) return; - final previousLength = _contentCache!.uses.length; - _contentCache!.uses.removeWhere( - (element) => removeIds.contains(element.msgId), - ); - if (previousLength > _contentCache!.uses.length) { - await client.setRoomStateWithKey( - _event.room.id, - _event.type, - _event.stateKey!, - _contentCache!.toJson(), - ); - } - } -} +// Future removeEdittedUses( +// List removeIds, +// Client client, +// ) async { +// _contentCache ??= ConstructUses.fromJson(event.content); +// if (_contentCache == null || _event.stateKey == null) return; +// final previousLength = _contentCache!.uses.length; +// _contentCache!.uses.removeWhere( +// (element) => removeIds.contains(element.msgId), +// ); +// if (previousLength > _contentCache!.uses.length) { +// await client.setRoomStateWithKey( +// _event.room.id, +// _event.type, +// _event.stateKey!, +// _contentCache!.toJson(), +// ); +// } +// } +// } diff --git a/lib/pangea/models/analytics/analytics_event.dart b/lib/pangea/models/analytics/analytics_event.dart new file mode 100644 index 000000000..2453e62ef --- /dev/null +++ b/lib/pangea/models/analytics/analytics_event.dart @@ -0,0 +1,59 @@ +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'; + +// superclass for all analytics events +abstract class AnalyticsEvent { + late Event _event; + AnalyticsModel? contentCache; + + AnalyticsEvent({required Event event}) { + _event = event; + } + + Event get event => _event; + + AnalyticsModel get content { + switch (_event.type) { + case PangeaEventTypes.summaryAnalytics: + contentCache ??= SummaryAnalyticsModel.fromJson(event.content); + break; + case PangeaEventTypes.construct: + contentCache ??= ConstructAnalyticsModel.fromJson(event.content); + break; + } + return contentCache!; + } + + static List analyticsEventTypes = [ + PangeaEventTypes.summaryAnalytics, + PangeaEventTypes.construct, + ]; + + static Future sendEvent( + Room analyticsRoom, + String type, + List analyticsContent, + ) async { + String? eventId; + switch (type) { + case PangeaEventTypes.summaryAnalytics: + eventId = await SummaryAnalyticsEvent.sendSummaryAnalyticsEvent( + analyticsRoom, + analyticsContent.cast(), + ); + break; + case PangeaEventTypes.construct: + eventId = await ConstructAnalyticsEvent.sendConstructsEvent( + analyticsRoom, + analyticsContent.cast(), + ); + break; + } + return eventId; + } +} diff --git a/lib/pangea/models/analytics/analytics_model.dart b/lib/pangea/models/analytics/analytics_model.dart new file mode 100644 index 000000000..bdb3bc6d5 --- /dev/null +++ b/lib/pangea/models/analytics/analytics_model.dart @@ -0,0 +1,19 @@ +import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; +import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; +import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; +import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart'; + +abstract class AnalyticsModel { + static List formatAnalyticsContent( + List recentMsgs, + String type, + ) { + switch (type) { + case PangeaEventTypes.summaryAnalytics: + return SummaryAnalyticsModel.formatSummaryContent(recentMsgs); + case PangeaEventTypes.construct: + return ConstructAnalyticsModel.formatConstructsContent(recentMsgs); + } + return []; + } +} diff --git a/lib/pangea/models/chart_analytics_model.dart b/lib/pangea/models/analytics/chart_analytics_model.dart similarity index 91% rename from lib/pangea/models/chart_analytics_model.dart rename to lib/pangea/models/analytics/chart_analytics_model.dart index af06a0958..7430ede2f 100644 --- a/lib/pangea/models/chart_analytics_model.dart +++ b/lib/pangea/models/analytics/chart_analytics_model.dart @@ -1,10 +1,10 @@ import 'dart:developer'; +import 'package:fluffychat/pangea/enum/time_span.dart'; +import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart'; import 'package:flutter/foundation.dart'; -import 'package:fluffychat/pangea/enum/time_span.dart'; -import 'package:fluffychat/pangea/models/student_analytics_summary_model.dart'; -import '../enum/use_type.dart'; +import '../../enum/use_type.dart'; class TimeSeriesTotals { int ta; @@ -137,4 +137,13 @@ class ChartAnalyticsModel { } timeSeries = intervals.values.toList().reversed.toList(); } + + DateTime? get lastMessageTime { + if (msgs.isEmpty) { + return null; + } + return msgs.map((msg) => msg.time).reduce( + (compare, recent) => compare.isAfter(recent) ? compare : recent, + ); + } } diff --git a/lib/pangea/models/analytics/constructs_event.dart b/lib/pangea/models/analytics/constructs_event.dart new file mode 100644 index 000000000..c2930faba --- /dev/null +++ b/lib/pangea/models/analytics/constructs_event.dart @@ -0,0 +1,36 @@ +import 'package:fluffychat/pangea/models/analytics/analytics_event.dart'; +import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; +import 'package:matrix/matrix.dart'; + +import '../../constants/pangea_event_types.dart'; + +class ConstructAnalyticsEvent extends AnalyticsEvent { + ConstructAnalyticsEvent({required Event event}) : super(event: event) { + if (event.type != PangeaEventTypes.construct) { + throw Exception( + "${event.type} should not be used to make a ConstructAnalyticsEvent", + ); + } + } + + @override + ConstructAnalyticsModel get content { + contentCache ??= ConstructAnalyticsModel.fromJson(event.content); + return contentCache as ConstructAnalyticsModel; + } + + static Future sendConstructsEvent( + Room analyticsRoom, + List uses, + ) async { + final ConstructAnalyticsModel constructsModel = ConstructAnalyticsModel( + uses: uses, + ); + + final String? eventId = await analyticsRoom.sendEvent( + constructsModel.toJson(), + type: PangeaEventTypes.construct, + ); + return eventId; + } +} diff --git a/lib/pangea/models/constructs_analytics_model.dart b/lib/pangea/models/analytics/constructs_model.dart similarity index 55% rename from lib/pangea/models/constructs_analytics_model.dart rename to lib/pangea/models/analytics/constructs_model.dart index a62145485..18c6d3d5a 100644 --- a/lib/pangea/models/constructs_analytics_model.dart +++ b/lib/pangea/models/analytics/constructs_model.dart @@ -1,69 +1,105 @@ import 'dart:developer'; -import 'package:fluffychat/pangea/constants/model_keys.dart'; +import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.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'; - -class ConstructUses { - String lemma; - ConstructType type; +import '../../enum/construct_type_enum.dart'; +class ConstructAnalyticsModel extends AnalyticsModel { List uses; - //PTODO - how to incorporate semantic similarity score into this? - - //PTODO - add variables for saving requests for - // 1) definitions - // 2) translations - // 3) examples??? (gpt suggested) - - ConstructUses({ - required this.lemma, - required this.type, + ConstructAnalyticsModel({ this.uses = const [], }); - factory ConstructUses.fromJson(Map json) { - // try { - debugger( - when: - kDebugMode && (json['uses'] == null || json[ModelKey.lemma] == null), + static const _usesKey = "uses"; + + factory ConstructAnalyticsModel.fromJson(Map json) { + final List uses = []; + if (json[_usesKey] is List) { + // This is the new format + uses.addAll( + json[_usesKey] + .map((use) => OneConstructUse.fromJson(use)) + .cast() + .toList(), + ); + } else { + // This is the old format. No data on production should be + // structured this way, but it's useful for testing. + try { + final useValues = (json[_usesKey] as Map).values; + for (final useValue in useValues) { + final lemma = useValue['lemma']; + final lemmaUses = useValue[_usesKey]; + for (final useData in lemmaUses) { + final use = OneConstructUse( + useType: ConstructUseType.ga, + chatId: useData["chatId"], + timeStamp: DateTime.parse(useData["timeStamp"]), + lemma: lemma, + form: useData["form"], + msgId: useData["msgId"], + constructType: ConstructType.grammar, + ); + uses.add(use); + } + } + } catch (err, s) { + debugPrint("Error parsing ConstructAnalyticsModel"); + ErrorHandler.logError( + e: err, + s: s, + m: "Error parsing ConstructAnalyticsModel", + ); + debugger(when: kDebugMode); + } + } + return ConstructAnalyticsModel( + uses: uses, ); - return ConstructUses( - lemma: json[ModelKey.lemma], - uses: (json['uses'] as Iterable) - .map( - (use) => use != null ? OneConstructUse.fromJson(use) : null, - ) - .where((element) => element != null) - .cast() - .toList(), - type: ConstructTypeUtil.fromString(json['type']), - ); - // } catch (err) { - // debugger(when: kDebugMode); - // rethrow; - // } } toJson() { return { - ModelKey.lemma: lemma, - 'uses': uses.map((use) => use.toJson()).toList(), - 'type': type.string, + _usesKey: uses.map((use) => use.toJson()).toList(), }; } - void addUsesByUseType(List uses) { - for (final use in uses) { - if (use.lemma != lemma) { - throw Exception('lemma mismatch'); - } - uses.add(use); + static List formatConstructsContent( + List recentMsgs, + ) { + final List filtered = List.from(recentMsgs); + final List 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? tokens = msg.originalSent?.tokens; + if (tokens == null) continue; + uses.addAll( + msg.originalSent!.choreo!.toVocabUse( + tokens, + msg.room.id, + msg.eventId, + msg.originServerTs, + ), + ); } + + return uses; } } @@ -153,6 +189,7 @@ class OneConstructUse { String chatId; String? msgId; DateTime timeStamp; + String? id; OneConstructUse({ required this.useType, @@ -162,6 +199,7 @@ class OneConstructUse { required this.form, required this.msgId, required this.constructType, + this.id, }); factory OneConstructUse.fromJson(Map json) { @@ -176,10 +214,11 @@ class OneConstructUse { constructType: json['constructType'] != null ? ConstructTypeUtil.fromString(json['constructType']) : null, + id: json['id'], ); } - Map toJson([bool condensed = true]) { + Map toJson([bool condensed = false]) { final Map data = { 'useType': useType.string, 'chatId': chatId, @@ -191,6 +230,7 @@ class OneConstructUse { if (!condensed && constructType != null) { data['constructType'] = constructType!.string; } + if (id != null) data['id'] = id; return data; } @@ -205,3 +245,15 @@ class OneConstructUse { return room.getEventById(msgId!); } } + +class ConstructUses { + final List uses; + final ConstructType constructType; + final String lemma; + + ConstructUses({ + required this.uses, + required this.constructType, + required this.lemma, + }); +} diff --git a/lib/pangea/models/analytics/summary_analytics_event.dart b/lib/pangea/models/analytics/summary_analytics_event.dart new file mode 100644 index 000000000..e7034eaa4 --- /dev/null +++ b/lib/pangea/models/analytics/summary_analytics_event.dart @@ -0,0 +1,35 @@ +import 'package:fluffychat/pangea/models/analytics/analytics_event.dart'; +import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart'; +import 'package:matrix/matrix.dart'; + +import '../../constants/pangea_event_types.dart'; + +class SummaryAnalyticsEvent extends AnalyticsEvent { + SummaryAnalyticsEvent({required Event event}) : super(event: event) { + if (event.type != PangeaEventTypes.summaryAnalytics) { + throw Exception( + "${event.type} should not be used to make a SummaryAnalyticsEvent", + ); + } + } + + @override + SummaryAnalyticsModel get content { + contentCache ??= SummaryAnalyticsModel.fromJson(event.content); + return contentCache as SummaryAnalyticsModel; + } + + static Future sendSummaryAnalyticsEvent( + Room analyticsRoom, + List records, + ) async { + final SummaryAnalyticsModel analyticsModel = SummaryAnalyticsModel( + messages: records, + ); + final String? eventId = await analyticsRoom.sendEvent( + analyticsModel.toJson(), + type: PangeaEventTypes.summaryAnalytics, + ); + return eventId; + } +} diff --git a/lib/pangea/models/student_analytics_summary_model.dart b/lib/pangea/models/analytics/summary_analytics_model.dart similarity index 70% rename from lib/pangea/models/student_analytics_summary_model.dart rename to lib/pangea/models/analytics/summary_analytics_model.dart index 69d237ea9..b09d0a870 100644 --- a/lib/pangea/models/student_analytics_summary_model.dart +++ b/lib/pangea/models/analytics/summary_analytics_model.dart @@ -1,10 +1,64 @@ import 'dart:convert'; +import 'package:fluffychat/pangea/enum/use_type.dart'; +import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; +import 'package:fluffychat/pangea/models/analytics/analytics_model.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:flutter/foundation.dart'; -import 'package:matrix/matrix.dart'; -import '../enum/use_type.dart'; +class SummaryAnalyticsModel extends AnalyticsModel { + late List _messages; + + SummaryAnalyticsModel({ + required List messages, + }) { + _messages = messages; + } + + List get messages => _messages; + + static const _messagesKey = "msgs"; + + Map toJson() => { + _messagesKey: jsonEncode(_messages.map((e) => e.toJson()).toList()), + }; + + factory SummaryAnalyticsModel.fromJson(json) { + List savedMessages = []; + try { + savedMessages = json[_messagesKey] != null + ? (jsonDecode(json[_messagesKey] ?? "[]") as Iterable) + .map((e) => RecentMessageRecord.fromJson(e)) + .toList() + .cast() + : []; + } catch (err, stack) { + if (kDebugMode) rethrow; + ErrorHandler.logError(e: err, s: stack); + } + return SummaryAnalyticsModel( + messages: savedMessages, + ); + } + + static List formatSummaryContent( + List recentMsgs, + ) { + final List filtered = List.from(recentMsgs); + final List records = filtered + .map( + (msg) => RecentMessageRecord( + eventId: msg.eventId, + chatId: msg.room.id, + useType: msg.useType, + time: msg.originServerTs, + ), + ) + .toList(); + + return records; + } +} class RecentMessageRecord { String eventId; @@ -55,62 +109,3 @@ class RecentMessageRecord { static const _typeOfUseKey = "typ"; static const _timeKey = "t"; } - -class StudentAnalyticsSummary { - late List _messages; - DateTime lastUpdated; - - StudentAnalyticsSummary({ - required List messages, - required this.lastUpdated, - }) { - _messages = messages; - } - - void addAll(List msgs) { - for (final msg in msgs) { - if (_messages.any((element) => element.eventId == msg.eventId)) { - ErrorHandler.logError( - m: "adding message twice in StudentAnalyticsSummary.add", - ); - } else { - _messages.add(msg); - } - } - } - - void removeEdittedMessages(Client client, List removeEventIds) { - _messages.removeWhere( - (element) => removeEventIds.contains(element.eventId), - ); - } - - List get messages => _messages; - - static const _messagesKey = "msgs"; - static const _lastUpdatedKey = "lupt"; - - Map toJson() => { - _messagesKey: jsonEncode(_messages.map((e) => e.toJson()).toList()), - _lastUpdatedKey: lastUpdated.toIso8601String(), - }; - - factory StudentAnalyticsSummary.fromJson(json) { - List savedMessages = []; - try { - savedMessages = json[_messagesKey] != null - ? (jsonDecode(json[_messagesKey] ?? "[]") as Iterable) - .map((e) => RecentMessageRecord.fromJson(e)) - .toList() - .cast() - : []; - } catch (err, stack) { - if (kDebugMode) rethrow; - ErrorHandler.logError(e: err, s: stack); - } - return StudentAnalyticsSummary( - messages: savedMessages, - lastUpdated: DateTime.parse(json[_lastUpdatedKey]), - ); - } -} diff --git a/lib/pangea/models/analytics_model_old.dart b/lib/pangea/models/analytics_model_old.dart deleted file mode 100644 index 8dc5159da..000000000 --- a/lib/pangea/models/analytics_model_old.dart +++ /dev/null @@ -1,100 +0,0 @@ -// import 'dart:convert'; - -// class UserTimeSeriesInterval { -// String? userId; -// int? taTotal; -// int? gaTotal; -// int? waTotal; - -// UserTimeSeriesInterval({ -// required this.userId, -// required this.taTotal, -// required this.gaTotal, -// required this.waTotal, -// }); - -// Map toJson() => -// {"usr": userId, "ta": taTotal, "ga": gaTotal, "wa": waTotal}; - -// factory UserTimeSeriesInterval.fromJson(json) => UserTimeSeriesInterval( -// userId: json["usr"], -// taTotal: json["ta"], -// gaTotal: json["ga"], -// waTotal: json["wa"], -// ); -// } - -// class TimeSeriesInterval { -// DateTime start; -// DateTime end; -// List users; - -// TimeSeriesInterval({ -// required this.start, -// required this.end, -// required this.users, -// }); - -// Map toJson() => { -// "strt": start, -// "end": end, -// "usrs": jsonEncode(users.map((e) => e.toJson()).toList()) -// }; - -// factory TimeSeriesInterval.fromJson(json) => TimeSeriesInterval( -// start: json["strt"], -// end: json["end"], -// users: ((jsonDecode(json["usrs"]) as Iterable) -// .map((e) => UserTimeSeriesInterval.fromJson(e)) -// .toList() -// .cast()), -// ); -// } - -// class RoomAnalyticsSummary { -// List monthlyTotalsForAllTime; -// List dailyTotalsForLast30Days; -// List hourlyTotalsForLast24Hours; - -// DateTime? updatedAt; - -// RoomAnalyticsSummary({ -// required this.monthlyTotalsForAllTime, -// required this.dailyTotalsForLast30Days, -// required this.hourlyTotalsForLast24Hours, -// }); - -// Map toJson() => { -// "mnths": -// jsonEncode(monthlyTotalsForAllTime.map((e) => e.toJson()).toList()), -// "dys": jsonEncode( -// dailyTotalsForLast30Days.map((e) => e.toJson()).toList()), -// "hrs": jsonEncode( -// hourlyTotalsForLast24Hours.map((e) => e.toJson()).toList()), -// }; - -// factory RoomAnalyticsSummary.fromJson(json) => RoomAnalyticsSummary( -// monthlyTotalsForAllTime: (jsonDecode(json["mnths"]) as Iterable) -// .map((e) => TimeSeriesInterval.fromJson(e)) -// .toList() -// .cast(), -// dailyTotalsForLast30Days: (jsonDecode(json["dys"]) as Iterable) -// .map((e) => TimeSeriesInterval.fromJson(e)) -// .toList() -// .cast(), -// hourlyTotalsForLast24Hours: (jsonDecode(json["hrs"]) as Iterable) -// .map((e) => TimeSeriesInterval.fromJson(e)) -// .toList() -// .cast(), -// ); -// } - -// class UserDirectChatAnalyticsSummary { -// // directChatRoomIds and analytics for those rooms -// // updated by user; -// Map? directChatSummaries; - -// Map toJson() => {}; -// } - -// // maybe search how to do date ranges in dart diff --git a/lib/pangea/models/analytics_model_older.dart b/lib/pangea/models/analytics_model_older.dart deleted file mode 100644 index 2ee817f0b..000000000 --- a/lib/pangea/models/analytics_model_older.dart +++ /dev/null @@ -1,124 +0,0 @@ -// import 'dart:convert'; - -// class ChatTimeSeriesInterval { -// String? chatId; -// int? taTotal; -// int? gaTotal; -// int? waTotal; - -// ChatTimeSeriesInterval({ -// required this.chatId, -// required this.taTotal, -// required this.gaTotal, -// required this.waTotal, -// }); - -// Map toJson() => -// {"id": chatId, "ta": taTotal, "ga": gaTotal, "wa": waTotal}; - -// factory ChatTimeSeriesInterval.fromJson(json) => ChatTimeSeriesInterval( -// chatId: json["id"], -// taTotal: json["ta"], -// gaTotal: json["ga"], -// waTotal: json["wa"], -// ); -// } - -// class TimeSeriesInterval { -// DateTime start; -// DateTime end; -// List chats; - -// TimeSeriesInterval({ -// required this.start, -// required this.end, -// required this.chats, -// }); - -// Map toJson() => { -// "strt": start, -// "end": end, -// "usrs": jsonEncode(chats.map((e) => e.toJson()).toList()) -// }; - -// factory TimeSeriesInterval.fromJson(json) => TimeSeriesInterval( -// start: DateTime(json["strt"]), -// end: DateTime(json["end"]), -// chats: ((jsonDecode(json["usrs"]) as Iterable) -// .map((e) => ChatTimeSeriesInterval.fromJson(e)) -// .toList() -// .cast()), -// ); -// } - -// // class RecentMessageRecord { -// // String eventId; -// // String typeOfUse; -// // String time; -// // } - -// class StudentAnalyticsSummary { -// /// event statekey = studentId -// // String studentId; - -// List monthlyTotalsForAllTime; -// List dailyTotalsForLast30Days; -// List hourlyTotalsForLast24Hours; - -// // List messages; - -// DateTime lastLogin; -// DateTime lastMessage; - -// DateTime lastUpdated; - -// StudentAnalyticsSummary({ -// // required this.studentId, -// required this.monthlyTotalsForAllTime, -// required this.dailyTotalsForLast30Days, -// required this.hourlyTotalsForLast24Hours, -// required this.lastLogin, -// required this.lastMessage, -// required this.lastUpdated, -// }); - -// // static const _studentIdKey = 'usr'; -// static const _monthKey = "mnths"; -// static const _dayKey = "dys"; -// static const _hoursKey = "hrs"; -// static const _lastLoginKey = "lgn"; -// static const _lastMessageKey = "msg"; -// static const _lastUpdated = "lupt"; - -// Map toJson() => { -// _monthKey: -// jsonEncode(monthlyTotalsForAllTime.map((e) => e.toJson()).toList()), -// _dayKey: jsonEncode( -// dailyTotalsForLast30Days.map((e) => e.toJson()).toList()), -// _hoursKey: jsonEncode( -// hourlyTotalsForLast24Hours.map((e) => e.toJson()).toList()), -// // _studentIdKey: studentId, -// _lastLoginKey: lastLogin.toIso8601String(), -// _lastMessageKey: lastMessage.toIso8601String(), -// _lastUpdated: lastUpdated.toIso8601String() -// }; - -// factory StudentAnalyticsSummary.fromJson(json) => StudentAnalyticsSummary( -// // studentId: json[_studentIdKey], -// monthlyTotalsForAllTime: (jsonDecode(json[_monthKey]) as Iterable) -// .map((e) => TimeSeriesInterval.fromJson(e)) -// .toList() -// .cast(), -// dailyTotalsForLast30Days: (jsonDecode(json[_dayKey]) as Iterable) -// .map((e) => TimeSeriesInterval.fromJson(e)) -// .toList() -// .cast(), -// hourlyTotalsForLast24Hours: (jsonDecode(json[_hoursKey]) as Iterable) -// .map((e) => TimeSeriesInterval.fromJson(e)) -// .toList() -// .cast(), -// lastLogin: DateTime(json[_lastLoginKey]), -// lastUpdated: DateTime(json[_lastLoginKey]), -// lastMessage: DateTime(json[_lastMessageKey]), -// ); -// } diff --git a/lib/pangea/models/analytics_model_oldest.dart b/lib/pangea/models/analytics_model_oldest.dart deleted file mode 100644 index f075c1fe4..000000000 --- a/lib/pangea/models/analytics_model_oldest.dart +++ /dev/null @@ -1,77 +0,0 @@ -// import 'dart:convert'; - -// class BaseDataModel { -// late int spanTotal; -// late int spanIT; -// late int spanIGC; -// late int spanDirect; - -// BaseDataModel(Map json) { -// fromJson(json); -// } - -// fromJson(Map json) { -// spanTotal = json["total"]; -// spanIT = json["it"]; -// spanIGC = json["igc"]; -// spanDirect = json["direct"]; -// } -// } - -// class TimeSeriesInterval extends BaseDataModel { -// //note: always in UTC -// late DateTime start; -// late DateTime end; - -// TimeSeriesInterval(Map json) : super(json) { -// fromJsonTimeSeriesInterval(json); -// } - -// fromJsonTimeSeriesInterval(Map json) { -// start = DateTime.parse(json["start"]); -// end = DateTime.parse(json["end"]); -// } -// } - -// class chartAnalytics extends BaseDataModel { -// late String id; -// late int allTotal; -// late int allIT; -// late int allIGC; -// late int allDirect; -// late String timeSpan; -// late DateTime fetchedAt; -// late List? chatIds; -// late List? userIds; -// late List? classIds; -// late List timeSeries; - -// chartAnalytics(Map json) : super(json) { -// fromJsonchartAnalytics(json); -// fetchedAt = DateTime.now(); -// } - -// fromJsonchartAnalytics(Map json) { -// id = json["id"]; -// timeSpan = json["timespan"]; -// allTotal = json["alltime"]["total"]; -// allIT = json["alltime"]["it"]; -// allIGC = json["alltime"]["igc"]; -// allDirect = json["alltime"]["direct"]; -// timeSeries = (json["timeseries"] as Iterable) -// .map( -// (timeSeriesJsonEntry) => TimeSeriesInterval(timeSeriesJsonEntry), -// ) -// .toList() -// .cast(); -// chatIds = json["chats"] != null && json["chats"] != [] -// ? (json["chats"] as List).cast() -// : null; -// userIds = json["users"] != null && json["userIds"] != [] -// ? (json["users"] as List).cast() -// : null; -// classIds = json["classes"] != null && json["classes"] != [] -// ? (json["classes"] as List).cast() -// : null; -// } -// } diff --git a/lib/pangea/models/choreo_record.dart b/lib/pangea/models/choreo_record.dart index d3612f8f4..3422a76a2 100644 --- a/lib/pangea/models/choreo_record.dart +++ b/lib/pangea/models/choreo_record.dart @@ -1,10 +1,11 @@ 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 'constructs_analytics_model.dart'; import 'it_step.dart'; import 'lemma.dart'; @@ -126,6 +127,7 @@ class ChoreoRecord { List tokens, String chatId, String msgId, + DateTime timestamp, ) { final List uses = []; final DateTime now = DateTime.now(); @@ -140,7 +142,7 @@ class ChoreoRecord { OneConstructUse( useType: type, chatId: chatId, - timeStamp: now, + timeStamp: timestamp, lemma: lemma.text, form: lemma.form, msgId: msgId, @@ -210,9 +212,12 @@ class ChoreoRecord { return uses; } - List toGrammarConstructUse(String msgId, String chatId) { + List toGrammarConstructUse( + String msgId, + String chatId, + DateTime timestamp, + ) { final List uses = []; - final DateTime now = DateTime.now(); for (final step in choreoSteps) { if (step.acceptedOrIgnoredMatch?.status == PangeaMatchStatus.accepted) { final String name = step.acceptedOrIgnoredMatch!.match.rule?.id ?? @@ -222,11 +227,12 @@ class ChoreoRecord { OneConstructUse( useType: ConstructUseType.ga, chatId: chatId, - timeStamp: now, + timeStamp: timestamp, lemma: name, form: name, msgId: msgId, constructType: ConstructType.grammar, + id: "${msgId}_${step.acceptedOrIgnoredMatch!.match.offset}_${step.acceptedOrIgnoredMatch!.match.length}", ), ); } diff --git a/lib/pangea/models/class_analytics_model.dart b/lib/pangea/models/class_analytics_model.dart deleted file mode 100644 index ba7f642ce..000000000 --- a/lib/pangea/models/class_analytics_model.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'package:intl/intl.dart'; - -class ClassAnalyticsModel { - ClassAnalyticsModel(); - late final Null classId; - late final List userIds; - late final List analytics; - get tableView {} - ClassAnalyticsModel.fromJson(Map json) { - classId = null; - userIds = List.castFrom(json['user_ids']); - analytics = - List.from(json['analytics']).map((e) => Analytics.fromJson(e)).toList(); - } - - Map toJson() { - final data = {}; - data['class_id'] = classId; - data['user_ids'] = userIds; - data['analytics'] = analytics.map((e) => e.toJson()).toList(); - return data; - } -} - -class Analytics { - Analytics({ - required this.title, - required this.section, - }); - late final String title; - late final List
section; - - Analytics.fromJson(Map json) { - title = json['title']; - section = - List.from(json['section']).map((e) => Section.fromJson(e)).toList(); - } - - Map toJson() { - final data = {}; - data['title'] = title; - data['section'] = section.map((e) => e.toJson()).toList(); - return data; - } -} - -class Section { - Section({ - required this.title, - required this.classTotal, - required this.data, - }); - late final String title; - late final String classTotal; - late final List data; - - Section.fromJson(Map json) { - title = json['title']; - classTotal = json['class_total']; - data = List.from(json['data']).map((e) => Data.fromJson(e)).toList(); - } - - Map toJson() { - final data = {}; - data['title'] = title; - data['class_total'] = classTotal; - (data['data'] as List).map((item) => Data.fromJson(item)).toList(); - return data; - } -} - -class Data { - Data(); - set value(String val) => _value = val; - String get value { - if (value_type == 'date') { - return DateFormat('yyyy/M/dd hh:mm a') - .format(DateTime.parse(_value).toLocal()) - .toString(); - } - return _value; - } - - late final String userId; - late final String _value; - late final String value_type; - Data.fromJson(Map json) { - userId = json['user_id']; - _value = json['value']; - value_type = json['value_type']; - } - - Map toJson() { - final data = {}; - data['user_id'] = userId; - data['value'] = _value; - data['value_type'] = value_type; - return data; - } -} diff --git a/lib/pangea/models/headwords.dart b/lib/pangea/models/headwords.dart index 3586253b8..497381fa1 100644 --- a/lib/pangea/models/headwords.dart +++ b/lib/pangea/models/headwords.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'dart:developer'; -import 'package:fluffychat/pangea/models/constructs_analytics_model.dart'; +import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; diff --git a/lib/pangea/models/student_analytics_event.dart b/lib/pangea/models/student_analytics_event.dart deleted file mode 100644 index cb2b7de0e..000000000 --- a/lib/pangea/models/student_analytics_event.dart +++ /dev/null @@ -1,161 +0,0 @@ -import 'dart:developer'; - -import 'package:fluffychat/pangea/constants/class_default_values.dart'; -import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart'; -import 'package:fluffychat/pangea/models/student_analytics_summary_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'; -import 'chart_analytics_model.dart'; - -class StudentAnalyticsEvent { - late Event _event; - StudentAnalyticsSummary? _contentCache; - List _messagesToSave = []; - - StudentAnalyticsEvent({required Event event}) { - if (event.type != PangeaEventTypes.studentAnalyticsSummary) { - throw Exception( - "${event.type} should not be used to make a StudentAnalyticsEvent", - ); - } - _event = event; - if (!classRoom.isSpace) { - throw Exception( - "non-class room should not be used to make a StudentAnalyticsEvent", - ); - } - _event = event; - - _messagesToSave = []; - } - - Room get classRoom => _event.room; - - Event get event => _event; - - StudentAnalyticsSummary get content { - _contentCache ??= StudentAnalyticsSummary.fromJson(event.content); - return _contentCache!; - } - - Future removeEdittedMessages( - RecentMessageRecord message, - ) async { - final List removeIds = await classRoom.client.getEditHistory( - message.chatId, - message.eventId, - ); - if (removeIds.isEmpty) return; - _messagesToSave.removeWhere( - (msg) => removeIds.any((e) => e == msg.eventId), - ); - content.removeEdittedMessages( - classRoom.client, - removeIds, - ); - } - - Future handleNewMessage( - RecentMessageRecord message, { - isEdit = false, - }) async { - if (classRoom.client.userID != _event.stateKey) { - debugger(when: kDebugMode); - ErrorHandler.logError( - m: "should not be in handleNewMessage ${classRoom.client.userID} != ${_event.stateKey}", - ); - return; - } - - if (isEdit) { - await removeEdittedMessages(message); - } - _addMessage(message); - - if (DateTime.now().difference(content.lastUpdated).inMinutes > - ClassDefaultValues.minutesDelayToUpdateMyAnalytics) { - _updateStudentAnalytics(); - } - } - - Future bulkUpdate(List messages) async { - if (classRoom.client.userID != _event.stateKey) { - debugger(when: kDebugMode); - ErrorHandler.logError( - m: "should not be in bulkUpdate ${classRoom.client.userID} != ${_event.stateKey}", - ); - return; - } - for (final message in messages) { - await removeEdittedMessages(message); - } - - _messagesToSave.addAll(messages); - _updateStudentAnalytics(); - } - - Future _updateStudentAnalytics() async { - content.lastUpdated = DateTime.now(); - content.addAll(_messagesToSave); - debugPrint("updating student analytics"); - _clearMessages(); - - await classRoom.client.setRoomStateWithKey( - classRoom.id, - _event.type, - _event.stateKey!, - content.toJson(), - ); - } - - _addMessage(RecentMessageRecord message) { - if (_messagesToSave.every((e) => e.eventId != message.eventId)) { - _messagesToSave.add(message); - } else { - debugger(when: kDebugMode); - ErrorHandler.logError( - m: "adding message twice in StudentAnalyticsEvent._addMessage", - ); - } - //PTODO - save to local storagge - } - - _clearMessages() { - _messagesToSave.clear(); - //PTODO - clear local storagge - } - - Future getTotals(String? chatId) async { - final TimeSeriesTotals totals = TimeSeriesTotals.empty; - final msgs = chatId == null - ? content.messages - : content.messages.where((msg) => msg.chatId == chatId); - for (final msg in msgs) { - totals.increment(msg); - } - return totals; - } - - Future getTimeServiesInterval( - DateTime start, - DateTime end, - String? chatId, - ) async { - final TimeSeriesInterval interval = TimeSeriesInterval( - start: start, - end: end, - totals: TimeSeriesTotals.empty, - ); - for (final msg in content.messages) { - if (msg.time.isAfter(start) && - msg.time.isBefore(end) && - (chatId == null || chatId == msg.chatId)) { - interval.totals.increment(msg); - } - } - return interval; - } -} diff --git a/lib/pangea/models/student_analytics_event_old.dart b/lib/pangea/models/student_analytics_event_old.dart deleted file mode 100644 index d2696eb01..000000000 --- a/lib/pangea/models/student_analytics_event_old.dart +++ /dev/null @@ -1,51 +0,0 @@ -// import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart'; -// import 'package:fluffychat/pangea/models/analytics_model_older.dart'; -// import 'package:matrix/matrix.dart'; - -// import '../constants/pangea_event_types.dart'; - -// class StudentAnalyticsEvent { -// late Event _event; -// StudentAnalyticsSummary? _contentCache; - -// StudentAnalyticsEvent({required Event event}) { -// if (event.type != PangeaEventTypes.studentAnalyticsSummary) { -// throw Exception( -// "${event.type} should not be used to make a StudentAnalyticsEvent", -// ); -// } -// _event = event; -// } - -// Event get event => _event; - -// StudentAnalyticsSummary get _content { -// _contentCache ??= event.getPangeaContent(); -// return _contentCache!; -// } - -// List get monthly => _content.monthlyTotalsForAllTime; -// List get daily => _content.dailyTotalsForLast30Days; -// List get hourly => _content.hourlyTotalsForLast24Hours; - -// // updateLocal -// // updateServer -// handleNewMessage() {} - -// /// if monthly.isNotEmpty && last.end.month < now.month -// /// push empty intervals until last.end.month >= now.month -// /// if daily.isEmpty -// /// push empty intervals until last.end.day >= now.day -// /// else if daily.where(e => e.month < now.month) -// /// sum and add to monthly -// /// -// /// if hourly.isEmpty || last.end.hour < now.hour -// /// push empty intervals until last.end.hour >= now.hour -// /// increment hourly - -// updateLocal() {} - -// // if server copy is older than x, push local version -// // get new server copy, local version = server copy -// updateServer() {} -// } diff --git a/lib/pangea/pages/analytics/analytics_list_tile.dart b/lib/pangea/pages/analytics/analytics_list_tile.dart index 1738223fb..53bd72922 100644 --- a/lib/pangea/pages/analytics/analytics_list_tile.dart +++ b/lib/pangea/pages/analytics/analytics_list_tile.dart @@ -1,3 +1,7 @@ +import 'dart:async'; + +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/enum/bar_chart_view_enum.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -7,122 +11,148 @@ import 'package:matrix/matrix.dart'; import '../../../../utils/date_time_extension.dart'; import '../../../widgets/avatar.dart'; import '../../../widgets/matrix.dart'; -import '../../models/chart_analytics_model.dart'; +import '../../models/analytics/chart_analytics_model.dart'; import 'base_analytics.dart'; import 'list_summary_analytics.dart'; class AnalyticsListTile extends StatefulWidget { const AnalyticsListTile({ super.key, - required this.model, - required this.displayName, - required this.avatar, - required this.type, - required this.id, - required this.allowNavigateOnSelect, + required this.defaultSelected, required this.selected, + required this.avatar, + required this.allowNavigateOnSelect, + required this.isSelected, required this.onTap, - this.enabled = true, - this.showSpaceAnalytics = true, + required this.pangeaController, + this.controller, + // this.isEnabled = true, + // this.showSpaceAnalytics = true, + this.refreshStream, }); - final Uri? avatar; - final String displayName; - final AnalyticsEntryType type; - final String id; - final ChartAnalyticsModel? model; - final bool allowNavigateOnSelect; final void Function(AnalyticsSelected) onTap; - final bool selected; - final bool enabled; - final bool showSpaceAnalytics; + + final AnalyticsSelected defaultSelected; + final AnalyticsSelected selected; + + final Uri? avatar; + + final bool allowNavigateOnSelect; + final bool isSelected; + // final bool isEnabled; + // final bool showSpaceAnalytics; + + final PangeaController pangeaController; + final BaseAnalyticsController? controller; + final StreamController? refreshStream; @override AnalyticsListTileState createState() => AnalyticsListTileState(); } class AnalyticsListTileState extends State { + ChartAnalyticsModel? tileData; + StreamSubscription? refreshSubscription; + + @override + void initState() { + super.initState(); + setTileData(); + refreshSubscription = widget.refreshStream?.stream.listen((forceUpdate) { + setTileData(forceUpdate: forceUpdate); + }); + } + + @override + void dispose() { + refreshSubscription?.cancel(); + super.dispose(); + } + + Future setTileData({forceUpdate = false}) async { + tileData = await MatrixState.pangeaController.analytics.getAnalytics( + defaultSelected: widget.defaultSelected, + selected: widget.selected, + forceUpdate: forceUpdate, + ); + if (mounted) setState(() {}); + } + @override Widget build(BuildContext context) { - final Room? room = Matrix.of(context).client.getRoomById(widget.id); + final Room? room = + Matrix.of(context).client.getRoomById(widget.selected.id); return Material( - color: widget.selected + color: widget.isSelected ? Theme.of(context).colorScheme.secondaryContainer : Colors.transparent, - child: Opacity( - opacity: widget.enabled ? 1 : 0.5, - child: Tooltip( - message: widget.enabled - ? "" - : widget.type == AnalyticsEntryType.room - ? L10n.of(context)!.joinToView - : L10n.of(context)!.studentAnalyticsNotAvailable, - child: ListTile( - leading: widget.type == AnalyticsEntryType.privateChats - ? CircleAvatar( - backgroundColor: Theme.of(context).primaryColor, - foregroundColor: Colors.white, - radius: Avatar.defaultSize / 2, - child: const Icon(Icons.forum), - ) - : Avatar( - mxContent: widget.avatar, - name: widget.displayName, - littleIcon: room?.roomTypeIcon, - ), - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - widget.displayName, - maxLines: 1, - overflow: TextOverflow.ellipsis, - softWrap: false, - style: TextStyle( - fontWeight: FontWeight.bold, - color: Theme.of(context).textTheme.bodyLarge!.color, - ), + child: Tooltip( + message: widget.selected.type == AnalyticsEntryType.room + ? L10n.of(context)!.joinToView + : L10n.of(context)!.studentAnalyticsNotAvailable, + child: ListTile( + leading: widget.selected.type == AnalyticsEntryType.privateChats + ? CircleAvatar( + backgroundColor: Theme.of(context).primaryColor, + foregroundColor: Colors.white, + radius: Avatar.defaultSize / 2, + child: const Icon(Icons.forum), + ) + : Avatar( + mxContent: widget.avatar, + name: widget.selected.displayName, + littleIcon: room?.roomTypeIcon, + ), + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + widget.selected.displayName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + softWrap: false, + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context).textTheme.bodyLarge!.color, ), ), - Tooltip( - message: L10n.of(context)!.timeOfLastMessage, - child: Text( - widget.model?.lastMessage?.localizedTimeShort(context) ?? - "", - style: TextStyle( - fontSize: 13, - color: Theme.of(context).textTheme.bodyMedium!.color, - ), + ), + Tooltip( + message: L10n.of(context)!.timeOfLastMessage, + child: Text( + tileData?.lastMessageTime?.localizedTimeShort(context) ?? "", + style: TextStyle( + fontSize: 13, + color: Theme.of(context).textTheme.bodyMedium!.color, ), ), - ], - ), - subtitle: widget.showSpaceAnalytics || !(room?.isSpace ?? false) - ? ListSummaryAnalytics( - chartAnalytics: widget.model, - ) - : null, - selected: widget.selected, - onTap: () { - (room?.isSpace ?? false) && widget.allowNavigateOnSelect - ? context.go( - '/rooms/analytics/${room!.id}', - ) - : widget.onTap( - AnalyticsSelected( - widget.id, - widget.type, - widget.displayName, - ), - ); - }, - trailing: (room?.isSpace ?? false) && - widget.type != AnalyticsEntryType.privateChats && - widget.allowNavigateOnSelect - ? const Icon(Icons.chevron_right) - : null, + ), + ], ), + subtitle: ListSummaryAnalytics( + chartAnalytics: tileData, + ), + selected: widget.isSelected, + onTap: () { + if (widget.controller?.widget.selectedView == null) { + widget.onTap(widget.selected); + return; + } + if ((room?.isSpace ?? false) && widget.allowNavigateOnSelect) { + final String selectedView = + widget.controller!.widget.selectedView!.route; + context.go('/rooms/analytics/${room!.id}/$selectedView'); + return; + } + widget.onTap(widget.selected); + }, + trailing: (room?.isSpace ?? false) && + widget.selected.type != AnalyticsEntryType.privateChats && + widget.allowNavigateOnSelect + ? const Icon(Icons.chevron_right) + : null, ), ), ); diff --git a/lib/pangea/pages/analytics/base_analytics.dart b/lib/pangea/pages/analytics/base_analytics.dart index 17fa4013a..2b548226b 100644 --- a/lib/pangea/pages/analytics/base_analytics.dart +++ b/lib/pangea/pages/analytics/base_analytics.dart @@ -1,7 +1,9 @@ import 'dart:async'; -import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; +import 'package:fluffychat/pangea/constants/pangea_event_types.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/analytics/analytics_event.dart'; import 'package:fluffychat/pangea/pages/analytics/base_analytics_view.dart'; import 'package:fluffychat/pangea/pages/analytics/student_analytics/student_analytics.dart'; import 'package:flutter/material.dart'; @@ -12,12 +14,12 @@ import '../../../widgets/matrix.dart'; import '../../controllers/pangea_controller.dart'; import '../../enum/bar_chart_view_enum.dart'; import '../../enum/time_span.dart'; -import '../../models/chart_analytics_model.dart'; +import '../../models/analytics/chart_analytics_model.dart'; class BaseAnalyticsPage extends StatefulWidget { final String pageTitle; final List tabs; - final Future Function(BuildContext) refreshData; + final BarChartViewSelection? selectedView; final AnalyticsSelected defaultSelected; final AnalyticsSelected? alwaysSelected; @@ -27,9 +29,9 @@ class BaseAnalyticsPage extends StatefulWidget { super.key, required this.pageTitle, required this.tabs, - required this.refreshData, required this.alwaysSelected, required this.defaultSelected, + this.selectedView, this.myAnalyticsController, }); @@ -39,156 +41,126 @@ class BaseAnalyticsPage extends StatefulWidget { class BaseAnalyticsController extends State { final PangeaController pangeaController = MatrixState.pangeaController; - BarChartViewSelection? selectedView; AnalyticsSelected? selected; String? currentLemma; + ChartAnalyticsModel? chartData; + StreamController refreshStream = StreamController.broadcast(); bool isSelected(String chatOrStudentId) => chatOrStudentId == selected?.id; - ChartAnalyticsModel? chartData( - BuildContext context, - AnalyticsSelected? selectedParam, - ) { - final AnalyticsSelected analyticsSelected = - selectedParam ?? widget.defaultSelected; + Room? get activeSpace { + if (widget.defaultSelected.type == AnalyticsEntryType.space) { + return Matrix.of(context).client.getRoomById(widget.defaultSelected.id); + } + return null; + } - if (analyticsSelected.type == AnalyticsEntryType.privateChats) { - return pangeaController.analytics.getAnalyticsLocal( - classId: analyticsSelected.id, - chatId: AnalyticsEntryType.privateChats.toString(), + @override + void initState() { + super.initState(); + if (widget.defaultSelected.type == AnalyticsEntryType.student) { + runFirstRefresh(); + } + setChartData(); + } + + @override + void didUpdateWidget(covariant BaseAnalyticsPage oldWidget) { + // when a user is a parent space's analytics and clicks on a subspace + super.didUpdateWidget(oldWidget); + if (oldWidget.defaultSelected.id != widget.defaultSelected.id) { + setChartData(); + refreshStream.add(false); + } + } + + Future runFirstRefresh() async { + final analyticsRooms = + pangeaController.matrixState.client.allMyAnalyticsRooms; + + final List analyticsEvent = []; + for (final analyticsRoom in analyticsRooms) { + final lastSummaryEvent = await analyticsRoom.getLastAnalyticsEvent( + PangeaEventTypes.summaryAnalytics, + Matrix.of(context).client.userID!, ); + final lastConstructEvent = await analyticsRoom.getLastAnalyticsEvent( + PangeaEventTypes.construct, + Matrix.of(context).client.userID!, + ); + if (lastSummaryEvent != null) { + analyticsEvent.add(lastSummaryEvent); + } + if (lastConstructEvent != null) { + analyticsEvent.add(lastConstructEvent); + } } - String? chatId = analyticsSelected.type == AnalyticsEntryType.room - ? analyticsSelected.id - : null; - chatId ??= widget.alwaysSelected?.type == AnalyticsEntryType.room - ? widget.alwaysSelected?.id - : null; + if (analyticsEvent.isNotEmpty) return; + onRefresh(); + } - String? studentId = analyticsSelected.type == AnalyticsEntryType.student - ? analyticsSelected.id - : null; - studentId ??= widget.alwaysSelected?.type == AnalyticsEntryType.student - ? widget.alwaysSelected?.id - : null; + Future onRefresh() async { + // postframe callback to avoid calling this function during build + WidgetsBinding.instance.addPostFrameCallback((_) async { + await showFutureLoadingDialog( + context: context, + future: () async { + debugPrint("updating analytics"); + await pangeaController.myAnalytics.updateAnalytics(); + await setChartData(forceUpdate: true); + refreshStream.add(true); + }, + ); + }); + } - String? classId = analyticsSelected.type == AnalyticsEntryType.space - ? analyticsSelected.id - : null; - classId ??= widget.alwaysSelected?.type == AnalyticsEntryType.space - ? widget.alwaysSelected?.id - : null; - - final data = pangeaController.analytics.getAnalyticsLocal( - classId: classId, - chatId: chatId, - studentId: studentId, + Future fetchChartData( + AnalyticsSelected? params, { + forceUpdate = false, + }) async { + final ChartAnalyticsModel data = + await pangeaController.analytics.getAnalytics( + defaultSelected: widget.defaultSelected, + selected: params, + forceUpdate: forceUpdate, ); return data; } + Future setChartData({forceUpdate = false}) async { + final ChartAnalyticsModel newData = await fetchChartData( + selected, + forceUpdate: forceUpdate, + ); + setState(() => chartData = newData); + } + TimeSpan get currentTimeSpan => pangeaController.analytics.currentAnalyticsTimeSpan; - void navigate() { - if (currentLemma != null) { - setCurrentLemma(null); - } else if (selectedView != null) { - setSelectedView(null); - } else { - Navigator.of(context).pop(); - } - } - Future toggleSelection(AnalyticsSelected selectedParam) async { - final bool joinSelectedRoom = - selectedParam.type == AnalyticsEntryType.room && - !enableSelection( - selectedParam, - ); - - if (joinSelectedRoom) { - await showFutureLoadingDialog( - context: context, - future: () async { - final waitForRoom = Matrix.of(context).client.waitForRoomInSync( - selectedParam.id, - join: true, - ); - await Matrix.of(context).client.joinRoom(selectedParam.id); - await waitForRoom; - }, - ); - } - setState(() { debugPrint("selectedParam.id is ${selectedParam.id}"); currentLemma = null; selected = isSelected(selectedParam.id) ? null : selectedParam; }); - - pangeaController.analytics.setConstructs( - constructType: ConstructType.grammar, - defaultSelected: widget.defaultSelected, - selected: selected, - removeIT: true, - ); - + await setChartData(); + refreshStream.add(false); Future.delayed(Duration.zero, () => setState(() {})); } Future toggleTimeSpan(BuildContext context, TimeSpan timeSpan) async { await pangeaController.analytics.setCurrentAnalyticsTimeSpan(timeSpan); - await widget.refreshData(context); - await pangeaController.analytics.setConstructs( - constructType: ConstructType.grammar, - defaultSelected: widget.defaultSelected, - selected: selected, - removeIT: true, - ); - setState(() {}); - } - - void setSelectedView(BarChartViewSelection? view) { - currentLemma = null; - selectedView = view; - if (!enableSelection(selected)) { - toggleSelection(selected!); - } - setState(() {}); + await setChartData(); + refreshStream.add(false); } void setCurrentLemma(String? lemma) { currentLemma = lemma; setState(() {}); - } - - bool enableSelection(AnalyticsSelected? selectedParam) { - if (selectedView == BarChartViewSelection.grammar) { - if (selectedParam?.type == AnalyticsEntryType.room) { - return Matrix.of(context) - .client - .getRoomById(selectedParam!.id) - ?.membership == - Membership.join; - } - - if (selectedParam?.type == AnalyticsEntryType.student) { - final String? langCode = - pangeaController.languageController.activeL2Code( - roomID: widget.defaultSelected.id, - ); - if (langCode == null) return false; - return Matrix.of(context).client.analyticsRoomLocal( - langCode, - selectedParam?.id, - ) != - null; - } - } - return true; + refreshStream.add(false); } @override @@ -221,6 +193,19 @@ class TabItem { enum AnalyticsEntryType { student, room, space, privateChats } +extension AnalyticsEntryTypeExtension on AnalyticsEntryType { + String get route { + switch (this) { + case AnalyticsEntryType.student: + return 'mylearning'; + case AnalyticsEntryType.space: + return 'analytics'; + default: + throw Exception('No route for $this'); + } + } +} + class AnalyticsSelected { String id; AnalyticsEntryType type; diff --git a/lib/pangea/pages/analytics/base_analytics_view.dart b/lib/pangea/pages/analytics/base_analytics_view.dart index 0c9bf3bc4..09805ef71 100644 --- a/lib/pangea/pages/analytics/base_analytics_view.dart +++ b/lib/pangea/pages/analytics/base_analytics_view.dart @@ -12,6 +12,7 @@ import 'package:fluffychat/widgets/layouts/max_width_body.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:go_router/go_router.dart'; class BaseAnalyticsView extends StatelessWidget { const BaseAnalyticsView({ @@ -22,17 +23,14 @@ class BaseAnalyticsView extends StatelessWidget { final BaseAnalyticsController controller; Widget chartView(BuildContext context) { - if (controller.selectedView == null) { + if (controller.widget.selectedView == null) { return const SizedBox(); } - switch (controller.selectedView!) { + switch (controller.widget.selectedView!) { case BarChartViewSelection.messages: return MessagesBarChart( - chartAnalytics: controller.chartData( - context, - controller.selected, - ), + chartAnalytics: controller.chartData, ); case BarChartViewSelection.grammar: return ConstructList( @@ -41,6 +39,7 @@ class BaseAnalyticsView extends StatelessWidget { selected: controller.selected, controller: controller, pangeaController: controller.pangeaController, + refreshStream: controller.refreshStream, ); } } @@ -62,55 +61,68 @@ class BaseAnalyticsView extends StatelessWidget { text: controller.widget.pageTitle, style: const TextStyle(decoration: TextDecoration.underline), recognizer: TapGestureRecognizer() - ..onTap = () => controller.selectedView != null - ? controller.setSelectedView(null) - : null, + ..onTap = () { + final String route = + "/rooms/${controller.widget.defaultSelected.type.route}"; + context.go(route); + }, ), - if (controller.selectedView != null) + if (controller.activeSpace != null) const TextSpan( text: " > ", ), - if (controller.selectedView != null) + if (controller.activeSpace != null) TextSpan( + text: controller.activeSpace!.getLocalizedDisplayname(), style: const TextStyle(decoration: TextDecoration.underline), - text: controller.selectedView!.string(context), recognizer: TapGestureRecognizer() - ..onTap = () => controller.currentLemma != null - ? controller.setCurrentLemma(null) - : null, + ..onTap = () { + if (controller.widget.selectedView == null) return; + String route = + "/rooms/${controller.widget.defaultSelected.type.route}"; + if (controller.widget.defaultSelected.type == + AnalyticsEntryType.space) { + route += "/${controller.widget.defaultSelected.id}"; + } + context.go(route); + }, ), - if (controller.currentLemma != null) + if (controller.widget.selectedView != null) const TextSpan( text: " > ", ), - if (controller.currentLemma != null) + if (controller.widget.selectedView != null) TextSpan( - text: controller.currentLemma, - style: const TextStyle(decoration: TextDecoration.underline), - recognizer: TapGestureRecognizer()..onTap = () {}, + text: controller.widget.selectedView!.string(context), ), ], ), overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, ), - leading: IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: controller.navigate, - ), - actions: [ - TimeSpanMenuButton( - value: controller.currentTimeSpan, - onChange: (TimeSpan value) => - controller.toggleTimeSpan(context, value), - ), - ], ), body: MaxWidthBody( withScrolling: false, - child: controller.selectedView != null + child: controller.widget.selectedView != null ? Column( children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (controller.widget.defaultSelected.type == + AnalyticsEntryType.student) + IconButton( + icon: const Icon(Icons.refresh), + onPressed: controller.onRefresh, + tooltip: L10n.of(context)!.refresh, + ), + TimeSpanMenuButton( + value: controller.currentTimeSpan, + onChange: (TimeSpan value) => + controller.toggleTimeSpan(context, value), + ), + ], + ), Expanded( flex: 1, child: chartView(context), @@ -153,29 +165,18 @@ class BaseAnalyticsView extends StatelessWidget { children: [ ...controller.widget.tabs[0].items.map( (item) => AnalyticsListTile( + refreshStream: + controller.refreshStream, avatar: item.avatar, - model: controller.chartData( - context, - AnalyticsSelected( - item.id, - controller.widget.tabs[0].type, - "", - ), + defaultSelected: controller + .widget.defaultSelected, + selected: AnalyticsSelected( + item.id, + controller.widget.tabs[0].type, + item.displayName, ), - displayName: item.displayName, - id: item.id, - type: - controller.widget.tabs[0].type, - selected: + isSelected: controller.isSelected(item.id), - enabled: controller.enableSelection( - AnalyticsSelected( - item.id, - controller.widget.tabs[0].type, - "", - ), - ), - showSpaceAnalytics: false, onTap: (_) => controller.toggleSelection( AnalyticsSelected( @@ -188,35 +189,35 @@ class BaseAnalyticsView extends StatelessWidget { .widget .tabs[0] .allowNavigateOnSelect, + pangeaController: + controller.pangeaController, + controller: controller, ), ), if (controller .widget.defaultSelected.type == AnalyticsEntryType.space) AnalyticsListTile( + refreshStream: + controller.refreshStream, + defaultSelected: controller + .widget.defaultSelected, avatar: null, - model: controller.chartData( - context, - AnalyticsSelected( - controller - .widget.defaultSelected.id, - AnalyticsEntryType.privateChats, - L10n.of(context)! - .allPrivateChats, - ), + selected: AnalyticsSelected( + controller + .widget.defaultSelected.id, + AnalyticsEntryType.privateChats, + L10n.of(context)!.allPrivateChats, ), - displayName: L10n.of(context)! - .allPrivateChats, - id: controller - .widget.defaultSelected.id, - type: - AnalyticsEntryType.privateChats, allowNavigateOnSelect: false, - selected: controller.isSelected( + isSelected: controller.isSelected( controller .widget.defaultSelected.id, ), onTap: controller.toggleSelection, + pangeaController: + controller.pangeaController, + controller: controller, ), ], ), @@ -226,36 +227,26 @@ class BaseAnalyticsView extends StatelessWidget { children: controller.widget.tabs[1].items .map( (item) => AnalyticsListTile( + refreshStream: + controller.refreshStream, avatar: item.avatar, - model: controller.chartData( - context, - AnalyticsSelected( - item.id, - controller - .widget.tabs[1].type, - "", - ), + defaultSelected: controller + .widget.defaultSelected, + selected: AnalyticsSelected( + item.id, + controller.widget.tabs[1].type, + item.displayName, ), - displayName: item.displayName, - id: item.id, - type: controller - .widget.tabs[1].type, - selected: controller + isSelected: controller .isSelected(item.id), onTap: controller.toggleSelection, allowNavigateOnSelect: controller .widget .tabs[1] .allowNavigateOnSelect, - enabled: - controller.enableSelection( - AnalyticsSelected( - item.id, - controller - .widget.tabs[1].type, - "", - ), - ), + pangeaController: + controller.pangeaController, + controller: controller, ), ) .toList(), @@ -275,7 +266,7 @@ class BaseAnalyticsView extends StatelessWidget { children: [ const Divider(height: 1), ListTile( - title: const Text("Error Analytics"), + title: Text(L10n.of(context)!.grammarAnalytics), leading: CircleAvatar( backgroundColor: Theme.of(context).scaffoldBackgroundColor, @@ -284,13 +275,20 @@ class BaseAnalyticsView extends StatelessWidget { child: Icon(BarChartViewSelection.grammar.icon), ), trailing: const Icon(Icons.chevron_right), - onTap: () => controller.setSelectedView( - BarChartViewSelection.grammar, - ), + onTap: () { + String route = + "/rooms/${controller.widget.defaultSelected.type.route}"; + if (controller.widget.defaultSelected.type == + AnalyticsEntryType.space) { + route += "/${controller.widget.defaultSelected.id}"; + } + route += "/${BarChartViewSelection.grammar.route}"; + context.go(route); + }, ), const Divider(height: 1), ListTile( - title: const Text("Message Analytics"), + title: Text(L10n.of(context)!.messageAnalytics), leading: CircleAvatar( backgroundColor: Theme.of(context).scaffoldBackgroundColor, @@ -299,9 +297,16 @@ class BaseAnalyticsView extends StatelessWidget { child: Icon(BarChartViewSelection.messages.icon), ), trailing: const Icon(Icons.chevron_right), - onTap: () => controller.setSelectedView( - BarChartViewSelection.messages, - ), + onTap: () { + String route = + "/rooms/${controller.widget.defaultSelected.type.route}"; + if (controller.widget.defaultSelected.type == + AnalyticsEntryType.space) { + route += "/${controller.widget.defaultSelected.id}"; + } + route += "/${BarChartViewSelection.messages.route}"; + context.go(route); + }, ), const Divider(height: 1), ], diff --git a/lib/pangea/pages/analytics/class_analytics/class_analytics.dart b/lib/pangea/pages/analytics/class_analytics/class_analytics.dart index 4de8d4184..9bf5ac7a3 100644 --- a/lib/pangea/pages/analytics/class_analytics/class_analytics.dart +++ b/lib/pangea/pages/analytics/class_analytics/class_analytics.dart @@ -1,12 +1,9 @@ import 'dart:async'; import 'dart:developer'; -import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/constants/pangea_room_types.dart'; -import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; +import 'package:fluffychat/pangea/enum/bar_chart_view_enum.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; -import 'package:fluffychat/pangea/models/chart_analytics_model.dart'; -import 'package:fluffychat/pangea/pages/analytics/base_analytics.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/widgets/common/list_placeholder.dart'; import 'package:fluffychat/pangea/widgets/common/p_circular_loader.dart'; @@ -16,92 +13,71 @@ import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; import '../../../../widgets/matrix.dart'; -import '../../../controllers/pangea_controller.dart'; import '../../../utils/sync_status_util_v2.dart'; import 'class_analytics_view.dart'; enum AnalyticsPageType { classList, student, classDetails } class ClassAnalyticsPage extends StatefulWidget { - // final AnalyticsPageType type; - const ClassAnalyticsPage({super.key}); + final BarChartViewSelection? selectedView; + const ClassAnalyticsPage({super.key, this.selectedView}); @override State createState() => ClassAnalyticsV2Controller(); } class ClassAnalyticsV2Controller extends State { - final PangeaController _pangeaController = MatrixState.pangeaController; bool _initialized = false; - StreamSubscription? stateSub; - Timer? refreshTimer; + // StreamSubscription? stateSub; + // Timer? refreshTimer; List chats = []; List students = []; - String? get classId => GoRouterState.of(context).pathParameters['classid']; - Room? _classRoom; + Room? get classRoom { if (_classRoom == null || _classRoom!.id != classId) { debugPrint("updating _classRoom"); _classRoom = classId != null ? Matrix.of(context).client.getRoomById(classId!) : null; - - getChatAndStudents() - .then( - (_) => _pangeaController.analytics.setConstructs( - constructType: ConstructType.grammar, - defaultSelected: AnalyticsSelected( - classId!, - AnalyticsEntryType.space, - className(context), - ), - removeIT: true, - forceUpdate: true, - ), - ) - .then( - (_) => getChatAndStudentAnalytics(context, true), - ); + if (_classRoom == null) { + context.go('/rooms/analytics'); + } + getChatAndStudents(); } return _classRoom; } - String className(BuildContext context) { - return classRoom?.name ?? ""; - } - @override void initState() { super.initState(); + debugPrint("init class analytics"); Future.delayed(Duration.zero, () async { if (classRoom == null || (!(classRoom?.isSpace ?? false))) { context.go('/rooms'); } - - stateSub = _pangeaController.matrixState.client.onRoomState.stream - .where( - (event) => - event.type == PangeaEventTypes.studentAnalyticsSummary && - event.roomId == classId, - ) - .listen(onStateUpdate); getChatAndStudents(); }); } Future getChatAndStudents() async { try { + await classRoom?.postLoad(); await classRoom?.requestParticipants(); if (classRoom != null) { final response = await Matrix.of(context).client.getSpaceHierarchy( classRoom!.id, - maxDepth: 1, ); + // set the latest fetched full hierarchy in message analytics controller + // we want to avoid calling this endpoint again and again, so whenever the + // data is made available, set it in the controller + MatrixState.pangeaController.analytics + .setLatestHierarchy(_classRoom!.id, response); + students = classRoom!.students; chats = response.rooms .where( @@ -122,21 +98,12 @@ class ClassAnalyticsV2Controller extends State { } } - void onStateUpdate(Event newState) { - if (!(refreshTimer?.isActive ?? false)) { - refreshTimer = Timer( - const Duration(seconds: 3), - () => getChatAndStudentAnalytics(context, true), - ); - } - } - - @override - void dispose() { - super.dispose(); - refreshTimer?.cancel(); - stateSub?.cancel(); - } + // @override + // void dispose() { + // super.dispose(); + // refreshTimer?.cancel(); + // stateSub?.cancel(); + // } @override Widget build(BuildContext context) { @@ -146,57 +113,10 @@ class ClassAnalyticsV2Controller extends State { // but this is computationally expensive! // key: UniqueKey(), shimmerChild: const ListPlaceholder(), - onFinish: () { - getChatAndStudentAnalytics(context); - }, + // onFinish: () { + // getChatAndStudentAnalytics(context); + // }, child: ClassAnalyticsView(this), ); } - - Future getChatAndStudentAnalytics( - BuildContext context, [ - forceUpdate = false, - ]) async { - try { - if (classRoom == null) { - debugger(when: kDebugMode); - ErrorHandler.logError(m: 'classroom should not be null'); - } - final List> analyticsFutures = []; - for (final student in students) { - analyticsFutures.add( - _pangeaController.analytics.getAnalytics( - classRoom: classRoom, - studentId: student.id, - forceUpdate: forceUpdate, - ), - ); - } - for (final chat in chats) { - analyticsFutures.add( - _pangeaController.analytics.getAnalytics( - classRoom: classRoom, - chatId: chat.roomId, - forceUpdate: forceUpdate, - ), - ); - } - analyticsFutures.add( - _pangeaController.analytics.getAnalytics( - classRoom: classRoom, - forceUpdate: forceUpdate, - ), - ); - analyticsFutures.add( - _pangeaController.analytics.getAnalyticsForPrivateChats( - classRoom: classRoom, - forceUpdate: forceUpdate, - ), - ); - await Future.wait(analyticsFutures); - if (mounted) setState(() {}); - } catch (err) { - debugger(when: kDebugMode); - } - } } diff --git a/lib/pangea/pages/analytics/class_analytics/class_analytics_view.dart b/lib/pangea/pages/analytics/class_analytics/class_analytics_view.dart index 88c15bdf5..5f609437b 100644 --- a/lib/pangea/pages/analytics/class_analytics/class_analytics_view.dart +++ b/lib/pangea/pages/analytics/class_analytics/class_analytics_view.dart @@ -48,18 +48,18 @@ class ClassAnalyticsView extends StatelessWidget { return controller.classId != null ? BaseAnalyticsPage( + selectedView: controller.widget.selectedView, pageTitle: pageTitle, tabs: [tab1, tab2], - refreshData: controller.getChatAndStudentAnalytics, alwaysSelected: AnalyticsSelected( controller.classId!, AnalyticsEntryType.space, - controller.className(context), + controller.classRoom?.name ?? "", ), defaultSelected: AnalyticsSelected( controller.classId!, AnalyticsEntryType.space, - controller.className(context), + controller.classRoom?.name ?? "", ), ) : const SizedBox(); diff --git a/lib/pangea/pages/analytics/class_list/class_list.dart b/lib/pangea/pages/analytics/class_list/class_list.dart index e96538a11..45aa6bb88 100644 --- a/lib/pangea/pages/analytics/class_list/class_list.dart +++ b/lib/pangea/pages/analytics/class_list/class_list.dart @@ -1,14 +1,15 @@ import 'dart:async'; import 'package:fluffychat/pangea/enum/time_span.dart'; +import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart'; +import 'package:fluffychat/pangea/pages/analytics/base_analytics.dart'; import 'package:fluffychat/pangea/pages/analytics/class_list/class_list_view.dart'; import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; import '../../../../widgets/matrix.dart'; -import '../../../constants/pangea_event_types.dart'; import '../../../controllers/pangea_controller.dart'; -import '../../../models/chart_analytics_model.dart'; +import '../../../models/analytics/chart_analytics_model.dart'; import '../../../utils/sync_status_util_v2.dart'; import '../../../widgets/common/list_placeholder.dart'; @@ -22,75 +23,57 @@ class AnalyticsClassList extends StatefulWidget { class AnalyticsClassListController extends State { PangeaController pangeaController = MatrixState.pangeaController; List models = []; - StreamSubscription? stateSub; - Map refreshTimer = {}; + List spaces = []; @override void initState() { super.initState(); - Future.delayed(Duration.zero, () async { - stateSub = pangeaController.matrixState.client.onRoomState.stream + Matrix.of(context).client.classesAndExchangesImTeaching.then((spaceList) { + spaceList = spaceList .where( - (event) => event.type == PangeaEventTypes.studentAnalyticsSummary, + (space) => !spaceList.any( + (parentSpace) => parentSpace.spaceChildren + .any((child) => child.roomId == space.id), + ), ) - .listen(onStateUpdate); + .toList(); + spaces = spaceList; + setState(() {}); }); } - void onStateUpdate(Event newState) { - if (!(refreshTimer[newState.room.id]?.isActive ?? false)) { - refreshTimer[newState.room.id] = Timer( - const Duration(seconds: 3), - () { - if (newState.room.isSpace) { - updateClassAnalytics(context, newState.room); - } - }, - ); - } - } - - @override - void dispose() { - super.dispose(); - for (final timer in refreshTimer.values) { - timer.cancel(); - } - stateSub?.cancel(); - } - @override Widget build(BuildContext context) { return PLoadingStatusV2( shimmerChild: const ListPlaceholder(), child: AnalyticsClassListView(this), onFinish: () { - getAllClassAnalytics(context); + // getAllClassAnalytics(context); }, ); } - Future getAllClassAnalytics(BuildContext context) async { - await pangeaController.analytics.allClassAnalytics(); - setState(() { - debugPrint("class list post getAllClassAnalytics"); - }); - } - - Future updateClassAnalytics( - BuildContext context, - Room classRoom, + Future updateClassAnalytics( + Room? space, ) async { - await pangeaController.analytics - .getAnalytics(classRoom: classRoom, forceUpdate: true); - setState(() { - debugPrint("class list post updateClassAnalytics"); - }); + if (space == null) { + return null; + } + + final data = await pangeaController.analytics.getAnalytics( + defaultSelected: AnalyticsSelected( + space.id, + AnalyticsEntryType.space, + space.name, + ), + forceUpdate: true, + ); + setState(() {}); + return data; } void toggleTimeSpan(BuildContext context, TimeSpan timeSpan) { pangeaController.analytics.setCurrentAnalyticsTimeSpan(timeSpan); setState(() {}); - getAllClassAnalytics(context); } } diff --git a/lib/pangea/pages/analytics/class_list/class_list_view.dart b/lib/pangea/pages/analytics/class_list/class_list_view.dart index 88bea1e01..fe63d7675 100644 --- a/lib/pangea/pages/analytics/class_list/class_list_view.dart +++ b/lib/pangea/pages/analytics/class_list/class_list_view.dart @@ -1,11 +1,9 @@ -import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart'; import 'package:fluffychat/pangea/pages/analytics/analytics_list_tile.dart'; import 'package:fluffychat/pangea/pages/analytics/time_span_menu_button.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; -import '../../../../widgets/matrix.dart'; import '../../../enum/time_span.dart'; import '../base_analytics.dart'; import 'class_list.dart'; @@ -45,24 +43,28 @@ class AnalyticsClassListView extends StatelessWidget { body: Column( children: [ Flexible( - child: FutureBuilder( - future: Matrix.of(context).client.classesAndExchangesImTeaching, - builder: (context, snapshot) => ListView.builder( - itemCount: snapshot.hasData ? snapshot.data?.length ?? 0 : 0, - itemBuilder: (context, i) => AnalyticsListTile( - avatar: snapshot.data![i].avatar, - model: controller.pangeaController.analytics - .getAnalyticsLocal(classId: snapshot.data![i].id), - displayName: snapshot.data![i].name, - id: snapshot.data![i].id, - type: AnalyticsEntryType.space, - // selected: false, - onTap: (selected) => context.go( - '/rooms/analytics/${selected.id}', - ), - allowNavigateOnSelect: true, - selected: false, + child: ListView.builder( + itemCount: controller.spaces.length, + itemBuilder: (context, i) => AnalyticsListTile( + defaultSelected: AnalyticsSelected( + controller.spaces[i].id, + AnalyticsEntryType.space, + "", ), + avatar: controller.spaces[i].avatar, + selected: AnalyticsSelected( + controller.spaces[i].id, + AnalyticsEntryType.space, + controller.spaces[i].name, + ), + onTap: (selected) { + context.go( + '/rooms/analytics/${selected.id}', + ); + }, + allowNavigateOnSelect: true, + isSelected: false, + pangeaController: controller.pangeaController, ), ), ), diff --git a/lib/pangea/pages/analytics/construct_list.dart b/lib/pangea/pages/analytics/construct_list.dart index f050222ec..e169922ca 100644 --- a/lib/pangea/pages/analytics/construct_list.dart +++ b/lib/pangea/pages/analytics/construct_list.dart @@ -2,13 +2,12 @@ import 'dart:async'; import 'package:collection/collection.dart'; import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; -import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_representation_event.dart'; -import 'package:fluffychat/pangea/models/constructs_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/pangea_match_model.dart'; import 'package:fluffychat/pangea/pages/analytics/base_analytics.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; @@ -25,6 +24,7 @@ class ConstructList extends StatefulWidget { final AnalyticsSelected? selected; final BaseAnalyticsController controller; final PangeaController pangeaController; + final StreamController refreshStream; const ConstructList({ super.key, @@ -32,6 +32,7 @@ class ConstructList extends StatefulWidget { required this.defaultSelected, required this.controller, required this.pangeaController, + required this.refreshStream, this.selected, }); @@ -40,29 +41,9 @@ class ConstructList extends StatefulWidget { } class ConstructListState extends State { - bool initialized = false; String? langCode; String? error; - @override - void initState() { - super.initState(); - widget.pangeaController.analytics - .setConstructs( - constructType: widget.constructType, - removeIT: true, - defaultSelected: widget.defaultSelected, - selected: widget.selected, - forceUpdate: true, - ) - .whenComplete(() => setState(() => initialized = true)); - } - - @override - void dispose() { - super.dispose(); - } - @override Widget build(BuildContext context) { return error != null @@ -72,11 +53,11 @@ class ConstructListState extends State { : Column( children: [ ConstructListView( - init: initialized, controller: widget.controller, pangeaController: widget.pangeaController, defaultSelected: widget.defaultSelected, selected: widget.selected, + refreshStream: widget.refreshStream, ), ], ); @@ -93,19 +74,18 @@ class ConstructListState extends State { // subtitle = total uses, equal to construct.content.uses.length // list has a fixed height of 400 and is scrollable class ConstructListView extends StatefulWidget { - // final List constructs; - final bool init; final BaseAnalyticsController controller; final PangeaController pangeaController; final AnalyticsSelected defaultSelected; final AnalyticsSelected? selected; + final StreamController refreshStream; const ConstructListView({ super.key, - required this.init, required this.controller, required this.pangeaController, required this.defaultSelected, + required this.refreshStream, this.selected, }); @@ -114,59 +94,55 @@ class ConstructListView extends StatefulWidget { } class ConstructListViewState extends State { + final ConstructType constructType = ConstructType.grammar; final Map _timelinesCache = {}; final Map _msgEventCache = {}; final List _msgEvents = []; + bool fetchingConstructs = true; bool fetchingUses = false; - - StreamSubscription? stateSub; - Timer? refreshTimer; + StreamSubscription? refreshSubscription; @override void initState() { super.initState(); - - stateSub = Matrix.of(context) - .client - .onRoomState - .stream - //could optimize here be determing if the vocab event is relevant for - //currently displayed data - .where((event) => event.type == PangeaEventTypes.vocab) - .listen(onStateUpdate); - } - - Future onStateUpdate(Event? newState) async { - debugPrint("onStateUpdate construct list"); - if (refreshTimer?.isActive ?? false) return; - refreshTimer = Timer( - const Duration(seconds: 3), - () async { - await widget.pangeaController.analytics.setConstructs( - constructType: ConstructType.grammar, + widget.pangeaController.analytics + .getConstructs( + constructType: constructType, removeIT: true, defaultSelected: widget.defaultSelected, selected: widget.selected, forceUpdate: true, - ); - await fetchUses(); - }, - ); + ) + .whenComplete(() => setState(() => fetchingConstructs = false)) + .then((value) => setState(() => _constructs = value)); + + refreshSubscription = widget.refreshStream.stream.listen((forceUpdate) { + // postframe callback to let widget rebuild with the new selected parameter + // before sending selected to getConstructs function + WidgetsBinding.instance.addPostFrameCallback((_) { + widget.pangeaController.analytics + .getConstructs( + constructType: constructType, + removeIT: true, + defaultSelected: widget.defaultSelected, + selected: widget.selected, + forceUpdate: true, + ) + .then( + (value) => setState(() { + _constructs = value; + }), + ); + }); + }); } @override void dispose() { + refreshSubscription?.cancel(); super.dispose(); - refreshTimer?.cancel(); - stateSub?.cancel(); } - // @override - // void didUpdateWidget(ConstructListView oldWidget) { - // super.didUpdateWidget(oldWidget); - // fetchUses(); - // } - int get lemmaIndex => constructs?.indexWhere( (element) => element.lemma == widget.controller.currentLemma, @@ -241,18 +217,47 @@ class ConstructListViewState extends State { } } - List? get constructs => - widget.pangeaController.analytics.constructs != null - ? widget.pangeaController.myAnalytics - .aggregateConstructData( - widget.pangeaController.analytics.constructs!, - ) - .sorted( - (a, b) => b.uses.length.compareTo(a.uses.length), - ) - : null; + List? _constructs; - AggregateConstructUses? get currentConstruct => constructs?.firstWhereOrNull( + List? get constructs { + if (_constructs == null) { + return null; + } + + final List filtered = List.from(_constructs!) + .map((event) => event.content.uses) + .expand((uses) => uses) + .cast() + .where((use) => use.constructType == constructType) + .toList(); + + final Map> lemmaToUses = {}; + for (final use in filtered) { + if (use.lemma == null) continue; + lemmaToUses[use.lemma!] ??= []; + lemmaToUses[use.lemma!]!.add(use); + } + + final constructUses = lemmaToUses.entries + .map( + (entry) => ConstructUses( + lemma: entry.key, + uses: entry.value, + constructType: constructType, + ), + ) + .toList(); + + constructUses.sort((a, b) { + final comp = b.uses.length.compareTo(a.uses.length); + if (comp != 0) return comp; + return a.lemma.compareTo(b.lemma); + }); + + return constructUses; + } + + ConstructUses? get currentConstruct => constructs?.firstWhereOrNull( (element) => element.lemma == widget.controller.currentLemma, ); @@ -297,13 +302,13 @@ class ConstructListViewState extends State { @override Widget build(BuildContext context) { - if (!widget.init || fetchingUses) { + if (fetchingConstructs || fetchingUses) { return const Expanded( child: Center(child: CircularProgressIndicator()), ); } - if ((constructs?.isEmpty ?? true) || - (widget.controller.currentLemma != null && currentConstruct == null)) { + + if (constructs?.isEmpty ?? true) { return Expanded( child: Center(child: Text(L10n.of(context)!.noDataFound)), ); @@ -341,7 +346,10 @@ class ConstructMessagesDialog extends StatelessWidget { @override Widget build(BuildContext context) { - if (controller.widget.controller.currentLemma == null) { + if (controller.widget.controller.currentLemma == null || + controller.constructs == null || + controller.lemmaIndex < 0 || + controller.lemmaIndex >= controller.constructs!.length) { return const AlertDialog(content: CircularProgressIndicator.adaptive()); } @@ -349,38 +357,40 @@ class ConstructMessagesDialog extends StatelessWidget { return AlertDialog( title: Center(child: Text(controller.widget.controller.currentLemma!)), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (controller.constructs![controller.lemmaIndex].uses.length > - controller._msgEvents.length) - Center( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text(L10n.of(context)!.roomDataMissing), + content: SizedBox( + height: 350, + width: 500, + child: Column( + children: [ + if (controller.constructs![controller.lemmaIndex].uses.length > + controller._msgEvents.length) + Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text(L10n.of(context)!.roomDataMissing), + ), + ), + Expanded( + child: ListView( + children: [ + ...msgEventMatches.mapIndexed( + (index, event) => Column( + children: [ + ConstructMessage( + msgEvent: event.msgEvent, + lemma: controller.widget.controller.currentLemma!, + errorMessage: event.lemmaMatch, + ), + if (index < msgEventMatches.length - 1) + const Divider(height: 1), + ], + ), + ), + ], ), ), - SingleChildScrollView( - child: Column( - children: [ - ...msgEventMatches.mapIndexed( - (index, event) => Column( - children: [ - ConstructMessage( - msgEvent: event.msgEvent, - lemma: controller.widget.controller.currentLemma!, - errorMessage: event.lemmaMatch, - ), - if (index < msgEventMatches.length - 1) - const Divider(height: 1), - ], - ), - ), - ], - ), - ), - ], + ], + ), ), actions: [ TextButton( @@ -474,21 +484,21 @@ class ConstructMessage extends StatelessWidget { class ConstructMessageBubble extends StatelessWidget { final String errorText; final String replacementText; - final int? start; - final int? end; + final int start; + final int end; const ConstructMessageBubble({ super.key, required this.errorText, required this.replacementText, - this.start, - this.end, + required this.start, + required this.end, }); @override Widget build(BuildContext context) { final defaultStyle = TextStyle( - color: Theme.of(context).colorScheme.onBackground, + color: Theme.of(context).colorScheme.onSurface, fontSize: AppConfig.messageFontSize * AppConfig.fontSizeFactor, height: 1.3, ); @@ -516,7 +526,7 @@ class ConstructMessageBubble extends StatelessWidget { vertical: 8, ), child: RichText( - text: (start == null || end == null) + text: (end == null) ? TextSpan( text: errorText, style: defaultStyle, @@ -528,7 +538,7 @@ class ConstructMessageBubble extends StatelessWidget { style: defaultStyle, ), TextSpan( - text: errorText.substring(start!, end), + text: errorText.substring(start, end), style: defaultStyle.merge( TextStyle( backgroundColor: Colors.red.withOpacity(0.25), @@ -547,7 +557,7 @@ class ConstructMessageBubble extends StatelessWidget { ), ), TextSpan( - text: errorText.substring(end!), + text: errorText.substring(end), style: defaultStyle, ), ], @@ -569,14 +579,7 @@ class ConstructMessageMetadata extends StatelessWidget { @override Widget build(BuildContext context) { - final String roomName = msgEvent.event.room.name.isEmpty - ? Matrix.of(context) - .client - .getRoomById(msgEvent.event.room.id) - ?.getLocalizedDisplayname() ?? - "" - : msgEvent.event.room.name; - + final String roomName = msgEvent.event.room.getLocalizedDisplayname(); return Padding( padding: const EdgeInsets.fromLTRB(10, 0, 30, 0), child: Column( diff --git a/lib/pangea/pages/analytics/list_summary_analytics.dart b/lib/pangea/pages/analytics/list_summary_analytics.dart index 5b2dde5de..bf388cea7 100644 --- a/lib/pangea/pages/analytics/list_summary_analytics.dart +++ b/lib/pangea/pages/analytics/list_summary_analytics.dart @@ -1,10 +1,9 @@ import 'dart:math'; +import 'package:fluffychat/pangea/models/analytics/chart_analytics_model.dart'; import 'package:flutter/material.dart'; - import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:fluffychat/pangea/models/chart_analytics_model.dart'; import '../../enum/use_type.dart'; class ListSummaryAnalytics extends StatelessWidget { diff --git a/lib/pangea/pages/analytics/messages_bar_chart.dart b/lib/pangea/pages/analytics/messages_bar_chart.dart index d90e44fe4..509270edb 100644 --- a/lib/pangea/pages/analytics/messages_bar_chart.dart +++ b/lib/pangea/pages/analytics/messages_bar_chart.dart @@ -10,7 +10,7 @@ import 'package:intl/intl.dart'; import '../../enum/time_span.dart'; import '../../enum/use_type.dart'; -import '../../models/chart_analytics_model.dart'; +import '../../models/analytics/chart_analytics_model.dart'; import 'bar_chart_card.dart'; import 'messages_legend_widget.dart'; @@ -58,10 +58,10 @@ class MessagesBarChartState extends State { getTitlesWidget: leftTitles, ), ), - topTitles: AxisTitles( + topTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), - rightTitles: AxisTitles( + rightTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), ); diff --git a/lib/pangea/pages/analytics/student_analytics/student_analytics.dart b/lib/pangea/pages/analytics/student_analytics/student_analytics.dart index 1970fa236..65bd533e8 100644 --- a/lib/pangea/pages/analytics/student_analytics/student_analytics.dart +++ b/lib/pangea/pages/analytics/student_analytics/student_analytics.dart @@ -1,9 +1,7 @@ import 'dart:async'; import 'dart:developer'; -import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; -import 'package:fluffychat/pangea/models/chart_analytics_model.dart'; +import 'package:fluffychat/pangea/enum/bar_chart_view_enum.dart'; import 'package:fluffychat/pangea/widgets/common/list_placeholder.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -11,13 +9,13 @@ import 'package:matrix/matrix.dart'; import '../../../../widgets/matrix.dart'; import '../../../controllers/pangea_controller.dart'; -import '../../../extensions/client_extension/client_extension.dart'; import '../../../utils/sync_status_util_v2.dart'; import '../base_analytics.dart'; import 'student_analytics_view.dart'; class StudentAnalyticsPage extends StatefulWidget { - const StudentAnalyticsPage({super.key}); + final BarChartViewSelection? selectedView; + const StudentAnalyticsPage({super.key, this.selectedView}); @override State createState() => StudentAnalyticsController(); @@ -26,37 +24,55 @@ class StudentAnalyticsPage extends StatefulWidget { class StudentAnalyticsController extends State { final PangeaController _pangeaController = MatrixState.pangeaController; AnalyticsSelected? selected; - StreamSubscription? stateSub; - Timer? refreshTimer; + StreamSubscription? stateSub; - List _chats = []; - List _spaces = []; + @override + void initState() { + super.initState(); - void onStateUpdate(Event newState) { - if (!(refreshTimer?.isActive ?? false)) { - refreshTimer = Timer( - const Duration(seconds: 3), - () => getClassAndChatAnalytics(context, true), - ); - } + final listFutures = [ + _pangeaController.myAnalytics.setStudentChats(), + _pangeaController.myAnalytics.setStudentSpaces(), + ]; + Future.wait(listFutures).then((_) => setState(() {})); + + stateSub = _pangeaController.myAnalytics.stateStream.listen((_) { + setState(() {}); + }); } @override void dispose() { - super.dispose(); - refreshTimer?.cancel(); stateSub?.cancel(); + super.dispose(); } - Future initialize() async { - await getClassAndChatAnalytics(context); - stateSub = _pangeaController.matrixState.client.onRoomState.stream - .where( - (event) => - event.type == PangeaEventTypes.studentAnalyticsSummary && - event.senderId == userId, - ) - .listen(onStateUpdate); + List get chats { + if (_pangeaController.myAnalytics.studentChats.isEmpty) { + _pangeaController.myAnalytics.setStudentChats().then((_) { + if (_pangeaController.myAnalytics.studentChats.isNotEmpty) { + setState(() {}); + } + }); + } + return _pangeaController.myAnalytics.studentChats; + } + + List get spaces { + if (_pangeaController.myAnalytics.studentSpaces.isEmpty) { + _pangeaController.myAnalytics.setStudentSpaces().then((_) { + if (_pangeaController.myAnalytics.studentSpaces.isNotEmpty) { + setState(() {}); + } + }); + } + return _pangeaController.myAnalytics.studentSpaces; + } + + String? get userId { + final id = _pangeaController.matrixState.client.userID; + debugger(when: kDebugMode && id == null); + return id; } @override @@ -66,96 +82,8 @@ class StudentAnalyticsController extends State { // but this is computationally expensive! // key: UniqueKey(), shimmerChild: const ListPlaceholder(), - onFinish: initialize, + // onFinish: initialize, child: StudentAnalyticsView(this), ); } - - Future getClassAndChatAnalytics( - BuildContext context, [ - forceUpdate = false, - ]) async { - final List> analyticsFutures = []; - for (final chat in (await getChats())) { - analyticsFutures.add( - _pangeaController.analytics.getAnalytics( - chatId: chat.id, - studentId: userId, - forceUpdate: forceUpdate, - ), - ); - } - for (final space in (await getSpaces())) { - analyticsFutures.add( - _pangeaController.analytics.getAnalytics( - classRoom: space, - studentId: userId, - forceUpdate: forceUpdate, - ), - ); - } - analyticsFutures.add( - _pangeaController.analytics.getAnalytics( - studentId: userId, - forceUpdate: forceUpdate, - ), - ); - await Future.wait(analyticsFutures); - setState(() {}); - } - - Future> getSpaces() async { - final List rooms = await _pangeaController - .matrixState.client.classesAndExchangesImStudyingIn; - setState(() => _spaces = rooms); - return rooms; - } - - List? get spaces { - try { - if (_spaces.isNotEmpty) return _spaces; - getSpaces(); - return _spaces; - } catch (err) { - debugger(when: kDebugMode); - return []; - } - } - - Future> getChats() async { - final List teacherRoomIds = - await Matrix.of(context).client.teacherRoomIds; - _chats = Matrix.of(context) - .client - .rooms - .where( - (r) => - !r.isSpace && - !r.isAnalyticsRoom && - !teacherRoomIds.contains(r.id), - ) - .toList(); - setState(() => _chats = _chats); - return _chats; - } - - List? get chats { - try { - if (_chats.isNotEmpty) return _chats; - getChats(); - return _chats; - } catch (err) { - debugger(when: kDebugMode); - return []; - } - } - - String? get userId { - final id = _pangeaController.matrixState.client.userID; - debugger(when: kDebugMode && id == null); - return id; - } - - String get username => - _pangeaController.matrixState.client.userID?.localpart ?? ""; } diff --git a/lib/pangea/pages/analytics/student_analytics/student_analytics_view.dart b/lib/pangea/pages/analytics/student_analytics/student_analytics_view.dart index 8d88cc685..5b8924581 100644 --- a/lib/pangea/pages/analytics/student_analytics/student_analytics_view.dart +++ b/lib/pangea/pages/analytics/student_analytics/student_analytics_view.dart @@ -15,7 +15,7 @@ class StudentAnalyticsView extends StatelessWidget { final TabData chatTabData = TabData( type: AnalyticsEntryType.room, icon: Icons.chat_bubble_outline, - items: (controller.chats ?? []) + items: (controller.chats) .map( (c) => TabItem( avatar: c.avatar, @@ -45,9 +45,9 @@ class StudentAnalyticsView extends StatelessWidget { return controller.userId != null ? BaseAnalyticsPage( + selectedView: controller.widget.selectedView, pageTitle: pageTitle, tabs: [chatTabData, classTabData], - refreshData: controller.getClassAndChatAnalytics, alwaysSelected: AnalyticsSelected( controller.userId!, AnalyticsEntryType.student, diff --git a/lib/pangea/pages/analytics/time_span_menu_button.dart b/lib/pangea/pages/analytics/time_span_menu_button.dart index df885d6cd..97a2ede4b 100644 --- a/lib/pangea/pages/analytics/time_span_menu_button.dart +++ b/lib/pangea/pages/analytics/time_span_menu_button.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; - import 'package:flutter_gen/gen_l10n/l10n.dart'; import '../../enum/time_span.dart'; @@ -16,6 +15,7 @@ class TimeSpanMenuButton extends StatelessWidget { @override Widget build(BuildContext context) { return PopupMenuButton( + offset: const Offset(0, 100), icon: const Icon(Icons.calendar_month_outlined), tooltip: L10n.of(context)!.changeDateRange, initialValue: value, diff --git a/lib/pangea/pages/class_settings/p_class_widgets/class_description_button.dart b/lib/pangea/pages/class_settings/p_class_widgets/class_description_button.dart index 0d6779e82..4fdb38604 100644 --- a/lib/pangea/pages/class_settings/p_class_widgets/class_description_button.dart +++ b/lib/pangea/pages/class_settings/p_class_widgets/class_description_button.dart @@ -1,9 +1,9 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:matrix/matrix.dart'; - import 'package:fluffychat/pages/chat_details/chat_details.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:future_loading_dialog/future_loading_dialog.dart'; +import 'package:matrix/matrix.dart'; class ClassDescriptionButton extends StatelessWidget { final Room room; @@ -20,21 +20,19 @@ class ClassDescriptionButton extends StatelessWidget { return Column( children: [ ListTile( - onTap: room.canSendEvent(EventTypes.RoomTopic) - ? controller.setTopicAction - : null, - leading: room.canSendEvent(EventTypes.RoomTopic) - ? CircleAvatar( - backgroundColor: Theme.of(context).scaffoldBackgroundColor, - foregroundColor: iconColor, - child: const Icon(Icons.topic_outlined), - ) - : null, + onTap: room.isRoomAdmin ? controller.setTopicAction : null, + leading: CircleAvatar( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + foregroundColor: iconColor, + child: const Icon(Icons.topic_outlined), + ), subtitle: Text( room.topic.isEmpty - ? (room.isSpace - ? L10n.of(context)!.classDescriptionDesc - : L10n.of(context)!.chatTopicDesc) + ? (room.isRoomAdmin + ? (room.isSpace + ? L10n.of(context)!.classDescriptionDesc + : L10n.of(context)!.chatTopicDesc) + : L10n.of(context)!.topicNotSet) : room.topic, ), title: Text( @@ -51,3 +49,53 @@ class ClassDescriptionButton extends StatelessWidget { ); } } + +void setClassTopic(Room room, BuildContext context) { + final TextEditingController textFieldController = + TextEditingController(text: room.topic); + showDialog( + context: context, + useRootNavigator: false, + builder: (BuildContext context) => AlertDialog( + title: Text( + room.isSpace + ? L10n.of(context)!.classDescription + : L10n.of(context)!.chatTopic, + ), + content: TextField( + controller: textFieldController, + keyboardType: TextInputType.multiline, + minLines: 1, + maxLines: 10, + maxLength: 2000, + ), + actions: [ + TextButton( + child: Text(L10n.of(context)!.cancel), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: Text(L10n.of(context)!.ok), + onPressed: () async { + if (textFieldController.text == "") return; + final success = await showFutureLoadingDialog( + context: context, + future: () => room.setDescription(textFieldController.text), + ); + if (success.error == null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: + Text(L10n.of(context)!.groupDescriptionHasBeenChanged), + ), + ); + Navigator.of(context).pop(); + } + }, + ), + ], + ), + ); +} diff --git a/lib/pangea/pages/class_settings/p_class_widgets/room_capacity_button.dart b/lib/pangea/pages/class_settings/p_class_widgets/room_capacity_button.dart new file mode 100644 index 000000000..ca876fae8 --- /dev/null +++ b/lib/pangea/pages/class_settings/p_class_widgets/room_capacity_button.dart @@ -0,0 +1,158 @@ +import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:fluffychat/pages/chat_details/chat_details.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:future_loading_dialog/future_loading_dialog.dart'; +import 'package:matrix/matrix.dart'; + +class RoomCapacityButton extends StatefulWidget { + final Room? room; + final ChatDetailsController? controller; + const RoomCapacityButton({ + super.key, + this.room, + this.controller, + }); + + @override + RoomCapacityButtonState createState() => RoomCapacityButtonState(); +} + +class RoomCapacityButtonState extends State { + int? capacity; + String? nonAdmins; + + RoomCapacityButtonState({Key? key}); + + @override + void initState() { + super.initState(); + capacity = widget.room?.capacity; + widget.room?.numNonAdmins.then( + (value) => setState(() { + nonAdmins = value.toString(); + overCapacity(); + }), + ); + } + + @override + void didUpdateWidget(RoomCapacityButton oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.room != widget.room) { + capacity = widget.room?.capacity; + widget.room?.numNonAdmins.then( + (value) => setState(() { + nonAdmins = value.toString(); + overCapacity(); + }), + ); + } + } + + Future overCapacity() async { + if ((widget.room?.isRoomAdmin ?? false) && + capacity != null && + nonAdmins != null && + int.parse(nonAdmins!) > capacity!) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + L10n.of(context)!.roomExceedsCapacity, + ), + ), + ); + } + } + + @override + Widget build(BuildContext context) { + final iconColor = Theme.of(context).textTheme.bodyLarge!.color; + return Column( + children: [ + ListTile( + onTap: () => + ((widget.room?.isRoomAdmin ?? true) ? (setRoomCapacity()) : null), + leading: CircleAvatar( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + foregroundColor: iconColor, + child: const Icon(Icons.reduce_capacity), + ), + subtitle: Text( + (capacity == null) + ? L10n.of(context)!.capacityNotSet + : (nonAdmins != null) + ? '$nonAdmins/$capacity' + : '$capacity', + ), + title: Text( + L10n.of(context)!.roomCapacity, + style: TextStyle( + color: Theme.of(context).colorScheme.secondary, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ); + } + + Future setCapacity(int newCapacity) async { + capacity = newCapacity; + } + + Future setRoomCapacity() async { + final input = await showTextInputDialog( + context: context, + title: L10n.of(context)!.roomCapacity, + message: L10n.of(context)!.roomCapacityExplanation, + okLabel: L10n.of(context)!.ok, + cancelLabel: L10n.of(context)!.cancel, + textFields: [ + DialogTextField( + initialText: ((capacity != null) ? '$capacity' : ''), + keyboardType: TextInputType.number, + maxLength: 3, + validator: (value) { + if (value == null || + value.isEmpty || + int.tryParse(value) == null || + int.parse(value) < 0) { + return L10n.of(context)!.enterNumber; + } + if (nonAdmins != null && int.parse(value) < int.parse(nonAdmins!)) { + return L10n.of(context)!.capacitySetTooLow; + } + return null; + }, + ), + ], + ); + if (input == null || + input.first == "" || + int.tryParse(input.first) == null) { + return; + } + + final newCapacity = int.parse(input.first); + final success = await showFutureLoadingDialog( + context: context, + future: () => ((widget.room != null) + ? (widget.room!.updateRoomCapacity( + capacity = newCapacity, + )) + : setCapacity(newCapacity)), + ); + if (success.error == null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + L10n.of(context)!.roomCapacityHasBeenChanged, + ), + ), + ); + setState(() {}); + } + } +} diff --git a/lib/pangea/repo/span_data_repo.dart b/lib/pangea/repo/span_data_repo.dart index e253bb1d0..f06363fd1 100644 --- a/lib/pangea/repo/span_data_repo.dart +++ b/lib/pangea/repo/span_data_repo.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:collection/collection.dart'; import 'package:fluffychat/pangea/config/environment.dart'; import 'package:fluffychat/pangea/enum/span_choice_type.dart'; import 'package:fluffychat/pangea/enum/span_data_type.dart'; @@ -80,15 +81,39 @@ class SpanDetailsRepoReqAndRes { bool operator ==(Object other) { if (identical(this, other)) return true; if (other is! SpanDetailsRepoReqAndRes) return false; - - return toJson().toString() == other.toJson().toString(); + if (other.userL1 != userL1) return false; + if (other.userL2 != userL2) return false; + if (other.enableIT != enableIT) return false; + if (other.enableIGC != enableIGC) return false; + if (other.span.choices + ?.firstWhere( + (choice) => choice.type == SpanChoiceType.bestCorrection, + ) + .value != + span.choices + ?.firstWhere( + (choice) => choice.type == SpanChoiceType.bestCorrection, + ) + .value) return false; + return true; } /// Overrides the hashCode getter to generate a hash code for the [SpanDetailsRepoReqAndRes] object. /// Used as keys in response cache in igc_controller. @override int get hashCode { - return toJson().toString().hashCode; + return Object.hashAll([ + userL1.hashCode, + userL2.hashCode, + enableIT.hashCode, + enableIGC.hashCode, + span.choices + ?.firstWhereOrNull( + (choice) => choice.type == SpanChoiceType.bestCorrection, + ) + ?.value + .hashCode, + ]); } } diff --git a/lib/pangea/utils/bot_style.dart b/lib/pangea/utils/bot_style.dart index 3791c205e..1a3a2f8fb 100644 --- a/lib/pangea/utils/bot_style.dart +++ b/lib/pangea/utils/bot_style.dart @@ -19,11 +19,7 @@ class BotStyle { AppConfig.fontSizeFactor * (big == true ? 1.2 : 1), fontStyle: italics ? FontStyle.italic : null, - color: setColor - ? Theme.of(context).brightness == Brightness.dark - ? AppConfig.primaryColorLight - : AppConfig.primaryColor - : null, + color: setColor ? Theme.of(context).colorScheme.primary : null, inherit: true, ); diff --git a/lib/pangea/utils/chat_list_handle_space_tap.dart b/lib/pangea/utils/chat_list_handle_space_tap.dart index cb58a3ea4..4930f7ecc 100644 --- a/lib/pangea/utils/chat_list_handle_space_tap.dart +++ b/lib/pangea/utils/chat_list_handle_space_tap.dart @@ -33,6 +33,9 @@ void chatListHandleSpaceTap( context: context, future: () async { await space.join(); + if (await space.leaveIfFull()) { + throw L10n.of(context)!.roomFull; + } await space.postLoad(); setActiveSpaceAndCloseChat(); }, @@ -65,6 +68,9 @@ void chatListHandleSpaceTap( context: context, future: () async { await space.join(); + if (await space.leaveIfFull()) { + throw L10n.of(context)!.roomFull; + } if (space.isSpace) { await space.joinAnalyticsRoomsInSpace(); } diff --git a/lib/pangea/utils/class_chat_power_levels.dart b/lib/pangea/utils/class_chat_power_levels.dart index de2b0bdeb..a4535924f 100644 --- a/lib/pangea/utils/class_chat_power_levels.dart +++ b/lib/pangea/utils/class_chat_power_levels.dart @@ -15,7 +15,6 @@ class ClassChatPowerLevels { final Map powerLevelOverride = {}; powerLevelOverride['events'] = { EventTypes.spaceChild: 0, - PangeaEventTypes.studentAnalyticsSummary: 0, }; powerLevelOverride['users'] = {}; diff --git a/lib/pangea/utils/set_class_topic.dart b/lib/pangea/utils/set_class_topic.dart deleted file mode 100644 index 91625af42..000000000 --- a/lib/pangea/utils/set_class_topic.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; -import 'package:matrix/matrix.dart'; - -void setClassTopic(Room room, BuildContext context) { - final TextEditingController textFieldController = - TextEditingController(text: room.topic); - showDialog( - context: context, - useRootNavigator: false, - builder: (BuildContext context) => AlertDialog( - title: Text( - room.isSpace - ? L10n.of(context)!.classDescription - : L10n.of(context)!.chatTopic, - ), - content: TextField( - controller: textFieldController, - keyboardType: TextInputType.multiline, - minLines: 1, - maxLines: 10, - maxLength: 2000, - ), - actions: [ - TextButton( - child: Text(L10n.of(context)!.cancel), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - TextButton( - child: Text(L10n.of(context)!.ok), - onPressed: () async { - if (textFieldController.text == "") return; - final success = await showFutureLoadingDialog( - context: context, - future: () => room.setDescription(textFieldController.text), - ); - if (success.error == null) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: - Text(L10n.of(context)!.groupDescriptionHasBeenChanged), - ), - ); - Navigator.of(context).pop(); - } - }, - ), - ], - ), - ); -} diff --git a/lib/pangea/widgets/chat/overlay_message.dart b/lib/pangea/widgets/chat/overlay_message.dart index e8143b376..bc25ffe14 100644 --- a/lib/pangea/widgets/chat/overlay_message.dart +++ b/lib/pangea/widgets/chat/overlay_message.dart @@ -47,6 +47,10 @@ class OverlayMessage extends StatelessWidget { } var color = Theme.of(context).colorScheme.surfaceVariant; + // #Pangea + final isLight = Theme.of(context).brightness == Brightness.light; + var lightness = isLight ? .05 : .85; + // Pangea# final textColor = ownMessage ? Theme.of(context).colorScheme.onPrimary : Theme.of(context).colorScheme.onBackground; @@ -98,7 +102,21 @@ class OverlayMessage extends StatelessWidget { if (ownMessage) { color = Theme.of(context).colorScheme.primary; + lightness = isLight ? .15 : .85; } + // Make overlay a little darker/lighter than the message + color = Color.fromARGB( + color.alpha, + isLight + ? (color.red + lightness * (255 - color.red)).round() + : (color.red * lightness).round(), + isLight + ? (color.green + lightness * (255 - color.green)).round() + : (color.green * lightness).round(), + isLight + ? (color.blue + lightness * (255 - color.blue)).round() + : (color.blue * lightness).round(), + ); // #Pangea final pangeaMessageEvent = PangeaMessageEvent( diff --git a/lib/utils/client_manager.dart b/lib/utils/client_manager.dart index cf8904d18..47805638e 100644 --- a/lib/utils/client_manager.dart +++ b/lib/utils/client_manager.dart @@ -113,10 +113,10 @@ abstract class ClientManager { // #Pangea PangeaEventTypes.classSettings, PangeaEventTypes.rules, - PangeaEventTypes.vocab, PangeaEventTypes.botOptions, EventTypes.RoomTopic, EventTypes.RoomAvatar, + PangeaEventTypes.capacity, // Pangea# }, logLevel: kReleaseMode ? Level.warning : Level.verbose, diff --git a/lib/utils/url_launcher.dart b/lib/utils/url_launcher.dart index 53de9b138..bcdda51ea 100644 --- a/lib/utils/url_launcher.dart +++ b/lib/utils/url_launcher.dart @@ -1,7 +1,12 @@ -import 'package:flutter/material.dart'; - import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart' show IterableExtension; +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; +import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:fluffychat/widgets/public_room_bottom_sheet.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:go_router/go_router.dart'; @@ -9,11 +14,6 @@ import 'package:matrix/matrix.dart'; import 'package:punycode/punycode.dart'; import 'package:url_launcher/url_launcher_string.dart'; -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart'; -import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; -import 'package:fluffychat/widgets/matrix.dart'; -import 'package:fluffychat/widgets/public_room_bottom_sheet.dart'; import 'platform_infos.dart'; class UrlLauncher { @@ -159,7 +159,10 @@ class UrlLauncher { room = matrix.client.getRoomById(roomId!); } servers.addAll(identityParts.via); - if (room != null) { + // #Pangea + if (room != null && room.membership != Membership.leave) { + // if (room != null) { + // Pangea# if (room.isSpace) { // TODO: Implement navigate to space context.go('/rooms/${room.id}'); @@ -202,7 +205,19 @@ class UrlLauncher { serverName: servers.isNotEmpty ? servers.toList() : null, ), ); - if (response.error != null) return; + // #Pangea + // if (response.error != null) return; + if (response.error != null || + (room != null && (await room.leaveIfFull()))) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + duration: const Duration(seconds: 10), + content: Text(L10n.of(context)!.roomFull), + ), + ); + return; + } + // Pangea# // wait for two seconds so that it probably came down /sync await showFutureLoadingDialog( context: context, diff --git a/lib/widgets/public_room_bottom_sheet.dart b/lib/widgets/public_room_bottom_sheet.dart index 19932f5b9..086162b3f 100644 --- a/lib/widgets/public_room_bottom_sheet.dart +++ b/lib/widgets/public_room_bottom_sheet.dart @@ -1,15 +1,15 @@ +import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; +import 'package:fluffychat/utils/fluffy_share.dart'; +import 'package:fluffychat/utils/url_launcher.dart'; +import 'package:fluffychat/widgets/avatar.dart'; +import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; - import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/utils/fluffy_share.dart'; -import 'package:fluffychat/utils/url_launcher.dart'; -import 'package:fluffychat/widgets/avatar.dart'; -import 'package:fluffychat/widgets/matrix.dart'; import '../utils/localized_exception_extension.dart'; class PublicRoomBottomSheet extends StatelessWidget { @@ -44,6 +44,12 @@ class PublicRoomBottomSheet extends StatelessWidget { if (client.getRoomById(roomId) == null) { await client.waitForRoomInSync(roomId); } + // #Pangea + final room = client.getRoomById(roomId); + if (room != null && (await room.leaveIfFull())) { + throw L10n.of(context)!.roomFull; + } + // Pangea# return roomId; }, ); diff --git a/needed-translations.txt b/needed-translations.txt index e29d36ddd..cbdc449be 100644 --- a/needed-translations.txt +++ b/needed-translations.txt @@ -850,7 +850,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "be": [ @@ -2299,7 +2310,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "bn": [ @@ -3210,7 +3232,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "bo": [ @@ -4121,7 +4154,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "ca": [ @@ -5032,7 +5076,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "cs": [ @@ -5943,7 +5998,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "de": [ @@ -6801,7 +6867,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "el": [ @@ -7712,7 +7789,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "eo": [ @@ -8623,24 +8711,22 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "es": [ - "suggestToChat", - "suggestToChatDesc", - "autoIGCToolName", - "autoIGCToolDescription", - "runGrammarCorrection", - "grammarCorrectionFailed", - "grammarCorrectionComplete", - "leaveRoomDescription", - "archiveSpaceDescription", - "leaveSpaceDescription", - "onlyAdminDescription", - "tooltipInstructionsTitle", - "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "addSpaceToSpaceDescription" ], "et": [ @@ -9494,7 +9580,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "eu": [ @@ -10348,7 +10445,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "fa": [ @@ -11259,7 +11367,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "fi": [ @@ -12170,7 +12289,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "fr": [ @@ -13081,7 +13211,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "ga": [ @@ -13992,7 +14133,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "gl": [ @@ -14846,7 +14998,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "he": [ @@ -15757,7 +15920,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "hi": [ @@ -16668,7 +16842,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "hr": [ @@ -17566,7 +17751,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "hu": [ @@ -18477,7 +18673,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "ia": [ @@ -19912,7 +20119,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "id": [ @@ -20823,7 +21041,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "ie": [ @@ -21734,7 +21963,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "it": [ @@ -22630,7 +22870,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "ja": [ @@ -23541,7 +23792,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "ko": [ @@ -24452,7 +24714,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "lt": [ @@ -25363,7 +25636,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "lv": [ @@ -26274,7 +26558,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "nb": [ @@ -27185,7 +27480,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "nl": [ @@ -28096,7 +28402,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "pl": [ @@ -29007,7 +29324,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "pt": [ @@ -29918,7 +30246,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "pt_BR": [ @@ -30798,7 +31137,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "pt_PT": [ @@ -31709,7 +32059,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "ro": [ @@ -32620,7 +32981,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "ru": [ @@ -33474,7 +33846,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "sk": [ @@ -34385,7 +34768,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "sl": [ @@ -35296,7 +35690,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "sr": [ @@ -36207,7 +36612,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "sv": [ @@ -37083,7 +37499,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "ta": [ @@ -37994,7 +38421,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "th": [ @@ -38905,7 +39343,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "tr": [ @@ -39801,7 +40250,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "uk": [ @@ -40655,7 +41115,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "vi": [ @@ -41566,7 +42037,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "zh": [ @@ -42420,7 +42902,18 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ], "zh_Hant": [ @@ -43331,6 +43824,17 @@ "onlyAdminDescription", "tooltipInstructionsTitle", "tooltipInstructionsMobileBody", - "tooltipInstructionsBrowserBody" + "tooltipInstructionsBrowserBody", + "addSpaceToSpaceDescription", + "roomCapacity", + "roomFull", + "topicNotSet", + "capacityNotSet", + "roomCapacityHasBeenChanged", + "roomExceedsCapacity", + "capacitySetTooLow", + "roomCapacityExplanation", + "enterNumber", + "buildTranslation" ] }