diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 69e457eeb..6fca89a86 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,6 +8,7 @@ on: env: PROJECT_PATH: commet + COMMET_PROD: 1 jobs: release-windows: diff --git a/commet/assets/l10n/intl_de.arb b/commet/assets/l10n/intl_de.arb index eea6681d3..2480381c9 100644 --- a/commet/assets/l10n/intl_de.arb +++ b/commet/assets/l10n/intl_de.arb @@ -1543,5 +1543,23 @@ "placeholders": { "user": {} } + }, + "promptCancelEventSend": "Abbrechen", + "@promptCancelEventSend": { + "description": "When a message failed to send, this prompts to cancel sending the event", + "type": "text", + "placeholders": {} + }, + "promptRetryEventSend": "Wiederholen", + "@promptRetryEventSend": { + "description": "When a message failed to send, this prompts to retry sending the event", + "type": "text", + "placeholders": {} + }, + "noPinnedMessages": "Es wurden noch keine Nachrichten angeheftet!", + "@noPinnedMessages": { + "description": "Placeholder label in the pinned messages menu that is shown when there are no pinned messages", + "type": "text", + "placeholders": {} } } diff --git a/commet/assets/l10n/intl_en.arb b/commet/assets/l10n/intl_en.arb index de8a29e82..16b0099a2 100644 --- a/commet/assets/l10n/intl_en.arb +++ b/commet/assets/l10n/intl_en.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2025-03-21T16:22:16.121908", + "@@last_modified": "2025-04-12T14:27:37.583027", "labelRoomsList": "Rooms", "@labelRoomsList": { "description": "Header label for the list of rooms", @@ -576,19 +576,17 @@ "proxyUrl": {} } }, - "labelEncryptedPreview": "URL Preview in Encrypted Chats (Experimental)", - "@labelEncryptedPreview": { - "description": "Label for the toggle for enabling and disabling encrypted url preview", + "labelUrlPreviewInEncryptedChatTitle": "URL Preview in Encrypted Chats", + "@labelUrlPreviewInEncryptedChatTitle": { + "description": "Label for the toggle for enabling and disabling use of url previews in encrypted chats", "type": "text", "placeholders": {} }, - "labelEncryptedPreviewDescription": "Enable use of a proxy server ({proxyUrl}) to get url preview in an encrypted chat. The content of these requests will be hidden from your homeserver using Commet's 'encrypted url preview'\nLearn more: https://github.com/commetchat/encrypted_url_preview", - "@labelEncryptedPreviewDescription": { - "description": "Explains briefly how encrypted url preview works", + "labelUrlPreviewInEncryptedChatDescription": "This will expose any URLs sent in your encrypted chats to your homeserver in order to fetch the preview", + "@labelUrlPreviewInEncryptedChatDescription": { + "description": "description for the toggle for enabling and disabling use of url previews in encrypted chats", "type": "text", - "placeholders": { - "proxyUrl": {} - } + "placeholders": {} }, "labelMessageEffectsTitle": "Message Effects", "@labelMessageEffectsTitle": { @@ -602,6 +600,36 @@ "type": "text", "placeholders": {} }, + "labelMediaPreviewSettingsTitle": "Media Preview", + "@labelMediaPreviewSettingsTitle": { + "description": "Header for the settings tile for for media preview toggles", + "type": "text", + "placeholders": {} + }, + "labelMediaPreviewPrivateRoomsToggle": "Private Rooms", + "@labelMediaPreviewPrivateRoomsToggle": { + "description": "Short label for the private rooms toggle in media previews section", + "type": "text", + "placeholders": {} + }, + "labelMediaPreviewPrivateRoomsToggleDescription": "Toggle previewing of images, videos, stickers and urls in private chats", + "@labelMediaPreviewPrivateRoomsToggleDescription": { + "description": "Label describing toggle of media previews for private rooms", + "type": "text", + "placeholders": {} + }, + "labelMediaPreviewPublicRoomsToggle": "Public Rooms", + "@labelMediaPreviewPublicRoomsToggle": { + "description": "Short label for the private rooms toggle in media previews section", + "type": "text", + "placeholders": {} + }, + "labelMediaPreviewPublicRoomsToggleDescription": "Toggle previewing of images, videos, stickers and urls in public chat rooms", + "@labelMediaPreviewPublicRoomsToggleDescription": { + "description": "Label describing toggle of media previews for public rooms", + "type": "text", + "placeholders": {} + }, "labelThemeDark": "Dark Theme", "@labelThemeDark": { "description": "Label for the dark theme", @@ -686,43 +714,21 @@ "type": "text", "placeholders": {} }, - "promptCreateEmoticonPack": "Create pack", - "@promptCreateEmoticonPack": { - "description": "Prompt to create a new emoticon pack, for emoji or stickers", - "type": "text", - "placeholders": {} - }, "promptImportPack": "Import pack", "@promptImportPack": { "description": "Prompt to import a set of emoticons from an existing pack", "type": "text", "placeholders": {} }, - "promptConfirmDeleteEmoticonPack": "Are you sure you want to delete the **{packName}** pack?", - "@promptConfirmDeleteEmoticonPack": { - "description": "Prompt to confirm deletion of an emoticon pack, supports markdown to emphasise the pack name", - "type": "text", - "placeholders": { - "packName": {} - } - }, "createEmoticonDialogTitle": "Create Emote", "@createEmoticonDialogTitle": { "description": "Title of a dialog that pops up when choosing to create a new emoticon", "type": "text", "placeholders": {} }, - "promptConfirmDeleteEmoticon": "Are you sure you want to delete **{emoticon}**?", - "@promptConfirmDeleteEmoticon": { - "description": "Prompt to confirm deletion of an emoticon pack, supports markdown to emphasise the emote name", - "type": "text", - "placeholders": { - "emoticon": {} - } - }, - "promptRenameEmoticon": "Rename emote", - "@promptRenameEmoticon": { - "description": "Tooltip for button to rename emoticon", + "editEmoticonDialogTitle": "Edit Emote", + "@editEmoticonDialogTitle": { + "description": "Title of a dialog that pops up when choosing to edit an existing emoticon", "type": "text", "placeholders": {} }, @@ -732,20 +738,14 @@ "type": "text", "placeholders": {} }, - "promptEmojiName": "Emoji name", - "@promptEmojiName": { + "promptEmoteName": "Emote name", + "@promptEmoteName": { "description": "Prompt for the input of the name of an emoji", "type": "text", "placeholders": {} }, - "promptStickerName": "Sticker name", - "@promptStickerName": { - "description": "Prompt for the input of a sticker", - "type": "text", - "placeholders": {} - }, - "promptConfirmCreateEmoticon": "Create!", - "@promptConfirmCreateEmoticon": { + "promptConfirmSaveEmoticon": "Save!", + "@promptConfirmSaveEmoticon": { "description": "Prompt to confirm the creation of an Emoticon Pack, Emoji, or Sticker", "type": "text", "placeholders": {} @@ -1350,6 +1350,12 @@ "type": "text", "placeholders": {} }, + "promptShowSticker": "Show sticker", + "@promptShowSticker": { + "description": "Prompt to display a sticker, shown when media previews are disabled", + "type": "text", + "placeholders": {} + }, "messageFailedToDecrypt": "Failed to decrypt event", "@messageFailedToDecrypt": { "description": "Placeholde text for when a message fails to decrypt", diff --git a/commet/assets/l10n/intl_et.arb b/commet/assets/l10n/intl_et.arb index 4a9e614e1..183249a99 100644 --- a/commet/assets/l10n/intl_et.arb +++ b/commet/assets/l10n/intl_et.arb @@ -1592,7 +1592,7 @@ "type": "text", "placeholders": {} }, - "messagePlaceholderUserUpdatedNameDetailed": "{user} muutis oma uueks kuvatavaks nimeks '{newName}'", + "messagePlaceholderUserUpdatedNameDetailed": "{user} muutis oma uueks kuvatavaks nimeks „{newName}“", "@messagePlaceholderUserUpdatedNameDetailed": { "description": "Message body for when a user updates their display name", "type": "text", @@ -1651,5 +1651,53 @@ "sender": {}, "user": {} } + }, + "labelEnableRoomIconsDescription": "Näita jututubade ikoonide asemel tunnuspilte", + "@labelEnableRoomIconsDescription": { + "description": "Description for the enable room icons setting", + "type": "text", + "placeholders": {} + }, + "labelUseRoomAvatarPlaceholders": "Kasuta kohatäitena loodud tunnuspilte", + "@labelUseRoomAvatarPlaceholders": { + "description": "Label for enabling generic icons in the appearance settings", + "type": "text", + "placeholders": {} + }, + "labelUseRoomAvatars": "Kasuta jututubade tunnuspilte", + "@labelUseRoomAvatars": { + "description": "Label for enabling using room avatars instead of icons", + "type": "text", + "placeholders": {} + }, + "labelUseRoomAvatarPlaceholdersDescription": "Kui jututoa pole tunnuspilti kirjeldatud või nende kasutamine on keelatud, siis loo tunnuspilt üldisest taustavärvist ja jututoa nime esitähest", + "@labelUseRoomAvatarPlaceholdersDescription": { + "description": "Description for the enable generic icons setting", + "type": "text", + "placeholders": {} + }, + "placeholderRoomAlias": "#vahva-jututuba", + "@placeholderRoomAlias": { + "description": "Placeholder / Example for a room alias.", + "type": "text", + "placeholders": {} + }, + "placeholderSpaceAlias": "#toimekas-kogukond", + "@placeholderSpaceAlias": { + "description": "Placeholder / Example for a space alias.", + "type": "text", + "placeholders": {} + }, + "promptCancelEventSend": "Katkesta", + "@promptCancelEventSend": { + "description": "When a message failed to send, this prompts to cancel sending the event", + "type": "text", + "placeholders": {} + }, + "promptRetryEventSend": "Proovi uuesti", + "@promptRetryEventSend": { + "description": "When a message failed to send, this prompts to retry sending the event", + "type": "text", + "placeholders": {} } } diff --git a/commet/assets/l10n/intl_eu.arb b/commet/assets/l10n/intl_eu.arb index 4033d4b2b..bae685661 100644 --- a/commet/assets/l10n/intl_eu.arb +++ b/commet/assets/l10n/intl_eu.arb @@ -550,13 +550,13 @@ "type": "text", "placeholders": {} }, - "placeholderRoomAddress": "#gela-zoragarria:matrix.org", + "placeholderRoomAddress": "#gela-zoragarria:example.com", "@placeholderRoomAddress": { "description": "Placeholder / Example for a room address. Please do not translate 'example.com' and keep the domain as is, this is a reserved domain specifically for use in examples, and translations could end up pointing to untrusted domains", "type": "text", "placeholders": {} }, - "placeholderSpaceAddress": "#gune-zoragarria:matrix.org", + "placeholderSpaceAddress": "#gune-zoragarria:example.com", "@placeholderSpaceAddress": { "description": "Placeholder / Example for a space address. Please do not translate 'example.com' and keep the domain as is, this is a reserved domain specifically for use in examples, and translations could end up pointing to untrusted domains", "type": "text", @@ -1544,7 +1544,7 @@ "type": "text", "placeholders": {} }, - "messagePlaceholderUserUpdatedNameDetailed": "{user}(e)k pantaila-izena '{newName}'(e)ra aldatu du", + "messagePlaceholderUserUpdatedNameDetailed": "{user}(e)k pantaila-izena \"{newName}\"(e)ra aldatu du", "@messagePlaceholderUserUpdatedNameDetailed": { "description": "Message body for when a user updates their display name", "type": "text", @@ -1645,5 +1645,53 @@ "description": "If a message was sent with an effect, this prompts to replay the effect", "type": "text", "placeholders": {} + }, + "labelUseRoomAvatars": "Erabili gelen abatarrak", + "@labelUseRoomAvatars": { + "description": "Label for enabling using room avatars instead of icons", + "type": "text", + "placeholders": {} + }, + "labelEnableRoomIconsDescription": "Gelen abatar-irudia erakutsiko da ikonoen ordez", + "@labelEnableRoomIconsDescription": { + "description": "Description for the enable room icons setting", + "type": "text", + "placeholders": {} + }, + "labelUseRoomAvatarPlaceholders": "Erabili abatar generikoak", + "@labelUseRoomAvatarPlaceholders": { + "description": "Label for enabling generic icons in the appearance settings", + "type": "text", + "placeholders": {} + }, + "labelUseRoomAvatarPlaceholdersDescription": "Gela batek abatarrik ezarrita ez duenean, edo aukera ezgaituta dagoenean, kolore generiko bat eta lehen hizkia erabiliko da irudi orokor gisa", + "@labelUseRoomAvatarPlaceholdersDescription": { + "description": "Description for the enable generic icons setting", + "type": "text", + "placeholders": {} + }, + "placeholderRoomAlias": "#gela-zoragarria", + "@placeholderRoomAlias": { + "description": "Placeholder / Example for a room alias.", + "type": "text", + "placeholders": {} + }, + "placeholderSpaceAlias": "#gune-zoragarria", + "@placeholderSpaceAlias": { + "description": "Placeholder / Example for a space alias.", + "type": "text", + "placeholders": {} + }, + "promptCancelEventSend": "Utzi", + "@promptCancelEventSend": { + "description": "When a message failed to send, this prompts to cancel sending the event", + "type": "text", + "placeholders": {} + }, + "promptRetryEventSend": "Saiatu berriro", + "@promptRetryEventSend": { + "description": "When a message failed to send, this prompts to retry sending the event", + "type": "text", + "placeholders": {} } } diff --git a/commet/assets/l10n/intl_ja.arb b/commet/assets/l10n/intl_ja.arb index 73401cdd6..cc14810e2 100644 --- a/commet/assets/l10n/intl_ja.arb +++ b/commet/assets/l10n/intl_ja.arb @@ -1543,5 +1543,47 @@ "description": "Title for permission to manage children", "type": "text", "placeholders": {} + }, + "promptPinMessage": "メッセージをピン留めする", + "@promptPinMessage": { + "description": "Label for the menu option to pin a message", + "type": "text", + "placeholders": {} + }, + "labelUseRoomAvatars": "ルームアバターを使用する", + "@labelUseRoomAvatars": { + "description": "Label for enabling using room avatars instead of icons", + "type": "text", + "placeholders": {} + }, + "promptUnpinMessage": "メッセージのピン留めを外す", + "@promptUnpinMessage": { + "description": "Label for the menu option to unpin a message", + "type": "text", + "placeholders": {} + }, + "promptReplyInThread": "スレッドで返信", + "@promptReplyInThread": { + "description": "Label for the menu option to reply to a message inside a thread", + "type": "text", + "placeholders": {} + }, + "labelMessageEffectsTitle": "メッセージエフェクト", + "@labelMessageEffectsTitle": { + "description": "Header for the settings tile for message effects, such as confetti", + "type": "text", + "placeholders": {} + }, + "labelMessageEffectsDescription": "紙吹雪のようなエフェクトを追加してメッセージを送信することができます", + "@labelMessageEffectsDescription": { + "description": "Label describing what message effects are", + "type": "text", + "placeholders": {} + }, + "promptShowSource": "ソースを表示", + "@promptShowSource": { + "description": "Label for the menu option to view the JSON source of an event", + "type": "text", + "placeholders": {} } } diff --git a/commet/assets/l10n/intl_pt.arb b/commet/assets/l10n/intl_pt.arb index 23e1faa42..65c180f8c 100644 --- a/commet/assets/l10n/intl_pt.arb +++ b/commet/assets/l10n/intl_pt.arb @@ -719,5 +719,910 @@ "description": "The header for the direct messages list on desktop", "type": "text", "placeholders": {} + }, + "labelRoomSettingsEmoticons": "Emoticons", + "@labelRoomSettingsEmoticons": { + "description": "Label for room Emoticon settings", + "type": "text", + "placeholders": {} + }, + "labelSettingsAppGeneral": "Geral", + "@labelSettingsAppGeneral": { + "description": "Label for the App General settings page", + "type": "text", + "placeholders": {} + }, + "promptMatrixVerifySession": "Verificar", + "@promptMatrixVerifySession": { + "description": "Text on the button to verify a session", + "type": "text", + "placeholders": {} + }, + "labelSettingsCategoryApp": "Configurações da app", + "@labelSettingsCategoryApp": { + "description": "Label for the settings category of the overall App settings/", + "type": "text", + "placeholders": {} + }, + "labelGifSearchDescription": "Ativar o uso de pesquisa de GIFs no Tenor. Solicitações irão por um servidor intermediário via {proxyUrl}", + "@labelGifSearchDescription": { + "description": "Explains that gifs will be fetched via proxy", + "type": "text", + "placeholders": { + "proxyUrl": {} + } + }, + "labelEncryptedPreview": "Prévia de URLs em conversas criptografadas (experimental)", + "@labelEncryptedPreview": { + "description": "Label for the toggle for enabling and disabling encrypted url preview", + "type": "text", + "placeholders": {} + }, + "labelStickerCompatibility": "Compatibilidade para figurinhas", + "@labelStickerCompatibility": { + "description": "Header for the settings to enable sticker compatibility mode", + "type": "text", + "placeholders": {} + }, + "promptLeaveSpace": "Sair do espaço", + "@promptLeaveSpace": { + "description": "Text on a button to leave a space", + "type": "text", + "placeholders": {} + }, + "promptLeaveRoom": "Sair da sala", + "@promptLeaveRoom": { + "description": "Text on a button to leave a room", + "type": "text", + "placeholders": {} + }, + "promptHomeserver": "Servidor local", + "@promptHomeserver": { + "description": "Placeholder text for homeserver field on login form", + "type": "text", + "placeholders": {} + }, + "promptUsername": "Nome de utilizador", + "@promptUsername": { + "description": "Placeholder text for username field on login form", + "type": "text", + "placeholders": {} + }, + "roomVisibilityPublicExplanation": "Esta sala será publicamente acessível por qualquer um na internet", + "@roomVisibilityPublicExplanation": { + "description": "Explains what 'public' visibility means", + "type": "text", + "placeholders": {} + }, + "promptConfirmRoomCreation": "Criar sala!", + "@promptConfirmRoomCreation": { + "description": "Label for a button which confirms the creation of a room", + "type": "text", + "placeholders": {} + }, + "promptRoomAddress": "Endereço da sala:", + "@promptRoomAddress": { + "description": "Short label to prompt for the input of a room address", + "type": "text", + "placeholders": {} + }, + "messageMatrixSessionVerificationRequest": "**{username}** solicitou verificar a sua sessão", + "@messageMatrixSessionVerificationRequest": { + "description": "Message to show when another user has requested to verify your matrix session. Supports markdown to emphasise the user name", + "type": "text", + "placeholders": { + "username": {} + } + }, + "promptConfirmEmojiMatches": "Eles combinam!", + "@promptConfirmEmojiMatches": { + "description": "Button text to confirm that the emoji matches", + "type": "text", + "placeholders": {} + }, + "promptHome": "Início", + "@promptHome": { + "description": "Generic prompt to go home, usually will go back to a main menu, or similar", + "type": "text", + "placeholders": {} + }, + "promptPoliteNo": "Não, obrigado", + "@promptPoliteNo": { + "description": "Generic message to decline something, using nice manners :)", + "type": "text", + "placeholders": {} + }, + "spaceVisibilityPrivateExplanation": "Esta sala só será acessível por convite", + "@spaceVisibilityPrivateExplanation": { + "description": "Explains what 'private' space visibility means", + "type": "text", + "placeholders": {} + }, + "spaceVisibilityPublicExplanation": "Esta sala será visível publicamente por qualquer um na internet", + "@spaceVisibilityPublicExplanation": { + "description": "Explains what 'public' space visibility means", + "type": "text", + "placeholders": {} + }, + "labelVisibilityPublic": "Pública", + "@labelVisibilityPublic": { + "description": "Short label for room visibility public", + "type": "text", + "placeholders": {} + }, + "promptConfirmSpaceCreation": "Criar espaço!", + "@promptConfirmSpaceCreation": { + "description": "Label for a button which confirms the creation of a space", + "type": "text", + "placeholders": {} + }, + "promptConfirmRoomJoin": "Entrar na sala!", + "@promptConfirmRoomJoin": { + "description": "Label for a button which confirms the joining of a room", + "type": "text", + "placeholders": {} + }, + "promptConfirmSpaceJoin": "Entrar no espaço!", + "@promptConfirmSpaceJoin": { + "description": "Label for a button which confirms the joining of a space", + "type": "text", + "placeholders": {} + }, + "labelCouldNotLoadRoomPreview": "Não foi possível carregar uma prévia desta sala", + "@labelCouldNotLoadRoomPreview": { + "description": "Error message for when a room preview was not able to be loaded", + "type": "text", + "placeholders": {} + }, + "promptAddSelectedRooms": "Adicionar salas selecionadas", + "@promptAddSelectedRooms": { + "description": "Prompt to add the selected rooms to a space", + "type": "text", + "placeholders": {} + }, + "promptJoinExistingSpace": "Entrar no espaço existente", + "@promptJoinExistingSpace": { + "description": "Prompt to join a space which already exists", + "type": "text", + "placeholders": {} + }, + "promptCreateNewRoom": "Criar sala", + "@promptCreateNewRoom": { + "description": "Prompt to create a new room", + "type": "text", + "placeholders": {} + }, + "promptJoinExistingRoom": "Entrar na sala existente", + "@promptJoinExistingRoom": { + "description": "Prompt to join a room which already exists", + "type": "text", + "placeholders": {} + }, + "promptUseExistingRoom": "Usar sala existente", + "@promptUseExistingRoom": { + "description": "Button text to choose to use an existing room when adding a room to a space", + "type": "text", + "placeholders": {} + }, + "messageWaitingOtherDeviceToAccept": "À espera do outro aceitar a solicitação", + "@messageWaitingOtherDeviceToAccept": { + "description": "Message to show while waiting for another device to accept a matrix session verification request", + "type": "text", + "placeholders": {} + }, + "messageVerificationComplete": "Verificação completa!", + "@messageVerificationComplete": { + "description": "Message to show when verification was completed successfully, and the session has been verified", + "type": "text", + "placeholders": {} + }, + "promptPassword": "Palavra-passe", + "@promptPassword": { + "description": "Placeholder text for password field on login form", + "type": "text", + "placeholders": {} + }, + "messageAlreadyLoggedIn": "Já entrou nesta conta", + "@messageAlreadyLoggedIn": { + "description": "An error message displayed when the user attempts to add an account which has already been logged in to on this device", + "type": "text", + "placeholders": {} + }, + "promptLeaveSpaceConfirmation": "Tem certeza que deseja sair de {spaceName}?", + "@promptLeaveSpaceConfirmation": { + "description": "Text for the popup dialog confirming the intent to leave a space", + "type": "text", + "placeholders": { + "spaceName": {} + } + }, + "promptLeaveRoomConfirmation": "Tem certeza que deseja sair de {roomName}?", + "@promptLeaveRoomConfirmation": { + "description": "Text for the popup dialog confirming the intent to leave", + "type": "text", + "placeholders": { + "roomName": {} + } + }, + "notificationSettingsNotSupported": "Notificações não são suportadas neste sistema operativo", + "@notificationSettingsNotSupported": { + "description": "Message to display when push notifications are not supported", + "type": "text", + "placeholders": {} + }, + "labelSettingsDeveloperMode": "Modo programador", + "@labelSettingsDeveloperMode": { + "description": "Header for the settings to enable developer mode", + "type": "text", + "placeholders": {} + }, + "labelSettingsDeveloperModeExplanation": "Mostrar informações adicionais, útil para programadores", + "@labelSettingsDeveloperModeExplanation": { + "description": "Explains what developer mode does", + "type": "text", + "placeholders": {} + }, + "labelSettingsStickerCompatibilityExplanation": "Em alguns clientes Matrix, enviar uma figurinha como \"m.sticker\" fará com que a figurinha não carregue corretamente. Ativar esta configuração enviará figurinhas como \"m.image\", o que as permitirá serem renderizadas corretamente", + "@labelSettingsStickerCompatibilityExplanation": { + "description": "Explains what sticker compatibility mode does", + "type": "text", + "placeholders": {} + }, + "labelSettingsAppAppearance": "Aparência", + "@labelSettingsAppAppearance": { + "description": "Label for the App Appearance settings page", + "type": "text", + "placeholders": {} + }, + "labelSettingsAppDeveloperUtils": "Ferramentas de programador", + "@labelSettingsAppDeveloperUtils": { + "description": "Label for the developer utils settings page, usually hidden unless developer mode is turned on", + "type": "text", + "placeholders": {} + }, + "labelSettingsWindowBehaviourTitle": "Comportamento da janela", + "@labelSettingsWindowBehaviourTitle": { + "description": "Header for the window behaviour section of settings", + "type": "text", + "placeholders": {} + }, + "labelSettingsMinimizeOnCloseToggle": "Minimizar ao fechar", + "@labelSettingsMinimizeOnCloseToggle": { + "description": "Label for the toggle to turn on and off minimize on close", + "type": "text", + "placeholders": {} + }, + "labelSettingsMinimizeOnCloseExplanation": "Ao fechar a janela, a app será minimizada em vez de fechado", + "@labelSettingsMinimizeOnCloseExplanation": { + "description": "Explains what the 'minimize on close' setting does", + "type": "text", + "placeholders": {} + }, + "labelThirdPartyServicesTitle": "Serviços de terceiros", + "@labelThirdPartyServicesTitle": { + "description": "Header for the third party services section in settings", + "type": "text", + "placeholders": {} + }, + "labelGifSearchToggle": "Pesquisa de GIFs", + "@labelGifSearchToggle": { + "description": "Label for the toggle for enabling and disabling gif search", + "type": "text", + "placeholders": {} + }, + "labelEncryptedPreviewDescription": "Ativar o uso de um servidor intermediário ({proxyUrl}) para ter prévia de URLs numa conversa criptografada. O conteúdo destas solicitações será escondido do seu servidor local usando a \"prévia de URLs criptografada\" do Commet\nAprenda mais: https://github.com/commetchat/encrypted_url_preview", + "@labelEncryptedPreviewDescription": { + "description": "Explains briefly how encrypted url preview works", + "type": "text", + "placeholders": { + "proxyUrl": {} + } + }, + "labelThemeDark": "Tema escuro", + "@labelThemeDark": { + "description": "Label for the dark theme", + "type": "text", + "placeholders": {} + }, + "labelThemeLight": "Tema claro", + "@labelThemeLight": { + "description": "Label for the light theme", + "type": "text", + "placeholders": {} + }, + "labelThemeAmoled": "AMOLED", + "@labelThemeAmoled": { + "description": "Label for the light theme", + "type": "text", + "placeholders": {} + }, + "labelAppScale": "Escala da app", + "@labelAppScale": { + "description": "Label for the setting which controls the UI scale of the overall app", + "type": "text", + "placeholders": {} + }, + "labelMatrixCrossSigningAndBackup": "Sessão múltipla e cópia de segurança", + "@labelMatrixCrossSigningAndBackup": { + "description": "Header label for matrix cross signing and message backup section", + "type": "text", + "placeholders": {} + }, + "labelMatrixAccountSessions": "Sessões", + "@labelMatrixAccountSessions": { + "description": "Title label for account sessions", + "type": "text", + "placeholders": {} + }, + "labelMatrixCrossSigningExplanation": "Configure para verificar e acompanhar todas as suas sessões", + "@labelMatrixCrossSigningExplanation": { + "description": "Explains what matrix cross signing does", + "type": "text", + "placeholders": {} + }, + "labelMatrixResetCrossSigningTitle": "Reiniciar múltiplas sessões", + "@labelMatrixResetCrossSigningTitle": { + "description": "Title for the popup dialog when resetting cross signing", + "type": "text", + "placeholders": {} + }, + "labelMatrixMessageBackupExplanation": "Mantém uma cópia de segurança do seu histórico de mensagens caso perca todas as suas sessões. As suas mensagens serão criptografadas antes do envio", + "@labelMatrixMessageBackupExplanation": { + "description": "Explains what matrix message backup does", + "type": "text", + "placeholders": {} + }, + "promptSetupMatrixMessageBackup": "Configurar cópia de segurança", + "@promptSetupMatrixMessageBackup": { + "description": "Text on the button to begin the setup process for message backup", + "type": "text", + "placeholders": {} + }, + "promptMatrixRecoveryKeyInput": "Chave de recuperação", + "@promptMatrixRecoveryKeyInput": { + "description": "Placeholder text for the recovery key input box", + "type": "text", + "placeholders": {} + }, + "promptConfirmWipingCrossSigningKeys": "Tem certeza que deseja apagar as suas chaves de sessão?", + "@promptConfirmWipingCrossSigningKeys": { + "description": "Asks the user if they are sure they want to wipe the keys", + "type": "text", + "placeholders": {} + }, + "labelMatrixSecurityPhraseShouldNotBePassword": "É recomendado que a sua frase de segurança seja diferente da palavra-passe da sua conta", + "@labelMatrixSecurityPhraseShouldNotBePassword": { + "description": "Tells the user to not use their password as their security phrase", + "type": "text", + "placeholders": {} + }, + "errorMatrixPassphraseMustContainNumber": "Frase-passe deve conter ao menos 1 número", + "@errorMatrixPassphraseMustContainNumber": { + "description": "Explains constraints of recovery passphrase", + "type": "text", + "placeholders": {} + }, + "placeholderMatrixEnterSecutiyPhrase": "Frase de segurança", + "@placeholderMatrixEnterSecutiyPhrase": { + "description": "Placeholder text for the passphrase text box", + "type": "text", + "placeholders": {} + }, + "labelMatrixExistingMessageBackupFound": "Cópia de segurança existente encontrada!", + "@labelMatrixExistingMessageBackupFound": { + "description": "Message to explain that existing backup has been found", + "type": "text", + "placeholders": {} + }, + "labelMatrixWarnResetKeysIsPermanent": "Redefinir as suas chaves é permanente e resultará na perda da cópia de segurança do seu histórico de conversas. Quase definitivamente não quer fazer isto!", + "@labelMatrixWarnResetKeysIsPermanent": { + "description": "Explains that resetting keys is permanent, should emphasize that this isnt really a great idea", + "type": "text", + "placeholders": {} + }, + "labelMatrixExplainOnlineKeyBackup": "A cópia de segurança online de chaves permitirá que recupere o histórico de mensagens caso perca acesso a todas as suas sessões", + "@labelMatrixExplainOnlineKeyBackup": { + "description": "Explains what the message backup does", + "type": "text", + "placeholders": {} + }, + "promptOptions": "Opções", + "@promptOptions": { + "description": "Generic prompt for options, generally would be used to open a settings menu, extra details or similar", + "type": "text", + "placeholders": {} + }, + "promptDownload": "Descarregar", + "@promptDownload": { + "description": "Generic prompt to download something", + "type": "text", + "placeholders": {} + }, + "promptReset": "Redefinir", + "@promptReset": { + "description": "Generic prompt to reset something", + "type": "text", + "placeholders": {} + }, + "promptEnable": "Ativar", + "@promptEnable": { + "description": "Generic prompt to enable something", + "type": "text", + "placeholders": {} + }, + "promptBack": "Voltar", + "@promptBack": { + "description": "Prompt text to go backwards, probably for navigation", + "type": "text", + "placeholders": {} + }, + "promptCopy": "Copiar", + "@promptCopy": { + "description": "Prompt to copy text", + "type": "text", + "placeholders": {} + }, + "labelOr": "Ou", + "@labelOr": { + "description": "Text that is placed between two or more options: [button1] or [button2]", + "type": "text", + "placeholders": {} + }, + "messageUserPinnedEvent": "{user} fixou uma mensagem!", + "@messageUserPinnedEvent": { + "description": "Message body for when a user adds an event to the room's pinned messages", + "type": "text", + "placeholders": { + "user": {} + } + }, + "messageUserUnpinnedEvent": "{user} desafixou uma mensagem", + "@messageUserUnpinnedEvent": { + "description": "Message body for when a user removes an event from the room's pinned messages", + "type": "text", + "placeholders": { + "user": {} + } + }, + "matrixClientOlmMissingMessage": "libolm não está instalada ou não foi encontrada. Criptografia de ponta a ponta não estará disponível até isto ser resolvido", + "@matrixClientOlmMissingMessage": { + "description": "Text that explains to the user that libolm dependency is not found", + "type": "text", + "placeholders": {} + }, + "labelMatrixCrossSigning": "Sessão múltipla", + "@labelMatrixCrossSigning": { + "description": "Title label for matrix cross signing", + "type": "text", + "placeholders": {} + }, + "labelMatrixMessageBackup": "Cópia de segurança de mensagens", + "@labelMatrixMessageBackup": { + "description": "TItle label for matrix message backup settings", + "type": "text", + "placeholders": {} + }, + "promptJoin": "Entrar", + "@promptJoin": { + "description": "Generic prompt to join a room", + "type": "text", + "placeholders": {} + }, + "labelInvitations": "Convites", + "@labelInvitations": { + "description": "Label for the list of incoming invitations", + "type": "text", + "placeholders": {} + }, + "labelInvitationsForUser": "Convites para {user}", + "@labelInvitationsForUser": { + "description": "Label for the list of incoming invitations, specifying which user these invitations are intended for", + "type": "text", + "placeholders": { + "user": {} + } + }, + "labelSpaceSubspacesList": "Espaços", + "@labelSpaceSubspacesList": { + "description": "Header label for the list of child spaces in a space", + "type": "text", + "placeholders": {} + }, + "labelEnableUnifiedPushEndpoint": "Endpoint", + "@labelEnableUnifiedPushEndpoint": { + "description": "Label for the Unified Push endpoint", + "type": "text", + "placeholders": {} + }, + "labelSpaceEmoticonSettings": "Emoticons", + "@labelSpaceEmoticonSettings": { + "description": "Label for space emoticon settings", + "type": "text", + "placeholders": {} + }, + "promptImportPack": "Importar pacote", + "@promptImportPack": { + "description": "Prompt to import a set of emoticons from an existing pack", + "type": "text", + "placeholders": {} + }, + "labelMatrixRecoveryKeyPromptExplanation": "Para desbloquear as suas mensagens antigas, por favor digite a sua chave de recuperação que foi gerada numa sessão anterior. A sua chave de recuperação NÃO é a sua palavra-passe.", + "@labelMatrixRecoveryKeyPromptExplanation": { + "description": "Shown when a user is attempting to recover their old messages, explains that they need the recovery key", + "type": "text", + "placeholders": {} + }, + "errorMatrixPassphraseMustContainUpperAndLowercase": "Frase-passe deve conter ao menos 1 letra maiúscula e minúscula", + "@errorMatrixPassphraseMustContainUpperAndLowercase": { + "description": "Explains constraints of recovery passphrase", + "type": "text", + "placeholders": {} + }, + "erroMatrixPassphraseMustBeLonger": "Frase-passe deve ter ao menos 10 caracteres", + "@erroMatrixPassphraseMustBeLonger": { + "description": "Explains constraints of recovery passphrase", + "type": "text", + "placeholders": {} + }, + "labelMatrixConfirmSecurityPhrase": "Confirme a frase de segurança:", + "@labelMatrixConfirmSecurityPhrase": { + "description": "Prompts the user to input their passphrase again", + "type": "text", + "placeholders": {} + }, + "placeholderMatrixConfirmSecurityPhrase": "Confirme a frase de segurança", + "@placeholderMatrixConfirmSecurityPhrase": { + "description": "Placeholder text for the confirm passphrase text input", + "type": "text", + "placeholders": {} + }, + "labelSettingsWindowBehaviour": "Comportamento da janela", + "@labelSettingsWindowBehaviour": { + "description": "Label for the Window Behaviour settings page", + "type": "text", + "placeholders": {} + }, + "labelSettingsAppAdvanced": "Avançado", + "@labelSettingsAppAdvanced": { + "description": "Label for the App Advanced settings page", + "type": "text", + "placeholders": {} + }, + "labelSettingsAppNotifications": "Notificações", + "@labelSettingsAppNotifications": { + "description": "Label for the App notifications settings page", + "type": "text", + "placeholders": {} + }, + "messageLoginFailed": "Falha ao iniciar sessão...", + "@messageLoginFailed": { + "description": "Generic text to show that an attempted login has failed", + "type": "text", + "placeholders": {} + }, + "messageLoginError": "Ocorreu um erro", + "@messageLoginError": { + "description": "A generic error message to convey that an error occured when attempting to login", + "type": "text", + "placeholders": {} + }, + "promptRoomName": "Nome da sala", + "@promptRoomName": { + "description": "Prompt to enter a room name, placeholder text for text input", + "type": "text", + "placeholders": {} + }, + "promptSpaceName": "Nome do espaço", + "@promptSpaceName": { + "description": "Prompt to enter a space name, placeholder text for text input", + "type": "text", + "placeholders": {} + }, + "promptSubmitLogin": "Entrar", + "@promptSubmitLogin": { + "description": "Prompt to submit the username and password, and attempt to login", + "type": "text", + "placeholders": {} + }, + "promptTopic": "Tópico (opcional)", + "@promptTopic": { + "description": "Prompt to enter a topic for room or space, specifying that doing so is optional", + "type": "text", + "placeholders": {} + }, + "roomVisibilityPrivateExplanation": "Esta sala só será acessível por convite", + "@roomVisibilityPrivateExplanation": { + "description": "Explains what 'private' room visibility means", + "type": "text", + "placeholders": {} + }, + "promptEnableEncryption": "Ativar criptografia", + "@promptEnableEncryption": { + "description": "Short prompt to enable encryption for a room", + "type": "text", + "placeholders": {} + }, + "encryptionCannotBeDisabledExplanation": "Se ativada, criptografia não pode ser desativada depois", + "@encryptionCannotBeDisabledExplanation": { + "description": "Explains that encryption cannot be disabled once enabled", + "type": "text", + "placeholders": {} + }, + "promptSpaceAddress": "Endereço do espaço:", + "@promptSpaceAddress": { + "description": "Short label to prompt for the input of a space address", + "type": "text", + "placeholders": {} + }, + "promptCreateNewSpace": "Criar espaço", + "@promptCreateNewSpace": { + "description": "Prompt to create a new space", + "type": "text", + "placeholders": {} + }, + "messageSasEmojiVerificationPrompt": "Verifique que os emojis são iguais e que estão na mesma ordem no outro dispositivo", + "@messageSasEmojiVerificationPrompt": { + "description": "Explains what to look for when verifying using emoji. Needs to portray that the emoji MUST be the same AND in the same order", + "type": "text", + "placeholders": {} + }, + "promptEmojiDoNotMatch": "Eles não combinam", + "@promptEmojiDoNotMatch": { + "description": "Button text to confirm that the emoji do NOT match", + "type": "text", + "placeholders": {} + }, + "promptReply": "Responder", + "@promptReply": { + "description": "Generic prompt to reply to a message", + "type": "text", + "placeholders": {} + }, + "promptSettings": "Configurações", + "@promptSettings": { + "description": "Generic prompt for settings, usually will open a settings menu or similar", + "type": "text", + "placeholders": {} + }, + "promptReject": "Negar", + "@promptReject": { + "description": "Generic prompt to reject something, probably a request of some kind", + "type": "text", + "placeholders": {} + }, + "promptApply": "Aplicar", + "@promptApply": { + "description": "Generic prompt to apply something, probably a setting", + "type": "text", + "placeholders": {} + }, + "promptDelete": "Apagar", + "@promptDelete": { + "description": "Generic prompt to delete something", + "type": "text", + "placeholders": {} + }, + "promptSubmit": "Enviar", + "@promptSubmit": { + "description": "Generic prompt to submit something", + "type": "text", + "placeholders": {} + }, + "promptContinue": "Continuar", + "@promptContinue": { + "description": "Generic prompt to continue with some action", + "type": "text", + "placeholders": {} + }, + "promptConfirm": "Confirmar", + "@promptConfirm": { + "description": "Generic prompt to confirm some action", + "type": "text", + "placeholders": {} + }, + "promptDone": "Feito", + "@promptDone": { + "description": "Generic prompt to confirm that you are done", + "type": "text", + "placeholders": {} + }, + "promptRestore": "Restaurar", + "@promptRestore": { + "description": "Generic prompt to restore something", + "type": "text", + "placeholders": {} + }, + "promptCopyComplete": "Copiado!", + "@promptCopyComplete": { + "description": "Prompt text for after a copy has been completed", + "type": "text", + "placeholders": {} + }, + "noPinnedMessages": "Nenhuma mensagem foi fixa!", + "@noPinnedMessages": { + "description": "Placeholder label in the pinned messages menu that is shown when there are no pinned messages", + "type": "text", + "placeholders": {} + }, + "promptAttachmentProcessingSendOriginal": "Enviar original", + "@promptAttachmentProcessingSendOriginal": { + "description": "Prompt text for the option to send a file in its original state, without any further processing such as removing metadata", + "type": "text", + "placeholders": {} + }, + "labelImageContainsLocationInfo": "Aviso: esta imagem contém metadados de localização", + "@labelImageContainsLocationInfo": { + "description": "Prompt text for the option to send a file in its original state, without any further processing such as removing metadata", + "type": "text", + "placeholders": {} + }, + "labelVisibilityPrivate": "Privada", + "@labelVisibilityPrivate": { + "description": "Short label for room visibility private", + "type": "text", + "placeholders": {} + }, + "promptShowSource": "Mostrar Fonte", + "@promptShowSource": { + "description": "Label for the menu option to view the JSON source of an event", + "type": "text", + "placeholders": {} + }, + "labelSettingsAppTheme": "Tema", + "@labelSettingsAppTheme": { + "description": "Label for theme section of app appearance", + "type": "text", + "placeholders": {} + }, + "labelMessageEffectsTitle": "Efeitos de Mensagem", + "@labelMessageEffectsTitle": { + "description": "Header for the settings tile for message effects, such as confetti", + "type": "text", + "placeholders": {} + }, + "labelMessageEffectsDescription": "Mensagens podem ser enviadas com efeitos adicionais, como confete", + "@labelMessageEffectsDescription": { + "description": "Label describing what message effects are", + "type": "text", + "placeholders": {} + }, + "labelRestoreMatrixBackupTitle": "Restaurar cópia de segurança", + "@labelRestoreMatrixBackupTitle": { + "description": "Title of the popup dialog for restoring message backup", + "type": "text", + "placeholders": {} + }, + "errorMatrixPassphraseMustContainSymbol": "Frase-passe deve conter ao menos 1 símbolo", + "@errorMatrixPassphraseMustContainSymbol": { + "description": "Explains constraints of recovery passphrase", + "type": "text", + "placeholders": {} + }, + "errorMatrixPassphrasesDontMatch": "Frases-passes não combinam", + "@errorMatrixPassphrasesDontMatch": { + "description": "Error when the user enters passphrase twice and the two dont match", + "type": "text", + "placeholders": {} + }, + "labelMatrixPromptPassphrase": "Frase de segurança:", + "@labelMatrixPromptPassphrase": { + "description": "Prompt the user to enter passphrase", + "type": "text", + "placeholders": {} + }, + "labelMatrixAskWipeBackupToContinue": "Se mudar as suas chaves de sessão, precisará limpar a sua cópia de segurança atual... continuar?", + "@labelMatrixAskWipeBackupToContinue": { + "description": "Asks the user if they want to wipe their existing backup to continue changing their cross signing keys", + "type": "text", + "placeholders": {} + }, + "labelMatrixAskEnableMessageBackup": "Gostaria de ativar a cópia de segurança online de chaves?", + "@labelMatrixAskEnableMessageBackup": { + "description": "Asks the user if they want to enable online backup", + "type": "text", + "placeholders": {} + }, + "labelSettingsTabEmoticons": "Emoticons", + "@labelSettingsTabEmoticons": { + "description": "Label for the Emoticons settings page", + "type": "text", + "placeholders": {} + }, + "promptPinMessage": "Fixar Mensagem", + "@promptPinMessage": { + "description": "Label for the menu option to pin a message", + "type": "text", + "placeholders": {} + }, + "promptUnpinMessage": "Desafixar Mensagem", + "@promptUnpinMessage": { + "description": "Label for the menu option to unpin a message", + "type": "text", + "placeholders": {} + }, + "promptReplayMessageEffect": "Reproduzir Efeito", + "@promptReplayMessageEffect": { + "description": "If a message was sent with an effect, this prompts to replay the effect", + "type": "text", + "placeholders": {} + }, + "labelInvitationBodyWithSender": "{user} convidou-o para uma sala", + "@labelInvitationBodyWithSender": { + "description": "Message body for when an invitation was received, and we have a name for the sender", + "type": "text", + "placeholders": { + "user": {} + } + }, + "promptReplyInThread": "Responder No Tópico", + "@promptReplyInThread": { + "description": "Label for the menu option to reply to a message inside a thread", + "type": "text", + "placeholders": {} + }, + "promptAddReaction": "Adicionar reação", + "@promptAddReaction": { + "description": "Generic prompt to add reaction to a message", + "type": "text", + "placeholders": {} + }, + "promptEdit": "Editar", + "@promptEdit": { + "description": "Generic prompt to edit something", + "type": "text", + "placeholders": {} + }, + "promptAccept": "Aceitar", + "@promptAccept": { + "description": "Generic prompt to accept something, probably a request of some kind", + "type": "text", + "placeholders": {} + }, + "messageUserRetractedInvite": "{sender} desconvidou {user}", + "@messageUserRetractedInvite": { + "description": "Message body for when a user's invitation to a room was withdrawn", + "type": "text", + "placeholders": { + "sender": {}, + "user": {} + } + }, + "messageUserAcceptedInvite": "{user} aceitou o convite", + "@messageUserAcceptedInvite": { + "description": "Message body for when a user accepted an invitation to a room", + "type": "text", + "placeholders": { + "user": {} + } + }, + "messageUserBanned": "{sender} baniu {user}", + "@messageUserBanned": { + "description": "Message body for when a user bans another user from the room", + "type": "text", + "placeholders": { + "sender": {}, + "user": {} + } + }, + "messageUserUnbanned": "{sender} desbaniu {user}", + "@messageUserUnbanned": { + "description": "Message body for when a user reverts a ban of another user", + "type": "text", + "placeholders": { + "sender": {}, + "user": {} + } + }, + "matrixClientEncryptionWarningTitle": "Aviso de criptografia", + "@matrixClientEncryptionWarningTitle": { + "description": "Title of a warning about encryption", + "type": "text", + "placeholders": {} + }, + "notificationModifiersPrivacyEnhanced": "Enviou uma mensagem", + "@notificationModifiersPrivacyEnhanced": { + "description": "Placeholder text to put in a notification when the user has privacy enhanced notifications enabled.", + "type": "text", + "placeholders": {} } } diff --git a/commet/assets/l10n/intl_pt_BR.arb b/commet/assets/l10n/intl_pt_BR.arb index d771944c4..3f547d4bd 100644 --- a/commet/assets/l10n/intl_pt_BR.arb +++ b/commet/assets/l10n/intl_pt_BR.arb @@ -1070,7 +1070,7 @@ "type": "text", "placeholders": {} }, - "placeholderRoomAddress": "#sala-incrível:matrix.org", + "placeholderRoomAddress": "#sala-incrível:example.com", "@placeholderRoomAddress": { "description": "Placeholder / Example for a room address. Please do not translate 'example.com' and keep the domain as is, this is a reserved domain specifically for use in examples, and translations could end up pointing to untrusted domains", "type": "text", @@ -1224,7 +1224,7 @@ "type": "text", "placeholders": {} }, - "placeholderSpaceAddress": "#espaço-incrível:matrix.org", + "placeholderSpaceAddress": "#espaço-incrível:example.com", "@placeholderSpaceAddress": { "description": "Placeholder / Example for a space address. Please do not translate 'example.com' and keep the domain as is, this is a reserved domain specifically for use in examples, and translations could end up pointing to untrusted domains", "type": "text", @@ -1592,7 +1592,7 @@ "type": "text", "placeholders": {} }, - "messagePlaceholderUserUpdatedNameDetailed": "{user} mudou seu nome para '{newName}'", + "messagePlaceholderUserUpdatedNameDetailed": "{user} mudou seu nome para \"{newName}\"", "@messagePlaceholderUserUpdatedNameDetailed": { "description": "Message body for when a user updates their display name", "type": "text", @@ -1651,5 +1651,53 @@ "placeholders": { "user": {} } + }, + "labelUseRoomAvatars": "Usar avatares da sala", + "@labelUseRoomAvatars": { + "description": "Label for enabling using room avatars instead of icons", + "type": "text", + "placeholders": {} + }, + "labelUseRoomAvatarPlaceholdersDescription": "Quando uma sala não tem um avatar configurado, ou seu uso está desativado, contingenciar para uma cor genérica + primeira letra substituta para a imagem", + "@labelUseRoomAvatarPlaceholdersDescription": { + "description": "Description for the enable generic icons setting", + "type": "text", + "placeholders": {} + }, + "labelEnableRoomIconsDescription": "Mostrar imagens do avatar da sala em vez de ícones", + "@labelEnableRoomIconsDescription": { + "description": "Description for the enable room icons setting", + "type": "text", + "placeholders": {} + }, + "labelUseRoomAvatarPlaceholders": "Usar avatares substitutos", + "@labelUseRoomAvatarPlaceholders": { + "description": "Label for enabling generic icons in the appearance settings", + "type": "text", + "placeholders": {} + }, + "placeholderRoomAlias": "#sala-incrível", + "@placeholderRoomAlias": { + "description": "Placeholder / Example for a room alias.", + "type": "text", + "placeholders": {} + }, + "promptCancelEventSend": "Cancelar", + "@promptCancelEventSend": { + "description": "When a message failed to send, this prompts to cancel sending the event", + "type": "text", + "placeholders": {} + }, + "placeholderSpaceAlias": "#espaço-incrível", + "@placeholderSpaceAlias": { + "description": "Placeholder / Example for a space alias.", + "type": "text", + "placeholders": {} + }, + "promptRetryEventSend": "Tentar novamente", + "@promptRetryEventSend": { + "description": "When a message failed to send, this prompts to retry sending the event", + "type": "text", + "placeholders": {} } } diff --git a/commet/assets/l10n/intl_uk.arb b/commet/assets/l10n/intl_uk.arb index adb9d13e9..039a9efc5 100644 --- a/commet/assets/l10n/intl_uk.arb +++ b/commet/assets/l10n/intl_uk.arb @@ -1601,7 +1601,7 @@ "emote": {} } }, - "messagePlaceholderUserUpdatedNameDetailed": "{user} змінив своє ім'я на '{newName}'", + "messagePlaceholderUserUpdatedNameDetailed": "{user} змінив своє ім'я на «{newName}»", "@messagePlaceholderUserUpdatedNameDetailed": { "description": "Message body for when a user updates their display name", "type": "text", @@ -1651,5 +1651,53 @@ "placeholders": { "user": {} } + }, + "labelUseRoomAvatars": "Використовувати аватари кімнат", + "@labelUseRoomAvatars": { + "description": "Label for enabling using room avatars instead of icons", + "type": "text", + "placeholders": {} + }, + "labelUseRoomAvatarPlaceholdersDescription": "Якщо в кімнаті немає набору аватарів або їх використання вимкнено, використовується загальний колір + заповнювач першої літери для зображення", + "@labelUseRoomAvatarPlaceholdersDescription": { + "description": "Description for the enable generic icons setting", + "type": "text", + "placeholders": {} + }, + "labelEnableRoomIconsDescription": "Показувати зображення аватарів кімнат замість значків", + "@labelEnableRoomIconsDescription": { + "description": "Description for the enable room icons setting", + "type": "text", + "placeholders": {} + }, + "labelUseRoomAvatarPlaceholders": "Використовувати аватарки-заповнювачі", + "@labelUseRoomAvatarPlaceholders": { + "description": "Label for enabling generic icons in the appearance settings", + "type": "text", + "placeholders": {} + }, + "placeholderSpaceAlias": "#крутецький-простір", + "@placeholderSpaceAlias": { + "description": "Placeholder / Example for a space alias.", + "type": "text", + "placeholders": {} + }, + "placeholderRoomAlias": "#крутецька-кімната", + "@placeholderRoomAlias": { + "description": "Placeholder / Example for a room alias.", + "type": "text", + "placeholders": {} + }, + "promptCancelEventSend": "Скасувати", + "@promptCancelEventSend": { + "description": "When a message failed to send, this prompts to cancel sending the event", + "type": "text", + "placeholders": {} + }, + "promptRetryEventSend": "Повторити", + "@promptRetryEventSend": { + "description": "When a message failed to send, this prompts to retry sending the event", + "type": "text", + "placeholders": {} } } diff --git a/commet/assets/l10n/intl_zh.arb b/commet/assets/l10n/intl_zh.arb index d88e2a102..4457d4f6c 100644 --- a/commet/assets/l10n/intl_zh.arb +++ b/commet/assets/l10n/intl_zh.arb @@ -1620,7 +1620,7 @@ "type": "text", "placeholders": {} }, - "messagePlaceholderUserUpdatedNameDetailed": "{user} 将他们的显示名称更改为 '{newName}'", + "messagePlaceholderUserUpdatedNameDetailed": "{user} 将他们的显示名称更改为 \"{newName}\"", "@messagePlaceholderUserUpdatedNameDetailed": { "description": "Message body for when a user updates their display name", "type": "text", @@ -1645,5 +1645,29 @@ "placeholders": { "user": {} } + }, + "labelEnableRoomIconsDescription": "显示房间头像图片而不是图标", + "@labelEnableRoomIconsDescription": { + "description": "Description for the enable room icons setting", + "type": "text", + "placeholders": {} + }, + "labelUseRoomAvatars": "使用房间头像", + "@labelUseRoomAvatars": { + "description": "Label for enabling using room avatars instead of icons", + "type": "text", + "placeholders": {} + }, + "labelUseRoomAvatarPlaceholders": "使用占位符头像", + "@labelUseRoomAvatarPlaceholders": { + "description": "Label for enabling generic icons in the appearance settings", + "type": "text", + "placeholders": {} + }, + "labelUseRoomAvatarPlaceholdersDescription": "当一个房间没有设置头像,或者使用头像被禁用时,回退到使用通用颜色加首字母的占位符作为图像", + "@labelUseRoomAvatarPlaceholdersDescription": { + "description": "Description for the enable generic icons setting", + "type": "text", + "placeholders": {} } } diff --git a/commet/devtools_options.yaml b/commet/devtools_options.yaml new file mode 100644 index 000000000..fa0b357c4 --- /dev/null +++ b/commet/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/commet/lib/cache/cache_file_provider.dart b/commet/lib/cache/cache_file_provider.dart index a86d21cc4..36579589b 100644 --- a/commet/lib/cache/cache_file_provider.dart +++ b/commet/lib/cache/cache_file_provider.dart @@ -19,4 +19,7 @@ class CacheFileProvider implements FileProvider { @override Future save(String filepath) async {} + + @override + Stream? get onProgressChanged => null; } diff --git a/commet/lib/cache/file_provider.dart b/commet/lib/cache/file_provider.dart index aece8e763..dff9a34cd 100644 --- a/commet/lib/cache/file_provider.dart +++ b/commet/lib/cache/file_provider.dart @@ -1,10 +1,19 @@ import 'dart:io'; +class DownloadProgress { + int downloaded; + int total; + + DownloadProgress(this.downloaded, this.total); +} + abstract class FileProvider { Future resolve(); Future save(String filepath); + Stream? get onProgressChanged; + String get fileIdentifier; } @@ -25,4 +34,7 @@ class SystemFileProvider implements FileProvider { } SystemFileProvider(this.file); + + @override + Stream? get onProgressChanged => null; } diff --git a/commet/lib/client/attachment.dart b/commet/lib/client/attachment.dart index d9ba8ff7b..44f05d08f 100644 --- a/commet/lib/client/attachment.dart +++ b/commet/lib/client/attachment.dart @@ -57,47 +57,47 @@ class PendingFileAttachment { } } -class ImageAttachment implements Attachment { +class FileAttachment implements Attachment { + @override + String name; + int? fileSize; + String? mimeType; + FileProvider file; + FileAttachment(this.file, {required this.name, this.fileSize, this.mimeType}); +} + +class ImageAttachment extends FileAttachment { final ImageProvider image; - final FileProvider file; final double? width; final double? height; - @override - String name; double get aspectRatio => (width != null && height != null) ? (width! / height!) : 1; - ImageAttachment(this.image, this.file, - {required this.name, this.width, this.height}); + ImageAttachment( + this.image, + super.file, { + required super.name, + required super.mimeType, + super.fileSize, + this.width, + this.height, + }); } -class VideoAttachment implements Attachment { +class VideoAttachment extends FileAttachment { final ImageProvider? thumbnail; - final FileProvider videoFile; final double? width; final double? height; - final int? fileSize; - @override - String name; double get aspectRatio => (width != null && height != null) ? (width! / height!) : 1; - VideoAttachment(this.videoFile, - {required this.name, + VideoAttachment(super.file, + {required super.name, + required super.mimeType, this.thumbnail, this.width, this.height, - this.fileSize}); -} - -class FileAttachment implements Attachment { - @override - String name; - int? fileSize; - String? mimeType; - FileProvider provider; - FileAttachment(this.provider, - {required this.name, this.fileSize, this.mimeType}); + super.fileSize}); } diff --git a/commet/lib/client/components/emoticon/emoji_pack.dart b/commet/lib/client/components/emoticon/emoji_pack.dart index 1ff2caa2b..8ac0fb264 100644 --- a/commet/lib/client/components/emoticon/emoji_pack.dart +++ b/commet/lib/client/components/emoticon/emoji_pack.dart @@ -11,7 +11,7 @@ abstract class EmoticonPack { String get ownerId; String get ownerDisplayName; - Stream get onEmoticonAdded; + bool get isGloballyAvailable; List get emotes; @@ -24,31 +24,37 @@ abstract class EmoticonPack { ImageProvider? get image; IconData? get icon; - bool get isStickerPack; - bool get isEmojiPack; - bool get isGloballyAvailable; + EmoticonUsage get usage; Future deleteEmoticon(Emoticon emoticon); - Future renameEmoticon(Emoticon emoticon, String name); - - Future markEmoticonAsSticker(Emoticon emoticon, bool isSticker); + Future setPackUsage(EmoticonUsage usage); - Future markEmoticonAsEmoji(Emoticon emoticon, bool isEmoji); + Future updatePack( + {EmoticonUsage? usage, String? name, Uint8List? imageData}); - Future markAsEmoji(bool isEmojiPack); + Emoticon? getByShortcode(String shortcode); - Future markAsSticker(bool isStickerPack); + bool get isStickerPack; - Emoticon? getByShortcode(String shortcode); + bool get isEmojiPack; - Future addEmoticon( - {required String slug, - String? shortcode, - required Uint8List data, - String? mimeType, - bool? isEmoji, - bool? isSticker}); + Future updateEmoticon({ + String? slug, + String? shortcode, + Uint8List? data, + String? mimeType, + EmoticonUsage? usage, + required Emoticon previous, + }); + + Future addEmoticon({ + required String slug, + String? shortcode, + required Uint8List data, + String? mimeType, + EmoticonUsage usage, + }); Future markAsGlobal(bool isGlobal); diff --git a/commet/lib/client/components/emoticon/emoticon.dart b/commet/lib/client/components/emoticon/emoticon.dart index 773342456..d9935ab2f 100644 --- a/commet/lib/client/components/emoticon/emoticon.dart +++ b/commet/lib/client/components/emoticon/emoticon.dart @@ -1,15 +1,24 @@ import 'dart:core'; import 'package:flutter/material.dart'; +enum EmoticonUsage { + sticker, + emoji, + all, + inherit, +} + abstract class Emoticon { ImageProvider? get image; String get slug; String? get shortcode; String get key; - bool get isMarkedEmoji; - bool get isMarkedSticker; + EmoticonUsage get usage; + + bool get isSticker => + usage == EmoticonUsage.sticker || usage == EmoticonUsage.all; - bool get isSticker; - bool get isEmoji; + bool get isEmoji => + usage == EmoticonUsage.emoji || usage == EmoticonUsage.all; } diff --git a/commet/lib/client/components/emoticon/emoticon_component.dart b/commet/lib/client/components/emoticon/emoticon_component.dart index df75a9761..dc6f5dfb6 100644 --- a/commet/lib/client/components/emoticon/emoticon_component.dart +++ b/commet/lib/client/components/emoticon/emoticon_component.dart @@ -14,7 +14,7 @@ abstract class EmoticonComponent implements Component { List globalPacks(); List get ownedPacks; bool get canCreatePack; - Stream get onOwnedPackAdded; + Stream get onStateChanged; Future createEmoticonPack(String name, Uint8List? avatarData); Future importEmoticonPack(String name, int avatarIndex, diff --git a/commet/lib/client/matrix/components/emoticon/matrix_emoticon.dart b/commet/lib/client/matrix/components/emoticon/matrix_emoticon.dart index 79141ed9f..f1c968cf3 100644 --- a/commet/lib/client/matrix/components/emoticon/matrix_emoticon.dart +++ b/commet/lib/client/matrix/components/emoticon/matrix_emoticon.dart @@ -18,57 +18,27 @@ class MatrixEmoticon implements Emoticon { Uri emojiUrl; - late bool _isEmojiPack; - late bool _isStickerPack; - late bool _isMarkedEmoji; - late bool _isMarkedSticker; - @override - bool get isEmoji => _isEmojiPack || _isMarkedEmoji; + EmoticonUsage usage; - @override - bool get isSticker => _isStickerPack || _isMarkedSticker; - - @override - bool get isMarkedEmoji => _isMarkedEmoji; - - @override - bool get isMarkedSticker => _isMarkedSticker; + EmoticonUsage packUsage; MatrixEmoticon(this.emojiUrl, matrix.Client client, - {required String shortcode, - bool isEmojiPack = true, - bool isStickerPack = true, - bool isMarkedSticker = false, - bool isMarkedEmoji = false}) { + {required this.packUsage, + required String shortcode, + required this.usage}) { _shortcode = shortcode; - _isEmojiPack = isEmojiPack; - _isStickerPack = isStickerPack; - _isMarkedEmoji = isMarkedEmoji; - _isMarkedSticker = isMarkedSticker; - _image = MatrixMxcImage(emojiUrl, client, doThumbnail: false); + _image = MatrixMxcImage(emojiUrl, client, + fullResHeight: 100, + doThumbnail: false, + doFullres: true, + autoLoadFullRes: true); } void setShortcode(String shortcode) { _shortcode = shortcode; } - void markAsEmoji(bool value) { - _isMarkedEmoji = value; - } - - void markAsSticker(bool value) { - _isMarkedSticker = value; - } - - void markPackAsEmoji(bool value) { - _isEmojiPack = value; - } - - void markPackAsSticker(bool value) { - _isStickerPack = value; - } - void setImage(MatrixMxcImage image) { _image = image; } @@ -91,4 +61,18 @@ class MatrixEmoticon implements Emoticon { int get hashCode { return key.hashCode; } + + @override + bool get isSticker => + usage == EmoticonUsage.sticker || + usage == EmoticonUsage.all || + (usage == EmoticonUsage.inherit && + [EmoticonUsage.sticker, EmoticonUsage.all].contains(packUsage)); + + @override + bool get isEmoji => + usage == EmoticonUsage.emoji || + usage == EmoticonUsage.all || + (usage == EmoticonUsage.inherit && + [EmoticonUsage.emoji, EmoticonUsage.all].contains(packUsage)); } diff --git a/commet/lib/client/matrix/components/emoticon/matrix_emoticon_component.dart b/commet/lib/client/matrix/components/emoticon/matrix_emoticon_component.dart index 1dd89887d..b19d13a9d 100644 --- a/commet/lib/client/matrix/components/emoticon/matrix_emoticon_component.dart +++ b/commet/lib/client/matrix/components/emoticon/matrix_emoticon_component.dart @@ -1,13 +1,14 @@ +import 'dart:async'; import 'dart:typed_data'; import 'package:commet/client/components/emoticon/emoji_pack.dart'; +import 'package:commet/client/components/emoticon/emoticon.dart'; import 'package:commet/client/components/emoticon/emoticon_component.dart'; import 'package:commet/client/matrix/components/emoticon/matrix_emoticon_pack.dart'; import 'package:commet/client/matrix/components/emoticon/matrix_emoticon_state_manager.dart'; import 'package:commet/client/matrix/components/emoticon/matrix_import_emoticon_pack_task.dart'; import 'package:commet/client/matrix/matrix_client.dart'; import 'package:commet/main.dart'; -import 'package:commet/utils/notifying_list.dart'; import 'package:flutter/material.dart'; /// Manages custom emoticon packs from the matrix user account state @@ -15,6 +16,8 @@ class MatrixEmoticonComponent extends EmoticonComponent { static const roomEmotesStateKey = "im.ponies.room_emotes"; static const globalEmoteRoomsStateKey = "im.ponies.emote_rooms"; + final StreamController _onStateChanged = StreamController.broadcast(); + @override bool get canCreatePack => ownedPacks.isEmpty; @@ -27,23 +30,37 @@ class MatrixEmoticonComponent extends EmoticonComponent { MatrixEmoticonStateManager state; - final NotifyingList _packs = - NotifyingList.empty(growable: true); - MatrixEmoticonComponent(this.client, this.state) { - loadFromState(state.getAllStates()); + refreshOwnedPacks(); - state.onStateChanged.listen((event) { - var s = state.getAllStates(); - loadFromState(s); + state.onStateChanged.listen((_) { + refreshOwnedPacks(); + _onStateChanged.add(null); }); } + void refreshOwnedPacks() { + final state = this.state.getAllStates(); + + _ownedPacks = state.entries.where((e) { + final val = e.value; + if (val is Map) { + return val.isNotEmpty; + } else { + return false; + } + }).map((e) { + return MatrixEmoticonPack(this, e.key, e.value); + }).toList(); + } + @override - Stream get onOwnedPackAdded => _packs.onAdd; + Stream get onStateChanged => _onStateChanged.stream; + + List _ownedPacks = List.empty(); @override - List get ownedPacks => _packs; + List get ownedPacks => _ownedPacks; String getDefaultDisplayName() { return "Personal"; @@ -61,29 +78,6 @@ class MatrixEmoticonComponent extends EmoticonComponent { return true; } - void loadFromState(Map newState) { - _packs.removeWhere( - (element) => newState.containsKey(element.identifier) == false); - - for (var key in newState.keys) { - var s = newState[key]; - if (s is! Map) continue; - if (s.isEmpty) { - _packs.removeWhere((element) => element.identifier == key); - continue; - } - - var existing = - _packs.where((element) => element.identifier == key).firstOrNull; - if (existing is MatrixEmoticonPack) { - existing.updateFromState(s); - } else { - var pack = MatrixEmoticonPack(this, key, s); - _packs.add(pack); - } - } - } - @override Future createEmoticonPack(String name, Uint8List? avatarData) async { Uri? avatar; @@ -150,7 +144,6 @@ class MatrixEmoticonComponent extends EmoticonComponent { @override Future deleteEmoticonPack(EmoticonPack pack) { - _packs.remove(pack); var matrixPack = pack as MatrixEmoticonPack; return state.setState(matrixPack.stateKey, {}); } @@ -219,62 +212,84 @@ class MatrixEmoticonComponent extends EmoticonComponent { return state.setState(packKey, content); } - Future renameEmoticon( - String packKey, String emoteName, String newName) async { + Future setPackUsages(String packKey, List? usages) async { var content = state.getState(packKey); - if (content.containsKey('images')) { - var images = content['images'] as Map; - var image = images[emoteName] as Map?; - images.remove(emoteName); + var pack = content['pack'] as Map?; - if (image != null) { - image['display_name'] = newName; - images[newName] = image; - } + if (pack == null) return; - content['images'] = images; - } + pack['usage'] = usages?.isEmpty == true ? null : usages; + content['pack'] = pack; return state.setState(packKey, content); } - Future setEmoticonUsages( - String packKey, String emoteName, List? usages) async { + Future updatePack(String packKey, + {EmoticonUsage? usage, String? name, Uint8List? imageData}) async { var content = state.getState(packKey); - if (content.containsKey('images')) { - var images = content['images'] as Map; - - if (images.containsKey(emoteName)) { - var emote = images[emoteName] as Map; - - emote.remove('usage'); - - if (usages != null && usages.isNotEmpty) { - emote['usage'] = usages; - } + if (usage != null) { + content['pack']['usage'] = switch (usage) { + EmoticonUsage.sticker => ["sticker"], + EmoticonUsage.emoji => ["emoticon"], + EmoticonUsage.all => ["emoticon", "sticker"], + EmoticonUsage.inherit => null, + }; + } - images[emoteName] = emote; - } + if (name != null) { + content['pack']['display_name'] = name; + } - content['images'] = images; + if (imageData != null) { + Uri url = await client.getMatrixClient().uploadContent(imageData); + content['pack']['avatar_url'] = url.toString(); } return state.setState(packKey, content); } - Future setPackUsages(String packKey, List? usages) async { + Future updateEmoticon( + String packKey, + String emoteName, { + Uint8List? data, + String? mimeType, + EmoticonUsage? usage, + required Emoticon previous, + }) async { var content = state.getState(packKey); - var pack = content['pack'] as Map?; + var emoteState = content['images'][previous.shortcode!]; - if (pack == null) return; + if (usage != null) { + emoteState['usage'] = switch (usage) { + EmoticonUsage.sticker => ["sticker"], + EmoticonUsage.emoji => ["emoticon"], + EmoticonUsage.all => ["emoticon", "sticker"], + EmoticonUsage.inherit => null, + }; + } - pack['usage'] = usages?.isEmpty == true ? null : usages; - content['pack'] = pack; + if (data != null) { + Uri url = await client.getMatrixClient().uploadContent(data); + emoteState['url'] = url.toString(); + } - return state.setState(packKey, content); + var pack = {"pack": content['pack'], "images": {}}; + + var keys = content['images'].keys.toList(); + + // construct a new map this way, to keep ordering :O + for (var key in keys) { + if (key == previous.shortcode) { + pack['images'][emoteName] = emoteState; + } else { + pack['images'][key] = content['images'][key]; + } + } + + await state.setState(packKey, pack); } Future>? createEmoticon( diff --git a/commet/lib/client/matrix/components/emoticon/matrix_emoticon_pack.dart b/commet/lib/client/matrix/components/emoticon/matrix_emoticon_pack.dart index 5ceb0d145..429bc628f 100644 --- a/commet/lib/client/matrix/components/emoticon/matrix_emoticon_pack.dart +++ b/commet/lib/client/matrix/components/emoticon/matrix_emoticon_pack.dart @@ -8,7 +8,6 @@ import 'package:commet/client/matrix/components/emoticon/matrix_room_emoticon_co import 'package:commet/client/matrix/components/emoticon/matrix_space_emoticon_component.dart'; import 'package:commet/client/matrix/extensions/matrix_client_extensions.dart'; import 'package:commet/client/matrix/matrix_mxc_image_provider.dart'; -import 'package:commet/utils/notifying_list.dart'; import 'package:flutter/widgets.dart'; import 'package:fuzzy/fuzzy.dart'; import 'package:matrix/matrix.dart'; @@ -18,116 +17,102 @@ class MatrixEmoticonPack implements EmoticonPack { String stateKey; @override - final NotifyingList emotes = - NotifyingList.empty(growable: true); + List get emotes { + final images = state.tryGetMap>("images"); - late Map shortcodeToEmoticon; + if (images == null) { + return List.empty(); + } - @override - late String displayName; + return images.entries.map((e) { + final shortCode = e.key; + final url = e.value.tryGet("url"); + final usages = e.value.tryGetList("usage"); - @override - ImageProvider? image; + if (url == null) { + throw UnimplementedError; + } + + final usage = usagesArrayToUsage(usages); + + return MatrixEmoticon(Uri.parse(url), component.client.getMatrixClient(), + packUsage: this.usage, shortcode: shortCode, usage: usage); + }).toList(); + } + + late Map shortcodeToEmoticon; + + late Map state; @override - IconData? icon; - - MatrixEmoticonPack( - this.component, - this.stateKey, - Map initialState, - ) { - updateFromState(initialState); + String get displayName { + return state + .tryGetMap("pack") + ?.tryGet("display_name") ?? + "Unnamed Pack"; } - void updateFromState(Map initialState) { - var info = initialState['pack']; - displayName = info?['display_name'] ?? component.getDefaultDisplayName(); - shortcodeToEmoticon = {}; - if (info?['avatar_url'] != null) { - try { - var uri = Uri.parse(info!['avatar_url']!); - image = MatrixMxcImage(uri, component.client.getMatrixClient()); - } catch (_) {} - } + @override + ImageProvider? get image { + final pack = state.tryGetMap("pack"); - image ??= component.getDefaultImage(); - icon = component.getDefaultIcon(); + final url = pack?.tryGet("avatar_url"); - var images = initialState['images'] as Map?; - if (images == null) return; + if (url != null) { + return MatrixMxcImage(Uri.parse(url), component.client.getMatrixClient(), + doFullres: true, fullResHeight: 64); + } - bool isStickerPackCache = isStickerPack; - bool isEmojiPackCache = isEmojiPack; + return null; + } - emotes.removeWhere((element) => images.containsKey(element.key) == false); - shortcodeToEmoticon.removeWhere((key, value) => - emotes.any((element) => element.shortcode == key) == false); + @override + IconData? get icon { + return component.getDefaultIcon(); + } - for (var image in images.keys) { - var url = images[image]['url']; - if (url == null) { - continue; - } - var uri = Uri.parse(url); + MatrixEmoticonPack(this.component, this.stateKey, this.state); - var usages = images[image]['usage'] as List?; + EmoticonUsage usagesArrayToUsage(List? usages) { + if ((usages?.contains("emoticon") == true) && + (usages?.contains("sticker") == false)) { + return EmoticonUsage.emoji; + } - bool markedSticker = false; - bool markedEmoji = false; - if (usages != null) { - markedSticker = usages.contains("sticker"); - markedEmoji = usages.contains("emoticon"); - } + if ((usages?.contains("sticker") == true) && + (usages?.contains("emoticon") == false)) { + return EmoticonUsage.sticker; + } - var existing = - emotes.where((element) => element.key == image).firstOrNull; - - if (existing != null) { - existing.markAsSticker(markedSticker); - existing.markAsEmoji(markedEmoji); - existing.markPackAsEmoji(isEmojiPackCache); - existing.markPackAsSticker(isStickerPackCache); - - if (uri != existing.emojiUrl) { - existing.emojiUrl = uri; - existing.setImage( - MatrixMxcImage(uri, component.client.getMatrixClient())); - } - } else { - var emote = MatrixEmoticon(uri, component.client.getMatrixClient(), - shortcode: image, - isEmojiPack: isEmojiPackCache, - isStickerPack: isStickerPackCache, - isMarkedEmoji: markedEmoji, - isMarkedSticker: markedSticker); - emotes.add(emote); - - if (emote.shortcode != null) { - shortcodeToEmoticon[emote.shortcode!] = emote; - } - } + if ((usages?.contains("sticker") == true) && + (usages?.contains("emoticon") == true)) { + return EmoticonUsage.all; } + + return EmoticonUsage.inherit; } @override - Future addEmoticon( - {required String slug, - String? shortcode, - required Uint8List data, - String? mimeType, - bool? isEmoji, - bool? isSticker}) async { + Future addEmoticon({ + required String slug, + String? shortcode, + required Uint8List data, + String? mimeType, + EmoticonUsage? usage, + }) async { await component.createEmoticon(identifier, shortcode!, data); } - List? _getUsage() { - var info = - component.state.getState(identifier)['pack'] as Map?; - if (info == null) return null; - - var usage = info.tryGet("usage") as List?; - return usage; + @override + Future updateEmoticon( + {String? slug, + String? shortcode, + Uint8List? data, + String? mimeType, + EmoticonUsage? usage, + required Emoticon previous}) async { + await component.updateEmoticon(identifier, shortcode!, + data: data, usage: usage, previous: previous); } @override @@ -136,11 +121,6 @@ class MatrixEmoticonPack implements EmoticonPack { @override Future deleteEmoticon(Emoticon emoticon) async { await component.deleteEmoticon(identifier, emoticon.shortcode!); - emotes.remove(emoticon); - - if (emoticon.shortcode != null) { - shortcodeToEmoticon.remove(emoticon.shortcode); - } } @override @@ -154,15 +134,6 @@ class MatrixEmoticonPack implements EmoticonPack { @override String get identifier => stateKey; - @override - bool get isEmojiPack => _getUsage()?.contains("emoticon") ?? true; - - @override - bool get isGloballyAvailable => component.isGloballyAvailable(identifier); - - @override - bool get isStickerPack => _getUsage()?.contains("sticker") ?? true; - @override Future markAsGlobal(bool isGlobal) async { late Room room; @@ -181,48 +152,28 @@ class MatrixEmoticonPack implements EmoticonPack { } } - @override - Future markAsEmoji(bool isEmojiPack) async { - await component.setPackUsages(identifier, - [if (isEmojiPack) 'emoticon', if (isStickerPack) 'sticker']); + List? _emoticonUsageToArray(EmoticonUsage usage) { + final usages = switch (usage) { + EmoticonUsage.sticker => ["sticker"], + EmoticonUsage.emoji => ["emoticon"], + EmoticonUsage.all => ["sticker", "emoticon"], + EmoticonUsage.inherit => null, + }; - for (var emote in emotes) { - emote.markPackAsEmoji(isEmojiPack); - } + return usages; } @override - Future markAsSticker(bool isStickerPack) async { - await component.setPackUsages(identifier, - [if (isEmojiPack) 'emoticon', if (isStickerPack) 'sticker']); - - for (var emote in emotes) { - emote.markPackAsSticker(isStickerPack); - } + Future setPackUsage(EmoticonUsage usage) { + final usages = _emoticonUsageToArray(usage); + return component.setPackUsages(identifier, usages); } @override - Future markEmoticonAsEmoji(Emoticon emoticon, bool isEmoji) async { - await component.setEmoticonUsages(identifier, emoticon.shortcode!, - [if (isEmoji) 'emoticon', if (emoticon.isMarkedSticker) 'sticker']); - - (emoticon as MatrixEmoticon).markAsEmoji(isEmoji); - } - - @override - Future markEmoticonAsSticker(Emoticon emoticon, bool isSticker) async { - await component.setEmoticonUsages(identifier, emoticon.shortcode!, - [if (emoticon.isMarkedEmoji) 'emoticon', if (isSticker) 'sticker']); - - (emoticon as MatrixEmoticon).markAsSticker(isSticker); - } - - @override - Stream get onEmoticonAdded => emotes.onAdd; - - @override - Future renameEmoticon(Emoticon emoticon, String name) { - return component.renameEmoticon(identifier, emoticon.shortcode!, name); + Future updatePack( + {EmoticonUsage? usage, String? name, Uint8List? imageData}) { + return component.updatePack(identifier, + usage: usage, name: name, imageData: imageData); } @override @@ -255,4 +206,64 @@ class MatrixEmoticonPack implements EmoticonPack { @override String get ownerDisplayName => component.ownerDisplayName; + + @override + bool operator ==(Object other) { + if (other is! MatrixEmoticonPack) return false; + if (other.component != component) return false; + + return (other.stateKey == stateKey && + other.component.state.id == component.state.id); + } + + @override + int get hashCode => stateKey.hashCode; + + @override + EmoticonUsage get usage { + final pack = state.tryGetMap("pack"); + var usages = pack?.tryGetList("usage"); + var usage = usagesArrayToUsage(usages); + if (usage == EmoticonUsage.inherit) { + return EmoticonUsage.all; + } else { + return usage; + } + } + + @override + bool get isEmojiPack => + [EmoticonUsage.emoji, EmoticonUsage.all].contains(usage); + + @override + bool get isStickerPack => + [EmoticonUsage.sticker, EmoticonUsage.all].contains(usage); + + @override + bool get isGloballyAvailable { + late Room room; + if (component is MatrixRoomEmoticonComponent) { + room = (component as MatrixRoomEmoticonComponent).room.matrixRoom; + } else if (component is MatrixSpaceEmoticonComponent) { + room = (component as MatrixSpaceEmoticonComponent).space.matrixRoom; + } else { + return false; + } + + final data = room + .client.accountData[MatrixEmoticonComponent.globalEmoteRoomsStateKey]; + if (data == null) { + return false; + } + + final rooms = data.content.tryGetMap("rooms"); + + if (rooms == null) { + return false; + } + + final roomData = rooms.tryGetMap(room.id); + + return roomData?.containsKey(identifier) ?? false; + } } diff --git a/commet/lib/client/matrix/components/emoticon/matrix_emoticon_state_manager.dart b/commet/lib/client/matrix/components/emoticon/matrix_emoticon_state_manager.dart index 8df1f1410..bf5de5ffa 100644 --- a/commet/lib/client/matrix/components/emoticon/matrix_emoticon_state_manager.dart +++ b/commet/lib/client/matrix/components/emoticon/matrix_emoticon_state_manager.dart @@ -12,6 +12,8 @@ abstract class MatrixEmoticonStateManager { Future setState(String packKey, Map content); Stream get onStateChanged; + + String get id; } class MatrixEmoticonPersonalStateManager implements MatrixEmoticonStateManager { @@ -33,9 +35,14 @@ class MatrixEmoticonPersonalStateManager implements MatrixEmoticonStateManager { onStateChangedController.add(null); }); - mx.onSync.stream - .where((e) => e.accountData != null) - .listen((_) => onStateChangedController.add(null)); + mx.onSync.stream.where((e) => e.accountData != null).listen((update) { + if (update.accountData?.any((e) => + e.type == "im.ponies.user_emotes" || + e.type == "im.ponies.emote_rooms") == + true) { + onStateChangedController.add(null); + } + }); } @override @@ -62,6 +69,9 @@ class MatrixEmoticonPersonalStateManager implements MatrixEmoticonStateManager { ?.content ?? {}; } + + @override + String get id => client.identifier; } class MatrixEmoticonRoomStateManager implements MatrixEmoticonStateManager { @@ -119,4 +129,7 @@ class MatrixEmoticonRoomStateManager implements MatrixEmoticonStateManager { @override Stream get onStateChanged => onStateChangedController.stream; + + @override + String get id => room.id; } diff --git a/commet/lib/client/matrix/components/emoticon/matrix_room_emoticon_component.dart b/commet/lib/client/matrix/components/emoticon/matrix_room_emoticon_component.dart index 165842ff3..8a70b6011 100644 --- a/commet/lib/client/matrix/components/emoticon/matrix_room_emoticon_component.dart +++ b/commet/lib/client/matrix/components/emoticon/matrix_room_emoticon_component.dart @@ -96,8 +96,15 @@ class MatrixRoomEmoticonComponent extends MatrixEmoticonComponent mimeType = Mime.lookupType("", data: data); } + var extension = ""; + + // element web wont render images if the body doesnt have an extension + if (mimeType != null) { + extension = ".${mimeType.split("/").last}"; + } + var content = { - "body": sticker.shortcode!, + "body": sticker.shortcode! + extension, "url": sticker.emojiUrl.toString(), if (preferences.stickerCompatibilityMode) "msgtype": "m.image", if (preferences.stickerCompatibilityMode) @@ -131,7 +138,7 @@ class MatrixRoomEmoticonComponent extends MatrixEmoticonComponent .where((element) => element.containsRoom(room.identifier))) { var component = space.getComponent(); if (component != null) { - result.addAll(component.ownedPacks); + result.addAll(component.ownedPacks.where((e) => !result.contains(e))); } } @@ -145,7 +152,8 @@ class MatrixRoomEmoticonComponent extends MatrixEmoticonComponent } if (globalComponent != null) { - result.addAll(globalComponent.ownedPacks); + result + .addAll(globalComponent.ownedPacks.where((e) => !result.contains(e))); } if (includeUnicode) result.addAll(UnicodeEmojis.packs!); diff --git a/commet/lib/client/matrix/components/url_preview/matrix_url_preview_component.dart b/commet/lib/client/matrix/components/url_preview/matrix_url_preview_component.dart index 82f5ad148..5991ffab1 100644 --- a/commet/lib/client/matrix/components/url_preview/matrix_url_preview_component.dart +++ b/commet/lib/client/matrix/components/url_preview/matrix_url_preview_component.dart @@ -1,5 +1,4 @@ import 'package:commet/client/components/url_preview/url_preview_component.dart'; -import 'package:commet/client/matrix/extensions/matrix_client_extensions.dart'; import 'package:commet/client/matrix/matrix_client.dart'; import 'package:commet/client/matrix/matrix_mxc_image_provider.dart'; import 'package:commet/client/matrix/matrix_room.dart'; @@ -11,7 +10,6 @@ import 'package:commet/main.dart'; import 'package:commet/utils/mime.dart'; import 'package:flutter/widgets.dart'; import 'package:matrix/matrix.dart' as matrix; -import 'package:encrypted_url_preview/encrypted_url_preview.dart'; import 'package:matrix/matrix_api_lite.dart'; class MatrixUrlPreviewComponent implements UrlPreviewComponent { @@ -22,24 +20,8 @@ class MatrixUrlPreviewComponent implements UrlPreviewComponent { Map cache = {}; - EncryptedUrlPreview? privatePreviewGetter; - bool? serverSupportsUrlPreview; - void createPrivatePreviewGetter() { - privatePreviewGetter = EncryptedUrlPreview( - proxyServerUrl: Uri.https("telescope.commet.chat"), - publicKeyPem: """-----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+sAi8PsT4QwjV/+xXK0 -vwavZJEjkwJyFODGWkoo7qB87Y2yU6/C4csul6kQpxBFu9ID7mCavAlr93/c70Qm -sgX791W7oOSpvyeffJe5iluzaglZ/KWYo6Bc0QajKT8rLdI5vUljVMyx/nR9rIhY -PvSJhSFLC2ZyUhhTb/ZeLm0arEtGeyfo1V3nLGsJZJx12UK8E0FpKP14S7Wke9zM -e05PDCU/llEQpUgQOJI9Vnji71Fgocii76aSULhXalGjQIzBGKib5MIYlb0Zgf8k -wKkRg6IrNt5kjad4PoRKocxj3ylvuxEtMN582ni3lO4gi1uzzVvFtJBzrhNMjTPC -pQIDAQAB ------END PUBLIC KEY-----"""); - } - @override Future getPreview( Timeline timeline, TimelineEvent event) async { @@ -66,11 +48,7 @@ pQIDAQAB UrlPreviewData? data; try { - if (room.isE2EE) { - data = await getEncryptedPreviewData(mxClient, uri); - } else { - data = await fetchPreviewData(mxClient, uri); - } + data = await fetchPreviewData(mxClient, uri); } catch (_) { return null; } @@ -130,59 +108,6 @@ pQIDAQAB } } - Future getEncryptedPreviewData( - matrix.Client client, Uri url) async { - if (privatePreviewGetter == null) { - createPrivatePreviewGetter(); - } - - var key = privatePreviewGetter!.getNextKey(); - var proxyUrl = privatePreviewGetter!.getProxyUrl(url, key); - - var response = await client.request( - matrix.RequestType.GET, await getRequestPath(), - query: {"url": proxyUrl.toString()}); - - var title = response['og:title'] as String?; - var siteName = response['og:site_name'] as String?; - var imageUrl = response['og:image'] as String?; - var description = response['og:description'] as String?; - - ImageProvider? image; - - if (imageUrl != null) { - var mxcUri = Uri.parse(imageUrl); - if (mxcUri.scheme == "mxc") { - try { - var response = await client.getContentFromUri(mxcUri); - var bytes = response.data; - var decrypted = privatePreviewGetter!.decryptContent(bytes, key); - - image = Image.memory(decrypted).image; - } catch (e, t) { - Log.onError(e, t, - content: "Failed to get encrypted url preview image data"); - } - } - } - - if (title != null) - title = privatePreviewGetter!.decryptContentString(title, key); - if (siteName != null) - siteName = privatePreviewGetter!.decryptContentString(siteName, key); - - if (description != null) - description = privatePreviewGetter! - .decryptContentString(description, key) - .replaceAll("\n", " "); - - return UrlPreviewData(url, - siteName: siteName, - title: title, - description: description, - image: image); - } - Future fetchPreviewData( matrix.Client client, Uri url) async { late Map response; diff --git a/commet/lib/client/matrix/matrix_client.dart b/commet/lib/client/matrix/matrix_client.dart index d10c47003..66ab87aa7 100644 --- a/commet/lib/client/matrix/matrix_client.dart +++ b/commet/lib/client/matrix/matrix_client.dart @@ -599,7 +599,7 @@ class MatrixClient extends Client { flows["flows"].where((element) => element['type'] == "m.login.sso").forEach( (element) { - element["identity_providers"].forEach((provider) { + element["identity_providers"]?.forEach((provider) { result.add(MatrixSSOLoginFlow.fromJson(this, provider)); }); }, diff --git a/commet/lib/client/matrix/matrix_member.dart b/commet/lib/client/matrix/matrix_member.dart index 58f2410f0..678b5cf7f 100644 --- a/commet/lib/client/matrix/matrix_member.dart +++ b/commet/lib/client/matrix/matrix_member.dart @@ -9,7 +9,11 @@ class MatrixMember implements Member { @override ImageProvider? get avatar => matrixUser.avatarUrl != null - ? MatrixMxcImage(matrixUser.avatarUrl!, client) + ? MatrixMxcImage(matrixUser.avatarUrl!, client, + doThumbnail: true, + autoLoadFullRes: false, + doFullres: false, + thumbnailHeight: 86) : null; @override diff --git a/commet/lib/client/matrix/matrix_mxc_file_provider.dart b/commet/lib/client/matrix/matrix_mxc_file_provider.dart index 6108f321c..4081a6e78 100644 --- a/commet/lib/client/matrix/matrix_mxc_file_provider.dart +++ b/commet/lib/client/matrix/matrix_mxc_file_provider.dart @@ -1,9 +1,11 @@ +import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; import 'package:commet/cache/file_provider.dart'; import 'package:commet/debug/log.dart'; import 'package:commet/main.dart'; import 'package:matrix/matrix.dart' as matrix; +import 'package:http/http.dart' as http; class MxcFileProvider implements FileProvider { final Uri uri; @@ -14,6 +16,9 @@ class MxcFileProvider implements FileProvider { MxcFileProvider(this.client, this.uri, {this.event}); + StreamController fileDownloadProgress = + StreamController.broadcast(); + @override Future resolve({String? savePath}) async { var cached = await fileCache?.getFile(fileIdentifier); @@ -49,7 +54,49 @@ class MxcFileProvider implements FileProvider { } if (event != null) { - var file = await event!.downloadAndDecryptAttachment(); + var file = await event!.downloadAndDecryptAttachment( + downloadCallback: (url) async { + var request = http.Request("GET", url); + request.headers + .addAll({'authorization': 'Bearer ${client.accessToken}'}); + final response = await http.Client().send(request); + + List downloadedBytes = response.contentLength != null + ? Uint8List(response.contentLength!) + : []; + + int downloaded = 0; + if (response.statusCode != 200) { + throw Exception("Unexpected response: ${response.statusCode}"); + } + + var lastUpdatedProgress = DateTime.now(); + + var data = response.stream.listen( + (event) { + if (response.contentLength != null) { + downloadedBytes.setAll(downloaded, event); + } else { + downloadedBytes.addAll(event); + } + downloaded += event.length; + + var now = DateTime.now(); + if (now.difference(lastUpdatedProgress).inMilliseconds > 16) { + lastUpdatedProgress = now; + + fileDownloadProgress.add( + DownloadProgress(downloaded, response.contentLength ?? -1)); + } + }, + cancelOnError: true, + ).asFuture(); + + await data; + + return Uint8List.fromList(downloadedBytes); + }, + ); bytes = file.bytes; } else { try { @@ -62,4 +109,8 @@ class MxcFileProvider implements FileProvider { return bytes; } + + @override + Stream? get onProgressChanged => + fileDownloadProgress.stream; } diff --git a/commet/lib/client/matrix/matrix_mxc_image_provider.dart b/commet/lib/client/matrix/matrix_mxc_image_provider.dart index ad4bdead9..e0fa180a6 100644 --- a/commet/lib/client/matrix/matrix_mxc_image_provider.dart +++ b/commet/lib/client/matrix/matrix_mxc_image_provider.dart @@ -16,8 +16,12 @@ class MatrixMxcImage extends LODImageProvider { bool? doFullres, bool cache = true, bool autoLoadFullRes = true, + super.thumbnailHeight, + super.fullResHeight, Event? matrixEvent}) : super( + id: + "$identifier-$doThumbnail-$doFullres-$thumbnailHeight-$fullResHeight", blurhash: blurhash, loadThumbnail: (doThumbnail == null || doThumbnail == true) ? () => loadMatrixThumbnail( diff --git a/commet/lib/client/matrix/matrix_profile.dart b/commet/lib/client/matrix/matrix_profile.dart index edb07d355..9ff8a0a90 100644 --- a/commet/lib/client/matrix/matrix_profile.dart +++ b/commet/lib/client/matrix/matrix_profile.dart @@ -12,7 +12,8 @@ class MatrixProfile implements Profile { @override ImageProvider? get avatar => profile.avatarUrl != null - ? MatrixMxcImage(profile.avatarUrl!, client) + ? MatrixMxcImage(profile.avatarUrl!, client, + autoLoadFullRes: false, thumbnailHeight: 128, fullResHeight: 128) : null; @override diff --git a/commet/lib/client/matrix/matrix_room.dart b/commet/lib/client/matrix/matrix_room.dart index 7ff5aa8f4..1c28fe4c6 100644 --- a/commet/lib/client/matrix/matrix_room.dart +++ b/commet/lib/client/matrix/matrix_room.dart @@ -159,11 +159,6 @@ class MatrixRoom extends Room { _displayName = room.getLocalizedDisplayname(); _components = ComponentRegistry.getMatrixRoomComponents(client, this); - if (room.avatar != null) { - _avatar = MatrixMxcImage(room.avatar!, _matrixRoom.client, - autoLoadFullRes: false); - } - _lastStateEventTimestamp = DateTime.fromMillisecondsSinceEpoch(0); matrix.Event? latest = room.lastEvent; @@ -187,12 +182,13 @@ class MatrixRoom extends Room { Future updateAvatar() async { if (_matrixRoom.avatar != null) { _avatar = MatrixMxcImage(_matrixRoom.avatar!, _matrixRoom.client, - autoLoadFullRes: false); + thumbnailHeight: 64, fullResHeight: 128, autoLoadFullRes: false); } else if (_matrixRoom.isDirectChat) { var url = await _matrixRoom.client .getAvatarUrl(_matrixRoom.directChatMatrixID!); if (url != null) { - _avatar = MatrixMxcImage(url, _matrixRoom.client); + _avatar = MatrixMxcImage(url, _matrixRoom.client, + thumbnailHeight: 64, fullResHeight: 128, autoLoadFullRes: false); } } @@ -657,4 +653,31 @@ class MatrixRoom extends Room { final mxEvent = event as MatrixTimelineEvent; await mxEvent.event.sendAgain(); } + + @override + bool get shouldPreviewMedia { + switch (_matrixRoom.joinRules) { + case matrix.JoinRules.public: + return preferences.previewMediaInPublicRooms; + + case matrix.JoinRules.knock: + case matrix.JoinRules.invite: + case matrix.JoinRules.private: + return preferences.previewMediaInPrivateRooms; + + case matrix.JoinRules.restricted: + if (_client.spaces.any((e) => + e.visibility == RoomVisibility.public && + e.containsRoom(_matrixRoom.id))) { + // if any public space contains this room, consider the room public + // this is kind of flawed, because there could be public spaces we are not a member of + return preferences.previewMediaInPublicRooms; + } else { + return preferences.previewMediaInPrivateRooms; + } + + default: + return false; + } + } } diff --git a/commet/lib/client/matrix/matrix_space.dart b/commet/lib/client/matrix/matrix_space.dart index 7da73a440..aa4e7589b 100644 --- a/commet/lib/client/matrix/matrix_space.dart +++ b/commet/lib/client/matrix/matrix_space.dart @@ -203,6 +203,9 @@ class MatrixSpace extends Space { void updateAvatar() { var avatar = MatrixMxcImage(_matrixRoom.avatar!, _matrixClient, + doThumbnail: true, + thumbnailHeight: 128, + fullResHeight: 384, autoLoadFullRes: false); _avatar = avatar; } diff --git a/commet/lib/client/matrix/timeline_events/matrix_timeline_event_message.dart b/commet/lib/client/matrix/timeline_events/matrix_timeline_event_message.dart index 81847c4b9..e88001e02 100644 --- a/commet/lib/client/matrix/timeline_events/matrix_timeline_event_message.dart +++ b/commet/lib/client/matrix/timeline_events/matrix_timeline_event_message.dart @@ -78,14 +78,17 @@ class MatrixTimelineEventMessage extends MatrixTimelineEvent @override Widget? buildFormattedContent({Timeline? timeline}) { + final room = client.getRoom(event.roomId!)!; + var displayEvent = getDisplayEvent(timeline); bool isFormatted = displayEvent.content.tryGet("format") != null; if (isFormatted) { - return MatrixHtmlParser.parse(_getFormattedBody(timeline: timeline), mx); + return MatrixHtmlParser.parse( + _getFormattedBody(timeline: timeline), mx, room); } else { var plain = _getPlaintextBody(timeline: timeline); if (plain != "") { - return MatrixHtmlParser.parse(plain, mx); + return MatrixHtmlParser.parse(plain, mx, room); } } @@ -116,7 +119,9 @@ class MatrixTimelineEventMessage extends MatrixTimelineEvent doThumbnail: true, matrixEvent: event), MxcFileProvider(mx, event.attachmentMxcUrl!, event: event), + mimeType: event.attachmentMimetype, width: width, + fileSize: event.infoMap['size'] as int?, name: filename, height: height); } else if (Mime.videoTypes.contains(event.attachmentMimetype)) { @@ -134,6 +139,7 @@ class MatrixTimelineEventMessage extends MatrixTimelineEvent matrixEvent: event) : null, name: filename, + mimeType: event.attachmentMimetype, width: width, fileSize: event.infoMap['size'] as int?, height: height); diff --git a/commet/lib/client/matrix/timeline_events/matrix_timeline_event_mixin_reactions.dart b/commet/lib/client/matrix/timeline_events/matrix_timeline_event_mixin_reactions.dart index 887b1a109..74a03f83a 100644 --- a/commet/lib/client/matrix/timeline_events/matrix_timeline_event_mixin_reactions.dart +++ b/commet/lib/client/matrix/timeline_events/matrix_timeline_event_mixin_reactions.dart @@ -70,7 +70,9 @@ mixin MatrixTimelineEventReactions on MatrixTimelineEvent if (key.startsWith("mxc://")) { return MatrixEmoticon(Uri.parse(key), timeline.room.client, - shortcode: event.content.tryGet("shortcode") ?? ""); + shortcode: event.content.tryGet("shortcode") ?? "", + packUsage: EmoticonUsage.all, + usage: EmoticonUsage.all); } return UnicodeEmoticon(key, shortcode: content['shortcode'] as String?); diff --git a/commet/lib/client/room.dart b/commet/lib/client/room.dart index d2494caeb..112a8db6e 100644 --- a/commet/lib/client/room.dart +++ b/commet/lib/client/room.dart @@ -38,6 +38,8 @@ abstract class Room { /// Returns true if the room is secured by end to end encryption bool get isE2EE; + bool get shouldPreviewMedia; + /// Returns a color to use for the room's avatar Color get defaultColor; diff --git a/commet/lib/config/preferences.dart b/commet/lib/config/preferences.dart index 7b6e7c2c5..34ea679eb 100644 --- a/commet/lib/config/preferences.dart +++ b/commet/lib/config/preferences.dart @@ -32,7 +32,7 @@ class Preferences { static const String _pushGateway = "push_gateway"; static const String _lastDownloadLocation = "last_download_location"; static const String _stickerCompatibilityMode = "sticker_compatibility_mode"; - static const String _urlPreviewInE2EEChat = "use_url_preview_in_e2ee_chat"; + static const String _urlPreviewInE2EEChat = "enable_url_preview_in_e2ee_chat"; static const String _messageEffectsEnabled = "message_effects_enabled"; static const String _lastForegroundServiceSucceeded = "did_last_foreground_service_run_succeed"; @@ -40,6 +40,12 @@ class Preferences { static const String _usePlaceholderRoomAvatars = "use_placeholder_room_avatars"; + static const String _previewMediaInPublicRooms = + "preview_media_in_public_rooms"; + + static const String _previewMediaInPrivateRooms = + "preview_media_in_private_rooms"; + final StreamController _onSettingChanged = StreamController.broadcast(); Stream get onSettingChanged => _onSettingChanged.stream; bool isInit = false; @@ -286,4 +292,20 @@ class Preferences { await _preferences!.setBool(_usePlaceholderRoomAvatars, value); _onSettingChanged.add(null); } + + bool get previewMediaInPublicRooms => + _preferences!.getBool(_previewMediaInPublicRooms) ?? false; + + Future setMediaPreviewInPublicRooms(bool value) async { + await _preferences!.setBool(_previewMediaInPublicRooms, value); + _onSettingChanged.add(null); + } + + bool get previewMediaInPrivateRooms => + _preferences!.getBool(_previewMediaInPrivateRooms) ?? true; + + Future setMediaPreviewInPrivateRooms(bool value) async { + await _preferences!.setBool(_previewMediaInPrivateRooms, value); + _onSettingChanged.add(null); + } } diff --git a/commet/lib/ui/atoms/emoji_widget.dart b/commet/lib/ui/atoms/emoji_widget.dart index 926b4a943..1d658e173 100644 --- a/commet/lib/ui/atoms/emoji_widget.dart +++ b/commet/lib/ui/atoms/emoji_widget.dart @@ -20,6 +20,7 @@ class EmojiWidget extends StatelessWidget { child: Image( filterQuality: FilterQuality.medium, isAntiAlias: true, + fit: BoxFit.fitHeight, width: height, height: height, image: emoji.image!, diff --git a/commet/lib/ui/atoms/message_attachment.dart b/commet/lib/ui/atoms/message_attachment.dart index 7bc6d9b27..6376c857c 100644 --- a/commet/lib/ui/atoms/message_attachment.dart +++ b/commet/lib/ui/atoms/message_attachment.dart @@ -12,10 +12,10 @@ import 'package:tiamat/tiamat.dart' as tiamat; class MessageAttachment extends StatefulWidget { const MessageAttachment(this.attachment, - {super.key, this.ignorePointer = false}); + {super.key, this.ignorePointer = false, this.previewMedia = false}); final Attachment attachment; final bool ignorePointer; - + final bool previewMedia; @override State createState() => _MessageAttachmentState(); } @@ -31,15 +31,18 @@ class _MessageAttachmentState extends State { @override Widget build(BuildContext context) { - if (widget.attachment is ImageAttachment) return buildImage(); - if (widget.attachment is VideoAttachment) { - if (BuildConfig.WEB) { - return buildFile(Icons.video_file, widget.attachment.name, null); + if (widget.previewMedia) { + if (widget.attachment is ImageAttachment) return buildImage(); + if (widget.attachment is VideoAttachment) { + if (BuildConfig.WEB) { + return buildFile(Icons.video_file, widget.attachment.name, null); + } + return buildVideo(); } - return buildVideo(); } - if (widget.attachment is FileAttachment) { - var attachment = widget.attachment as FileAttachment; + + final attachment = widget.attachment; + if (attachment is FileAttachment) { return buildFile(Mime.toIcon(attachment.mimeType), attachment.name, attachment.fileSize); } @@ -62,9 +65,7 @@ class _MessageAttachmentState extends State { child: AspectRatio( aspectRatio: attachment.aspectRatio, child: InkWell( - onTap: () { - Lightbox.show(context, image: attachment.image); - }, + onTap: fullscreenAttachment, child: Image( image: attachment.image, filterQuality: FilterQuality.medium, @@ -86,7 +87,8 @@ class _MessageAttachmentState extends State { width: attachment.aspectRatio * 200, child: Panel( mainAxisSize: MainAxisSize.min, - header: attachment.name, + header: + "${attachment.name} ${attachment.fileSize != null ? "- ${TextUtils.readableFileSize(attachment.fileSize!)}" : ""}", mode: TileType.surfaceContainerLow, padding: 0, child: SizedBox( @@ -97,7 +99,7 @@ class _MessageAttachmentState extends State { child: isFullscreen ? null : VideoPlayer( - attachment.videoFile, + attachment.file, thumbnail: attachment.thumbnail, fileName: attachment.name, doThumbnail: true, @@ -109,13 +111,24 @@ class _MessageAttachmentState extends State { ); } + void fullscreenAttachment() { + if (widget.attachment is ImageAttachment) { + final attachment = widget.attachment as ImageAttachment; + Lightbox.show(context, image: attachment.image); + } + + if (widget.attachment is VideoAttachment) { + fullscreenVideo(); + } + } + void fullscreenVideo() { var attachment = (widget.attachment as VideoAttachment); setState(() { isFullscreen = true; }); Lightbox.show(context, - video: attachment.videoFile, + video: attachment.file, aspectRatio: attachment.aspectRatio, thumbnail: attachment.thumbnail, key: videoPlayerKey) @@ -164,18 +177,29 @@ class _MessageAttachmentState extends State { ], ), ), - Padding( - padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), - child: tiamat.IconButton( - size: 20, - icon: Icons.download, - onPressed: () async { - if (widget.attachment is FileAttachment) { - downloadAttachment(widget.attachment as FileAttachment); - } - }, - ), - ) + if (widget.attachment is ImageAttachment || + widget.attachment is VideoAttachment) + Padding( + padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), + child: tiamat.IconButton( + size: 20, + icon: Icons.visibility, + onPressed: fullscreenAttachment, + ), + ) + else + Padding( + padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), + child: tiamat.IconButton( + size: 20, + icon: Icons.download, + onPressed: () async { + if (widget.attachment is FileAttachment) { + downloadAttachment(widget.attachment as FileAttachment); + } + }, + ), + ) ], ), ), @@ -188,7 +212,7 @@ class _MessageAttachmentState extends State { Future downloadTask( FileAttachment attachment, String path) async { - await attachment.provider.save(path); + await attachment.file.save(path); return BackgroundTaskStatus.completed; } diff --git a/commet/lib/ui/atoms/rich_text/matrix_html_parser.dart b/commet/lib/ui/atoms/rich_text/matrix_html_parser.dart index 9a6dd948a..da51817d3 100644 --- a/commet/lib/ui/atoms/rich_text/matrix_html_parser.dart +++ b/commet/lib/ui/atoms/rich_text/matrix_html_parser.dart @@ -1,5 +1,7 @@ +import 'package:commet/client/components/emoticon/emoticon.dart'; import 'package:commet/client/matrix/components/emoticon/matrix_emoticon.dart'; import 'package:commet/client/matrix/matrix_mxc_image_provider.dart'; +import 'package:commet/client/room.dart'; import 'package:commet/ui/atoms/code_block.dart'; import 'package:commet/ui/atoms/emoji_widget.dart'; import 'package:commet/ui/atoms/rich_text/spans/link.dart'; @@ -14,20 +16,24 @@ import 'package:html/dom.dart' as dom; import 'package:matrix/matrix.dart' as matrix; import 'package:tiamat/config/style/theme_extensions.dart'; +import 'package:tiamat/tiamat.dart' as tiamat; + class MatrixHtmlParser { - static Widget parse(String text, matrix.Client client) { + static Widget parse(String text, matrix.Client client, Room room) { return MatrixHtmlState( text, client, + room, key: GlobalKey(), ); } } class MatrixHtmlState extends StatefulWidget { - const MatrixHtmlState(this.text, this.client, {super.key}); + const MatrixHtmlState(this.text, this.client, this.room, {super.key}); final String text; final matrix.Client client; + final Room room; @override State createState() => _MatrixHtmlStateState(); @@ -98,8 +104,9 @@ class _MatrixHtmlStateState extends State { bool big = shouldDoBigEmoji(document); // Making a new one of these for every message we pass might make a lot of garbage - var extension = MatrixEmoticonHtmlExtension(widget.client, big); - var imageExtension = MatrixImageExtension(widget.client); + var extension = + MatrixEmoticonHtmlExtension(widget.client, widget.room, big); + var imageExtension = MatrixImageExtension(widget.client, widget.room); var result = Html( data: widget.text, extensions: [ @@ -185,8 +192,9 @@ bool shouldDoBigEmoji(dom.Document document) { class MatrixEmoticonHtmlExtension extends HtmlExtension { final matrix.Client client; + final Room room; final bool bigEmoji; - const MatrixEmoticonHtmlExtension(this.client, this.bigEmoji); + const MatrixEmoticonHtmlExtension(this.client, this.room, this.bigEmoji); double get emojiSize => bigEmoji ? 48 : 20; @@ -212,14 +220,40 @@ class MatrixEmoticonHtmlExtension extends HtmlExtension { if (context.attributes.containsKey("src")) { uri = Uri.parse(context.attributes["src"]!); } + if (uri == null) { return TextSpan(text: context.attributes["alt"] ?? ""); } + if (room.shouldPreviewMedia == false) { + return WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Tooltip( + padding: const EdgeInsets.all(0), + decoration: const BoxDecoration( + color: Colors.transparent, + ), + richMessage: WidgetSpan( + child: EmojiWidget( + MatrixEmoticon(uri, client, + shortcode: context.attributes["alt"] ?? "", + packUsage: EmoticonUsage.all, + usage: EmoticonUsage.emoji), + height: 48, + )), + child: tiamat.Text.labelLow(context.attributes["alt"] ?? ""))); + } + return WidgetSpan( - child: EmojiWidget( - MatrixEmoticon(uri, client, shortcode: context.attributes["alt"] ?? ""), - height: emojiSize, + child: Tooltip( + message: context.attributes["alt"] ?? "", + child: EmojiWidget( + MatrixEmoticon(uri, client, + shortcode: context.attributes["alt"] ?? "", + packUsage: EmoticonUsage.all, + usage: EmoticonUsage.emoji), + height: emojiSize, + ), )); } @@ -377,7 +411,9 @@ class MatrixImageExtension extends HtmlExtension { final double defaultDimension; final matrix.Client client; - const MatrixImageExtension(this.client, {this.defaultDimension = 64}); + final Room room; + const MatrixImageExtension(this.client, this.room, + {this.defaultDimension = 64}); @override Set get supportedTags => {'img'}; @@ -385,11 +421,12 @@ class MatrixImageExtension extends HtmlExtension { @override InlineSpan build(ExtensionContext context) { final mxcUrl = Uri.tryParse(context.attributes['src'] ?? ''); + if (mxcUrl == null) { return TextSpan(text: context.attributes['alt']); } - if (mxcUrl.scheme != 'mxc') { + if (mxcUrl.scheme != 'mxc' || !room.shouldPreviewMedia) { return LinkSpan.create(mxcUrl.toString(), destination: mxcUrl, context: context.buildContext!); } diff --git a/commet/lib/ui/atoms/tiny_pill.dart b/commet/lib/ui/atoms/tiny_pill.dart index 6150013ef..71c3e11e6 100644 --- a/commet/lib/ui/atoms/tiny_pill.dart +++ b/commet/lib/ui/atoms/tiny_pill.dart @@ -3,8 +3,10 @@ import 'package:flutter/material.dart'; import 'package:tiamat/tiamat.dart' as tiamat; class TinyPill extends StatelessWidget { - const TinyPill(this.text, {super.key}); + const TinyPill(this.text, {this.background, this.foreground, super.key}); final String text; + final Color? background; + final Color? foreground; @override Widget build(BuildContext context) { @@ -13,12 +15,14 @@ class TinyPill extends StatelessWidget { child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(5), - color: Theme.of(context).colorScheme.primaryContainer), + color: + background ?? Theme.of(context).colorScheme.primaryContainer), child: Padding( padding: const EdgeInsets.all(2.0), child: tiamat.Text.tiny( text, - color: Theme.of(context).colorScheme.onPrimaryContainer, + color: + foreground ?? Theme.of(context).colorScheme.onPrimaryContainer, ), ), ), diff --git a/commet/lib/ui/molecules/account_selector.dart b/commet/lib/ui/molecules/account_selector.dart index e1b0fa2dc..e3ff21a97 100644 --- a/commet/lib/ui/molecules/account_selector.dart +++ b/commet/lib/ui/molecules/account_selector.dart @@ -4,18 +4,35 @@ import 'package:flutter/widgets.dart'; import 'package:tiamat/tiamat.dart' as tiamat; -class AccountSelector extends StatelessWidget { +class AccountSelector extends StatefulWidget { const AccountSelector(this.clients, {super.key, this.onClientSelected}); final List clients; final Function(Client client)? onClientSelected; + @override + State createState() => _AccountSelectorState(); +} + +class _AccountSelectorState extends State { + late Client selectedClient; + + @override + void initState() { + selectedClient = widget.clients.first; + super.initState(); + } + @override Widget build(BuildContext context) { return tiamat.DropdownSelector( - items: clients, + items: widget.clients, + value: selectedClient, itemHeight: 65, onItemSelected: (item) { - onClientSelected?.call(item); + setState(() { + selectedClient = item; + }); + widget.onClientSelected?.call(item); }, itemBuilder: (item) { return Padding( diff --git a/commet/lib/ui/molecules/room_timeline_widget/room_timeline_widget_view.dart b/commet/lib/ui/molecules/room_timeline_widget/room_timeline_widget_view.dart index 5016b3028..b9c086431 100644 --- a/commet/lib/ui/molecules/room_timeline_widget/room_timeline_widget_view.dart +++ b/commet/lib/ui/molecules/room_timeline_widget/room_timeline_widget_view.dart @@ -110,7 +110,7 @@ class RoomTimelineWidgetViewState extends State { timeline.onEventAdded.stream.listen(onEventAdded), timeline.onChange.stream.listen(onEventChanged), timeline.onRemove.stream.listen(onEventRemoved), - timeline.onLoadingStatusChanged.listen(onLoadingStatusChanged) + timeline.onLoadingStatusChanged.listen(onLoadingStatusChanged), ]; eventKeys = List.from( @@ -377,6 +377,8 @@ class RoomTimelineWidgetViewState extends State { setReplyingEvent: widget.setReplyingEvent, isThreadTimeline: widget.isThreadTimeline, highlightedEventId: highlightedEventId, + previewMedia: + widget.timeline.room.shouldPreviewMedia, jumpToEvent: jumpToEvent, initialIndex: timelineIndex), ); @@ -424,6 +426,8 @@ class RoomTimelineWidgetViewState extends State { setReplyingEvent: widget.setReplyingEvent, isThreadTimeline: widget.isThreadTimeline, highlightedEventId: highlightedEventId, + previewMedia: + widget.timeline.room.shouldPreviewMedia, jumpToEvent: jumpToEvent, initialIndex: timelineIndex), ); diff --git a/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_attachments.dart b/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_attachments.dart index 6036a94af..ee31de7d8 100644 --- a/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_attachments.dart +++ b/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_attachments.dart @@ -3,8 +3,11 @@ import 'package:commet/ui/atoms/message_attachment.dart'; import 'package:flutter/material.dart'; class TimelineEventViewAttachments extends StatelessWidget { - const TimelineEventViewAttachments({required this.attachments, super.key}); + const TimelineEventViewAttachments( + {required this.attachments, this.previewMedia = false, super.key}); final List attachments; + final bool previewMedia; + @override Widget build(BuildContext context) { return Wrap( @@ -14,6 +17,7 @@ class TimelineEventViewAttachments extends StatelessWidget { child: RepaintBoundary( child: MessageAttachment( e, + previewMedia: previewMedia, ), ), )) diff --git a/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_message.dart b/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_message.dart index a1286304a..b65c2da39 100644 --- a/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_message.dart +++ b/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_message.dart @@ -36,6 +36,7 @@ class TimelineEventViewMessage extends StatefulWidget { this.overrideShowSender = false, this.jumpToEvent, this.detailed = false, + this.previewMedia = false, required this.initialIndex}); final Function(String eventId)? jumpToEvent; @@ -47,6 +48,7 @@ class TimelineEventViewMessage extends StatefulWidget { final bool overrideShowSender; final bool detailed; final bool isThreadTimeline; + final bool previewMedia; @override State createState() => @@ -66,6 +68,7 @@ class _TimelineEventViewMessageState extends State GlobalKey urlPreviewsKey = GlobalKey(); Widget? formattedContent; + String? body; ImageProvider? senderAvatar; List? attachments; ImageProvider? sticker; @@ -91,7 +94,9 @@ class _TimelineEventViewMessageState extends State var room = widget.room ?? widget.timeline?.room; var client = room!.client; currentUserIdentifier = client.self!.identifier; - previewComponent = client.getComponent(); + if (widget.previewMedia) { + previewComponent = client.getComponent(); + } if (!widget.isThreadTimeline) { threadComponent = client.getComponent(); @@ -118,9 +123,18 @@ class _TimelineEventViewMessageState extends State timestamp: timestampToString(sentTime), edited: edited, attachments: attachments != null - ? TimelineEventViewAttachments(attachments: attachments!) + ? TimelineEventViewAttachments( + attachments: attachments!, + previewMedia: widget.previewMedia, + ) + : null, + sticker: sticker != null + ? TimelineEventViewSticker( + sticker!, + stickerName: body, + previewMedia: widget.previewMedia, + ) : null, - sticker: sticker != null ? TimelineEventViewSticker(sticker!) : null, inResponseTo: isInResponse && widget.timeline != null ? TimelineEventViewReply( timeline: widget.timeline!, @@ -206,6 +220,7 @@ class _TimelineEventViewMessageState extends State if (event is TimelineEventSticker) { sticker = event.stickerImage; + body = event.plainTextBody; } isInResponse = event is TimelineEventFeatureRelated && diff --git a/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_sticker.dart b/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_sticker.dart index 1f7f2797f..e8dfb076b 100644 --- a/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_sticker.dart +++ b/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_sticker.dart @@ -1,21 +1,78 @@ import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; -class TimelineEventViewSticker extends StatelessWidget { - const TimelineEventViewSticker(this.image, {super.key}); +import 'package:tiamat/tiamat.dart' as tiamat; +class TimelineEventViewSticker extends StatefulWidget { + const TimelineEventViewSticker(this.image, + {this.previewMedia = false, this.stickerName, super.key}); + final bool previewMedia; + final String? stickerName; final ImageProvider image; + @override + State createState() => + _TimelineEventViewStickerState(); +} + +class _TimelineEventViewStickerState extends State { + bool showSticker = false; + + static String get promptShowSticker => Intl.message( + "Show sticker", + name: "promptShowSticker", + desc: + "Prompt to display a sticker, shown when media previews are disabled", + ); + + @override + void initState() { + showSticker = widget.previewMedia; + super.initState(); + } + @override Widget build(BuildContext context) { return ClipRRect( borderRadius: BorderRadius.circular(10), child: SizedBox( height: 200, - child: Image( - image: image, - fit: BoxFit.cover, - filterQuality: FilterQuality.medium, - ), + child: showSticker + ? GestureDetector( + onTap: widget.previewMedia == false + ? () => setState(() { + showSticker = !showSticker; + }) + : null, + child: Image( + image: widget.image, + fit: BoxFit.cover, + filterQuality: FilterQuality.medium, + ), + ) + : SizedBox( + width: 200, + child: Material( + color: Theme.of(context).colorScheme.surfaceBright, + child: InkWell( + onTap: () => setState(() { + showSticker = !showSticker; + }), + child: Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + tiamat.Text.label(promptShowSticker), + if (widget.stickerName != null) + tiamat.Text.labelLow(widget.stickerName!) + ], + ), + )), + ), + ), + ), ), ); } diff --git a/commet/lib/ui/molecules/timeline_events/timeline_event_menu_dialog.dart b/commet/lib/ui/molecules/timeline_events/timeline_event_menu_dialog.dart index 068b0c085..656a1e6b9 100644 --- a/commet/lib/ui/molecules/timeline_events/timeline_event_menu_dialog.dart +++ b/commet/lib/ui/molecules/timeline_events/timeline_event_menu_dialog.dart @@ -56,6 +56,7 @@ class TimelineEventMenuDialog extends StatelessWidget { child: TimelineViewEntry( timeline: timeline, singleEvent: true, + previewMedia: timeline.room.shouldPreviewMedia, initialIndex: timeline.events.indexOf(event), showDetailed: true, ), diff --git a/commet/lib/ui/molecules/timeline_events/timeline_event_view_single.dart b/commet/lib/ui/molecules/timeline_events/timeline_event_view_single.dart index 111ac0476..02b9156d7 100644 --- a/commet/lib/ui/molecules/timeline_events/timeline_event_view_single.dart +++ b/commet/lib/ui/molecules/timeline_events/timeline_event_view_single.dart @@ -22,6 +22,7 @@ class TimelineEventViewSingle extends StatelessWidget { initialIndex: 0, detailed: true, initialEvent: event, + previewMedia: room.shouldPreviewMedia, ); if (type == TimelineEventWidgetDisplayType.generic) return TimelineEventViewGeneric( diff --git a/commet/lib/ui/molecules/timeline_events/timeline_view_entry.dart b/commet/lib/ui/molecules/timeline_events/timeline_view_entry.dart index 6d7d47a0e..6f711dcf5 100644 --- a/commet/lib/ui/molecules/timeline_events/timeline_view_entry.dart +++ b/commet/lib/ui/molecules/timeline_events/timeline_view_entry.dart @@ -29,6 +29,7 @@ class TimelineViewEntry extends StatefulWidget { this.showDetailed = false, this.singleEvent = false, this.isThreadTimeline = false, + this.previewMedia = false, this.highlightedEventId, super.key}); final Timeline timeline; @@ -40,6 +41,7 @@ class TimelineViewEntry extends StatefulWidget { final bool showDetailed; final bool isThreadTimeline; final String? highlightedEventId; + final bool previewMedia; // Should be true if we are showing this event on its own, and not as part of a timeline final bool singleEvent; @@ -97,6 +99,7 @@ class TimelineViewEntryState extends State void loadState(int eventIndex) { var event = widget.timeline.events[eventIndex]; redacted = widget.timeline.isEventRedacted(event); + eventId = event.eventId; status = event.status; index = eventIndex; @@ -309,6 +312,7 @@ class TimelineViewEntryState extends State detailed: widget.showDetailed || selected, overrideShowSender: widget.singleEvent, jumpToEvent: widget.jumpToEvent, + previewMedia: widget.previewMedia, initialIndex: widget.initialIndex); if (_widgetType == TimelineEventWidgetDisplayType.generic) return TimelineEventViewGeneric( diff --git a/commet/lib/ui/molecules/video_player/video_player.dart b/commet/lib/ui/molecules/video_player/video_player.dart index 639e6d6ff..72af62031 100644 --- a/commet/lib/ui/molecules/video_player/video_player.dart +++ b/commet/lib/ui/molecules/video_player/video_player.dart @@ -2,7 +2,9 @@ import 'dart:async'; import 'package:commet/cache/file_provider.dart'; import 'package:commet/config/build_config.dart'; +import 'package:commet/ui/atoms/tiny_pill.dart'; import 'package:commet/ui/molecules/video_player/video_player_implementation.dart'; +import 'package:commet/utils/text_utils.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:tiamat/tiamat.dart' as tiamat; @@ -41,6 +43,7 @@ class VideoPlayerState extends State { bool playing = false; bool inited = false; bool buffering = false; + DownloadProgress? downloadProgress; late bool showThumbnail; bool shouldShowControls = true; bool isCompleted = false; @@ -48,53 +51,58 @@ class VideoPlayerState extends State { bool updateSlider = true; Timer? uiHideTimer; - StreamSubscription? bufferingListener; - StreamSubscription? completedListener; - StreamSubscription? progressListener; + late List subscriptions; @override void initState() { showThumbnail = widget.doThumbnail; controller = widget.controller ?? VideoPlayerController(); - bufferingListener = controller.isBuffering.listen((isBuffering) { - setState(() { - buffering = isBuffering; - if (!isBuffering) { - showThumbnail = false; - } - }); - }); - - completedListener = controller.isCompleted.listen((event) { - setState(() { - isCompleted = event; - if (isCompleted) shouldShowControls = true; - }); - }); + subscriptions = [ + controller.isBuffering.listen((isBuffering) { + setState(() { + buffering = isBuffering; - progressListener = controller.onProgressed.listen((event) async { - var length = await controller.getLength(); - if (updateSlider) { + if (!isBuffering) { + showThumbnail = false; + } + }); + }), + controller.isCompleted.listen((event) { setState(() { - videoProgress = clampDouble( - event.inMilliseconds.toDouble() / - length.inMilliseconds.toDouble(), - 0, - 1); + isCompleted = event; + if (isCompleted) shouldShowControls = true; }); - } - }); + }), + controller.onDownloadProgressed.listen((event) { + setState(() { + downloadProgress = event; + }); + }), + controller.onProgressed.listen((event) async { + var length = await controller.getLength(); + if (updateSlider) { + setState(() { + videoProgress = clampDouble( + event.inMilliseconds.toDouble() / + length.inMilliseconds.toDouble(), + 0, + 1); + }); + } + }) + ]; super.initState(); } @override void dispose() { - bufferingListener?.cancel(); - completedListener?.cancel(); - progressListener?.cancel(); + for (var sub in subscriptions) { + sub.cancel(); + } + subscriptions.clear(); super.dispose(); } @@ -105,11 +113,45 @@ class VideoPlayerState extends State { children: [ if (widget.decodeFirstFrame || inited) pickPlayer(), if (showThumbnail) thumbnail(), + if (buffering) bufferingWidget(), controls() ], ); } + Widget bufferingWidget() { + double? progress; + final download = downloadProgress; + + if (download != null) { + progress = download.downloaded.toDouble() / download.total.toDouble(); + } + + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 50, + height: 50, + child: CircularProgressIndicator( + value: progress, + ), + ), + if (download != null) + Padding( + padding: const EdgeInsets.fromLTRB(0, 16, 0, 0), + child: TinyPill( + background: Theme.of(context).colorScheme.secondaryContainer, + foreground: + Theme.of(context).colorScheme.onSecondaryContainer, + "${TextUtils.readableFileSize(download.downloaded)} / ${TextUtils.readableFileSize(download.total)}"), + ) + ], + ), + ); + } + Widget thumbnail() { return widget.thumbnail != null ? Image( @@ -123,7 +165,15 @@ class VideoPlayerState extends State { Widget controls() { return GestureDetector( - onTap: showControls, + onTap: () { + if (BuildConfig.MOBILE) { + if (shouldShowControls) { + hideControls(); + } else { + showControls(); + } + } + }, child: MouseRegion( onEnter: (_) { showControls(); @@ -232,6 +282,7 @@ class VideoPlayerState extends State { setState(() { inited = true; playing = true; + shouldShowControls = false; controller.play(); if (BuildConfig.MOBILE) hideControls(); }); @@ -241,6 +292,7 @@ class VideoPlayerState extends State { setState(() { playing = true; controller.replay(); + shouldShowControls = false; if (BuildConfig.MOBILE) hideControls(); }); } diff --git a/commet/lib/ui/molecules/video_player/video_player_controller.dart b/commet/lib/ui/molecules/video_player/video_player_controller.dart index d83d64182..88a9949a7 100644 --- a/commet/lib/ui/molecules/video_player/video_player_controller.dart +++ b/commet/lib/ui/molecules/video_player/video_player_controller.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:typed_data'; +import 'package:commet/cache/file_provider.dart'; import 'package:flutter/material.dart'; class VideoPlayerController { @@ -20,6 +21,9 @@ class VideoPlayerController { final StreamController _isBuffering = StreamController.broadcast(); + final StreamController _downloadProgress = + StreamController.broadcast(); + final StreamController _isCompleted = StreamController.broadcast(); final StreamController _onProgressed = StreamController.broadcast(); @@ -30,6 +34,8 @@ class VideoPlayerController { Stream get onProgressed => _onProgressed.stream; + Stream get onDownloadProgressed => _downloadProgress.stream; + void attach( {required Future Function() pause, required Future Function() play, @@ -71,6 +77,10 @@ class VideoPlayerController { _isBuffering.add(isBuffering); } + void setBufferingProgress(DownloadProgress progress) { + _downloadProgress.add(progress); + } + void setCompleted(bool isBuffering) { _isCompleted.add(isBuffering); } diff --git a/commet/lib/ui/molecules/video_player/video_player_implementation.dart b/commet/lib/ui/molecules/video_player/video_player_implementation.dart index b08028117..ea6eb4af0 100644 --- a/commet/lib/ui/molecules/video_player/video_player_implementation.dart +++ b/commet/lib/ui/molecules/video_player/video_player_implementation.dart @@ -59,7 +59,14 @@ class _VideoPlayerImplementationState extends State { controller = VideoController(player); Future.microtask(() async { + widget.controller.setBuffering(true); + var sub = widget.videoFile.onProgressChanged?.listen((data) { + widget.controller.setBufferingProgress(data); + }); file = await widget.videoFile.resolve(); + + sub?.cancel(); + await player.open(Playlist([Media(file.toString())]), play: !widget.decodeFirstFrame); widget.controller.setBuffering(false); @@ -70,12 +77,19 @@ class _VideoPlayerImplementationState extends State { }); } + @override + void dispose() { + player.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { if (loaded) { return Video( fit: BoxFit.cover, controller: controller!, + controls: null, ); } return Container(); @@ -94,7 +108,8 @@ class _VideoPlayerImplementationState extends State { } Future replay() async { - await player.open(Playlist([Media(file.toString())])); + await player.seek(Duration.zero); + await player.play(); } Future seekTo(Duration duration) async { diff --git a/commet/lib/ui/pages/add_space_or_room/add_space_or_room_view.dart b/commet/lib/ui/pages/add_space_or_room/add_space_or_room_view.dart index 6eacd0a89..28195ffe2 100644 --- a/commet/lib/ui/pages/add_space_or_room/add_space_or_room_view.dart +++ b/commet/lib/ui/pages/add_space_or_room/add_space_or_room_view.dart @@ -275,6 +275,7 @@ class _AddSpaceOrRoomViewState extends State { child: tiamat.DropdownSelector( items: const [RoomVisibility.private, RoomVisibility.public], itemHeight: 90, + value: visibility, onItemSelected: (item) { setState(() { visibility = item; diff --git a/commet/lib/ui/pages/matrix/room_address_settings/matrix_room_address_settings_view.dart b/commet/lib/ui/pages/matrix/room_address_settings/matrix_room_address_settings_view.dart index dc496c7d0..deb9486c4 100644 --- a/commet/lib/ui/pages/matrix/room_address_settings/matrix_room_address_settings_view.dart +++ b/commet/lib/ui/pages/matrix/room_address_settings/matrix_room_address_settings_view.dart @@ -44,19 +44,11 @@ class MatrixRoomAddressSettingsView extends StatefulWidget { class _MatrixRoomAddressSettingsViewState extends State { - int? mainAliasIndex; String? errorMessage; StreamSubscription? subscription; - GlobalKey stateKey = GlobalKey(); @override void initState() { - if (widget.mainAlias != null) { - mainAliasIndex = widget.knownAliases.indexOf(widget.mainAlias!); - if (mainAliasIndex == -1) { - mainAliasIndex = null; - } - } subscription = widget.mainAliasChangedStream.listen(onMainAliasChanged); super.initState(); @@ -69,9 +61,7 @@ class _MatrixRoomAddressSettingsViewState } void onMainAliasChanged(String? value) { - stateKey.currentState?.setState(() { - stateKey.currentState?.value = value; - }); + setState(() {}); } @override @@ -219,21 +209,20 @@ class _MatrixRoomAddressSettingsViewState crossAxisAlignment: CrossAxisAlignment.start, children: [ tiamat.DropdownSelector( - key: stateKey, items: widget.knownAliases, - defaultIndex: mainAliasIndex, + value: widget.mainAlias, itemHeight: 60, hint: tiamat.Text.labelLow(widget.canChangeMainAlias ? "Select a main room address" : "This room does not have a set main alias"), - onItemSelected: (item) => widget.setMainAlias(item), + onItemSelected: (item) => widget.setMainAlias(item!), itemBuilder: (item) { return Row( children: [ Flexible( child: Padding( padding: const EdgeInsets.all(8.0), - child: tiamat.Text.label(item), + child: tiamat.Text.label(item!), ), ), if (item == widget.mainAlias) const TinyPill("Main"), diff --git a/commet/lib/ui/pages/settings/categories/account/account_emoji/account_emoji_tab.dart b/commet/lib/ui/pages/settings/categories/account/account_emoji/account_emoji_tab.dart index a8415d591..d9f17f1bf 100644 --- a/commet/lib/ui/pages/settings/categories/account/account_emoji/account_emoji_tab.dart +++ b/commet/lib/ui/pages/settings/categories/account/account_emoji/account_emoji_tab.dart @@ -62,19 +62,14 @@ class _AccountEmojiTabState extends State { // I dont love using a key here, is there a better way to do this? i dont know key: ValueKey("account_emoji_editor_key_${selectedClient!.identifier}"), children: [ - RoomEmojiPackSettingsView(component!.ownedPacks, - createNewPack: createPack, - defaultExpanded: true, - canCreatePack: component!.canCreatePack, - deleteEmoticon: deleteEmoticon, - deletePack: deletePack, - renameEmoticon: renameEmoticon, - onPackCreated: component!.onOwnedPackAdded), + RoomEmojiPackSettingsView( + component: component!, + editable: true, + ), const SizedBox( height: 5, ), - if (component!.globalPacks().isNotEmpty) - AccountEmojiView(component!.globalPacks(), component!.ownedPacks), + if (component!.globalPacks().isNotEmpty) AccountEmojiView(component!), ], ); } @@ -83,11 +78,6 @@ class _AccountEmojiTabState extends State { return component!.createEmoticonPack(name, avatarData); } - Future renameEmoticon( - EmoticonPack pack, Emoticon emoticon, String name) { - return pack.renameEmoticon(emoticon, name); - } - Future deleteEmoticon(EmoticonPack pack, Emoticon emoticon) { return pack.deleteEmoticon(emoticon); } diff --git a/commet/lib/ui/pages/settings/categories/account/account_emoji/account_emoji_view.dart b/commet/lib/ui/pages/settings/categories/account/account_emoji/account_emoji_view.dart index fe4d49479..f02c8c710 100644 --- a/commet/lib/ui/pages/settings/categories/account/account_emoji/account_emoji_view.dart +++ b/commet/lib/ui/pages/settings/categories/account/account_emoji/account_emoji_view.dart @@ -1,27 +1,44 @@ +import 'dart:async'; + import 'package:commet/client/components/emoticon/emoji_pack.dart'; +import 'package:commet/client/components/emoticon/emoticon_component.dart'; import 'package:commet/main.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:tiamat/tiamat.dart' as tiamat; class AccountEmojiView extends StatefulWidget { - const AccountEmojiView(this.globalPacks, this.personalPacks, {super.key}); - final List globalPacks; - final List personalPacks; + const AccountEmojiView(this.component, {super.key}); + final EmoticonComponent component; @override State createState() => _AccountEmojiViewState(); } class _AccountEmojiViewState extends State { + late List globalPacks; + StreamSubscription? sub; + + @override + void initState() { + sub = widget.component.onStateChanged.listen((_) => updateState()); + updateState(); + super.initState(); + } + + void updateState() { + setState(() { + globalPacks = widget.component.globalPacks(); + }); + } + @override Widget build(BuildContext context) { return Column( children: [ tiamat.Panel( - header: "Global Packs", + header: "Favorite Packs", mode: tiamat.TileType.surfaceContainerLow, - child: Column( - children: widget.globalPacks.map((e) => packSummary(e)).toList()), + child: + Column(children: globalPacks.map((e) => packSummary(e)).toList()), ), ], ); @@ -33,25 +50,42 @@ class _AccountEmojiViewState extends State { child: SizedBox( height: 40, child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - if (pack.image != null) - Image( - image: pack.image!, - filterQuality: FilterQuality.medium, - ), - Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 0, 0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - tiamat.Text.labelEmphasised(pack.displayName), - tiamat.Text.labelLow(preferences.developerMode - ? "${pack.ownerDisplayName} - (${pack.ownerId})" - : pack.ownerDisplayName), - ], - ), + Row( + children: [ + if (pack.image != null) + SizedBox( + width: 40, + height: 40, + child: Image( + image: pack.image!, + filterQuality: FilterQuality.medium, + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 0, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + tiamat.Text.labelEmphasised(pack.displayName), + tiamat.Text.labelLow(preferences.developerMode + ? "${pack.ownerDisplayName} - (${pack.ownerId})" + : pack.ownerDisplayName), + ], + ), + ), + ], ), + SizedBox( + width: 40, + height: 40, + child: tiamat.IconButton( + icon: Icons.heart_broken, + onPressed: () => pack.markAsGlobal(false), + ), + ) ], ), ), diff --git a/commet/lib/ui/pages/settings/categories/app/general_settings_page.dart b/commet/lib/ui/pages/settings/categories/app/general_settings_page.dart index e7eb977db..2de7528c9 100644 --- a/commet/lib/ui/pages/settings/categories/app/general_settings_page.dart +++ b/commet/lib/ui/pages/settings/categories/app/general_settings_page.dart @@ -31,17 +31,17 @@ class GeneralSettingsPageState extends State { args: [proxyUrl], name: "labelGifSearchDescription"); - String get labelEncryptedPreview => Intl.message( - "URL Preview in Encrypted Chats (Experimental)", + String get labelUrlPreviewInEncryptedChatTitle => Intl.message( + "URL Preview in Encrypted Chats", desc: - "Label for the toggle for enabling and disabling encrypted url preview", - name: "labelEncryptedPreview"); + "Label for the toggle for enabling and disabling use of url previews in encrypted chats", + name: "labelUrlPreviewInEncryptedChatTitle"); - String labelEncryptedPreviewDescription(proxyUrl) => Intl.message( - "Enable use of a proxy server ($proxyUrl) to get url preview in an encrypted chat. The content of these requests will be hidden from your homeserver using Commet's 'encrypted url preview'\nLearn more: https://github.com/commetchat/encrypted_url_preview", - desc: "Explains briefly how encrypted url preview works", - args: [proxyUrl], - name: "labelEncryptedPreviewDescription"); + String get labelUrlPreviewInEncryptedChatDescription => Intl.message( + "This will expose any URLs sent in your encrypted chats to your homeserver in order to fetch the preview", + desc: + "description for the toggle for enabling and disabling use of url previews in encrypted chats", + name: "labelUrlPreviewInEncryptedChatDescription"); String get labelMessageEffectsTitle => Intl.message("Message Effects", desc: @@ -53,6 +53,34 @@ class GeneralSettingsPageState extends State { desc: "Label describing what message effects are", name: "labelMessageEffectsDescription"); + String get labelMediaPreviewSettingsTitle => Intl.message("Media Preview", + desc: "Header for the settings tile for for media preview toggles", + name: "labelMediaPreviewSettingsTitle"); + + String get labelMediaPreviewPrivateRoomsToggle => Intl.message( + "Private Rooms", + desc: + "Short label for the private rooms toggle in media previews section", + name: "labelMediaPreviewPrivateRoomsToggle", + ); + + String get labelMediaPreviewPrivateRoomsToggleDescription => Intl.message( + "Toggle previewing of images, videos, stickers and urls in private chats", + desc: "Label describing toggle of media previews for private rooms", + name: "labelMediaPreviewPrivateRoomsToggleDescription"); + + String get labelMediaPreviewPublicRoomsToggle => Intl.message( + "Public Rooms", + desc: + "Short label for the private rooms toggle in media previews section", + name: "labelMediaPreviewPublicRoomsToggle", + ); + + String get labelMediaPreviewPublicRoomsToggleDescription => Intl.message( + "Toggle previewing of images, videos, stickers and urls in public chat rooms", + desc: "Label describing toggle of media previews for public rooms", + name: "labelMediaPreviewPublicRoomsToggleDescription"); + @override void initState() { enableTenor = preferences.tenorGifSearchEnabled; @@ -87,9 +115,8 @@ class GeneralSettingsPageState extends State { ), settingToggle( enableEncryptedPreview, - title: labelEncryptedPreview, - description: - labelEncryptedPreviewDescription("telescope.commet.chat"), + title: labelUrlPreviewInEncryptedChatTitle, + description: labelUrlPreviewInEncryptedChatDescription, onChanged: (value) async { setState(() { enableEncryptedPreview = value; @@ -120,6 +147,33 @@ class GeneralSettingsPageState extends State { ), ]), ), + const SizedBox( + height: 10, + ), + Panel( + header: labelMediaPreviewSettingsTitle, + mode: TileType.surfaceContainerLow, + child: Column(children: [ + settingToggle( + preferences.previewMediaInPrivateRooms, + title: labelMediaPreviewPrivateRoomsToggle, + description: labelMediaPreviewPrivateRoomsToggleDescription, + onChanged: (value) async { + await preferences.setMediaPreviewInPrivateRooms(value); + setState(() {}); + }, + ), + settingToggle( + preferences.previewMediaInPublicRooms, + title: labelMediaPreviewPublicRoomsToggle, + description: labelMediaPreviewPublicRoomsToggleDescription, + onChanged: (value) async { + await preferences.setMediaPreviewInPublicRooms(value); + setState(() {}); + }, + ), + ]), + ), ], ); } diff --git a/commet/lib/ui/pages/settings/categories/room/appearance/room_appearance_settings_view.dart b/commet/lib/ui/pages/settings/categories/room/appearance/room_appearance_settings_view.dart index 160d92f89..9ef59b110 100644 --- a/commet/lib/ui/pages/settings/categories/room/appearance/room_appearance_settings_view.dart +++ b/commet/lib/ui/pages/settings/categories/room/appearance/room_appearance_settings_view.dart @@ -1,6 +1,7 @@ import 'dart:typed_data'; import 'package:commet/ui/pages/settings/categories/account/profile/profile_edit_view.dart'; +import 'package:commet/utils/image/lod_image.dart'; import 'package:flutter/material.dart'; class RoomAppearanceSettingsView extends StatefulWidget { @@ -27,6 +28,15 @@ class RoomAppearanceSettingsView extends StatefulWidget { class _RoomAppearanceSettingsViewState extends State { + @override + void initState() { + final avatar = widget.avatar; + if (avatar is LODImageProvider) { + avatar.fetchFullRes(); + } + super.initState(); + } + @override Widget build(BuildContext context) { return ProfileEditView( diff --git a/commet/lib/ui/pages/settings/categories/room/emoji_packs/room_emoji_pack_settings_page.dart b/commet/lib/ui/pages/settings/categories/room/emoji_packs/room_emoji_pack_settings_page.dart index cc645bdef..48fbc50c8 100644 --- a/commet/lib/ui/pages/settings/categories/room/emoji_packs/room_emoji_pack_settings_page.dart +++ b/commet/lib/ui/pages/settings/categories/room/emoji_packs/room_emoji_pack_settings_page.dart @@ -1,10 +1,6 @@ -import 'dart:typed_data'; - import 'package:commet/client/client.dart'; import 'package:commet/client/components/emoticon/emoticon_component.dart'; import 'package:commet/ui/pages/settings/categories/room/emoji_packs/room_emoji_pack_settings_view.dart'; -import 'package:commet/client/components/emoticon/emoji_pack.dart'; -import 'package:commet/client/components/emoticon/emoticon.dart'; import 'package:flutter/widgets.dart'; class RoomEmojiPackSettingsPage extends StatefulWidget { @@ -28,37 +24,8 @@ class _RoomEmojiPackSettingsPageState extends State { @override Widget build(BuildContext context) { return RoomEmojiPackSettingsView( - component.ownedPacks, - createNewPack: createNewPack, - onPackCreated: component.onOwnedPackAdded, - deletePack: deletePack, - deleteEmoticon: deleteEmoticon, - canCreatePack: component.canCreatePack, - renameEmoticon: renameEmoticon, + component: component, editable: widget.room.permissions.canEditRoomEmoticons, - importPack: importPack, ); } - - Future createNewPack(String name, Uint8List? avatarData) async { - await component.createEmoticonPack(name, avatarData); - } - - Future deletePack(EmoticonPack pack) async { - await component.deleteEmoticonPack(pack); - } - - Future deleteEmoticon(EmoticonPack pack, Emoticon emoticon) async { - await pack.deleteEmoticon(emoticon); - } - - Future renameEmoticon( - EmoticonPack pack, Emoticon emoticon, String name) async { - await pack.renameEmoticon(emoticon, name); - } - - Future importPack(String name, int avatarIndex, List names, - List imageDatas) async { - component.importEmoticonPack(name, avatarIndex, names, imageDatas); - } } diff --git a/commet/lib/ui/pages/settings/categories/room/emoji_packs/room_emoji_pack_settings_view.dart b/commet/lib/ui/pages/settings/categories/room/emoji_packs/room_emoji_pack_settings_view.dart index 9923516ed..2071efdbd 100644 --- a/commet/lib/ui/pages/settings/categories/room/emoji_packs/room_emoji_pack_settings_view.dart +++ b/commet/lib/ui/pages/settings/categories/room/emoji_packs/room_emoji_pack_settings_view.dart @@ -1,65 +1,33 @@ import 'dart:async'; import 'dart:typed_data'; -import 'package:commet/ui/atoms/emoji_widget.dart'; -import 'package:commet/ui/molecules/editable_label.dart'; +import 'package:commet/client/components/emoticon/emoji_pack.dart'; +import 'package:commet/client/components/emoticon/emoticon.dart'; +import 'package:commet/client/components/emoticon/emoticon_component.dart'; import 'package:commet/ui/molecules/image_picker.dart'; import 'package:commet/ui/navigation/adaptive_dialog.dart'; import 'package:commet/ui/pages/settings/categories/room/emoji_packs/bulk_import_view.dart'; -import 'package:commet/utils/common_animation.dart'; -import 'package:commet/client/components/emoticon/emoticon.dart'; -import 'package:commet/client/components/emoticon/emoji_pack.dart'; import 'package:commet/utils/common_strings.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; -import 'package:tiamat/atoms/circle_button.dart'; import 'package:tiamat/tiamat.dart' as tiamat; import 'package:path/path.dart' as path; class RoomEmojiPackSettingsView extends StatefulWidget { - final List packs; - final Stream? onPackCreated; - final Future Function(String name, Uint8List? avatarData)? - createNewPack; - final Future Function(EmoticonPack pack)? deletePack; - final Future Function(EmoticonPack pack, Emoticon emoticon)? - deleteEmoticon; - - final Future Function( - EmoticonPack pack, Emoticon emoticon, String name)? renameEmoticon; - - final Function(String name, int avatarIndex, List names, - List imageDatas)? importPack; - + const RoomEmojiPackSettingsView( + {required this.component, this.editable = true, super.key}); + final EmoticonComponent component; final bool editable; - final bool canCreatePack; - final bool defaultExpanded; - final bool showBulkImport; - const RoomEmojiPackSettingsView(this.packs, - {this.createNewPack, - super.key, - this.onPackCreated, - this.deletePack, - this.editable = true, - this.canCreatePack = true, - this.defaultExpanded = false, - this.showBulkImport = true, - this.importPack, - this.renameEmoticon, - this.deleteEmoticon}); - @override State createState() => _RoomEmojiPackSettingsViewState(); } class _RoomEmojiPackSettingsViewState extends State { - int itemCount = 0; - final GlobalKey _listKey = GlobalKey(); - StreamSubscription? onItemAddedSubscription; - String get promptCreateEmoticonPack => Intl.message("Create pack", - name: "promptCreateEmoticonPack", - desc: "Prompt to create a new emoticon pack, for emoji or stickers"); + late List packs; + + StreamSubscription? sub; + bool canCreatePack = false; String get promptImportPack => Intl.message("Import pack", name: "promptImportPack", @@ -67,121 +35,24 @@ class _RoomEmojiPackSettingsViewState extends State { @override void initState() { - itemCount = widget.packs.length; - onItemAddedSubscription = widget.onPackCreated?.listen(onPackAdded); super.initState(); - } - @override - void dispose() { - onItemAddedSubscription?.cancel(); - super.dispose(); + sub = widget.component.onStateChanged.listen((_) => updateState()); + + updateState(); } - void onPackAdded(int index) { + void updateState() { setState(() { - _listKey.currentState?.insertItem(index); + packs = widget.component.ownedPacks; + canCreatePack = widget.component.canCreatePack; }); } @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - AnimatedList( - initialItemCount: itemCount, - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - key: _listKey, - itemBuilder: (context, index, animation) { - return SizeTransition( - sizeFactor: CommonAnimations.easeOut(animation), - child: EmojiPackEditor( - widget.packs[index], - deletePack: () => deletePack(index), - deleteEmoticon: (emoticon) => deleteEmoticon(index, emoticon), - editable: widget.editable, - initiallyExpanded: widget.defaultExpanded, - renameEmoticon: (emoticon, name) => - renameEmoticon(index, emoticon, name), - ), - ); - }, - ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (widget.editable && - widget.canCreatePack && - widget.showBulkImport) - Padding( - padding: const EdgeInsets.all(4.0), - child: Align( - alignment: Alignment.centerRight, - child: CircleButton( - radius: 20, - icon: Icons.auto_awesome_motion, - onPressed: promptBulkImport, - ), - ), - ), - if (widget.editable && widget.canCreatePack) - Padding( - padding: const EdgeInsets.all(4.0), - child: Align( - alignment: Alignment.centerRight, - child: CircleButton( - radius: 20, - icon: Icons.add, - onPressed: promptNewPack, - ), - ), - ), - ], - ) - ], - ); - } - - void deletePack(int index) { - var pack = widget.packs[index]; - widget.deletePack?.call(pack).then((_) { - setState(() { - itemCount--; - _listKey.currentState?.removeItem( - index, - (context, animation) => SizeTransition( - sizeFactor: CommonAnimations.easeOut(animation), - child: EmojiPackEditor( - pack, - ), - )); - }); - }); - } - - Future deleteEmoticon(int index, Emoticon emoticon) async { - var pack = widget.packs[index]; - await widget.deleteEmoticon?.call(pack, emoticon); - } - - Future renameEmoticon(int index, Emoticon emoticon, String name) async { - var pack = widget.packs[index]; - await widget.renameEmoticon?.call(pack, emoticon, name); - } - - void promptNewPack() async { - await AdaptiveDialog.show( - context, - title: promptCreateEmoticonPack, - builder: (context) { - return EmoticonCreator( - pack: true, - create: widget.createNewPack, - ); - }, - ); + void dispose() { + sub?.cancel; + super.dispose(); } void promptBulkImport() async { @@ -191,405 +62,272 @@ class _RoomEmojiPackSettingsViewState extends State { builder: (context) { return EmoticonBulkImportDialog( importPack: (name, avatarIndex, names, imageDatas) { - widget.importPack?.call(name, avatarIndex, names, imageDatas); + widget.component + .importEmoticonPack(name, avatarIndex, names, imageDatas); Navigator.pop(context); }, ); }, ); } -} - -class EmojiPackEditor extends StatefulWidget { - const EmojiPackEditor(this.pack, - {super.key, - this.deletePack, - this.deleteEmoticon, - this.renameEmoticon, - this.initiallyExpanded = false, - this.showDeleteButton = true, - this.editable = false}); - final EmoticonPack pack; - final Function()? deletePack; - final bool editable; - final bool initiallyExpanded; - final bool showDeleteButton; - final Future Function(Emoticon)? deleteEmoticon; - final Future Function(Emoticon, String)? renameEmoticon; - - @override - State createState() => _EmojiPackEditorState(); -} - -class _EmojiPackEditorState extends State { - final GlobalKey _listKey = GlobalKey(); - StreamSubscription? onCreate; - late int _itemCount; - late bool isPackEmoji; - late bool isPackSticker; - late bool isGlobalPack; - - String promptConfirmDeleteEmoticonPack(packName) => Intl.message( - "Are you sure you want to delete the **$packName** pack?", - args: [packName], - name: "promptConfirmDeleteEmoticonPack", - desc: - "Prompt to confirm deletion of an emoticon pack, supports markdown to emphasise the pack name"); - - String get createEmoticonDialogTitle => Intl.message("Create Emote", - name: "createEmoticonDialogTitle", - desc: - "Title of a dialog that pops up when choosing to create a new emoticon"); - - @override - void initState() { - widget.pack.onEmoticonAdded.listen(onEmojiInsert); - _itemCount = widget.pack.emotes.length; - isPackEmoji = widget.pack.isEmojiPack; - isPackSticker = widget.pack.isStickerPack; - isGlobalPack = widget.pack.isGloballyAvailable; - super.initState(); - } - - void onEmojiInsert(int index) { - setState(() { - _itemCount++; - _listKey.currentState?.insertItem(index); - }); - } - - void setIsEmojiPack(bool isEmoji) { - //if (!(isEmoji || isPackSticker)) return; - - setState(() { - isPackEmoji = isEmoji; - }); - - widget.pack.markAsEmoji(isEmoji); - } - - void setIsStickerPack(bool isSticker) { - //if (!(isSticker || isPackEmoji)) return; - - setState(() { - isPackSticker = isSticker; - }); - - widget.pack.markAsSticker(isSticker); - } - - void setIsGlobal(bool isGlobal) { - setState(() { - isGlobalPack = isGlobal; - }); - - widget.pack.markAsGlobal(isGlobal); - } - - void deleteEmoji(int index) { - var emoji = widget.pack.emotes[index]; - widget.deleteEmoticon?.call(emoji).then((value) { - setState(() { - _itemCount--; - _listKey.currentState?.removeItem( - index, - (context, animation) => SizeTransition( - sizeFactor: CommonAnimations.easeOut(animation), - child: Padding( - padding: const EdgeInsets.fromLTRB(8, 2, 0, 2), - child: EmojiEditor( - emoji, - editable: widget.editable, - ), - ), - )); - }); - }); - } - - void renameEmoji(int index, String name) { - var emoji = widget.pack.emotes[index]; - widget.renameEmoticon?.call(emoji, name); - } @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(2.0), - child: ClipRRect( - borderRadius: BorderRadius.circular(10), - child: ExpansionTile( - initiallyExpanded: widget.initiallyExpanded, - backgroundColor: Theme.of(context).colorScheme.surfaceContainerLow, - collapsedBackgroundColor: - Theme.of(context).colorScheme.surfaceContainerLow, - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - if (widget.pack.image != null || widget.pack.icon != null) - SizedBox( - width: 30, - height: 30, - child: widget.pack.image != null - ? Image( - image: widget.pack.image!, - filterQuality: FilterQuality.medium, - ) - : Icon( - widget.pack.icon!, - )), - const SizedBox( - width: 10, - ), - tiamat.Text.labelEmphasised(widget.pack.displayName), - ], - ), - Row( - children: [ - SizedBox( - width: 40, - height: 40, - child: tiamat.IconToggle( - icon: Icons.public, - size: 20, - state: isGlobalPack, - onPressed: (newState) => setIsGlobal(newState), - ), - ), - if (widget.editable) - SizedBox( - width: 40, - height: 40, - child: tiamat.IconToggle( - icon: Icons.sticky_note_2_rounded, - size: 20, - state: isPackSticker, - onPressed: (newState) => setIsStickerPack(newState), + return Column( + children: [ + Column( + children: packs + .map( + (e) => Padding( + padding: const EdgeInsets.fromLTRB(0, 2, 0, 2), + child: ExpansionTile( + collapsedBackgroundColor: + Theme.of(context).colorScheme.surfaceContainer, + backgroundColor: + Theme.of(context).colorScheme.surfaceContainer, + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + if (e.image != null) + Padding( + padding: const EdgeInsets.fromLTRB(0, 0, 8, 0), + child: SizedBox( + width: 40, + height: 40, + child: Image(image: e.image!)), + ), + tiamat.Text.label(e.displayName), + ], ), - ), - if (widget.editable) - const SizedBox( - width: 4, - ), - if (widget.editable) - SizedBox( - width: 40, - height: 40, - child: tiamat.IconToggle( - size: 20, - icon: Icons.emoji_emotions, - state: isPackEmoji, - onPressed: (newState) => setIsEmojiPack(newState), + Row( + children: [ + Row( + children: [ + if (widget.editable) + tiamat.IconButton( + size: 20, + icon: Icons.edit, + onPressed: () => AdaptiveDialog.show( + context, + builder: (context) => EmoticonCreator( + pack: e, + createPack: true, + onCreate: (name, usage, + newImageData) async { + await e.updatePack( + name: name, + usage: usage, + imageData: newImageData, + ); + return true; + }, + onDelete: () { + return widget.component + .deleteEmoticonPack(e); + }, + ))), + if (e.isEmojiPack) + Icon( + color: Theme.of(context) + .colorScheme + .secondaryContainer, + size: 20, + Icons.emoji_emotions), + if (e.isStickerPack) + Icon( + color: Theme.of(context) + .colorScheme + .secondaryContainer, + Icons.sticky_note_2_rounded), + tiamat.IconToggle( + icon: Icons.favorite, + size: 17, + state: e.isGloballyAvailable, + onPressed: (newState) async { + await e.markAsGlobal(newState); + updateState(); + }, + ), + ], + ), + ], + ) + ], + ), + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(4, 0, 4, 0), + child: EmoticonPackEditor( + pack: e, + editable: widget.editable, ), ) - ], + ], + ), + ), + ) + .toList(), + ), + if (canCreatePack) + Align( + alignment: Alignment.topRight, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + tiamat.CircleButton( + icon: Icons.auto_awesome_motion, + onPressed: promptBulkImport), + const SizedBox( + width: 10, ), + tiamat.CircleButton( + icon: Icons.add, + onPressed: () => AdaptiveDialog.show(context, + builder: (context) => EmoticonCreator( + createPack: true, + creatingNew: true, + onCreate: (name, usage, newImageData) async { + await widget.component + .createEmoticonPack(name, newImageData); + + return true; + }, + ))), ], ), - children: [ - AnimatedList( - shrinkWrap: true, - key: _listKey, - physics: const NeverScrollableScrollPhysics(), - initialItemCount: _itemCount, - itemBuilder: (context, index, animation) { - return SizeTransition( - sizeFactor: CommonAnimations.easeOut(animation), - child: Padding( - padding: const EdgeInsets.fromLTRB(8, 2, 9, 2), - child: index >= widget.pack.emotes.length - ? Container() - : EmojiEditor(widget.pack.emotes[index], - deleteEmoji: () => deleteEmoji(index), - editable: widget.editable, - setIsEmoji: (value) => widget.pack - .markEmoticonAsEmoji( - widget.pack.emotes[index], value), - setIsSticker: (value) => widget.pack - .markEmoticonAsSticker( - widget.pack.emotes[index], value), - renameEmoji: (name) => renameEmoji(index, name)), - ), - ); - }, - ), - if (widget.editable) - Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - if (widget.showDeleteButton) - tiamat.Button.danger( - text: CommonStrings.promptDelete, - onTap: () async { - var result = await AdaptiveDialog.confirmation( - context, - dangerous: true, - prompt: promptConfirmDeleteEmoticonPack( - widget.pack.displayName)); - - if (result == true) widget.deletePack?.call(); - }), - //Just putting a widget here to make the circle button stay on the right - if (!widget.showDeleteButton) const SizedBox(), - CircleButton( - radius: 20, - icon: Icons.add, - onPressed: createEmoticon, - ), - ], - ), - ) - ]), - ), + ) + ], ); } - - void createEmoticon() async { - await AdaptiveDialog.show(context, - builder: (context) => EmoticonCreator( - emoji: true, - create: (name, data) async { - await widget.pack - .addEmoticon(slug: name, shortcode: name, data: data!); - }, - ), - title: createEmoticonDialogTitle); - } } -class EmojiEditor extends StatefulWidget { - final Emoticon emoji; - const EmojiEditor(this.emoji, - {super.key, - this.deleteEmoji, - this.editable = false, - this.renameEmoji, - this.setIsEmoji, - this.setIsSticker}); - final void Function()? deleteEmoji; - final void Function(String)? renameEmoji; +class EmoticonPackEditor extends StatelessWidget { + const EmoticonPackEditor( + {required this.pack, this.editable = false, super.key}); + final EmoticonPack pack; final bool editable; - final void Function(bool)? setIsEmoji; - final void Function(bool)? setIsSticker; - - @override - State createState() => _EmojiEditorState(); -} -class _EmojiEditorState extends State { - late bool isSticker; - late bool isEmoji; - - String promptConfirmDeleteEmoticon(emoticon) => Intl.message( - "Are you sure you want to delete **$emoticon**?", - args: [emoticon], - name: "promptConfirmDeleteEmoticon", + String get createEmoticonDialogTitle => Intl.message("Create Emote", + name: "createEmoticonDialogTitle", desc: - "Prompt to confirm deletion of an emoticon pack, supports markdown to emphasise the emote name"); - - String get promptRenameEmoticon => Intl.message("Rename emote", - name: "promptRenameEmoticon", - desc: "Tooltip for button to rename emoticon"); - - @override - void initState() { - isSticker = widget.emoji.isMarkedSticker; - isEmoji = widget.emoji.isMarkedEmoji; - super.initState(); - } - - void setSticker(bool newValue) { - setState(() { - isSticker = newValue; - }); - - widget.setIsSticker?.call(newValue); - } - - void setEmoji(bool newValue) { - setState(() { - isEmoji = newValue; - }); + "Title of a dialog that pops up when choosing to create a new emoticon"); - widget.setIsEmoji?.call(newValue); - } + String get editEmoticonDialogTitle => Intl.message("Edit Emote", + name: "editEmoticonDialogTitle", + desc: + "Title of a dialog that pops up when choosing to edit an existing emoticon"); @override Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + return Column( children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - if (widget.editable) - tiamat.IconButton( - icon: Icons.remove_circle_outline, - size: 20, - onPressed: () async { - var result = await AdaptiveDialog.confirmation(context, - prompt: - promptConfirmDeleteEmoticon(widget.emoji.shortcode!), - dangerous: true); - - if (result == true) { - widget.deleteEmoji?.call(); - } - }, - ), - SizedBox( - height: 50, - width: 50, - child: EmojiWidget(widget.emoji), - ), - const SizedBox( - width: 10, - ), - widget.editable - ? EditableLabel( - initialText: widget.emoji.shortcode!, - changeTooltip: promptRenameEmoticon, - onTextConfirmed: (newText) => - widget.renameEmoji?.call(newText!), - ) - : tiamat.Text.label(widget.emoji.shortcode!), - ], + Column( + children: pack.emotes + .map((e) => Padding( + padding: const EdgeInsets.fromLTRB(0, 2, 0, 2), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Material( + child: InkWell( + onTap: !editable + ? null + : () => AdaptiveDialog.show(context, + title: editEmoticonDialogTitle, + builder: (context) => EmoticonCreator( + pack: pack, + initialEmoticon: e, + onCreate: + (name, usage, newImageData) async { + await pack.updateEmoticon( + previous: e, + shortcode: name, + usage: usage, + data: newImageData, + ); + return true; + }, + onDelete: () async { + await pack.deleteEmoticon(e); + }, + ), + dismissible: true), + child: Padding( + padding: const EdgeInsets.all(4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + SizedBox( + width: 40, + height: 40, + child: Image(image: e.image!)), + const SizedBox( + width: 10, + ), + tiamat.Text.label(e.shortcode!) + ], + ), + Row( + children: [ + if (e.isEmoji) + Icon( + color: Theme.of(context) + .colorScheme + .secondaryContainer, + size: 20, + Icons.emoji_emotions), + if (e.isSticker) + Icon( + color: Theme.of(context) + .colorScheme + .secondaryContainer, + Icons.sticky_note_2_rounded), + if (e.usage == EmoticonUsage.inherit) + Icon( + color: Theme.of(context) + .colorScheme + .secondaryContainer, + Icons.arrow_downward) + ], + ) + ], + ), + ), + ), + ), + ), + )) + .toList(), ), - if (widget.editable) - Row( - children: [ - SizedBox( - width: 40, - height: 40, - child: tiamat.IconToggle( - icon: Icons.sticky_note_2_rounded, - size: 20, - state: isSticker, - onPressed: setSticker, - ), - ), - const SizedBox( - width: 4, - ), - SizedBox( - width: 40, - height: 40, - child: tiamat.IconToggle( - size: 20, - icon: Icons.emoji_emotions, - state: isEmoji, - onPressed: setEmoji, - ), + if (editable) + Align( + alignment: Alignment.topRight, + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 0, 8), + child: tiamat.CircleButton( + icon: Icons.add, + onPressed: () => AdaptiveDialog.show(context, + title: createEmoticonDialogTitle, + builder: (context) => EmoticonCreator( + pack: pack, + creatingNew: true, + onCreate: (name, usage, newImageData) async { + await pack.addEmoticon( + slug: name, + shortcode: name, + data: newImageData!, + usage: usage); + + return true; + }, + ), + dismissible: true), ), - const SizedBox(width: 43) - ], - ), + ), + ) ], ); } @@ -597,19 +335,33 @@ class _EmojiEditorState extends State { class EmoticonCreator extends StatefulWidget { const EmoticonCreator( - {super.key, this.create, this.emoji, this.pack, this.sticker}); - final bool? pack; - final bool? sticker; - final bool? emoji; - final Future Function(String name, Uint8List? data)? create; + {this.initialEmoticon, + this.pack, + this.createPack = false, + this.onCreate, + this.onDelete, + this.creatingNew = false, + super.key}); + + final Emoticon? initialEmoticon; + final EmoticonPack? pack; + final bool createPack; + final bool creatingNew; + + final Future Function( + String name, EmoticonUsage usage, Uint8List? newImageData)? onCreate; + + final Future Function()? onDelete; @override State createState() => _EmoticonCreatorState(); } class _EmoticonCreatorState extends State { + late EmoticonUsage usage; + ImageProvider? image; + Uint8List? imageData; - ImageProvider? pickedImage; TextEditingController controller = TextEditingController(); bool loading = false; @@ -617,100 +369,185 @@ class _EmoticonCreatorState extends State { name: "promptEmoticonPackName", desc: "Prompt for the input of the name of an emoticon pack"); - String get promptEmojiName => Intl.message("Emoji name", - name: "promptEmojiName", + String get promptEmoteName => Intl.message("Emote name", + name: "promptEmoteName", desc: "Prompt for the input of the name of an emoji"); - String get promptStickerName => Intl.message("Sticker name", - name: "promptStickerName", desc: "Prompt for the input of a sticker"); - - String get promptConfirmCreateEmoticon => Intl.message("Create!", - name: "promptConfirmCreateEmoticon", + String get promptConfirmSaveEmoticon => Intl.message("Save!", + name: "promptConfirmSaveEmoticon", desc: "Prompt to confirm the creation of an Emoticon Pack, Emoji, or Sticker"); + @override + void initState() { + super.initState(); + + if (widget.createPack) { + if (widget.pack != null) { + usage = widget.pack!.usage; + controller.text = widget.pack!.displayName; + image = widget.pack!.image; + } else { + usage = EmoticonUsage.all; + } + } else { + if (widget.initialEmoticon != null) { + usage = widget.initialEmoticon!.usage; + } else { + usage = EmoticonUsage.inherit; + } + controller.text = widget.initialEmoticon?.shortcode ?? ""; + image = widget.initialEmoticon?.image; + } + } + @override Widget build(BuildContext context) { - if (loading) - return const Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox(width: 50, height: 50, child: CircularProgressIndicator()), - ], - ); - - return Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - width: 64, - height: 64, - child: ImagePicker( - size: 64, - icon: Icons.add_a_photo, - withData: true, - currentImage: pickedImage, - onImageRead: (bytes, mimeType, filepath) { - imageData = bytes; - var name = path.basename(filepath).split('.').first; - if (controller.text.isEmpty && !(widget.pack == true)) { - controller.text = name; - } - - pickedImage = Image.memory(bytes).image; - }, - ), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: ConstrainedBox( - constraints: const BoxConstraints(minWidth: 300), - child: tiamat.TextInput( - placeholder: widget.pack == true - ? promptEmoticonPackName - : widget.emoji == true - ? promptEmojiName - : promptStickerName, - controller: controller, + return Stack( + alignment: Alignment.center, + children: [ + AnimatedOpacity( + opacity: loading ? 0.5 : 1, + duration: Durations.short2, + child: IgnorePointer( + ignoring: loading, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 50, + height: 50, + child: ImagePicker( + size: 50, + icon: Icons.add_a_photo, + withData: true, + currentImage: image, + onImageRead: (bytes, mimeType, filepath) { + imageData = bytes; + var name = path.basename(filepath).split('.').first; + if (controller.text.isEmpty && !widget.createPack) { + controller.text = name; + } + + image = Image.memory(bytes).image; + }, + ), + ), + const SizedBox( + width: 4, + ), + Expanded( + child: ConstrainedBox( + constraints: const BoxConstraints(minWidth: 300), + child: tiamat.TextInput( + maxLines: 1, + placeholder: widget.createPack + ? promptEmoticonPackName + : promptEmoteName, + controller: controller, + ), + ), + ) + ], + ), + const SizedBox( + height: 4, + ), + SizedBox( + height: 40, + width: 40, + child: tiamat.DropdownSelector( + itemHeight: 40, + items: [ + EmoticonUsage.emoji, + EmoticonUsage.sticker, + EmoticonUsage.all, + if (!widget.createPack) EmoticonUsage.inherit, + ], + value: usage, + onItemSelected: (item) { + setState(() { + usage = item; + }); + }, + itemBuilder: (item) { + return Row( + children: [ + Icon(switch (item) { + EmoticonUsage.sticker => Icons.sticky_note_2, + EmoticonUsage.emoji => Icons.emoji_emotions, + EmoticonUsage.inherit => Icons.arrow_downward, + EmoticonUsage.all => Icons.star + }), + const SizedBox( + width: 8, + ), + tiamat.Text.label(switch (item) { + EmoticonUsage.sticker => "Sticker", + EmoticonUsage.emoji => "Emoji", + EmoticonUsage.inherit => "Follow Pack Settings", + EmoticonUsage.all => "Emoji & Sticker", + }) + ], + ); + }, ), ), - ), - ) - ], - ), - Padding( - padding: const EdgeInsets.fromLTRB(0, 8, 0, 8), - child: SizedBox( - height: 48, - child: tiamat.Button( - text: promptConfirmCreateEmoticon, - onTap: () { - if (controller.text.isNotEmpty) { - setState(() { - loading = true; - }); - - if (widget.create != null) { - widget.create! - .call(controller.text, imageData) - .then((value) { - Navigator.pop(context); - }); - } - } - }, + const SizedBox( + height: 4, + ), + SizedBox( + height: 48, + child: tiamat.Button( + text: promptConfirmSaveEmoticon, + onTap: () async { + if (controller.text.isNotEmpty) { + setState(() { + loading = true; + }); + + await widget.onCreate + ?.call(controller.text, usage, imageData); + + if (context.mounted) Navigator.of(context).pop(); + } + }, + ), + ), + const SizedBox( + height: 4, + ), + if (!widget.creatingNew) + tiamat.Button.danger( + text: CommonStrings.promptDelete, + onTap: () async { + final confirm = + await AdaptiveDialog.confirmation(context); + + if (confirm == true) { + setState(() { + loading = true; + }); + + await widget.onDelete?.call(); + + if (context.mounted) Navigator.of(context).pop(); + } + }, + ) + ], ), ), - ) - ], - ), + ), + ), + if (loading) const Center(child: CircularProgressIndicator()) + ], ); } } diff --git a/commet/lib/ui/pages/settings/categories/space/space_emoji_pack_settings.dart b/commet/lib/ui/pages/settings/categories/space/space_emoji_pack_settings.dart index 2a14e357d..77eacd021 100644 --- a/commet/lib/ui/pages/settings/categories/space/space_emoji_pack_settings.dart +++ b/commet/lib/ui/pages/settings/categories/space/space_emoji_pack_settings.dart @@ -1,10 +1,6 @@ -import 'dart:typed_data'; - import 'package:commet/client/client.dart'; import 'package:commet/client/components/emoticon/emoticon_component.dart'; import 'package:commet/ui/pages/settings/categories/room/emoji_packs/room_emoji_pack_settings_view.dart'; -import 'package:commet/client/components/emoticon/emoji_pack.dart'; -import 'package:commet/client/components/emoticon/emoticon.dart'; import 'package:flutter/widgets.dart'; class SpaceEmojiPackSettings extends StatefulWidget { @@ -27,40 +23,8 @@ class _SpaceEmojiPackSettingsState extends State { @override Widget build(BuildContext context) { return RoomEmojiPackSettingsView( - component.ownedPacks, - createNewPack: createNewPack, - onPackCreated: component.onOwnedPackAdded, - deletePack: deletePack, - deleteEmoticon: deleteEmoticon, - canCreatePack: component.canCreatePack, + component: component, editable: widget.space.permissions.canEditRoomEmoticons, - renameEmoticon: renameEmoticon, - importPack: importPack, - ); - } - - Future createNewPack(String name, Uint8List? avatarData) async { - await component.createEmoticonPack( - name, - avatarData, ); } - - Future deletePack(EmoticonPack pack) async { - await component.deleteEmoticonPack(pack); - } - - Future deleteEmoticon(EmoticonPack pack, Emoticon emoticon) async { - await pack.deleteEmoticon(emoticon); - } - - Future renameEmoticon( - EmoticonPack pack, Emoticon emoticon, String name) async { - await pack.renameEmoticon(emoticon, name); - } - - Future importPack(String name, int avatarIndex, List names, - List imageDatas) async { - component.importEmoticonPack(name, avatarIndex, names, imageDatas); - } } diff --git a/commet/lib/utils/download_utils.dart b/commet/lib/utils/download_utils.dart index 2645505e4..5b32460dc 100644 --- a/commet/lib/utils/download_utils.dart +++ b/commet/lib/utils/download_utils.dart @@ -9,16 +9,9 @@ class DownloadUtils { FileProvider? file; String name = "untitled"; - // this is so dumb - if (attachment is ImageAttachment) { + if (attachment is FileAttachment) { file = attachment.file; name = attachment.name; - } else if (attachment is VideoAttachment) { - file = attachment.videoFile; - name = attachment.name; - } else if (attachment is FileAttachment) { - file = attachment.provider; - name = attachment.name; } backgroundTaskManager.addTask(AsyncTask(() async { diff --git a/commet/lib/utils/emoji/unicode_emoji.dart b/commet/lib/utils/emoji/unicode_emoji.dart index 49aff1e6d..93fa3aee7 100644 --- a/commet/lib/utils/emoji/unicode_emoji.dart +++ b/commet/lib/utils/emoji/unicode_emoji.dart @@ -91,9 +91,6 @@ class UnicodeEmoticonPack implements EmoticonPack { @override String get identifier => throw UnimplementedError(); - @override - Stream get onEmoticonAdded => throw UnimplementedError(); - @override List get emotes => _emoji!; @@ -149,55 +146,16 @@ class UnicodeEmoticonPack implements EmoticonPack { } } - @override - Future addEmoticon( - {required String slug, - String? shortcode, - required Uint8List data, - String? mimeType, - bool? isEmoji, - bool? isSticker}) { - throw UnimplementedError(); - } - @override Future deleteEmoticon(Emoticon emoticon) { throw UnimplementedError(); } - @override - Future renameEmoticon(Emoticon emoticon, String name) { - throw UnimplementedError(); - } - - @override - Future markEmoticonAsEmoji(Object emoticon, bool isEmoji) { - throw UnimplementedError(); - } - - @override - Future markEmoticonAsSticker(Emoticon emoticon, bool isSticker) { - throw UnimplementedError(); - } - - @override - Future markAsEmoji(bool isEmojiPack) { - throw UnimplementedError(); - } - - @override - Future markAsSticker(bool isStickerPack) { - throw UnimplementedError(); - } - @override Future markAsGlobal(bool isGlobal) { throw UnimplementedError(); } - @override - bool get isGloballyAvailable => true; - @override List getShortcodes() { return emoji.map((e) => e.shortcode!).toList(); @@ -228,6 +186,42 @@ class UnicodeEmoticonPack implements EmoticonPack { @override String get ownerId => ""; + + @override + Future setPackUsage(EmoticonUsage usage) async {} + + @override + EmoticonUsage get usage => EmoticonUsage.emoji; + + @override + Future addEmoticon( + {required String slug, + String? shortcode, + required Uint8List data, + String? mimeType, + EmoticonUsage? usage}) { + throw UnimplementedError(); + } + + @override + Future updateEmoticon( + {String? slug, + String? shortcode, + Uint8List? data, + String? mimeType, + EmoticonUsage? usage, + required Emoticon previous}) { + throw UnimplementedError(); + } + + @override + Future updatePack( + {EmoticonUsage? usage, String? name, Uint8List? imageData}) { + throw UnimplementedError(); + } + + @override + bool get isGloballyAvailable => false; } class UnicodeEmoticon extends Emoticon { @@ -251,12 +245,6 @@ class UnicodeEmoticon extends Emoticon { @override bool get isSticker => false; - @override - bool get isMarkedEmoji => true; - - @override - bool get isMarkedSticker => false; - UnicodeEmoticon(String text, {String? shortcode}) { _shortcode = shortcode; slug = text; @@ -305,4 +293,7 @@ class UnicodeEmoticon extends Emoticon { int get hashCode { return slug.hashCode; } + + @override + EmoticonUsage get usage => EmoticonUsage.emoji; } diff --git a/commet/lib/utils/image/lod_image.dart b/commet/lib/utils/image/lod_image.dart index 9f4e43c4d..26357debb 100644 --- a/commet/lib/utils/image/lod_image.dart +++ b/commet/lib/utils/image/lod_image.dart @@ -13,18 +13,24 @@ enum LODImageType { fullres, } -class LODImageProvider extends ImageProvider { +class LODImageProvider extends ImageProvider { LODImageProvider( {this.blurhash, this.loadThumbnail, this.loadFullRes, + this.thumbnailHeight, + required this.id, + this.fullResHeight, this.autoLoadFullRes = true}); + String id; String? blurhash; String? get mimeType => completer?.mimeType; bool autoLoadFullRes; Future Function()? loadThumbnail; Future Function()? loadFullRes; LODImageCompleter? completer; + int? thumbnailHeight; + int? fullResHeight; Future hasCachedFullres() async { return false; @@ -35,25 +41,35 @@ class LODImageProvider extends ImageProvider { } @override - Future obtainKey(ImageConfiguration configuration) { - return SynchronousFuture(this); + Future obtainKey(ImageConfiguration configuration) { + return SynchronousFuture(id); } @override - ImageStreamCompleter loadImage( - LODImageProvider key, ImageDecoderCallback decode) { + void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, + String key, ImageErrorListener handleError) { + super.resolveStreamForKey(configuration, stream, key, handleError); + + completer = stream.completer as LODImageCompleter; + } + + @override + ImageStreamCompleter loadImage(String key, ImageDecoderCallback decode) { completer = LODImageCompleter( blurhash: blurhash, loadThumbnail: loadThumbnail, loadFullRes: loadFullRes, + callback: decode, hasCachedFullres: hasCachedFullres, hasCachedThumbnail: hasCachedThumbnail, + thumbnailHeight: thumbnailHeight, + fullResHeight: fullResHeight, autoLoadFullres: autoLoadFullRes); return completer!; } - void fetchFullRes() { - completer?.fetchFullRes(); + Future fetchFullRes() async { + await completer?.fetchFullRes(); } } @@ -69,32 +85,42 @@ class LODImageCompleter extends ImageStreamCompleter { FrameInfo? _nextFrame; Codec? _codec; late Duration _shownTimestamp; + ImageDecoderCallback callback; Duration? _frameDuration; bool _frameCallbackScheduled = false; bool autoLoadFullres; String? mimeType; int _framesEmitted = 0; + int? thumbnailHeight; + int? fullResHeight; double scale = 1; Timer? _timer; bool _isFullResLoading = false; LODImageCompleter( {this.blurhash, + required this.callback, this.loadThumbnail, this.loadFullRes, this.hasCachedFullres, this.hasCachedThumbnail, + this.thumbnailHeight, + this.fullResHeight, this.autoLoadFullres = true}) { loadImages(); } Future loadImages() async { - if (hasCachedFullres != null && await (hasCachedFullres!.call()) == true) { + if (loadFullRes != null && + autoLoadFullres && + hasCachedFullres != null && + await (hasCachedFullres!.call()) == true) { _loadFullRes(); return; } - if (hasCachedThumbnail != null && + if (loadThumbnail != null && + hasCachedThumbnail != null && await (hasCachedThumbnail!.call()) == true) { _loadThumbnail(); return; @@ -121,7 +147,12 @@ class LODImageCompleter extends ImageStreamCompleter { mimeType = Mime.lookupType("", data: bytes); - var codec = await instantiateImageCodec(bytes); + var codec = await callback( + await ImmutableBuffer.fromUint8List(bytes), + getTargetSize: (intrinsicWidth, intrinsicHeight) { + return TargetImageSize(height: thumbnailHeight); + }, + ); _setCodec(LODImageType.thumbnail, codec); } @@ -137,8 +168,12 @@ class LODImageCompleter extends ImageStreamCompleter { if (bytes == null) return; mimeType = Mime.lookupType("", data: bytes); - - var codec = await instantiateImageCodec(bytes); + var codec = await callback( + await ImmutableBuffer.fromUint8List(bytes), + getTargetSize: (intrinsicWidth, intrinsicHeight) { + return TargetImageSize(height: fullResHeight); + }, + ); _setCodec(LODImageType.fullres, codec); } diff --git a/commet/lib/utils/mime.dart b/commet/lib/utils/mime.dart index 7b0d1f2a1..462b6026d 100644 --- a/commet/lib/utils/mime.dart +++ b/commet/lib/utils/mime.dart @@ -24,7 +24,12 @@ class Mime { static bool isText(String mime) => mime.startsWith("text/"); - static const videoTypes = {"video/mp4", "video/mpeg", "video/webm"}; + static const videoTypes = { + "video/mp4", + "video/mpeg", + "video/webm", + "video/quicktime" + }; static const archiveTypes = { "application/x-7z-compressed", diff --git a/commet/lib/utils/shortcuts_manager.dart b/commet/lib/utils/shortcuts_manager.dart index 5c74f07a3..4ae3ab79c 100644 --- a/commet/lib/utils/shortcuts_manager.dart +++ b/commet/lib/utils/shortcuts_manager.dart @@ -1,3 +1,4 @@ +import 'dart:math'; import 'dart:ui'; import 'package:commet/client/matrix/matrix_mxc_image_provider.dart'; @@ -6,6 +7,7 @@ import 'package:commet/config/platform_utils.dart'; import 'package:commet/main.dart'; import 'package:commet/utils/custom_uri.dart'; import 'package:commet/utils/event_bus.dart'; +import 'package:commet/utils/image/lod_image.dart'; import 'package:commet/utils/image_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_shortcuts/flutter_shortcuts.dart'; @@ -126,11 +128,21 @@ class ShortcutsManager { if (imageProvider != null) { c.drawColor(Colors.transparent, BlendMode.dstATop); + + if (imageProvider is LODImageProvider) { + await imageProvider.fetchFullRes(); + } + var image = await ImageUtils.imageProviderToImage(imageProvider); + var smallestDimension = min(image.width, image.height).toDouble(); c.drawImageRect( image, - Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()), + Rect.fromCenter( + center: Offset( + image.width.toDouble() / 2, image.height.toDouble() / 2), + width: smallestDimension, + height: smallestDimension), Rect.fromCenter( center: center, width: size.width, height: size.height), Paint()..filterQuality = FilterQuality.medium); diff --git a/commet/lib/utils/text_utils.dart b/commet/lib/utils/text_utils.dart index 473bf1416..10905b103 100644 --- a/commet/lib/utils/text_utils.dart +++ b/commet/lib/utils/text_utils.dart @@ -1,5 +1,3 @@ -import 'dart:math'; - import 'package:commet/client/matrix/matrix_client.dart'; import 'package:commet/main.dart'; import 'package:commet/ui/atoms/rich_text/spans/link.dart'; @@ -170,15 +168,33 @@ class TextUtils { } static String readableFileSize(num number, {bool base1024 = true}) { - final base = base1024 ? 1024 : 1000; - if (number <= 0) return "0"; - final units = ["B", "kB", "MB", "GB", "TB"]; - int digitGroups = (log(number) / log(base)).round(); - // ignore: prefer_interpolation_to_compose_strings - return intl.NumberFormat("#,##0.#") - .format(number / pow(base, digitGroups)) + - " " + - units[digitGroups]; + const List affixes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; + const useBase1024 = true; + const int round = 2; + + // ignore: dead_code + num divider = useBase1024 ? 1024 : 1000; + + num size = number; + num runningDivider = divider; + num runningPreviousDivider = 0; + int affix = 0; + + while (size >= runningDivider && affix < affixes.length - 1) { + runningPreviousDivider = runningDivider; + runningDivider *= divider; + affix++; + } + + String result = + (runningPreviousDivider == 0 ? size : size / runningPreviousDivider) + .toStringAsFixed(round); + + //Check if the result ends with .00000 (depending on how many decimals) and remove it if found. + if (result.endsWith("0" * round)) + result = result.substring(0, result.length - round - 1); + + return "$result ${affixes[affix]}"; } static String redactSensitiveInfo(String text) { diff --git a/commet/linux/CMakeLists.txt b/commet/linux/CMakeLists.txt index f7f905298..8d3d34789 100644 --- a/commet/linux/CMakeLists.txt +++ b/commet/linux/CMakeLists.txt @@ -7,7 +7,14 @@ project(runner LANGUAGES CXX) set(BINARY_NAME "commet") # The unique GTK application identifier for this application. See: # https://wiki.gnome.org/HowDoI/ChooseApplicationID -set(APPLICATION_ID "chat.commet.commetapp") + +if ($ENV{COMMET_PROD} MATCHES "1") + set(APPLICATION_ID "chat.commet.commetapp") + message("Building in PRODUCTION mode") +else() + set(APPLICATION_ID "chat.commet.commetapp.develop") + message("Building in DEVELOP mode") +endif() # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. diff --git a/commet/pubspec.lock b/commet/pubspec.lock index 708335df8..022770e9e 100644 --- a/commet/pubspec.lock +++ b/commet/pubspec.lock @@ -70,14 +70,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" - basic_utils: - dependency: transitive - description: - name: basic_utils - sha256: "2064b21d3c41ed7654bc82cc476fd65542e04d60059b74d5eed490a4da08fc6c" - url: "https://pub.dev" - source: hosted - version: "5.7.0" blurhash_dart: dependency: transitive description: @@ -375,15 +367,6 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.3" - encrypted_url_preview: - dependency: "direct main" - description: - path: "." - ref: HEAD - resolved-ref: "38ae4d905739237277decea9c564d7b7ebf2d52e" - url: "https://github.com/commetchat/encrypted_url_preview.git" - source: git - version: "1.0.0" enhanced_enum: dependency: transitive description: diff --git a/commet/pubspec.yaml b/commet/pubspec.yaml index 6fea80dcd..56e2400d2 100644 --- a/commet/pubspec.yaml +++ b/commet/pubspec.yaml @@ -75,8 +75,7 @@ dependencies: ref: main matrix_dart_sdk_drift_db: git: https://github.com/commetchat/matrix-dart-sdk-drift-db.git - encrypted_url_preview: - git: https://github.com/commetchat/encrypted_url_preview.git + signal_sticker_api: git: https://github.com/commetchat/signal-sticker-api.git starfield: diff --git a/tiamat/lib/atoms/dropdown_selector.dart b/tiamat/lib/atoms/dropdown_selector.dart index 880c73a2e..5ad71befe 100644 --- a/tiamat/lib/atoms/dropdown_selector.dart +++ b/tiamat/lib/atoms/dropdown_selector.dart @@ -1,145 +1,23 @@ import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:flutter/material.dart'; -import 'package:tiamat/atoms/text.dart'; -import 'package:tiamat/config/config.dart'; -import 'package:widgetbook_annotation/widgetbook_annotation.dart'; -import 'package:tiamat/tiamat.dart' as tiamat; - -@UseCase(name: 'String Selector', type: DropdownSelector) -Widget wbDropdownSelector(BuildContext context) { - return tiamat.Tile.low2( - child: Padding( - padding: EdgeInsets.all(10.0), - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: EdgeInsets.all(8.0), - child: DropdownSelector( - items: ["Alpha", "Bravo", "Charlie", "Delta"], - itemBuilder: (item) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: tiamat.Text(item), - ); - }, - ), - ), - ], - ), - ), - ), - ); -} - -@UseCase(name: 'Multi Line Text', type: DropdownSelector) -Widget wbDropdownSelectorMultiLine(BuildContext context) { - return tiamat.Tile.low2( - child: Padding( - padding: EdgeInsets.all(10.0), - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: EdgeInsets.all(8.0), - child: DropdownSelector( - itemHeight: 80, - items: [loremIpsum, "Bravo", loremIpsum + " ", "Delta"], - itemBuilder: (item) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: tiamat.Text(item), - ); - }, - ), - ), - ], - ), - ), - ), - ); -} - -@UseCase(name: 'Avatar Selector', type: DropdownSelector) -Widget wbDropdownAvatarSelector(BuildContext context) { - return tiamat.Tile( - child: Padding( - padding: EdgeInsets.all(10.0), - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: EdgeInsets.all(8.0), - child: SizedBox( - child: DropdownSelector( - itemHeight: 70, - items: [ - AssetImage( - "assets/images/placeholder/generic/checker_purple.png"), - AssetImage( - "assets/images/placeholder/generic/checker_red.png"), - AssetImage( - "assets/images/placeholder/generic/checker_green.png"), - AssetImage( - "assets/images/placeholder/generic/checker_orange.png") - ], - itemBuilder: (item) { - return Row( - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: tiamat.Avatar.medium(image: item), - ), - tiamat.Text.labelEmphasised("Avatar with text") - ], - ); - }, - ), - ), - ), - ], - ), - ), - ), - ); -} - -class DropdownSelector extends StatefulWidget { +class DropdownSelector extends StatelessWidget { const DropdownSelector( {required this.items, required this.itemBuilder, this.itemHeight = 50, this.onItemSelected, - this.defaultIndex = 0, this.hint, + required this.value, super.key}); + final List items; final Widget Function(T item) itemBuilder; final void Function(T item)? onItemSelected; - final int? defaultIndex; final double itemHeight; final Widget? hint; - @override - State> createState() => DropdownSelectorState(); -} - -class DropdownSelectorState extends State> { - T? value; - - @override - void initState() { - if (widget.defaultIndex != null) { - value = widget.items[widget.defaultIndex!]; - } - super.initState(); - } + final T value; @override Widget build(BuildContext context) { @@ -156,29 +34,26 @@ class DropdownSelectorState extends State> { builder: (BuildContext context, BoxConstraints constraints) { return DropdownButtonHideUnderline( child: DropdownButton2( - menuItemStyleData: MenuItemStyleData(height: widget.itemHeight), + menuItemStyleData: MenuItemStyleData(height: itemHeight), value: value, - hint: widget.hint, + hint: hint, dropdownStyleData: DropdownStyleData( decoration: BoxDecoration( borderRadius: BorderRadius.only( bottomRight: Radius.circular(10), bottomLeft: Radius.circular(10)), color: Theme.of(context).colorScheme.surfaceContainerHigh)), - items: widget.items.map((value) { + items: items.map((value) { return DropdownMenuItem( alignment: Alignment.centerLeft, value: value, child: SizedBox( width: constraints.maxWidth - 60, - child: widget.itemBuilder(value)), + child: itemBuilder(value)), ); }).toList(), onChanged: (newValue) { - setState(() { - value = newValue!; - }); - widget.onItemSelected?.call(newValue!); + onItemSelected?.call(newValue!); }, )); }), diff --git a/tiamat/lib/atoms/icon_toggle.dart b/tiamat/lib/atoms/icon_toggle.dart index a5a5f81ae..93d555aad 100644 --- a/tiamat/lib/atoms/icon_toggle.dart +++ b/tiamat/lib/atoms/icon_toggle.dart @@ -76,8 +76,8 @@ class _IconToggleState extends State { icon: widget.icon, size: widget.size, iconColor: widget.state - ? m.Theme.of(context).colorScheme.onPrimary - : m.Theme.of(context).colorScheme.secondary, + ? m.Theme.of(context).colorScheme.primary + : m.Theme.of(context).colorScheme.onPrimary, onPressed: () => widget.onPressed?.call(!widget.state), backgroundColor: widget.backgroundColor); } diff --git a/tiamat/pubspec.lock b/tiamat/pubspec.lock index 3ebc2d3f2..8b756ad06 100644 --- a/tiamat/pubspec.lock +++ b/tiamat/pubspec.lock @@ -344,18 +344,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: @@ -392,18 +392,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.15.0" mime: dependency: transitive description: @@ -549,10 +549,10 @@ packages: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.2" timing: dependency: transitive description: @@ -581,10 +581,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "14.2.5" watcher: dependency: transitive description: