Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions frontend/src/apps/mail/pages/MailboxView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -1247,7 +1247,12 @@ const onResetSuccess = () => {
const fresh = freshWindow.filter(
(t: Thread) => !existing.has(t.thread_id) && !recentlyRemoved.has(t.thread_id),
)
threadsResource.value.data = [...fresh, ...refreshSnapshot]
// Already-loaded threads take their fresh row (new reply's snippet, unread state, timestamp)
// but keep their position — updating in place, not re-sorting, so the list doesn't jump under
// the reader. Threads outside the fresh window keep their snapshot row unchanged.
const freshById = new Map(freshWindow.map((t: Thread) => [t.thread_id, t]))
const updatedSnapshot = refreshSnapshot.map((t) => freshById.get(t.thread_id) ?? t)
threadsResource.value.data = [...fresh, ...updatedSnapshot]
// Keep the reader where they were: shift scroll by the height the prepended rows added. If they
// were already at the top, leave them there so the new mail is visible.
nextTick(() => {
Expand Down Expand Up @@ -1598,12 +1603,20 @@ watch(
)

// Periodically refresh the mailbox list (keeps sidebar counts current), then merge in new threads only
// when the mailbox's thread count actually changed — so a quiet mailbox isn't touched (and the reader
// isn't disturbed) every 30s.
// when the mailbox's counts actually changed — so a quiet mailbox isn't touched (and the reader isn't
// disturbed) every 30s. Every count matters: a reply landing in an already-loaded thread moves only
// total_emails (and unread_* if unread) while total_threads stays put, so comparing threads alone
// would miss it and the list would go stale (issue #171).
const mailboxCounts = () => {
const m = mailboxObj.value
if (!m) return undefined
return `${m.total_threads}:${m.unread_threads}:${m.total_emails}:${m.unread_emails}`
}

const pollForChanges = async () => {
const prevTotal = mailboxObj.value?.total_threads
const prevCounts = mailboxCounts()
await mailboxes.reload()
if (mailboxObj.value?.total_threads !== prevTotal) refreshThreads(false)
if (mailboxCounts() !== prevCounts) refreshThreads(false)
}

onMounted(() => {
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/apps/mail/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,8 @@ export interface MailboxData {
role: string | null
total_threads: number
unread_threads: number
total_emails: number
unread_emails: number
_name: string
subscribed: 0 | 1
icon?: string
Expand Down
12 changes: 11 additions & 1 deletion suite/mail/api/mail.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,17 @@ def get_mailboxes(account: str) -> list[dict]:
if not mailboxes:
return []

fields = ["name", "id", "_name", "role", "total_threads", "unread_threads", "subscribed"]
fields = [
"name",
"id",
"_name",
"role",
"total_threads",
"unread_threads",
"total_emails",
"unread_emails",
"subscribed",
]

mailbox_settings = frappe.db.get_all(
"Mailbox Settings",
Expand Down
Loading