feat: add option to disable conversation threading #97

This commit is contained in:
Linus Rath
2026-03-26 13:04:34 +01:00
parent e48672cf77
commit 47918aab1b
13 changed files with 61 additions and 5 deletions
+4 -2
View File
@@ -89,10 +89,12 @@ export function EmailList({
searchQuery, searchQuery,
} = useEmailStore(); } = useEmailStore();
const disableThreading = useSettingsStore((state) => state.disableThreading);
const threadGroups = useMemo(() => { const threadGroups = useMemo(() => {
const groups = groupEmailsByThread(emails); const groups = groupEmailsByThread(emails, disableThreading);
return sortThreadGroups(groups); return sortThreadGroups(groups);
}, [emails]); }, [emails, disableThreading]);
const { contextMenu, openContextMenu, closeContextMenu, menuRef } = useContextMenu<Email>(); const { contextMenu, openContextMenu, closeContextMenu, menuRef } = useContextMenu<Email>();
const { dialogProps: confirmDialogProps, confirm: confirmDialog } = useConfirmDialog(); const { dialogProps: confirmDialogProps, confirm: confirmDialog } = useConfirmDialog();
+9
View File
@@ -39,6 +39,7 @@ export function EmailSettings() {
deleteAction, deleteAction,
permanentlyDeleteJunk, permanentlyDeleteJunk,
showPreview, showPreview,
disableThreading,
emailsPerPage, emailsPerPage,
externalContentPolicy, externalContentPolicy,
mailAttachmentAction, mailAttachmentAction,
@@ -210,6 +211,14 @@ export function EmailSettings() {
</SettingItem> </SettingItem>
)} )}
{/* Disable Thread Grouping */}
<SettingItem label={t('disable_threading.label')} description={t('disable_threading.description')}>
<ToggleSwitch
checked={disableThreading}
onChange={(checked) => updateSetting('disableThreading', checked)}
/>
</SettingItem>
{/* Quick Hover Actions */} {/* Quick Hover Actions */}
{isFeatureEnabled('hoverActionsConfigEnabled') && ( {isFeatureEnabled('hoverActionsConfigEnabled') && (
<div className="py-3 border-b border-border space-y-3"> <div className="py-3 border-b border-border space-y-3">
+5 -3
View File
@@ -3,17 +3,19 @@ import type { Email, ThreadGroup } from "./jmap/types";
/** /**
* Groups emails by their threadId and creates ThreadGroup objects for UI display. * Groups emails by their threadId and creates ThreadGroup objects for UI display.
* Single-email threads are still returned as ThreadGroups with emailCount=1. * 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) { if (!emails || emails.length === 0) {
return []; return [];
} }
// Group emails by threadId // Group emails by threadId (or by message ID when threading is disabled)
const threadMap = new Map<string, Email[]>(); const threadMap = new Map<string, Email[]>();
for (const email of emails) { for (const email of emails) {
const threadId = email.threadId; const threadId = disableThreading ? email.id : email.threadId;
if (!threadMap.has(threadId)) { if (!threadMap.has(threadId)) {
threadMap.set(threadId, []); threadMap.set(threadId, []);
} }
+4
View File
@@ -798,6 +798,10 @@
"label": "Vorschautext anzeigen", "label": "Vorschautext anzeigen",
"description": "E-Mail-Vorschau in der Liste 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": { "attachment_click_action": {
"label": "Aktion beim Klick auf Anhänge", "label": "Aktion beim Klick auf Anhänge",
"description": "Festlegen, ob ein Dateianhang beim Anklicken in der Vorschau geöffnet oder sofort heruntergeladen wird", "description": "Festlegen, ob ein Dateianhang beim Anklicken in der Vorschau geöffnet oder sofort heruntergeladen wird",
+4
View File
@@ -798,6 +798,10 @@
"label": "Show Preview Text", "label": "Show Preview Text",
"description": "Display email preview in the list" "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": { "attachment_click_action": {
"label": "Attachment Click Action", "label": "Attachment Click Action",
"description": "Choose whether clicking a file attachment previews it or downloads it immediately", "description": "Choose whether clicking a file attachment previews it or downloads it immediately",
+4
View File
@@ -794,6 +794,10 @@
"label": "Eliminar spam permanentemente", "label": "Eliminar spam permanentemente",
"description": "Eliminar permanentemente los correos de la carpeta Spam en lugar de moverlos a la Papelera" "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": { "show_preview": {
"label": "Mostrar Vista Previa", "label": "Mostrar Vista Previa",
"description": "Mostrar vista previa del correo en la lista" "description": "Mostrar vista previa del correo en la lista"
+4
View File
@@ -794,6 +794,10 @@
"label": "Supprimer définitivement les indésirables", "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" "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": { "show_preview": {
"label": "Afficher l'aperçu", "label": "Afficher l'aperçu",
"description": "Afficher l'aperçu de l'email dans la liste" "description": "Afficher l'aperçu de l'email dans la liste"
+4
View File
@@ -794,6 +794,10 @@
"label": "Elimina spam definitivamente", "label": "Elimina spam definitivamente",
"description": "Elimina definitivamente i messaggi dalla cartella Spam invece di spostarli nel cestino" "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": { "show_preview": {
"label": "Mostra anteprima testo", "label": "Mostra anteprima testo",
"description": "Visualizza l'anteprima del messaggio nell'elenco" "description": "Visualizza l'anteprima del messaggio nell'elenco"
+4
View File
@@ -794,6 +794,10 @@
"label": "迷惑メールを完全に削除", "label": "迷惑メールを完全に削除",
"description": "迷惑メールフォルダのメールをゴミ箱に移動せずに完全に削除する" "description": "迷惑メールフォルダのメールをゴミ箱に移動せずに完全に削除する"
}, },
"disable_threading": {
"label": "会話グループ化を無効にする",
"description": "メールを会話ごとにグループ化せず、個別のメッセージとして表示します"
},
"show_preview": { "show_preview": {
"label": "プレビューテキストを表示", "label": "プレビューテキストを表示",
"description": "リストにメールのプレビューを表示" "description": "リストにメールのプレビューを表示"
+4
View File
@@ -794,6 +794,10 @@
"label": "Spam permanent verwijderen", "label": "Spam permanent verwijderen",
"description": "E-mails uit de map Spam permanent verwijderen in plaats van naar de prullenbak te verplaatsen" "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": { "show_preview": {
"label": "Voorbeeldtekst tonen", "label": "Voorbeeldtekst tonen",
"description": "E-mailvoorbeeld weergeven in de lijst" "description": "E-mailvoorbeeld weergeven in de lijst"
+4
View File
@@ -794,6 +794,10 @@
"label": "Excluir spam permanentemente", "label": "Excluir spam permanentemente",
"description": "Excluir permanentemente e-mails da pasta Spam em vez de movê-los para a Lixeira" "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": { "show_preview": {
"label": "Mostrar Texto de Visualização", "label": "Mostrar Texto de Visualização",
"description": "Exibir visualização do e-mail na lista" "description": "Exibir visualização do e-mail na lista"
+4
View File
@@ -794,6 +794,10 @@
"label": "Удалять спам навсегда", "label": "Удалять спам навсегда",
"description": "Удалять письма из папки «Спам» навсегда вместо перемещения в корзину" "description": "Удалять письма из папки «Спам» навсегда вместо перемещения в корзину"
}, },
"disable_threading": {
"label": "Отключить группировку по беседам",
"description": "Отображать письма как отдельные сообщения, а не сгруппированные по беседам"
},
"show_preview": { "show_preview": {
"label": "Показывать текст предпросмотра", "label": "Показывать текст предпросмотра",
"description": "Отображать предпросмотр письма в списке" "description": "Отображать предпросмотр письма в списке"
+7
View File
@@ -145,6 +145,9 @@ interface SettingsState {
toolbarPosition: ToolbarPosition; toolbarPosition: ToolbarPosition;
showToolbarLabels: boolean; showToolbarLabels: boolean;
// Email Display
disableThreading: boolean; // Show emails as individual messages instead of grouped by conversation
// Experimental // Experimental
senderFavicons: boolean; senderFavicons: boolean;
@@ -258,6 +261,9 @@ const DEFAULT_SETTINGS = {
toolbarPosition: 'top' as ToolbarPosition, toolbarPosition: 'top' as ToolbarPosition,
showToolbarLabels: true, showToolbarLabels: true,
// Email Display
disableThreading: false,
// Experimental // Experimental
senderFavicons: true, senderFavicons: true,
@@ -325,6 +331,7 @@ export const useSettingsStore = create<SettingsState>()(
attachmentPosition: state.attachmentPosition, attachmentPosition: state.attachmentPosition,
archiveMode: state.archiveMode, archiveMode: state.archiveMode,
hoverActions: state.hoverActions, hoverActions: state.hoverActions,
disableThreading: state.disableThreading,
trustedSenders: state.trustedSenders, trustedSenders: state.trustedSenders,
autoSaveDraftInterval: state.autoSaveDraftInterval, autoSaveDraftInterval: state.autoSaveDraftInterval,
sendConfirmation: state.sendConfirmation, sendConfirmation: state.sendConfirmation,