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 <https://www.gnu.org/licenses/>. +// ---------- 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