Skip to content

Commit 183b739

Browse files
committed
feat(e2ee): clarify encrypted-message placeholder when OpenPGP off or decryption fails
The placeholder previously opened the unlock dialog regardless of the OpenPGP master toggle, which led to a dead-end "Plugin OpenPGP indisponible" error when the toggle was off (registerE2EEPlugins early-returns and never loads the plugin). Now: - Toggle off: a click routes to /settings/encryption so re-enabling is an explicit user choice rather than a side effect of clicking a stale encrypted message. - Key unlocked but decryption still fails (sibling-device or rotated key — XEP-0373 has no multi-device encryption): a tooltip explains the cause so the user understands why retrying won't help.
1 parent 5684cff commit 183b739

34 files changed

Lines changed: 105 additions & 6 deletions

apps/fluux/src/components/conversation/EncryptedPlaceholder.tsx

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import { useTranslation } from 'react-i18next'
33
import { Lock, LockOpen } from 'lucide-react'
44
import { useWebKeyLocked } from '@/hooks/useWebKeyLocked'
55
import { useWebUnlockDialogStore } from '@/stores/webUnlockDialogStore'
6+
import { useEncryptionSettingsStore } from '@/stores/encryptionSettingsStore'
7+
import { useRouteSync } from '@/hooks/useRouteSync'
8+
import { Tooltip } from '@/components/Tooltip'
69

