← Blog Home

Messages non lus vs messages totaux : comprendre les statuts et mises à jour en temps réel

fr 2026-02-03 10:11:06

Messages non lus vs messages totaux : comprendre les statuts et mises à jour en temps réel

Dans la plupart des applications de messagerie, deux chiffres reviennent partout : le nombre de messages non lus (“Unread”) et le total de messages (“Total”). Sur l’écran, c’est limpide : l’un indique ce qui demande votre attention, l’autre la quantité globale reçue. Pourtant, dès qu’on observe le comportement en conditions réelles — plusieurs appareils, notifications, rafraîchissements, dossiers, filtres, mode hors ligne — ces deux compteurs peuvent sembler incohérents.

Ce n’est pas forcément un bug. La plupart du temps, c’est la conséquence de règles de calcul (statuts), de mises à jour asynchrones (sync), et d’une question centrale : qu’est-ce qui “fait foi” au moment où l’interface affiche un nombre ?

Dans cet article, on décortique le sujet comme le feraient une équipe produit et une équipe mobile : définitions, sources de vérité, événements, edge cases, et bonnes pratiques UX pour que l’utilisateur comprenne “ce qu’il voit”.


1) Définitions : “Unread” et “Total”, ce que ça veut dire vraiment

Unread (non lus)

Le compteur “Unread” représente le nombre de messages dont l’état est non lu. Mais cette définition dépend d’une convention : est-ce qu’un message devient “lu” dès qu’il est affiché dans une liste ? seulement quand on ouvre le détail ? lorsqu’il est rendu visible X secondes ? ou quand l’app reçoit un accusé de lecture côté serveur ?

Dans beaucoup d’apps, “Unread” est un statut métier. Il peut être modifié par :

  • l’ouverture du message (lecture côté client),
  • une action explicite “Marquer comme lu”,
  • une règle automatique (ex. : “marquer comme lu après prévisualisation”),
  • une synchronisation serveur (ex. : lecture sur un autre appareil).

Total (total de messages)

Le “Total” correspond au nombre de messages présents dans un périmètre donné : une boîte de réception, un dossier, un alias, ou une conversation. Là encore, il dépend d’un “scope” :

  • Total dans la boîte principale,
  • Total toutes boîtes confondues,
  • Total après filtrage (ex. : seulement les messages avec pièces jointes),
  • Total après purge (ex. : messages expirés, supprimés, ou archivés).

En pratique, “Total” est souvent plus stable que “Unread”, mais il peut bouger quand l’app :

  • reçoit de nouveaux messages,
  • charge une page supplémentaire (pagination),
  • applique une politique de rétention (suppression automatique),
  • fusionne ou déduplique des éléments.

2) Pourquoi ces compteurs “bougent” : l’idée de source de vérité

Un compteur affiché à l’écran dépend toujours d’une source de vérité. Dans une app moderne, il peut y en avoir plusieurs :

  • Serveur : l’état officiel, cohérent entre appareils, mais parfois plus lent (latence).
  • Cache local : rapide, fluide, utilisable hors ligne, mais potentiellement en retard.
  • État UI en mémoire : ce que l’écran “croit” à l’instant T, souvent optimiste.

La magie (et la difficulté) consiste à synchroniser ces sources sans faire “clignoter” l’interface. Exemple classique : vous ouvrez un message, l’app le marque comme lu immédiatement (UX fluide), mais le serveur ne confirme que quelques secondes plus tard. Entre-temps, si un rafraîchissement arrive, l’app doit décider : on garde la version optimiste, ou on revient au statut serveur ?


3) Comment une mise à jour de statut est propagée

Le schéma le plus courant ressemble à ceci :

  1. Événement entrant : un nouveau message arrive (push, websocket, polling).
  2. Stockage : l’app écrit le message en base locale (ou cache mémoire), avec un statut initial (souvent non lu).
  3. Calcul : l’app met à jour les compteurs (Total +1, Unread +1).
  4. Rendu UI : la liste et les badges se mettent à jour.
  5. Synchronisation : l’app confirme auprès du serveur (ou reçoit la confirmation) pour verrouiller l’état.

