From f45a9337357b21db1c4f1d3fe8b0c65e9aa40989 Mon Sep 17 00:00:00 2001
From: dttxorg <96994200+dttxorg@users.noreply.github.com>
Date: Wed, 13 May 2026 13:21:02 +0800
Subject: [PATCH 1/3] test(app): cover sub-app page titles
---
src/app/page-title.service.spec.ts | 47 ++++++++++++++++++++++++++++++
1 file changed, 47 insertions(+)
create mode 100644 src/app/page-title.service.spec.ts
diff --git a/src/app/page-title.service.spec.ts b/src/app/page-title.service.spec.ts
new file mode 100644
index 000000000..a299ca728
--- /dev/null
+++ b/src/app/page-title.service.spec.ts
@@ -0,0 +1,47 @@
+// --------- BEGIN RUNBOX LICENSE ---------
+// Copyright (C) 2016-2018 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 { Title } from '@angular/platform-browser';
+import { NavigationEnd, Router } from '@angular/router';
+import { Subject } from 'rxjs';
+import { buildRunboxPageTitle, PageTitleService } from './page-title.service';
+
+describe('PageTitleService', () => {
+ it('builds a mail title for the root route', () => {
+ expect(buildRunboxPageTitle('/')).toBe('Mail - Runbox 7');
+ });
+
+ it('builds sub-app titles from the current route', () => {
+ expect(buildRunboxPageTitle('/calendar')).toBe('Calendar - Runbox 7');
+ expect(buildRunboxPageTitle('/app/account/identities')).toBe('Account - Runbox 7');
+ expect(buildRunboxPageTitle('/contacts/settings?foo=bar')).toBe('Contacts - Runbox 7');
+ });
+
+ it('updates the document title when navigation ends', () => {
+ const events = new Subject();
+ const router = { url: '/', events } as Partial as Router;
+ const title = jasmine.createSpyObj('Title', ['setTitle']);
+
+ new PageTitleService(router, title);
+ events.next(new NavigationEnd(1, '/calendar', '/calendar'));
+
+ expect(title.setTitle).toHaveBeenCalledWith('Mail - Runbox 7');
+ expect(title.setTitle).toHaveBeenCalledWith('Calendar - Runbox 7');
+ });
+});
From 4c055d12ed3c26be7ff540030853450f2d4c5811 Mon Sep 17 00:00:00 2001
From: dttxorg <96994200+dttxorg@users.noreply.github.com>
Date: Wed, 13 May 2026 13:21:26 +0800
Subject: [PATCH 2/3] fix(app): update page title per sub-app
Set the document title from the active route so browser tabs identify the current Runbox sub-app.
Fixes #284
---
src/app/app.module.ts | 10 ++++--
src/app/page-title.service.ts | 63 +++++++++++++++++++++++++++++++++++
2 files changed, 71 insertions(+), 2 deletions(-)
create mode 100644 src/app/page-title.service.ts
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index bced66edb..98c005a7e 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -88,6 +88,7 @@ import { SavedSearchesService } from './saved-searches/saved-searches.service';
import { HelpComponent } from './help/help.component';
import { HelpModule } from './help/help.module';
import { DomainRegisterRedirectComponent } from './domainregister/domreg-redirect.component';
+import { PageTitleService } from './page-title.service';
window.addEventListener('dragover', (event) => event.preventDefault());
@@ -197,6 +198,7 @@ const routes: Routes = [
RMM,
RMMAuthGuardService,
ContactsService,
+ PageTitleService,
SavedSearchesService,
StorageService,
{ provide: HTTP_INTERCEPTORS, useClass: RMMHttpInterceptorService, multi: true },
@@ -207,10 +209,14 @@ const routes: Routes = [
bootstrap: [MainContainerComponent]
})
export class AppModule {
- constructor (matIconRegistry: MatIconRegistry, domSanitizer: DomSanitizer) {
+ constructor (
+ matIconRegistry: MatIconRegistry,
+ domSanitizer: DomSanitizer,
+ pageTitleService: PageTitleService,
+ ) {
+ void pageTitleService;
matIconRegistry.addSvgIconSet(
domSanitizer.bypassSecurityTrustResourceUrl('./assets/mdi.svg')
);
}
}
-
diff --git a/src/app/page-title.service.ts b/src/app/page-title.service.ts
new file mode 100644
index 000000000..16347c24b
--- /dev/null
+++ b/src/app/page-title.service.ts
@@ -0,0 +1,63 @@
+// --------- BEGIN RUNBOX LICENSE ---------
+// Copyright (C) 2016-2018 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 { Injectable } from '@angular/core';
+import { Title } from '@angular/platform-browser';
+import { NavigationEnd, Router } from '@angular/router';
+import { filter } from 'rxjs/operators';
+
+const titleBySubApp = new Map([
+ ['account', 'Account'],
+ ['calendar', 'Calendar'],
+ ['changelog', 'Changelog'],
+ ['compose', 'Compose'],
+ ['contacts', 'Contacts'],
+ ['dev', 'Components'],
+ ['dkim', 'DKIM'],
+ ['help', 'Help'],
+ ['login', 'Login'],
+ ['onscreen', 'Video meeting'],
+ ['overview', 'Overview'],
+ ['start', 'Overview'],
+ ['welcome', 'Welcome'],
+]);
+
+export function buildRunboxPageTitle(url: string): string {
+ const path = url.split(/[?#]/)[0];
+ const segments = path
+ .split('/')
+ .filter((segment) => segment && !['app', 'appdev', 'index_dev.html'].includes(segment));
+ const subAppTitle = titleBySubApp.get(segments[0]) || 'Mail';
+
+ return `${subAppTitle} - Runbox 7`;
+}
+
+@Injectable()
+export class PageTitleService {
+ constructor(router: Router, private title: Title) {
+ this.updateTitle(router.url);
+ router.events
+ .pipe(filter((event): event is NavigationEnd => event instanceof NavigationEnd))
+ .subscribe((event) => this.updateTitle(event.urlAfterRedirects));
+ }
+
+ private updateTitle(url: string) {
+ this.title.setTitle(buildRunboxPageTitle(url));
+ }
+}
From ad005bfba66693c15b477b7c6cd3da37a44c3e38 Mon Sep 17 00:00:00 2001
From: dttxorg <96994200+dttxorg@users.noreply.github.com>
Date: Wed, 13 May 2026 13:23:42 +0800
Subject: [PATCH 3/3] docs(app): document page title builder
---
src/app/page-title.service.ts | 3 +++
1 file changed, 3 insertions(+)
diff --git a/src/app/page-title.service.ts b/src/app/page-title.service.ts
index 16347c24b..495cc12bd 100644
--- a/src/app/page-title.service.ts
+++ b/src/app/page-title.service.ts
@@ -38,6 +38,9 @@ const titleBySubApp = new Map([
['welcome', 'Welcome'],
]);
+/**
+ * Builds the browser document title for a Runbox route.
+ */
export function buildRunboxPageTitle(url: string): string {
const path = url.split(/[?#]/)[0];
const segments = path