diff --git a/src/app/menu/headertoolbar.component.html b/src/app/menu/headertoolbar.component.html
index 72e1f6395..b07681caa 100644
--- a/src/app/menu/headertoolbar.component.html
+++ b/src/app/menu/headertoolbar.component.html
@@ -2,8 +2,15 @@
diff --git a/src/app/menu/headertoolbar.component.spec.ts b/src/app/menu/headertoolbar.component.spec.ts
new file mode 100644
index 000000000..a5f4d441a
--- /dev/null
+++ b/src/app/menu/headertoolbar.component.spec.ts
@@ -0,0 +1,92 @@
+// --------- BEGIN RUNBOX LICENSE ---------
+// Copyright (C) 2016-2026 Runbox Solutions AS (runbox.com).
+//
+// This file is part of Runbox 7.
+//
+// Runbox 7 is free software: You can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the
+// Free Software Foundation, either version 3 of the License, or (at your
+// option) any later version.
+//
+// Runbox 7 is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Runbox 7. If not, see .
+// ---------- END RUNBOX LICENSE ----------
+
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { MatIconTestingModule } from '@angular/material/icon/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+import { NoopAnimationsModule } from '@angular/platform-browser/animations';
+import { of, ReplaySubject } from 'rxjs';
+
+import { HeaderToolbarComponent } from './headertoolbar.component';
+import { MenuModule } from './menu.module';
+import { LogoutService } from '../login/logout.service';
+import { RunboxMe, RunboxWebmailAPI } from '../rmmapi/rbwebmail';
+import { RMMOfflineService } from '../rmmapi/rmmoffline.service';
+import { FolderMessageCountMap, MessageListService } from '../rmmapi/messagelist.service';
+
+class MockRunboxWebmailAPI {
+ me = of({
+ is_trial: false,
+ owner: null,
+ } as RunboxMe);
+}
+
+class MockRMMOfflineService {
+ is_offline = false;
+}
+
+class MockLogoutService {
+ logout() {}
+}
+
+class MockMessageListService {
+ folderMessageCountSubject = new ReplaySubject(1);
+}
+
+describe('HeaderToolbarComponent', () => {
+ let fixture: ComponentFixture;
+ let messageListService: MockMessageListService;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [
+ MenuModule,
+ MatIconTestingModule,
+ NoopAnimationsModule,
+ RouterTestingModule,
+ ],
+ providers: [
+ { provide: RunboxWebmailAPI, useClass: MockRunboxWebmailAPI },
+ { provide: RMMOfflineService, useClass: MockRMMOfflineService },
+ { provide: LogoutService, useClass: MockLogoutService },
+ { provide: MessageListService, useClass: MockMessageListService },
+ ]
+ }).compileComponents();
+
+ messageListService = TestBed.inject(MessageListService) as unknown as MockMessageListService;
+ });
+
+ it('shows the unread mail count on the Mail menu item', () => {
+ fixture = TestBed.createComponent(HeaderToolbarComponent);
+
+ messageListService.folderMessageCountSubject.next({
+ Inbox: { unread: 2, total: 12 },
+ 'Lists.News': { unread: 5, total: 20 },
+ Spam: { unread: 0, total: 4 },
+ });
+ fixture.detectChanges();
+
+ const mailLink = fixture.debugElement.query(By.css('a[routerLink="/"]')).nativeElement as HTMLElement;
+ const badge = mailLink.querySelector('.mat-badge-content');
+
+ expect(badge?.textContent.trim()).toBe('7');
+ expect(mailLink.getAttribute('aria-label')).toBe('Mail, 7 unread messages');
+ });
+});
diff --git a/src/app/menu/headertoolbar.component.ts b/src/app/menu/headertoolbar.component.ts
index ae584a1f3..7abae0ce6 100644
--- a/src/app/menu/headertoolbar.component.ts
+++ b/src/app/menu/headertoolbar.component.ts
@@ -18,11 +18,11 @@
// ---------- END RUNBOX LICENSE ----------
import { Component, OnInit } from '@angular/core';
-import { RunboxWebmailAPI } from '../rmmapi/rbwebmail';
+import { RunboxMe, RunboxWebmailAPI } from '../rmmapi/rbwebmail';
import { RMMOfflineService } from '../rmmapi/rmmoffline.service';
import { Router } from '@angular/router';
import { LogoutService } from '../login/logout.service';
-import { RunboxMe } from '../rmmapi/rbwebmail';
+import { FolderMessageCountMap, MessageListService } from '../rmmapi/messagelist.service';
@Component({
// eslint-disable-next-line @angular-eslint/component-selector
@@ -34,16 +34,21 @@ export class HeaderToolbarComponent implements OnInit {
rmm6tooltip = 'This area isn\'t upgraded to Runbox 7 yet and will open in a new tab';
user_is_trial = false;
isMainAccount: boolean;
+ mailUnreadCount = 0;
constructor(
public rmmapi: RunboxWebmailAPI,
public rmmoffline: RMMOfflineService,
private router: Router,
- public logoutservice: LogoutService
+ public logoutservice: LogoutService,
+ messagelistservice: MessageListService
) {
rmmapi.me.subscribe((me: RunboxMe) => {
this.isMainAccount = !me.owner;
});
+ messagelistservice.folderMessageCountSubject.subscribe(counts => {
+ this.mailUnreadCount = this.sumUnreadMail(counts);
+ });
}
ngOnInit() {
@@ -63,4 +68,24 @@ export class HeaderToolbarComponent implements OnInit {
public contacts() {
this.router.navigate(['contacts']);
}
+
+ get mailMenuAriaLabel(): string {
+ if (this.mailUnreadCount === 0) {
+ return 'Mail';
+ }
+
+ const messageLabel = this.mailUnreadCount === 1 ? 'message' : 'messages';
+ return `Mail, ${this.mailUnreadCount} unread ${messageLabel}`;
+ }
+
+ private sumUnreadMail(counts: FolderMessageCountMap): number {
+ if (!counts) {
+ return 0;
+ }
+
+ return Object.keys(counts).reduce((total, folder) => {
+ const unread = counts[folder]?.unread || 0;
+ return total + (unread > 0 ? unread : 0);
+ }, 0);
+ }
}
diff --git a/src/app/menu/menu.module.ts b/src/app/menu/menu.module.ts
index 9967cb900..8efdeccc8 100644
--- a/src/app/menu/menu.module.ts
+++ b/src/app/menu/menu.module.ts
@@ -19,6 +19,7 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
+import { MatBadgeModule } from '@angular/material/badge';
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
import { MatIconModule } from '@angular/material/icon';
import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu';
@@ -32,6 +33,7 @@ import { SidenavMenuComponent } from './sidenav-menu.component';
@NgModule({
imports: [
CommonModule,
+ MatBadgeModule,
MatIconModule,
MatButtonModule,
MatToolbarModule,