Pour la lecture, c’est l’inverse :

  1. Vous ouvrez un message.
  2. L’app applique un marquage local (Unread -1) pour une réponse instantanée.
  3. Elle envoie une requête au serveur “mark as read”.
  4. Le serveur confirme, ou rejette (rare, mais possible).
  5. En cas de rejet, l’app doit réconcilier les états.

C’est là que naissent les situations où “Unread” semble faire du yo-yo.


4) Les cas qui créent de la confusion (et comment les expliquer)

A) Lecture sur un autre appareil

Vous lisez sur votre ordinateur, votre téléphone affiche encore “3 non lus”. Quand la sync arrive, le compteur descend d’un coup. Si l’app n’a pas de canal temps réel (websocket/push fiable), elle peut rester en retard jusqu’au prochain rafraîchissement.

B) Prévisualisation vs ouverture

Certaines apps montrent un aperçu du contenu dans la liste. Si l’app considère qu’un aperçu suffit à marquer “lu”, le compteur “Unread” baisse sans que l’utilisateur ait eu l’impression d’ouvrir quoi que ce soit. C’est une décision UX : confortable pour certains, déroutant pour d’autres.

C) Pagination et “total partiel”

Le “Total” peut être une estimation tant que toutes les pages ne sont pas chargées. Exemple : la liste montre 50 éléments, puis en charge 50 de plus. Si l’app calcule “Total” à partir des éléments chargés, le chiffre augmente alors que l’utilisateur n’a pas reçu de nouveaux messages. Pour éviter ça, beaucoup d’apps affichent un total serveur distinct, ou un libellé implicite (sans le dire) en gardant “Total” basé sur une statistique serveur.

D) Suppression automatique / expiration

Dans les systèmes d’e-mails temporaires, la rétention est essentielle. Un message peut expirer après X minutes/heures. Résultat : le “Total” peut baisser sans action de votre part. Et si un message non lu expire, “Unread” baisse aussi, parfois sans notification visible. C’est cohérent techniquement, mais il faut une UX qui “assume” la règle (ex. : une mention de rétention dans le produit, un historique, ou un indicateur “expiré”).

E) Déduplication

Deux notifications arrivent pour le même message (retries réseau, rediffusion). Si l’app déduplique, “Total” ne doit pas augmenter deux fois. Sans une clé d’idempotence robuste (message_id unique), on peut voir des compteurs “fantômes”.


5) Comment les systèmes calculent réellement “Unread”

Il existe deux grandes approches :

Approche 1 : calcul “à la volée”

Le compteur Unread est calculé en requêtant la base : “combien de messages ont is_read=false”. Avantage : cohérence. Inconvénient : coût (surtout si la liste est grande) et dépendance à l’indexation.

Approche 2 : compteur maintenu (agrégat)

On maintient un compteur Unread dans une table/stat. À chaque insertion, on +1, à chaque lecture, on -1. Avantage : rapide. Inconvénient : risque de désynchronisation si un événement est manqué (crash, réseau, rollback). D’où la nécessité de reconstructions périodiques ou de vérifications (recalc).

Les meilleures implémentations combinent souvent les deux : un agrégat pour la fluidité, et un recalcul ponctuel pour corriger les dérives.


6) Temps réel : push, polling, websocket… et leurs effets sur les compteurs

Le “ressenti” de l’utilisateur dépend du mode de mise à jour :

  • Polling : l’app interroge le serveur toutes les X secondes. Les compteurs sont “par paliers”.
  • Push notifications : on reçoit un signal qu’un message existe, mais pas toujours le contenu complet. Il faut ensuite sync.
  • Websocket / SSE : l’app reçoit les événements quasi instantanément, et les compteurs sont très réactifs.

