+
{groups.map((group, index) => {
const open = !collapsed.has(group.id);
const listId = `${idPrefix}-${index}`;
@@ -163,6 +195,20 @@ export function PluginSearchResults({ surface, onOpenEmail }: PluginSearchResult
);
})}
+ {waiting && (
+
+
+
+ {t('more_results_searching')}
+
+ {[0, 1, 2].map((row) => (
+
+ ))}
+
+ )}
);
}
diff --git a/lib/__tests__/plugin-search-results.test.ts b/lib/__tests__/plugin-search-results.test.ts
index 1b92e9c62..1fa5bda1e 100644
--- a/lib/__tests__/plugin-search-results.test.ts
+++ b/lib/__tests__/plugin-search-results.test.ts
@@ -55,12 +55,21 @@ describe('checkSearchResults', () => {
});
});
+/** Asks the plugins, collecting each answer in the order it came. */
+function ask(query = 'q') {
+ const answers: { pluginId: string; keys: string[] }[] = [];
+ const asked = provideSearchResults(query, {}, (pluginId, results) => {
+ answers.push({ pluginId, keys: results.map((r) => r.key) });
+ });
+ return { asked, answers };
+}
+
describe('provideSearchResults', () => {
- it('asks no one without handlers', async () => {
- expect(await provideSearchResults('q', {})).toEqual([]);
+ it('asks no one without handlers', () => {
+ expect(ask().asked).toEqual([]);
});
- it('asks the plugins at once, each with an empty list', async () => {
+ it('asks the plugins at once, each with an empty list, and hands over each answer as it comes', async () => {
const order: string[] = [];
emailHooks.onProvideSearchResults.register('slow', async (initial: unknown[]) => {
order.push(`slow ${initial.length}`);
@@ -72,27 +81,43 @@ describe('provideSearchResults', () => {
return [{ key: 'f', title: 'F' }];
});
- const results = await provideSearchResults('q', {});
+ const { asked, answers } = ask();
+ expect(asked).toEqual(['slow', 'fast']);
expect(order).toEqual(['slow 0', 'fast 0']);
- expect(results.map((r) => `${r.pluginId}:${r.key}`)).toEqual(['slow:s', 'fast:f']);
+ await vi.waitFor(() => expect(answers).toHaveLength(2));
+ expect(answers).toEqual([{ pluginId: 'fast', keys: ['f'] }, { pluginId: 'slow', keys: ['s'] }]);
});
- it('leaves out a plugin that throws', async () => {
+ it('checks each answer', async () => {
+ emailHooks.onProvideSearchResults.register('p', () => [{ key: 'a', title: 'A' }, { key: 'a', title: 'twice' }, 'text']);
+
+ const { answers } = ask();
+
+ await vi.waitFor(() => expect(answers).toEqual([{ pluginId: 'p', keys: ['a'] }]));
+ });
+
+ it('answers nothing for a plugin that throws', async () => {
emailHooks.onProvideSearchResults.register('broken', () => { throw new Error('down'); });
emailHooks.onProvideSearchResults.register('ok', () => [{ key: 'a', title: 'A' }]);
- expect((await provideSearchResults('q', {})).map((r) => r.pluginId)).toEqual(['ok']);
+ const { answers } = ask();
+
+ await vi.waitFor(() => expect(answers).toHaveLength(2));
+ expect(answers).toContainEqual({ pluginId: 'broken', keys: [] });
+ expect(answers).toContainEqual({ pluginId: 'ok', keys: ['a'] });
});
- it('leaves out a plugin that runs out of time', async () => {
+ it('answers nothing for a plugin that runs out of time, the others long before', async () => {
vi.useFakeTimers();
emailHooks.onProvideSearchResults.register('hung', () => new Promise(() => {}));
emailHooks.onProvideSearchResults.register('ok', () => [{ key: 'a', title: 'A' }]);
- const pending = provideSearchResults('q', {});
- await vi.advanceTimersByTimeAsync(10_000);
+ const { answers } = ask();
+ await vi.advanceTimersByTimeAsync(0);
+ expect(answers).toEqual([{ pluginId: 'ok', keys: ['a'] }]);
- expect((await pending).map((r) => r.pluginId)).toEqual(['ok']);
+ await vi.advanceTimersByTimeAsync(10_000);
+ expect(answers).toEqual([{ pluginId: 'ok', keys: ['a'] }, { pluginId: 'hung', keys: [] }]);
});
});
diff --git a/lib/plugin-hooks.ts b/lib/plugin-hooks.ts
index 5d16dbf46..6b83e5fa5 100644
--- a/lib/plugin-hooks.ts
+++ b/lib/plugin-hooks.ts
@@ -306,21 +306,25 @@ export class HookBus
any> {
/**
* Ask every handler at once, each with the same initial value, and keep
* each answer with the plugin that gave it, for hooks whose results the
- * host shows per plugin (onProvideSearchResults). A handler that throws or
- * runs out of time gives nothing.
+ * host shows per plugin (onProvideSearchResults): the plugins asked, in
+ * their order, each with its answer to come, so the host can show one as
+ * soon as it comes and knows who it still waits for. A handler that throws
+ * or runs out of time answers null.
*/
- async collect(initial: V, ...rest: unknown[]): Promise<{ pluginId: string; value: unknown }[]> {
+ collect(initial: V, ...rest: unknown[]): { pluginId: string; answer: Promise<{ value: unknown } | null> }[] {
const asked = this.handlers.filter(({ pluginId }) => !pluginErrorTracker.isDisabled(pluginId));
- const answers = await Promise.all(asked.map(async ({ pluginId, handler }) => {
- try {
- const value: unknown = await withTimeout(pluginId, handler(initial, ...rest), this.timeoutMs);
- return { pluginId, value };
- } catch (err) {
- pluginErrorTracker.record(pluginId, err);
- return null;
- }
+ return asked.map(({ pluginId, handler }) => ({
+ pluginId,
+ answer: (async () => {
+ try {
+ const value: unknown = await withTimeout(pluginId, handler(initial, ...rest), this.timeoutMs);
+ return { value };
+ } catch (err) {
+ pluginErrorTracker.record(pluginId, err);
+ return null;
+ }
+ })(),
}));
- return answers.filter((a): a is { pluginId: string; value: unknown } => a !== null);
}
/** Fire handlers as transforms - each receives the output of the previous */
diff --git a/lib/plugin-search-results.ts b/lib/plugin-search-results.ts
index 91b5f2120..01d15ff36 100644
--- a/lib/plugin-search-results.ts
+++ b/lib/plugin-search-results.ts
@@ -71,9 +71,21 @@ export function checkSearchResults(pluginId: string, value: unknown): ProvidedSe
return out;
}
-/** Ask every plugin for results to a search; each answer checked, in the plugins' order. */
-export async function provideSearchResults(query: string, filters: unknown): Promise {
+/**
+ * Ask every plugin for results to a search. Returns the plugins asked, in
+ * their order; each answer goes to `onAnswer` as it comes, checked (empty for
+ * a plugin that throws or runs out of time), so a slow plugin keeps no other
+ * waiting.
+ */
+export function provideSearchResults(
+ query: string,
+ filters: unknown,
+ onAnswer: (pluginId: string, results: ProvidedSearchResult[]) => void,
+): string[] {
if (emailHooks.onProvideSearchResults.size === 0) return [];
- const answers = await emailHooks.onProvideSearchResults.collect([] as ExternalSearchResult[], { query, filters });
- return answers.flatMap(({ pluginId, value }) => checkSearchResults(pluginId, value));
+ const asked = emailHooks.onProvideSearchResults.collect([] as ExternalSearchResult[], { query, filters });
+ for (const { pluginId, answer } of asked) {
+ void answer.then((given) => onAnswer(pluginId, given ? checkSearchResults(pluginId, given.value) : []));
+ }
+ return asked.map(({ pluginId }) => pluginId);
}
diff --git a/locales/ar/common.json b/locales/ar/common.json
index 53d1387de..cc7693fbe 100644
--- a/locales/ar/common.json
+++ b/locales/ar/common.json
@@ -296,6 +296,7 @@
"no_emails_description": "صندوق البريد هذا فارغ",
"no_search_results": "لم يتم العثور على نتائج",
"no_search_results_description": "حاول تعديل البحث أو عوامل التصفية",
+ "more_results_searching": "جارٍ البحث عن نتائج أخرى…",
"loading": "جارٍ تحميل الرسائل...",
"unread": "غير مقروءة",
"to_me": "إليّ",
diff --git a/locales/ca/common.json b/locales/ca/common.json
index f6518c120..cf555e325 100644
--- a/locales/ca/common.json
+++ b/locales/ca/common.json
@@ -296,6 +296,7 @@
"no_emails_description": "Aquesta bústia és buida",
"no_search_results": "No s'ha trobat cap resultat",
"no_search_results_description": "Proveu d'ajustar la cerca o els filtres",
+ "more_results_searching": "S'estan cercant més resultats…",
"loading": "Carregant els correus...",
"unread": "no llegit",
"to_me": "Per a mi",
diff --git a/locales/cs/common.json b/locales/cs/common.json
index a52d94e20..f06d96efb 100644
--- a/locales/cs/common.json
+++ b/locales/cs/common.json
@@ -296,6 +296,7 @@
"no_emails_description": "Tato schránka je prázdná",
"no_search_results": "Nenalezeny žádné výsledky",
"no_search_results_description": "Zkuste změnit hledání nebo filtry",
+ "more_results_searching": "Hledají se další výsledky…",
"loading": "Načítání zpráv...",
"unread": "nepřečtené",
"to_me": "Pro mě",
diff --git a/locales/da/common.json b/locales/da/common.json
index 3e04e5ef2..80ac5bfee 100644
--- a/locales/da/common.json
+++ b/locales/da/common.json
@@ -296,6 +296,7 @@
"no_emails_description": "Denne postkasse er tom",
"no_search_results": "Ingen resultater fundet",
"no_search_results_description": "Prøv at justere din søgning eller filtre",
+ "more_results_searching": "Søger efter flere resultater…",
"loading": "Indlæser e-mails...",
"unread": "ulæst",
"to_me": "Til mig",
diff --git a/locales/de/common.json b/locales/de/common.json
index 87f90a0e2..cc0b61079 100644
--- a/locales/de/common.json
+++ b/locales/de/common.json
@@ -296,6 +296,7 @@
"no_emails_description": "Dieses Postfach ist leer",
"no_search_results": "Keine Ergebnisse gefunden",
"no_search_results_description": "Passen Sie Ihre Suche oder Filter an",
+ "more_results_searching": "Weitere Treffer werden gesucht…",
"loading": "E-Mails werden geladen...",
"unread": "ungelesen",
"to_me": "An mich",
diff --git a/locales/en/common.json b/locales/en/common.json
index e370559a0..108046bde 100644
--- a/locales/en/common.json
+++ b/locales/en/common.json
@@ -296,6 +296,7 @@
"no_emails_description": "This mailbox is empty",
"no_search_results": "No results found",
"no_search_results_description": "Try adjusting your search or filters",
+ "more_results_searching": "Searching for more results…",
"loading": "Loading emails...",
"unread": "unread",
"to_me": "To me",
diff --git a/locales/es/common.json b/locales/es/common.json
index 45d4255d7..96f9382dc 100644
--- a/locales/es/common.json
+++ b/locales/es/common.json
@@ -296,6 +296,7 @@
"no_emails_description": "Este buzón está vacío",
"no_search_results": "No se encontraron resultados",
"no_search_results_description": "Intente ajustar su búsqueda o filtros",
+ "more_results_searching": "Buscando más resultados…",
"loading": "Cargando correos...",
"unread": "no leído",
"to_me": "Para mí",
diff --git a/locales/et/common.json b/locales/et/common.json
index 409dc7914..cd2f35480 100644
--- a/locales/et/common.json
+++ b/locales/et/common.json
@@ -296,6 +296,7 @@
"no_emails_description": "See postkast on tühi",
"no_search_results": "Tulemusi ei leitud",
"no_search_results_description": "Proovi otsingut või filtreid muuta",
+ "more_results_searching": "Otsitakse veel tulemusi…",
"loading": "Kirjade laadimine...",
"unread": "lugemata",
"to_me": "Minule",
diff --git a/locales/fa/common.json b/locales/fa/common.json
index c67b0be54..412bdb3a9 100644
--- a/locales/fa/common.json
+++ b/locales/fa/common.json
@@ -296,6 +296,7 @@
"no_emails_description": "این صندوق پستی خالی است",
"no_search_results": "نتیجهای یافت نشد",
"no_search_results_description": "جستجو یا فیلترهای خود را تنظیم کنید",
+ "more_results_searching": "در حال جستجوی نتایج بیشتر…",
"loading": "در حال بارگذاری ایمیلها...",
"unread": "خوانده نشده",
"to_me": "به من",
diff --git a/locales/fr/common.json b/locales/fr/common.json
index 05d4a448c..a46fa3309 100644
--- a/locales/fr/common.json
+++ b/locales/fr/common.json
@@ -296,6 +296,7 @@
"no_emails_description": "Cette boîte est vide",
"no_search_results": "Aucun résultat trouvé",
"no_search_results_description": "Essayez de modifier votre recherche ou vos filtres",
+ "more_results_searching": "Recherche d'autres résultats…",
"loading": "Chargement des emails...",
"unread": "non lu",
"to_me": "À moi",
diff --git a/locales/he/common.json b/locales/he/common.json
index 639fb6c7a..814966400 100644
--- a/locales/he/common.json
+++ b/locales/he/common.json
@@ -261,6 +261,7 @@
"no_emails_description": "תיבת הדואר הזו ריקה",
"no_search_results": "לא נמצאו תוצאות",
"no_search_results_description": "נסה להתאים את החיפוש או המסננים שלך",
+ "more_results_searching": "מחפש תוצאות נוספות…",
"loading": "טוען אימיילים...",
"unread": "לא נקרא",
"to_me": "לִי",
diff --git a/locales/hu/common.json b/locales/hu/common.json
index d73d1024c..4d4dd1505 100644
--- a/locales/hu/common.json
+++ b/locales/hu/common.json
@@ -296,6 +296,7 @@
"no_emails_description": "Ez a postafiók üres",
"no_search_results": "Nincs találat",
"no_search_results_description": "Próbáld módosítani a keresést vagy a szűrőket",
+ "more_results_searching": "További találatok keresése…",
"loading": "E-mailek betöltése...",
"unread": "olvasatlan",
"to_me": "Nekem",
diff --git a/locales/it/common.json b/locales/it/common.json
index d233c37f2..3e8481c22 100644
--- a/locales/it/common.json
+++ b/locales/it/common.json
@@ -296,6 +296,7 @@
"no_emails_description": "Questa casella è vuota",
"no_search_results": "Nessun risultato trovato",
"no_search_results_description": "Prova a modificare la ricerca o i filtri",
+ "more_results_searching": "Ricerca di altri risultati…",
"loading": "Caricamento messaggi...",
"unread": "non letto",
"to_me": "A me",
diff --git a/locales/ja/common.json b/locales/ja/common.json
index 14443e59d..0b0772285 100644
--- a/locales/ja/common.json
+++ b/locales/ja/common.json
@@ -296,6 +296,7 @@
"no_emails_description": "このメールボックスは空です",
"no_search_results": "結果が見つかりません",
"no_search_results_description": "検索やフィルターを調整してください",
+ "more_results_searching": "ほかの結果を検索しています…",
"loading": "メールを読み込み中...",
"unread": "未読",
"to_me": "宛先: 自分",
diff --git a/locales/ko/common.json b/locales/ko/common.json
index 96e51cd9f..d785a92d2 100644
--- a/locales/ko/common.json
+++ b/locales/ko/common.json
@@ -296,6 +296,7 @@
"no_emails_description": "편지함이 비어있어요",
"no_search_results": "검색 결과가 없어요",
"no_search_results_description": "검색어나 필터를 변경해 보세요",
+ "more_results_searching": "추가 결과를 검색하는 중…",
"loading": "메일을 불러오는 중...",
"unread": "읽지 않음",
"to_me": "나에게",
diff --git a/locales/lv/common.json b/locales/lv/common.json
index ea3047e14..eae3fe3e0 100644
--- a/locales/lv/common.json
+++ b/locales/lv/common.json
@@ -296,6 +296,7 @@
"no_emails_description": "Mape ir tukša",
"no_search_results": "Nekas netika atrasts",
"no_search_results_description": "Mēģiniet mainīt meklēšanas vaicājumu vai filtrus",
+ "more_results_searching": "Meklē vēl rezultātus…",
"loading": "Ielādē vēstules...",
"unread": "nelasītas",
"to_me": "Man",
diff --git a/locales/mn/common.json b/locales/mn/common.json
index e6ea18a15..9873f45b9 100644
--- a/locales/mn/common.json
+++ b/locales/mn/common.json
@@ -296,6 +296,7 @@
"no_emails_description": "Энэ шуудангийн хайрцаг хоосон",
"no_search_results": "Илэрц олдсонгүй",
"no_search_results_description": "Хайлт эсвэл шүүлтүүрээ тохируулаад үзээрэй",
+ "more_results_searching": "Нэмэлт үр дүн хайж байна…",
"loading": "Имэйл ачаалж байна...",
"unread": "уншаагүй",
"to_me": "Надад",
diff --git a/locales/nb/common.json b/locales/nb/common.json
index 6794e637c..02a358448 100644
--- a/locales/nb/common.json
+++ b/locales/nb/common.json
@@ -296,6 +296,7 @@
"no_emails_description": "Denne postkassen er tom",
"no_search_results": "Ingen resultater funnet",
"no_search_results_description": "Prøv å justere søket eller filtrene",
+ "more_results_searching": "Søker etter flere resultater…",
"loading": "Laster inn e-poster...",
"unread": "ulest",
"to_me": "Til meg",
diff --git a/locales/nl/common.json b/locales/nl/common.json
index 21f2232f5..e9e39b86a 100644
--- a/locales/nl/common.json
+++ b/locales/nl/common.json
@@ -296,6 +296,7 @@
"no_emails_description": "Deze mailbox is leeg",
"no_search_results": "Geen resultaten gevonden",
"no_search_results_description": "Pas je zoekopdracht of filters aan",
+ "more_results_searching": "Meer resultaten zoeken…",
"loading": "E-mails laden...",
"unread": "ongelezen",
"to_me": "Aan mij",
diff --git a/locales/pl/common.json b/locales/pl/common.json
index b0aae170a..bd20acb0b 100644
--- a/locales/pl/common.json
+++ b/locales/pl/common.json
@@ -296,6 +296,7 @@
"no_emails_description": "Ta skrzynka jest pusta",
"no_search_results": "Nie znaleziono wyników",
"no_search_results_description": "Spróbuj zmienić wyszukiwanie lub filtry",
+ "more_results_searching": "Wyszukiwanie kolejnych wyników…",
"loading": "Ładowanie wiadomości...",
"unread": "nieprzeczytane",
"to_me": "Do mnie",
diff --git a/locales/pt/common.json b/locales/pt/common.json
index 3603dd30d..6b8b3bf46 100644
--- a/locales/pt/common.json
+++ b/locales/pt/common.json
@@ -296,6 +296,7 @@
"no_emails_description": "Esta caixa de correio está vazia",
"no_search_results": "Nenhum resultado encontrado",
"no_search_results_description": "Tente ajustar sua pesquisa ou filtros",
+ "more_results_searching": "A procurar mais resultados…",
"loading": "Carregando e-mails...",
"unread": "não lido",
"to_me": "Para mim",
diff --git a/locales/ro/common.json b/locales/ro/common.json
index 6f8502c27..1948972bf 100644
--- a/locales/ro/common.json
+++ b/locales/ro/common.json
@@ -296,6 +296,7 @@
"no_emails_description": "Această căsuță poștală este goală",
"no_search_results": "Nu s-au găsit rezultate",
"no_search_results_description": "Încercați să modificați criteriile de căutare sau filtrele",
+ "more_results_searching": "Se caută mai multe rezultate…",
"loading": "Se încarcă e-mailurile...",
"unread": "necitit",
"to_me": "Pentru mine",
diff --git a/locales/ru/common.json b/locales/ru/common.json
index b88e0e8f0..8e95280a5 100644
--- a/locales/ru/common.json
+++ b/locales/ru/common.json
@@ -296,6 +296,7 @@
"no_emails_description": "Папка пуста",
"no_search_results": "Результатов не найдено",
"no_search_results_description": "Попробуйте изменить поисковый запрос или фильтры",
+ "more_results_searching": "Ищем другие результаты…",
"loading": "Загрузка писем...",
"unread": "непрочитанные",
"to_me": "Мне",
diff --git a/locales/sk/common.json b/locales/sk/common.json
index 7c2579d36..ddcb7969b 100644
--- a/locales/sk/common.json
+++ b/locales/sk/common.json
@@ -296,6 +296,7 @@
"no_emails_description": "Táto schránka je prázdna",
"no_search_results": "Neboli nájdené žiadne výsledky",
"no_search_results_description": "Skúste zmeniť hľadanie alebo filtre",
+ "more_results_searching": "Hľadajú sa ďalšie výsledky…",
"loading": "Načítavanie e-mailov...",
"unread": "neprečítané",
"to_me": "Pre mňa",
diff --git a/locales/th/common.json b/locales/th/common.json
index e8ce9fe3b..543139f70 100644
--- a/locales/th/common.json
+++ b/locales/th/common.json
@@ -534,6 +534,7 @@
"no_emails_description": "กล่องจดหมายนี้ว่างเปล่า",
"no_search_results": "ไม่พบผลการค้นหา",
"no_search_results_description": "ลองปรับเปลี่ยนคำค้นหาหรือตัวกรองของคุณ",
+ "more_results_searching": "กำลังค้นหาผลลัพธ์เพิ่มเติม…",
"loading": "กำลังโหลดอีเมล...",
"unread": "ยังไม่อ่าน",
"to_me": "ถึงฉัน",
diff --git a/locales/tr/common.json b/locales/tr/common.json
index 848d215f2..7c774ad6a 100644
--- a/locales/tr/common.json
+++ b/locales/tr/common.json
@@ -296,6 +296,7 @@
"no_emails_description": "Bu posta kutusu boş",
"no_search_results": "Sonuç bulunamadı",
"no_search_results_description": "Arama teriminizi veya filtrelerinizi değiştirmeyi deneyin",
+ "more_results_searching": "Başka sonuçlar aranıyor…",
"loading": "E-postalar yükleniyor...",
"unread": "okunmamış",
"to_me": "Bana",
diff --git a/locales/uk/common.json b/locales/uk/common.json
index f6acc947b..356060a52 100644
--- a/locales/uk/common.json
+++ b/locales/uk/common.json
@@ -296,6 +296,7 @@
"no_emails_description": "Ця поштова скринька порожня",
"no_search_results": "Результатів не знайдено",
"no_search_results_description": "Спробуйте налаштувати пошук або фільтри",
+ "more_results_searching": "Шукаємо інші результати…",
"loading": "Завантаження електронних листів...",
"unread": "непрочитаний",
"to_me": "мені",
diff --git a/locales/zh-TW/common.json b/locales/zh-TW/common.json
index f3aa1b8b8..6737b2737 100644
--- a/locales/zh-TW/common.json
+++ b/locales/zh-TW/common.json
@@ -296,6 +296,7 @@
"no_emails_description": "此信件匣中沒有郵件",
"no_search_results": "找不到結果",
"no_search_results_description": "請調整搜尋條件或篩選器",
+ "more_results_searching": "正在搜尋更多結果…",
"loading": "正在載入郵件…",
"unread": "未讀",
"to_me": "寄給我",
diff --git a/locales/zh/common.json b/locales/zh/common.json
index 3e0ae974b..417cae6b4 100644
--- a/locales/zh/common.json
+++ b/locales/zh/common.json
@@ -296,6 +296,7 @@
"no_emails_description": "此邮箱为空",
"no_search_results": "未找到结果",
"no_search_results_description": "请调整搜索条件或筛选器",
+ "more_results_searching": "正在搜索更多结果…",
"loading": "正在加载邮件...",
"unread": "未读",
"to_me": "发给我",
diff --git a/stores/__tests__/email-store-plugin-search-results.test.ts b/stores/__tests__/email-store-plugin-search-results.test.ts
index 1ad4ab937..c914d1109 100644
--- a/stores/__tests__/email-store-plugin-search-results.test.ts
+++ b/stores/__tests__/email-store-plugin-search-results.test.ts
@@ -50,6 +50,7 @@ beforeEach(() => {
searchFilters: { ...DEFAULT_SEARCH_FILTERS },
searchAbortController: null,
externalSearchResults: [],
+ externalSearchPending: [],
emails: [],
});
});
@@ -123,4 +124,40 @@ describe('onProvideSearchResults', () => {
await vi.waitFor(() => expect(useEmailStore.getState().externalSearchResults).toHaveLength(1));
expect(useEmailStore.getState().externalSearchResults[0].pluginId).toBe('crm');
});
+
+ it('keeps the plugins it still waits for as pending, and shows each one\'s entries as soon as it answers', async () => {
+ const slow = deferred();
+ const fast = deferred();
+ emailHooks.onProvideSearchResults.register('slow', () => slow.promise);
+ emailHooks.onProvideSearchResults.register('fast', () => fast.promise);
+
+ await useEmailStore.getState().searchEmails(client, 'resolution');
+ expect(useEmailStore.getState().externalSearchPending).toEqual(['slow', 'fast']);
+
+ fast.resolve([entry('f')]);
+ await vi.waitFor(() => expect(useEmailStore.getState().externalSearchPending).toEqual(['slow']));
+ expect(useEmailStore.getState().externalSearchResults.map((r) => r.key)).toEqual(['f']);
+
+ slow.resolve([entry('s')]);
+ await vi.waitFor(() => expect(useEmailStore.getState().externalSearchPending).toEqual([]));
+ // In the plugins' order, not the answers'.
+ expect(useEmailStore.getState().externalSearchResults.map((r) => r.key)).toEqual(['s', 'f']);
+ });
+
+ it('waits for no one without plugins', async () => {
+ await useEmailStore.getState().searchEmails(client, 'resolution');
+
+ expect(useEmailStore.getState().externalSearchPending).toEqual([]);
+ });
+
+ it('waits no more for the plugins of a search that was replaced', async () => {
+ emailHooks.onProvideSearchResults.register('crm', (_results: ExternalSearchResult[], { query }: { query: string }) =>
+ (query === 'one' ? new Promise(() => {}) : [entry('two')]));
+
+ await useEmailStore.getState().searchEmails(client, 'one');
+ await useEmailStore.getState().searchEmails(client, 'two');
+
+ await vi.waitFor(() => expect(useEmailStore.getState().externalSearchResults).toHaveLength(1));
+ expect(useEmailStore.getState().externalSearchPending).toEqual([]);
+ });
});
diff --git a/stores/email-store.ts b/stores/email-store.ts
index 384df23f6..014731285 100644
--- a/stores/email-store.ts
+++ b/stores/email-store.ts
@@ -302,6 +302,8 @@ interface EmailStore {
searchAbortController: AbortController | null;
/** Plugins' results from elsewhere for the search running (onProvideSearchResults), drawn above its list. */
externalSearchResults: ProvidedSearchResult[];
+ /** The plugins asked for the search running that have not answered yet, so the block can say more is coming. */
+ externalSearchPending: string[];
// Unified mailbox state
// Bumped by any action that changes which mailbox/keyword/unified view is
@@ -3230,6 +3232,8 @@ async function pluginSearchResultIds(query: string, filters: unknown, emails: Em
* Plugins' results for the search that is running (onProvideSearchResults).
* Asked beside the search, not before its list is shown: a slow plugin never
* delays the messages, and the answer to a search that was replaced is dropped.
+ * Each plugin's entries show as soon as it answers, in the plugins' order; the
+ * ones still asked are kept as pending.
*/
let providedSearchRun: object | null = null;
@@ -3240,15 +3244,24 @@ function startProvidedSearchResults(
): void {
const run = {};
providedSearchRun = run;
+ const answers = new Map();
+ let asked: string[] = [];
// Written only when it changes: a search without plugin results renders as before.
- const show = (results: ProvidedSearchResult[]) => {
- if (results.length > 0 || store.get().externalSearchResults.length > 0) store.set({ externalSearchResults: results });
+ const show = () => {
+ const results = asked.flatMap((id) => answers.get(id) ?? []);
+ const pending = asked.filter((id) => !answers.has(id));
+ const shown = store.get();
+ if (results.length > 0 || pending.length > 0 || shown.externalSearchResults.length > 0 || shown.externalSearchPending.length > 0) {
+ store.set({ externalSearchResults: results, externalSearchPending: pending });
+ }
};
- show([]);
- if (emailHooks.onProvideSearchResults.size === 0) return;
- void provideSearchResults(query, filters).then((results) => {
- if (providedSearchRun === run) show(results);
+ show();
+ asked = provideSearchResults(query, filters, (pluginId, results) => {
+ if (providedSearchRun !== run) return;
+ answers.set(pluginId, results);
+ show();
});
+ show();
}
export const useEmailStore = create((set, get) => ({
@@ -3300,6 +3313,7 @@ export const useEmailStore = create((set, get) => ({
isAdvancedSearchOpen: false,
searchAbortController: null,
externalSearchResults: [],
+ externalSearchPending: [],
// Unified mailbox state
viewToken: 0,