From 47918aab1bff7c556347501e286c835ab839c0b4 Mon Sep 17 00:00:00 2001 From: Linus Rath Date: Thu, 26 Mar 2026 10:48:49 +0100 Subject: [PATCH] feat: add option to disable conversation threading #97 --- components/email/email-list.tsx | 6 ++++-- components/settings/email-settings.tsx | 9 +++++++++ lib/thread-utils.ts | 8 +++++--- locales/de/common.json | 4 ++++ locales/en/common.json | 4 ++++ locales/es/common.json | 4 ++++ locales/fr/common.json | 4 ++++ locales/it/common.json | 4 ++++ locales/ja/common.json | 4 ++++ locales/nl/common.json | 4 ++++ locales/pt/common.json | 4 ++++ locales/ru/common.json | 4 ++++ stores/settings-store.ts | 7 +++++++ 13 files changed, 61 insertions(+), 5 deletions(-) diff --git a/components/email/email-list.tsx b/components/email/email-list.tsx index 47248399..0c57267c 100644 --- a/components/email/email-list.tsx +++ b/components/email/email-list.tsx @@ -89,10 +89,12 @@ export function EmailList({ searchQuery, } = useEmailStore(); + const disableThreading = useSettingsStore((state) => state.disableThreading); + const threadGroups = useMemo(() => { - const groups = groupEmailsByThread(emails); + const groups = groupEmailsByThread(emails, disableThreading); return sortThreadGroups(groups); - }, [emails]); + }, [emails, disableThreading]); const { contextMenu, openContextMenu, closeContextMenu, menuRef } = useContextMenu(); const { dialogProps: confirmDialogProps, confirm: confirmDialog } = useConfirmDialog(); diff --git a/components/settings/email-settings.tsx b/components/settings/email-settings.tsx index 5bb5cba7..1861ff61 100644 --- a/components/settings/email-settings.tsx +++ b/components/settings/email-settings.tsx @@ -39,6 +39,7 @@ export function EmailSettings() { deleteAction, permanentlyDeleteJunk, showPreview, + disableThreading, emailsPerPage, externalContentPolicy, mailAttachmentAction, @@ -210,6 +211,14 @@ export function EmailSettings() { )} + {/* Disable Thread Grouping */} + + updateSetting('disableThreading', checked)} + /> + + {/* Quick Hover Actions */} {isFeatureEnabled('hoverActionsConfigEnabled') && (
diff --git a/lib/thread-utils.ts b/lib/thread-utils.ts index 7a7de164..68b53f10 100644 --- a/lib/thread-utils.ts +++ b/lib/thread-utils.ts @@ -3,17 +3,19 @@ import type { Email, ThreadGroup } from "./jmap/types"; /** * Groups emails by their threadId and creates ThreadGroup objects for UI display. * Single-email threads are still returned as ThreadGroups with emailCount=1. + * When disableThreading is true, each email is placed into its own group using + * its message ID as the key, so the list shows individual messages. */ -export function groupEmailsByThread(emails: Email[]): ThreadGroup[] { +export function groupEmailsByThread(emails: Email[], disableThreading = false): ThreadGroup[] { if (!emails || emails.length === 0) { return []; } - // Group emails by threadId + // Group emails by threadId (or by message ID when threading is disabled) const threadMap = new Map(); for (const email of emails) { - const threadId = email.threadId; + const threadId = disableThreading ? email.id : email.threadId; if (!threadMap.has(threadId)) { threadMap.set(threadId, []); } diff --git a/locales/de/common.json b/locales/de/common.json index 7fa33906..3ad471ce 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -798,6 +798,10 @@ "label": "Vorschautext anzeigen", "description": "E-Mail-Vorschau in der Liste anzeigen" }, + "disable_threading": { + "label": "Konversationsgruppierung deaktivieren", + "description": "E-Mails als einzelne Nachrichten statt nach Konversation gruppiert anzeigen" + }, "attachment_click_action": { "label": "Aktion beim Klick auf Anhänge", "description": "Festlegen, ob ein Dateianhang beim Anklicken in der Vorschau geöffnet oder sofort heruntergeladen wird", diff --git a/locales/en/common.json b/locales/en/common.json index 8a17b927..398ff121 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -798,6 +798,10 @@ "label": "Show Preview Text", "description": "Display email preview in the list" }, + "disable_threading": { + "label": "Disable Conversation Grouping", + "description": "Show emails as individual messages instead of grouped by conversation" + }, "attachment_click_action": { "label": "Attachment Click Action", "description": "Choose whether clicking a file attachment previews it or downloads it immediately", diff --git a/locales/es/common.json b/locales/es/common.json index 5cc08745..a03a9432 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -794,6 +794,10 @@ "label": "Eliminar spam permanentemente", "description": "Eliminar permanentemente los correos de la carpeta Spam en lugar de moverlos a la Papelera" }, + "disable_threading": { + "label": "Desactivar agrupación de conversaciones", + "description": "Mostrar correos como mensajes individuales en lugar de agruparlos por conversación" + }, "show_preview": { "label": "Mostrar Vista Previa", "description": "Mostrar vista previa del correo en la lista" diff --git a/locales/fr/common.json b/locales/fr/common.json index 9ab79c29..684062cb 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -794,6 +794,10 @@ "label": "Supprimer définitivement les indésirables", "description": "Supprimer définitivement les e-mails du dossier Indésirables au lieu de les déplacer vers la corbeille" }, + "disable_threading": { + "label": "Désactiver le regroupement par conversation", + "description": "Afficher les e-mails comme messages individuels plutôt que groupés par conversation" + }, "show_preview": { "label": "Afficher l'aperçu", "description": "Afficher l'aperçu de l'email dans la liste" diff --git a/locales/it/common.json b/locales/it/common.json index 4c7cbfea..b11e9617 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -794,6 +794,10 @@ "label": "Elimina spam definitivamente", "description": "Elimina definitivamente i messaggi dalla cartella Spam invece di spostarli nel cestino" }, + "disable_threading": { + "label": "Disabilita raggruppamento conversazioni", + "description": "Mostra le email come messaggi singoli anziché raggruppati per conversazione" + }, "show_preview": { "label": "Mostra anteprima testo", "description": "Visualizza l'anteprima del messaggio nell'elenco" diff --git a/locales/ja/common.json b/locales/ja/common.json index 995279c3..938cf6ec 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -794,6 +794,10 @@ "label": "迷惑メールを完全に削除", "description": "迷惑メールフォルダのメールをゴミ箱に移動せずに完全に削除する" }, + "disable_threading": { + "label": "会話グループ化を無効にする", + "description": "メールを会話ごとにグループ化せず、個別のメッセージとして表示します" + }, "show_preview": { "label": "プレビューテキストを表示", "description": "リストにメールのプレビューを表示" diff --git a/locales/nl/common.json b/locales/nl/common.json index 7bacf534..4379a8b9 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -794,6 +794,10 @@ "label": "Spam permanent verwijderen", "description": "E-mails uit de map Spam permanent verwijderen in plaats van naar de prullenbak te verplaatsen" }, + "disable_threading": { + "label": "Conversatiegroepering uitschakelen", + "description": "Toon e-mails als afzonderlijke berichten in plaats van gegroepeerd op gesprek" + }, "show_preview": { "label": "Voorbeeldtekst tonen", "description": "E-mailvoorbeeld weergeven in de lijst" diff --git a/locales/pt/common.json b/locales/pt/common.json index cddb0ce7..042486a3 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -794,6 +794,10 @@ "label": "Excluir spam permanentemente", "description": "Excluir permanentemente e-mails da pasta Spam em vez de movê-los para a Lixeira" }, + "disable_threading": { + "label": "Desativar agrupamento de conversas", + "description": "Mostrar e-mails como mensagens individuais em vez de agrupados por conversa" + }, "show_preview": { "label": "Mostrar Texto de Visualização", "description": "Exibir visualização do e-mail na lista" diff --git a/locales/ru/common.json b/locales/ru/common.json index 580c18ce..5e5cc306 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -794,6 +794,10 @@ "label": "Удалять спам навсегда", "description": "Удалять письма из папки «Спам» навсегда вместо перемещения в корзину" }, + "disable_threading": { + "label": "Отключить группировку по беседам", + "description": "Отображать письма как отдельные сообщения, а не сгруппированные по беседам" + }, "show_preview": { "label": "Показывать текст предпросмотра", "description": "Отображать предпросмотр письма в списке" diff --git a/stores/settings-store.ts b/stores/settings-store.ts index f89643dc..caaede37 100644 --- a/stores/settings-store.ts +++ b/stores/settings-store.ts @@ -145,6 +145,9 @@ interface SettingsState { toolbarPosition: ToolbarPosition; showToolbarLabels: boolean; + // Email Display + disableThreading: boolean; // Show emails as individual messages instead of grouped by conversation + // Experimental senderFavicons: boolean; @@ -258,6 +261,9 @@ const DEFAULT_SETTINGS = { toolbarPosition: 'top' as ToolbarPosition, showToolbarLabels: true, + // Email Display + disableThreading: false, + // Experimental senderFavicons: true, @@ -325,6 +331,7 @@ export const useSettingsStore = create()( attachmentPosition: state.attachmentPosition, archiveMode: state.archiveMode, hoverActions: state.hoverActions, + disableThreading: state.disableThreading, trustedSenders: state.trustedSenders, autoSaveDraftInterval: state.autoSaveDraftInterval, sendConfirmation: state.sendConfirmation,