Mais même avec du temps réel, la vérité reste nuancée : la notification peut arriver avant que le message soit vraiment disponible via l’API (consistance éventuelle), ou l’événement peut être reçu dans le désordre. D’où l’importance d’une logique “robuste” : idempotence, versioning, et réconciliation.


7) Pourquoi “Unread” et “Total” ne sont pas toujours liés

On pourrait croire : Total = Unread + Read. En théorie, oui. En pratique, il y a des statuts intermédiaires :

  • Pending : message reçu côté serveur, pas encore téléchargé.
  • Hidden/Filtered : message masqué par un filtre (spam, promotions).
  • Archived : message compté dans Total global mais pas dans la vue courante.
  • Expired : supprimé automatiquement, peut disparaître de Total.
  • Failed state : erreur de sync, message visible mais statut incertain.

Selon l’UX, on peut choisir d’inclure ou non ces états dans “Total”. L’important, c’est la cohérence : le même écran ne doit pas mélanger un total “global serveur” avec un unread “local filtré” sans signaux clairs, sinon on crée de la dissonance.


8) Bonnes pratiques UX : comment éviter l’impression de bug

Afficher le bon compteur au bon endroit

  • Dans un onglet “Inbox”, afficher des compteurs Inbox-scopés, pas un total global.
  • Dans un header global, afficher un “Unread global” si l’app fonctionne en multi-boîtes.

Éviter le clignotement

Si la source de vérité change, évitez de “réécrire” l’écran trop agressivement. Une stratégie fréquente : appliquer des mises à jour optimistes et n’afficher la correction serveur que si l’écart persiste.

Rendre les règles implicites intuitives

  • Si un message devient lu dès l’ouverture, c’est attendu.
  • Si un message devient lu dès la prévisualisation, c’est surprenant : prévoyez une option, ou au moins une cohérence stricte.
  • Si des messages expirent, une mention de rétention dans le produit réduit la confusion.

Protéger l’utilisateur des latences

Quand l’app est hors ligne ou sur réseau instable, l’interface doit rester utile : afficher une indication “mise à jour en cours”, conserver les statuts locaux, et réconcilier ensuite sans faire “perdre” des messages.


9) Mini-scénario : pourquoi votre badge affiche “1” alors que vous ne voyez rien

Cas fréquent : le badge indique un non lu, mais la liste semble vide. Plusieurs causes possibles :

  • Le message est arrivé mais est dans un autre dossier (filtre actif, alias différent).
  • L’événement push a mis à jour le compteur, mais la sync du contenu n’est pas terminée.
  • Le message a expiré (ou a été supprimé) entre le moment du push et l’ouverture de l’app.
  • Un état local est en retard par rapport au serveur, et la réconciliation n’a pas encore eu lieu.

La solution n’est pas seulement technique : UX-wise, un bouton “Actualiser”, un état “Synchronisation…” bien visible, ou une vue “Tous les messages” aide à dissiper le doute.


10) Conclusion : ce que vous devez retenir

Unread est un état dynamique, sensible aux actions utilisateur, aux appareils multiples et aux délais de synchronisation. Total est un volume, mais il dépend du périmètre (boîte, filtre, dossier) et peut varier avec la pagination, la déduplication ou l’expiration.

Quand une app affiche ces deux chiffres de manière fiable, ce n’est pas parce que c’est “simple”. C’est parce qu’elle a fait des choix : une source de vérité, une stratégie de sync, une logique d’agrégation, et une UX qui assume les règles. En tant qu’utilisateur, si vous voyez un compteur qui change, la plupart du temps, c’est le signe que l’application est en train de réconcilier le monde réel — événements, réseau, et états — plutôt que de vous mentir avec un chiffre figé.

Et si vous concevez vous-même une interface de messagerie : souvenez-vous que la clarté ne vient pas d’un “nombre parfait”, mais d’une cohérence entre ce que le compteur veut dire, et ce que l’utilisateur peut vérifier à l’écran.

Tip: Temporary inboxes are best for low-risk sign-ups and verification. Avoid sensitive accounts that require long-term recovery access.