# Cohérence de l'interface Communications — cap « feeling Missive » > Statut : **PLAN** (2026-06-18). Refactor d'AFFICHAGE + threading — ne change pas les données stockées. > Objectif : que notre Boîte soit aussi cohérente et intuitive que Missive. ## Diagnostic : une seule cause méta Presque toutes les incohérences viennent du même défaut : **le même concept est calculé à plusieurs endroits avec des logiques différentes** (liste vs détail vs ingestion). Missive est cohérent parce que chaque concept a **une source de vérité**. → **Principe directeur : un *resolver* unique par concept, importé partout.** ## Inventaire (causes racines, vérifiées dans le code) ### 1. « à moi » au lieu de « à support » (attribution du destinataire) - Rendu : `ConversationPanel.vue:126` (entête, `convTo`) + `:414` (`msgTo` → `msgRecipientList`, :1163-1186). - Cause : `meAddr = msg.toEmail || convTo` puis `if addr === meAddr → 'moi'`. « moi » = **la boîte qui a REÇU** le courriel, PAS l'utilisateur connecté (`meEmail`, :1395, jamais consulté ici). Aucune table adresse-de-groupe → nom d'équipe. - Correctif : table `support@→Support`, `facturation@→Facturation`… (depuis `SENDER_IDENTITIES` / `queueLabels`) ; « moi » seulement si l'adresse == `meEmail`. ### 2. Nom d'expéditeur vide / incohérent - Rendu : liste `MessageList.vue:104` (finit par « Inconnu », OK) ; détail `msgWho` (:1577-1582) + avatar (:1569-1570). - Cause : `fromName=''` si le From n'a pas de display-name → retombe sur `activeDiscussion.customerName` (peut être un AUTRE expéditeur dans un fil multi-personnes) ; avatar `staffInitials('')` → vide. Et `shortAgent` (local part minuscule, ex. « gilles.drolet ») vs `noteAuthorName` (Capitalisé) = **même personne affichée de 2 façons**. - Correctif : un resolver `identity()` partagé, jamais vide, agents toujours capitalisés (réutiliser `noteAuthorName`). ### 3. Sur-regroupement (fils fusionnés à tort) - Lieux : ingestion `conversation.js:722-734` + re-bucketing liste `listConversations` (:435-461). - Causes : (a) courriel **sans sujet** → `findConversationByEmail:681` fusionne par email seul (garde sautée si un sujet est vide) ; (b) clé `:active` (:444) fusionne TOUS les fils actifs d'un même `baseKey` **sans borne de date** ; (c) SMS/téléphone fusionne par numéro **pour toujours** (sans sujet ni fenêtre) ; (d) `thread_id` Gmail pris tel quel (réutilisation de sujet → vieux fil) ; (e) **In-Reply-To/References stockés mais PAS utilisés** pour regrouper. - Correctif : exiger un **signal de fil positif** (thread_id OU match References) avant de fusionner ; sujet vide/auto = non-concordant ; **fenêtre de récence** sur la clé `:active`. ### 4. Canal incohérent - `conv.channel` (conversation) vs `msg.via` (message) — vocabulaires différents ; le **courriel n'a AUCUN badge** (`MessageList.vue:102` le masque) ; pas de badge par message. - Correctif : `msg.via` = canal par message (vocabulaire normalisé) ; badge cohérent sur CHAQUE ligne/message, courriel inclus. ### 5. FILE + identité éclatées - FILE : entête `activeConv.queue` (chargé async) vs liste `d.queue` → décalage bref. Identité : titre/chip/message utilisent **3 champs différents** (`customerName` / `coordAccountName` / `email`) → 3 noms pour le même fil. - Correctif : FILE = une source (réconcilier une fois `activeConv` arrivé) ; « qui » = un seul computed. ### 6. Finitions incohérentes - **Deux `formatTime`/`formatDate`** : local `ConversationPanel:2070-2087` (≠ canonique `useFormatters`) → liste en « DD/MM » (relTime) vs entête de message « mois jour HH:MM » ; le local **n'a pas le garde `isNaN`** → « Invalid Date ». - Tri : message `emailDate||ts` vs liste/`lastTs` = `ts` → ordre divergent sur fils re-synchronisés. - Non-lu / « en train d'écrire » pris du `latest` seul, pas du fil fusionné entier. - Couleurs d'avatar : clés de hash différentes liste vs détail → même personne, couleurs différentes. ## Les resolvers (colonne vertébrale du correctif) Un module partagé (ex. `composables/useConversationDisplay.js`) exporte **un resolver par concept**, utilisé par la liste ET le détail (et côté hub, l'ingestion) : | Concept | Resolver | Règle | |---|---|---| | Qui | `identity(p)` | nom → local part → tél → « Inconnu » ; agents capitalisés ; avatar/couleur/initiales dérivés du MÊME id | | Destinataire | `recipientLabel(addr)` | adresse de groupe → nom d'équipe ; « moi » seulement si == utilisateur ; sinon nom/adresse | | Canal | `channelOf(msg)` | depuis `via` normalisé → badge | | Fil | `threadKey(msg)` | signal positif requis ; sujet vide = non-match ; fenêtre de récence | | File | `fileOf(conv)` | une seule source | | Temps | `useFormatters` partout | supprimer les copies locales | ## Patterns Missive à adopter (extraits du tour vidéo) - **Ligne uniforme** : avatar + **badge de canal** + nom + heure + sujet + extrait + chips. - **1 fil = 1 sujet** (titre visible → toute fausse fusion saute aux yeux). - **Fil unifié** : messages client (blocs pleine largeur) + **notes internes (bulles)** + **tâches inline**, avec **2 composeurs** (répondre au client / note d'équipe). - **Assign / Snooze / Close** en actions de barre supérieure (dropdowns riches). - **Toasts Undo** sur actions réversibles. ## Plan par phases | Phase | Contenu | Risque | |---|---|---| | **P1 — Cohérence** | resolvers `identity` + `recipientLabel` (« à support ») + temps unifié (tuer les `formatTime` locaux) + `fileOf` une source | **faible** (affichage) | | **P2 — Threading strict** | signal de fil positif, sujet vide = non-match, fenêtre de récence, In-Reply-To/References utilisés | moyen (logique de fil → tester sur fils réels) | | **P3 — Canal** | `channelOf` + badge partout (Messenger/courriel/SMS) | faible | | **P4 — UX Missive** | fil unifié + 2 composeurs + assign/snooze + undo | moyen (UI) | ## Garde-fous - Refactor d'**affichage** d'abord — ne touche pas aux données stockées. - Threading (P2) : **préférer sur-séparer que sur-fusionner** — un faux split est bénin ; une fuite de conversation entre deux clients ne l'est pas. - Tester P2 sur de vrais fils : multi-expéditeur, SMS long, adresse de groupe, sujet réutilisé. - Chaque resolver = testable isolément (entrées → libellé attendu) avant branchement UI.