- Shipped
- August 6, 2026 at 11:59 AM UTC
- Author
- kamo
- Commit
- 328a0bf
MailButton ran setInterval(fetchUnreadCount, 30_000) in every open tab, and each tick called /api/email/folders — an endpoint that opens every folder in the mailbox to produce the one integer the badge needs. The email WebSocket was already connected but only reported that mail had arrived, so its handler's job was to schedule another fetch. EmailService now publishes the count itself on /topic/email/unread/<memberId>: a snapshot on connect, then one on every change, including \Seen set from another mail client — which the poll could only notice up to 30s late and the old WebSocket could not notice at all. useEmailWebSocket subscribes to that topic and exposes inboxUnread through EmailWebSocketContext. Snapshots carry the instant they were read and older ones are discarded, so a redelivery cannot resurrect a badge the member just cleared. inboxUnread stays null until the first snapshot lands, so "not known yet" never renders as a cleared badge. The REST path survives only as a cold fallback for a browser whose socket never came up: it fires when the socket reports it has given up, when no snapshot has arrived within the connect window, and — while disconnected only — on tab focus and on email:folder-changed. None of those is a timer.