710
export interface EncryptedPlaceholderProps {
811
/**
@@ -19,22 +22,48 @@ export interface EncryptedPlaceholderProps {
1922
/**
2023
* Rendered inside a message bubble when {@link BaseMessage.encryptedPayload}
2124
* is set — meaning the SDK received an E2EE-claimed stanza but could not
22-
* decrypt it. Two visual states:
25+
* decrypt it. Three visual states:
2326
*
27+
* - **OpenPGP disabled** (toggle off in Settings): a click routes to
28+
* `/settings/encryption` so the user can re-enable explicitly.
29+
* We intentionally don't offer the unlock dialog here — turning the
30+
* toggle on republishes the key and flips the send policy, which the
31+
* user must opt back into rather than have happen as a side effect of
32+
* clicking a placeholder.
2433
* - **Locked** (`useWebKeyLocked()` is true): a click prompts for the
2534
* session passphrase. Once unlocked, the SDK's
2635
* `retryPendingDecrypts()` re-runs and the placeholder is replaced
2736
* by the real body on success.
28-
* - **Unlocked**: the cipher rejected the unlocked key (revoked
29-
* identity, wrong recipient, corrupt payload). The placeholder is
30-
* static — clicking again won't help.
37+
* - **Unlocked, decryption still failed**: the cipher rejected the
38+
* unlocked key — most often because the message was encrypted to a
39+
* sibling-device or rotated key that this browser doesn't hold
40+
* (XEP-0373 has no multi-device encryption). The placeholder is
41+
* static; a tooltip explains why clicking again won't help.
3142
*/
3243
export const EncryptedPlaceholder = memo(function EncryptedPlaceholder(
3344
_props: EncryptedPlaceholderProps,
3445
) {
3546
const { t } = useTranslation()
3647
const locked = useWebKeyLocked()
48+
const openpgpEnabled = useEncryptionSettingsStore((s) => s.openpgpEnabled)
3749
const openWebUnlockDialog = useWebUnlockDialogStore((s) => s.openWebUnlockDialog)
50+
const { navigateToSettings } = useRouteSync()
51+
52+
if (!openpgpEnabled) {
53+
return (
54+
<button
55+
type="button"
56+
onClick={() => navigateToSettings('encryption')}
57+
className="flex items-center gap-2 text-fluux-muted italic hover:text-fluux-text transition-colors cursor-pointer text-start"
58+
aria-label={t('chat.encryption.encryptedDisabled')}
59+
>
60+
<Lock className="w-3.5 h-3.5 flex-shrink-0 text-fluux-muted" aria-hidden="true" />
61+
<span className="underline underline-offset-2 decoration-dotted">
62+
{t('chat.encryption.encryptedDisabled')}
63+
</span>
64+
</button>
65+
)
66+
}
3867

3968
if (locked) {
4069
return (
@@ -53,9 +82,13 @@ export const EncryptedPlaceholder = memo(function EncryptedPlaceholder(
5382
}
5483

5584
return (
56-
<div className="flex items-center gap-2 text-fluux-muted italic">
85+
<Tooltip
86+
content={t('chat.encryption.couldNotDecryptTooltip')}
87+
position="top"
88+
className="flex items-center gap-2 text-fluux-muted italic"
89+
>
5790
<LockOpen className="w-3.5 h-3.5 flex-shrink-0" aria-hidden="true" />
5891
<span>{t('chat.encryption.encryptedCouldNotDecrypt')}</span>
59-
</div>
92+
</Tooltip>
6093
)
6194
})

apps/fluux/src/i18n/locales/ar.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,8 @@
486486
"keyLockedTooltip": "التشفير مقفل — انقر للفتح",
487487
"encryptedClickToUnlock": "رسالة مشفرة — انقر للفتح",
488488
"encryptedCouldNotDecrypt": "تعذر فك تشفير هذه الرسالة",
489+
"encryptedDisabled": "التشفير معطّل — قم بتفعيله من الإعدادات",
490+
"couldNotDecryptTooltip": "رسالة مشفّرة بمفتاح غير متوفّر على هذا الجهاز",
489491
"rejected": "الشهادة مرفوضة",
490492
"rejectedTooltip": "شهادة OpenPGP الخاصة بالطرف الآخر غير صالحة — انقر للتفاصيل",
491493
"rejectedTitle": "الشهادة مرفوضة",

apps/fluux/src/i18n/locales/be.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,8 @@
486486
"keyLockedTooltip": "Шыфраванне заблакаванае — націсніце, каб разблакаваць",
487487
"encryptedClickToUnlock": "Зашыфраванае паведамленне — націсніце для разблакоўкі",
488488
"encryptedCouldNotDecrypt": "Не атрымалася расшыфраваць гэта паведамленне",
489+
"encryptedDisabled": "Шыфраванне адключана — уключыце ў наладах",
490+
"couldNotDecryptTooltip": "Паведамленне зашыфравана ключом, недаступным на гэтай прыладзе",
489491
"rejected": "Сертыфікат адхілены",
490492
"rejectedTooltip": "Сертыфікат OpenPGP суразмоўцы несапраўдны — націсніце для падрабязнасцей",
491493
"rejectedTitle": "Сертыфікат адхілены",

apps/fluux/src/i18n/locales/bg.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,8 @@
486486
"keyLockedTooltip": "Криптирането е заключено — щракнете за отключване",
487487
"encryptedClickToUnlock": "Шифровано съобщение — щракнете за отключване",
488488
"encryptedCouldNotDecrypt": "Това съобщение не можа да бъде дешифрирано",
489+
"encryptedDisabled": "Криптирането е изключено — активирайте го в настройките",
490+
"couldNotDecryptTooltip": "Съобщението е криптирано с ключ, който не е достъпен на това устройство",
489491
"rejected": "Сертификатът е отхвърлен",
490492
"rejectedTooltip": "OpenPGP сертификатът на отсрещната страна е невалиден — щракнете за подробности",
491493
"rejectedTitle": "Сертификатът е отхвърлен",

apps/fluux/src/i18n/locales/ca.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,8 @@
488488
"keyLockedTooltip": "Xifratge bloquejat — feu clic per desbloquejar",
489489
"encryptedClickToUnlock": "Missatge xifrat — feu clic per desbloquejar",
490490
"encryptedCouldNotDecrypt": "No s'ha pogut desxifrar aquest missatge",
491+
"encryptedDisabled": "Xifratge desactivat — activeu-lo a la configuració",
492+
"couldNotDecryptTooltip": "Missatge xifrat per a una clau no disponible en aquest dispositiu",
491493
"rejected": "Certificat rebutjat",
492494
"rejectedTooltip": "El certificat OpenPGP del contacte no és vàlid — feu clic per veure'n els detalls",
493495
"rejectedTitle": "Certificat rebutjat",

apps/fluux/src/i18n/locales/cs.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,8 @@
488488
"keyLockedTooltip": "Šifrování uzamčeno — odemkněte kliknutím",
489489
"encryptedClickToUnlock": "Šifrovaná zpráva — odemkněte kliknutím",
490490
"encryptedCouldNotDecrypt": "Tuto zprávu se nepodařilo dešifrovat",
491+
"encryptedDisabled": "Šifrování je vypnuto — zapněte jej v nastavení",
492+
"couldNotDecryptTooltip": "Zpráva zašifrovaná pro klíč, který není na tomto zařízení k dispozici",
491493
"rejected": "Certifikát odmítnut",
492494
"rejectedTooltip": "OpenPGP certifikát protistrany je neplatný — klikněte pro podrobnosti",
493495
"rejectedTitle": "Certifikát odmítnut",

apps/fluux/src/i18n/locales/da.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,8 @@
486486
"keyLockedTooltip": "Kryptering låst — klik for at låse op",
487487
"encryptedClickToUnlock": "Krypteret besked — klik for at låse op",
488488
"encryptedCouldNotDecrypt": "Kunne ikke dekryptere denne besked",
489+
"encryptedDisabled": "Kryptering deaktiveret — aktivér den i indstillingerne",
490+
"couldNotDecryptTooltip": "Beskeden er krypteret til en nøgle, der ikke er tilgængelig på denne enhed",
489491
"rejected": "Certifikat afvist",
490492
"rejectedTooltip": "Modpartens OpenPGP-certifikat er ugyldigt — klik for detaljer",
491493
"rejectedTitle": "Certifikat afvist",

apps/fluux/src/i18n/locales/de.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,8 @@
488488
"keyLockedTooltip": "Verschlüsselung gesperrt — zum Entsperren klicken",
489489
"encryptedClickToUnlock": "Verschlüsselte Nachricht — zum Entsperren klicken",
490490
"encryptedCouldNotDecrypt": "Diese Nachricht konnte nicht entschlüsselt werden",
491+
"encryptedDisabled": "Verschlüsselung deaktiviert — in den Einstellungen aktivieren",
492+
"couldNotDecryptTooltip": "Nachricht für einen auf diesem Gerät nicht verfügbaren Schlüssel verschlüsselt",
491493
"rejected": "Zertifikat abgelehnt",
492494
"rejectedTooltip": "Das OpenPGP-Zertifikat des Gesprächspartners ist ungültig — klicke für Details",
493495
"rejectedTitle": "Zertifikat abgelehnt",

apps/fluux/src/i18n/locales/el.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,8 @@
487487
"keyLockedTooltip": "Η κρυπτογράφηση είναι κλειδωμένη — κάντε κλικ για ξεκλείδωμα",
488488
"encryptedClickToUnlock": "Κρυπτογραφημένο μήνυμα — κάντε κλικ για ξεκλείδωμα",
489489
"encryptedCouldNotDecrypt": "Δεν ήταν δυνατή η αποκρυπτογράφηση αυτού του μηνύματος",
490+
"encryptedDisabled": "Η κρυπτογράφηση είναι απενεργοποιημένη — ενεργοποιήστε την στις ρυθμίσεις",
491+
"couldNotDecryptTooltip": "Μήνυμα κρυπτογραφημένο για κλειδί που δεν είναι διαθέσιμο σε αυτή τη συσκευή",
490492
"rejected": "Πιστοποιητικό απορρίφθηκε",
491493
"rejectedTooltip": "Το πιστοποιητικό OpenPGP του συνομιλητή δεν είναι έγκυρο — κάντε κλικ για λεπτομέρειες",
492494
"rejectedTitle": "Πιστοποιητικό απορρίφθηκε",

apps/fluux/src/i18n/locales/en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,8 @@
485485
"keyLockedTooltip": "Encryption locked — click to unlock",
486486
"encryptedClickToUnlock": "Encrypted message — click to unlock",
487487
"encryptedCouldNotDecrypt": "Couldn't decrypt this message",
488+
"encryptedDisabled": "Encryption disabled — enable it in the settings",
489+
"couldNotDecryptTooltip": "Message encrypted to a key not available on this device",
488490
"rejected": "Certificate rejected",
489491
"rejectedTooltip": "Peer's OpenPGP certificate is invalid — click for details",
490492
"rejectedTitle": "Certificate rejected",

0 commit comments

Comments
 (0)