feat: search mail for selected text from the desktop app's right-click menu

This commit is contained in:
Linus Rath
2026-11-04 06:21:06 +01:00
parent acb827bf18
commit 0823a531fa
33 changed files with 145 additions and 0 deletions
@@ -220,6 +220,42 @@ describe('DesktopHost', () => {
});
});
// mail-25: "Search mail for '…'" on selected text in the native menu.
describe('Search mail for selected text', () => {
const searchFor = (text: string) => `mail.searchText:${JSON.stringify(text)}`;
it('runs on the mail page', async () => {
const shell = fakeBridge();
render(<DesktopHost />);
const heard: unknown[] = [];
const listen = (e: Event) => { e.preventDefault(); heard.push((e as CustomEvent).detail); };
window.addEventListener('bulwark:mail-search', listen);
try {
shell.run(searchFor(' quarterly report \n'));
} finally {
window.removeEventListener('bulwark:mail-search', listen);
}
expect(heard).toEqual([{ text: 'quarterly report' }]);
expect(push).not.toHaveBeenCalled();
});
it('goes to Mail first from another app', async () => {
const shell = fakeBridge();
render(<DesktopHost />);
shell.run(searchFor('report'));
expect(push).toHaveBeenCalledWith('/');
});
it('is on offer to the shell when signed in, with its label in the interface language', async () => {
const shell = fakeBridge();
render(<DesktopHost />);
await waitFor(() => expect(shell.setState).toHaveBeenCalled());
expect(shell.setState.mock.lastCall![0]).toMatchObject({ 'mail.searchText': true });
await waitFor(() => expect(shell.setLanguages).toHaveBeenCalled());
expect(shell.setLanguages.mock.lastCall![0].labels.contextSearchMail).toBe('desktop_mail.context_search_mail');
});
});
it('hands the shell its menus in the interface language', async () => {
const shell = fakeBridge();
act(() => useLocaleStore.setState({ locale: 'fr' }));
+18
View File
@@ -19,6 +19,7 @@ import { useOfflineStore } from "@/lib/offline/state";
import { openNewComposerWindow } from "@/lib/popout/open";
import { onReturnedComposerElsewhere } from "@/lib/popout/return-to-main";
import { isPopoutWindow } from "@/lib/popout/window-role";
import { parkMailSearch, requestMailSearch } from "@/lib/mail-search-request";
import { onBeforeSignOut, onFullSignOut } from "@/lib/sign-out-cleanup";
import { useAuthStore } from "@/stores/auth-store";
import { useEmailStore } from "@/stores/email-store";
@@ -89,6 +90,22 @@ export function DesktopHost() {
}
return;
}
// Search mail for the text selected under a right-click.
if (id.startsWith("mail.searchText:")) {
try {
const text = JSON.parse(id.slice("mail.searchText:".length)) as unknown;
if (typeof text === "string" && text.trim()) {
const request = { text: text.trim() };
if (!requestMailSearch(request)) {
parkMailSearch(request);
routerRef.current.push("/");
}
}
} catch {
// Only the shell sends this, and it sends JSON.
}
return;
}
if (!runDesktopCommand(id)) fallback[id]?.();
});
}, []);
@@ -101,6 +118,7 @@ export function DesktopHost() {
const popout = isPopoutWindow();
setDesktopBaseState({
"link.check": true,
"mail.searchText": signedIn && !popout,
"shortcuts.singleKeys": singleKeys,
"policy.download": canDownloadAnywhere(),
"file.print": canPrint,
+1
View File
@@ -139,6 +139,7 @@ export function shellLabels(t: (key: string, values?: Record<string, string>) =>
contextCopyAddress: t('desktopUi.context.copy_address'),
contextSaveImage: t('desktopUi.context.save_image'),
contextCopyImage: t('desktopUi.context.copy_image'),
contextSearchMail: t('desktop_mail.context_search_mail', { text: '{text}' }),
unsavedTitle: t('desktopUi.closing.unsaved_title'),
unsavedDetail: t('desktopUi.closing.unsaved_detail'),
closeAnyway: t('desktopUi.closing.close_anyway'),
+3
View File
@@ -6810,6 +6810,9 @@
"close": "إغلاق"
}
},
"desktop_mail": {
"context_search_mail": "البحث في البريد عن «{text}»"
},
"appLock": {
"section_title": "قفل التطبيق",
"section_desc": "يطلب رمز PIN بعد فترة دون استخدام، في كل نافذة وعلامة تبويب. أثناء القفل يبقى البريد على هذا الجهاز مشفرًا ولا يملك التطبيق مفتاحه.",
+3
View File
@@ -6810,6 +6810,9 @@
"close": "Tanca"
}
},
"desktop_mail": {
"context_search_mail": "Cerca «{text}» al correu"
},
"appLock": {
"section_title": "Bloqueig de l'app",
"section_desc": "Demana un PIN després d'un temps sense ús, a cada finestra i pestanya. Mentre està bloquejada, el correu d'aquest dispositiu continua xifrat i l'app no en té la clau.",
+3
View File
@@ -6810,6 +6810,9 @@
"close": "Zavřít"
}
},
"desktop_mail": {
"context_search_mail": "Hledat v poště „{text}“"
},
"appLock": {
"section_title": "Zámek aplikace",
"section_desc": "Po čase bez použití žádá o PIN, v každém okně a na každé kartě. Dokud je zamčeno, zůstává pošta na tomto zařízení zašifrovaná a aplikace k ní nemá klíč.",
+3
View File
@@ -6810,6 +6810,9 @@
"close": "Luk"
}
},
"desktop_mail": {
"context_search_mail": "Søg efter “{text}” i mail"
},
"appLock": {
"section_title": "Applås",
"section_desc": "Beder om en pinkode efter en tid uden brug, i alle vinduer og faner. Mens den er låst, forbliver mail på denne enhed krypteret, og appen har ingen nøgle til den.",
+3
View File
@@ -6810,6 +6810,9 @@
"close": "Schließen"
}
},
"desktop_mail": {
"context_search_mail": "E-Mails nach „{text}“ durchsuchen"
},
"appLock": {
"section_title": "App-Sperre",
"section_desc": "Fragt nach einer Zeit ohne Nutzung in jedem Fenster und Tab nach einer PIN. Solange gesperrt ist, bleiben E-Mails auf diesem Gerät verschlüsselt und die App hat keinen Schlüssel dafür.",
+3
View File
@@ -6810,6 +6810,9 @@
"close": "Close"
}
},
"desktop_mail": {
"context_search_mail": "Search mail for \"{text}\""
},
"appLock": {
"section_title": "App lock",
"section_desc": "Asks for a PIN after a time without use, in every window and tab. While it's locked, mail on this device stays encrypted and the app has no key for it.",
+3
View File
@@ -6810,6 +6810,9 @@
"close": "Cerrar"
}
},
"desktop_mail": {
"context_search_mail": "Buscar «{text}» en el correo"
},
"appLock": {
"section_title": "Bloqueo de la app",
"section_desc": "Pide un PIN tras un tiempo sin uso, en cada ventana y pestaña. Mientras está bloqueada, el correo de este dispositivo sigue cifrado y la app no tiene la clave.",
+3
View File
@@ -6810,6 +6810,9 @@
"close": "Sulge"
}
},
"desktop_mail": {
"context_search_mail": "Otsi kirjadest „{text}”"
},
"appLock": {
"section_title": "Rakenduse lukk",
"section_desc": "Küsib pärast kasutamata aega PIN-koodi, igas aknas ja vahekaardil. Lukus olles jääb selle seadme e-post krüpteerituks ja rakendusel pole selle võtit.",
+3
View File
@@ -6810,6 +6810,9 @@
"close": "بستن"
}
},
"desktop_mail": {
"context_search_mail": "جستجوی «{text}» در نامه‌ها"
},
"appLock": {
"section_title": "قفل برنامه",
"section_desc": "پس از مدتی بی‌استفاده، در هر پنجره و زبانه پین می‌خواهد. تا وقتی قفل است، ایمیل‌های این دستگاه رمزگذاری‌شده می‌مانند و برنامه کلید آن‌ها را ندارد.",
+3
View File
@@ -6810,6 +6810,9 @@
"close": "Fermer"
}
},
"desktop_mail": {
"context_search_mail": "Rechercher « {text} » dans les messages"
},
"appLock": {
"section_title": "Verrouillage de l'app",
"section_desc": "Demande un code PIN après un temps sans utilisation, dans chaque fenêtre et onglet. Tant que l'app est verrouillée, les e-mails sur cet appareil restent chiffrés et l'app n'a pas de clé pour les lire.",
+3
View File
@@ -6811,6 +6811,9 @@
"close": "סגירה"
}
},
"desktop_mail": {
"context_search_mail": "חיפוש “{text}” בדואר"
},
"appLock": {
"section_title": "נעילת אפליקציה",
"section_desc": "מבקשת קוד PIN אחרי זמן ללא שימוש, בכל חלון וכרטיסייה. בזמן הנעילה הדואר במכשיר הזה נשאר מוצפן ולאפליקציה אין מפתח אליו.",
+3
View File
@@ -6810,6 +6810,9 @@
"close": "Bezárás"
}
},
"desktop_mail": {
"context_search_mail": "„{text}” keresése a levelek között"
},
"appLock": {
"section_title": "Alkalmazászár",
"section_desc": "Használat nélküli idő után PIN-kódot kér, minden ablakban és lapon. Amíg zárolva van, az eszközön lévő levelek titkosítva maradnak, és az alkalmazásnak nincs hozzájuk kulcsa.",
+3
View File
@@ -6810,6 +6810,9 @@
"close": "Chiudi"
}
},
"desktop_mail": {
"context_search_mail": "Cerca «{text}» nella posta"
},
"appLock": {
"section_title": "Blocco dell'app",
"section_desc": "Chiede un PIN dopo un periodo senza utilizzo, in ogni finestra e scheda. Finché è bloccata, la posta su questo dispositivo resta cifrata e l'app non ha la chiave.",
+3
View File
@@ -6810,6 +6810,9 @@
"close": "閉じる"
}
},
"desktop_mail": {
"context_search_mail": "メールで「{text}」を検索"
},
"appLock": {
"section_title": "アプリのロック",
"section_desc": "一定時間使わないと、すべてのウィンドウとタブで PIN を求めます。ロック中、このデバイスのメールは暗号化されたままで、アプリには鍵がありません。",
+3
View File
@@ -6810,6 +6810,9 @@
"close": "닫기"
}
},
"desktop_mail": {
"context_search_mail": "메일에서 “{text}” 검색"
},
"appLock": {
"section_title": "앱 잠금",
"section_desc": "일정 시간 사용하지 않으면 모든 창과 탭에서 PIN을 요청합니다. 잠겨 있는 동안 이 기기의 메일은 암호화된 상태로 유지되며 앱에는 키가 없습니다.",
+3
View File
@@ -6810,6 +6810,9 @@
"close": "Aizvērt"
}
},
"desktop_mail": {
"context_search_mail": "Meklēt pastā “{text}”"
},
"appLock": {
"section_title": "Lietotnes bloķēšana",
"section_desc": "Pēc laika bez lietošanas prasa PIN kodu katrā logā un cilnē. Kamēr lietotne ir bloķēta, e-pasts šajā ierīcē paliek šifrēts, un lietotnei nav tā atslēgas.",
+3
View File
@@ -6810,6 +6810,9 @@
"close": "Хаах"
}
},
"desktop_mail": {
"context_search_mail": "Захидлаас “{text}”-г хайх"
},
"appLock": {
"section_title": "Аппын түгжээ",
"section_desc": "Хэсэг хугацаанд ашиглаагүй бол цонх, таб бүрт ПИН код асууна. Түгжээтэй үед энэ төхөөрөмжийн имэйл шифрлэгдсэн хэвээр байх бөгөөд аппад түлхүүр нь байхгүй.",
+3
View File
@@ -6810,6 +6810,9 @@
"close": "Lukk"
}
},
"desktop_mail": {
"context_search_mail": "Søk etter «{text}» i e-post"
},
"appLock": {
"section_title": "Applås",
"section_desc": "Ber om en PIN-kode etter en tid uten bruk, i alle vinduer og faner. Mens den er låst, forblir e-post på denne enheten kryptert, og appen har ingen nøkkel til den.",
+3
View File
@@ -6810,6 +6810,9 @@
"close": "Sluit"
}
},
"desktop_mail": {
"context_search_mail": "Zoek ‘{text}’ in e-mail"
},
"appLock": {
"section_title": "App-vergrendeling",
"section_desc": "Vraagt na een tijd zonder gebruik om een pincode, in elk venster en tabblad. Zolang de app vergrendeld is, blijft de mail op dit apparaat versleuteld en heeft de app er geen sleutel voor.",
+3
View File
@@ -6810,6 +6810,9 @@
"close": "Zamknij"
}
},
"desktop_mail": {
"context_search_mail": "Szukaj „{text}” w poczcie"
},
"appLock": {
"section_title": "Blokada aplikacji",
"section_desc": "Prosi o PIN po czasie bezczynności, w każdym oknie i na każdej karcie. Gdy aplikacja jest zablokowana, poczta na tym urządzeniu pozostaje zaszyfrowana, a aplikacja nie ma do niej klucza.",
+3
View File
@@ -6810,6 +6810,9 @@
"close": "Fechar"
}
},
"desktop_mail": {
"context_search_mail": "Pesquisar «{text}» no correio"
},
"appLock": {
"section_title": "Bloqueio da app",
"section_desc": "Pede um PIN após um tempo sem uso, em todas as janelas e separadores. Enquanto está bloqueada, o correio neste dispositivo fica cifrado e a app não tem a chave.",
+3
View File
@@ -6810,6 +6810,9 @@
"close": "Închidere"
}
},
"desktop_mail": {
"context_search_mail": "Căutare „{text}” în e-mail"
},
"appLock": {
"section_title": "Blocarea aplicației",
"section_desc": "Cere un PIN după un timp fără utilizare, în fiecare fereastră și filă. Cât timp e blocată, e-mailurile de pe acest dispozitiv rămân criptate și aplicația nu are cheia lor.",
+3
View File
@@ -6810,6 +6810,9 @@
"close": "Закрыть"
}
},
"desktop_mail": {
"context_search_mail": "Найти «{text}» в почте"
},
"appLock": {
"section_title": "Блокировка приложения",
"section_desc": "Запрашивает PIN-код после периода бездействия, в каждом окне и вкладке. Пока приложение заблокировано, почта на этом устройстве остаётся зашифрованной, и у приложения нет ключа к ней.",
+3
View File
@@ -6810,6 +6810,9 @@
"close": "Zavrieť"
}
},
"desktop_mail": {
"context_search_mail": "Hľadať „{text}“ v pošte"
},
"appLock": {
"section_title": "Zámok aplikácie",
"section_desc": "Po čase bez používania žiada o PIN, v každom okne a na každej karte. Kým je zamknuté, pošta na tomto zariadení zostáva šifrovaná a aplikácia k nej nemá kľúč.",
+3
View File
@@ -6818,6 +6818,9 @@
"close": "ปิด"
}
},
"desktop_mail": {
"context_search_mail": "ค้นหา “{text}” ในอีเมล"
},
"appLock": {
"section_title": "ล็อกแอป",
"section_desc": "ขอ PIN หลังจากไม่ได้ใช้งานระยะหนึ่ง ในทุกหน้าต่างและแท็บ ระหว่างล็อก อีเมลบนอุปกรณ์นี้ยังคงเข้ารหัสอยู่และแอปไม่มีกุญแจ",
+3
View File
@@ -6810,6 +6810,9 @@
"close": "Kapat"
}
},
"desktop_mail": {
"context_search_mail": "E-postalarda “{text}” ara"
},
"appLock": {
"section_title": "Uygulama kilidi",
"section_desc": "Bir süre kullanılmadığında her pencerede ve sekmede PIN ister. Kilitliyken bu cihazdaki e-postalar şifreli kalır ve uygulamada anahtarı olmaz.",
+3
View File
@@ -6810,6 +6810,9 @@
"close": "Закрити"
}
},
"desktop_mail": {
"context_search_mail": "Шукати «{text}» у пошті"
},
"appLock": {
"section_title": "Блокування застосунку",
"section_desc": "Запитує PIN-код після часу без використання, у кожному вікні й вкладці. Поки застосунок заблоковано, пошта на цьому пристрої залишається зашифрованою, і застосунок не має ключа до неї.",
+3
View File
@@ -6822,6 +6822,9 @@
"close": "Đóng"
}
},
"desktop_mail": {
"context_search_mail": "Tìm “{text}” trong thư"
},
"appLock": {
"section_title": "Khóa ứng dụng",
"section_desc": "Yêu cầu mã PIN sau một thời gian không dùng, trong mọi cửa sổ và thẻ. Khi đang khóa, thư trên thiết bị này vẫn được mã hóa và ứng dụng không có khóa của chúng.",
+3
View File
@@ -6810,6 +6810,9 @@
"close": "關閉"
}
},
"desktop_mail": {
"context_search_mail": "在郵件中搜尋「{text}」"
},
"appLock": {
"section_title": "應用程式鎖定",
"section_desc": "一段時間未使用後,在每個視窗和分頁中要求輸入 PIN。鎖定期間,此裝置上的郵件保持加密,應用程式沒有金鑰。",
+3
View File
@@ -6810,6 +6810,9 @@
"close": "关闭"
}
},
"desktop_mail": {
"context_search_mail": "在邮件中搜索“{text}”"
},
"appLock": {
"section_title": "应用锁",
"section_desc": "一段时间未使用后,在每个窗口和标签页中要求输入 PIN。锁定期间,此设备上的邮件保持加密,应用没有密钥。",