From 0959f22d8ce9a65f443b7940698fe759b0176a8c Mon Sep 17 00:00:00 2001 From: kei Date: Thu, 11 Jun 2026 04:42:28 +0200 Subject: [PATCH] fix(mail): show child counts for collapsed folders --- src/app/folder/folderlist.component.html | 16 +++--- src/app/folder/folderlist.component.spec.ts | 37 +++++++++++++ src/app/folder/folderlist.component.ts | 58 +++++++++++++++++++++ 3 files changed, 102 insertions(+), 9 deletions(-) diff --git a/src/app/folder/folderlist.component.html b/src/app/folder/folderlist.component.html index c68508305..ba0176488 100644 --- a/src/app/folder/folderlist.component.html +++ b/src/app/folder/folderlist.component.html @@ -41,20 +41,18 @@ {{node.folderName}} - - -   - + +   - - {{ messageCounts[node.folderPath].total }} + + {{ getDisplayedTotalCount(node, messageCounts) }} N/A diff --git a/src/app/folder/folderlist.component.spec.ts b/src/app/folder/folderlist.component.spec.ts index c7d05637d..339c2b673 100644 --- a/src/app/folder/folderlist.component.spec.ts +++ b/src/app/folder/folderlist.component.spec.ts @@ -20,6 +20,7 @@ import { FolderListComponent, DropPosition, CreateFolderEvent, MoveFolderEvent } from './folderlist.component'; import { RunboxWebmailAPI } from '../rmmapi/rbwebmail'; import { FolderListEntry } from '../common/folderlistentry'; +import { FolderMessageCountMap } from '../rmmapi/messagelist.service'; import { BehaviorSubject, firstValueFrom, of } from 'rxjs'; import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; import { TestBed } from '@angular/core/testing'; @@ -252,4 +253,40 @@ describe('FolderListComponent', () => { expect(newListOfFolders[2].folderName).toEqual('testtest'); expect(newListOfFolders[2].folderLevel).toEqual(0); }); + + it('should include hidden child folder counts when a parent is collapsed', () => { + const comp = new FolderListComponent(dialog, hotkeyMock); + const parentFolder = new FolderListEntry(1, 1, 10, 'user', 'Parent', 'Parent', 0); + const childFolder = new FolderListEntry(2, 2, 20, 'user', 'Child', 'Parent.Child', 1); + const grandchildFolder = new FolderListEntry(3, 3, 30, 'user', 'Grandchild', 'Parent.Child.Grandchild', 2); + const siblingFolder = new FolderListEntry(4, 4, 40, 'user', 'Sibling', 'Sibling', 0); + comp.folders = new BehaviorSubject([ + parentFolder, + childFolder, + grandchildFolder, + siblingFolder + ]); + comp.ngOnChanges(); + + const messageCounts: FolderMessageCountMap = { + Parent: { unread: 1, total: 10 }, + 'Parent.Child': { unread: 2, total: 20 }, + 'Parent.Child.Grandchild': { unread: 3, total: 30 }, + Sibling: { unread: 4, total: 40 }, + }; + + expect(comp.getDisplayedUnreadCount(parentFolder, messageCounts)).toBe(6); + expect(comp.getDisplayedTotalCount(parentFolder, messageCounts)).toBe(60); + expect(comp.getDisplayedUnreadCount(childFolder, messageCounts)).toBe(5); + expect(comp.getDisplayedTotalCount(childFolder, messageCounts)).toBe(50); + expect(comp.getDisplayedUnreadCount(siblingFolder, messageCounts)).toBe(4); + expect(comp.getDisplayedTotalCount(siblingFolder, messageCounts)).toBe(40); + + comp.treeControl.expand(parentFolder); + + expect(comp.getDisplayedUnreadCount(parentFolder, messageCounts)).toBe(1); + expect(comp.getDisplayedTotalCount(parentFolder, messageCounts)).toBe(10); + expect(comp.getDisplayedUnreadCount(childFolder, messageCounts)).toBe(5); + expect(comp.getDisplayedTotalCount(childFolder, messageCounts)).toBe(50); + }); }); diff --git a/src/app/folder/folderlist.component.ts b/src/app/folder/folderlist.component.ts index 831826457..cf8d9fd9d 100644 --- a/src/app/folder/folderlist.component.ts +++ b/src/app/folder/folderlist.component.ts @@ -90,6 +90,8 @@ export class FolderListComponent implements OnChanges { dataSource: MatTreeFlatDataSource; storedexpandedFolderIds: number[] = []; + private descendantFolderPathsByFolderPath: { [folderPath: string]: string[] } = {}; + constructor( public dialog: MatDialog, private hotkeysService: HotkeysService @@ -156,6 +158,7 @@ export class FolderListComponent implements OnChanges { private updateFolderTree(folders: FolderListEntry[]) { const treedata: FolderNode[] = []; + this.descendantFolderPathsByFolderPath = this.buildDescendantFolderPathsByFolderPath(folders); let currentFolderLevel = 0; const parentStack: FolderNode[] = []; @@ -191,6 +194,61 @@ export class FolderListComponent implements OnChanges { this.dataSource.data = treedata; } + private buildDescendantFolderPathsByFolderPath(folders: FolderListEntry[]): { [folderPath: string]: string[] } { + const descendantFolderPaths: { [folderPath: string]: string[] } = {}; + + folders.forEach((folder, index) => { + descendantFolderPaths[folder.folderPath] = []; + for (let i = index + 1; i < folders.length && folders[i].folderLevel > folder.folderLevel; i++) { + descendantFolderPaths[folder.folderPath].push(folders[i].folderPath); + } + }); + + return descendantFolderPaths; + } + + hasDisplayedMessageCounts(node: FolderListEntry, messageCounts: FolderMessageCountMap): boolean { + return messageCounts[node.folderPath] !== undefined; + } + + getDisplayedUnreadCount(node: FolderListEntry, messageCounts: FolderMessageCountMap): number { + return this.getDisplayedMessageCount(node, messageCounts, 'unread'); + } + + getDisplayedTotalCount(node: FolderListEntry, messageCounts: FolderMessageCountMap): number { + return this.getDisplayedMessageCount(node, messageCounts, 'total'); + } + + private getDisplayedMessageCount( + node: FolderListEntry, + messageCounts: FolderMessageCountMap, + countKind: 'unread' | 'total' + ): number { + const directCounts = messageCounts[node.folderPath]; + + if (!directCounts) { + return 0; + } + + if (!node.isExpandable || this.treeControl.isExpanded(node)) { + return directCounts[countKind]; + } + + const descendantFolderPaths = this.descendantFolderPathsByFolderPath[node.folderPath] || []; + + return descendantFolderPaths.reduce( + (displayCount, folderPath) => { + const childCounts = messageCounts[folderPath]; + if (childCounts) { + displayCount += childCounts[countKind]; + } + + return displayCount; + }, + directCounts[countKind] + ); + } + private _getLevel = (node: FolderListEntry) => node.folderLevel; private _isExpandable = (node: FolderListEntry) => node.isExpandable ? true : false;