import { ChangeDetectionStrategy, Component, DestroyRef, computed, inject, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { NavigationEnd, Router, RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; import { MatIconModule } from '@angular/material/icon'; import { MatTooltipModule } from '@angular/material/tooltip'; import { JtRowActionsComponent, RowAction } from '@jambotron/aria-grid'; import { filter } from 'rxjs'; import { ApiService } from '../../core/api.service'; import { AuthService } from '../../core/auth.service'; import { MailStateService } from '../../core/mail-state.service'; import { Folder, MailboxEntry, errorText, folderIcon, folderLabel, formatBytes } from '../../core/models'; import { BackgroundComponent } from '../../ui/background.component'; import { DialogService } from '../../ui/dialog.service'; import { THEMES, ThemeService } from '../../ui/theme.service'; import { ToastService } from '../../ui/toast.service'; /** Header, folders sidebar and the page — everything after login. */ @Component({ selector: 'app-shell', imports: [FormsModule, RouterOutlet, RouterLink, RouterLinkActive, MatIconModule, MatTooltipModule, JtRowActionsComponent, BackgroundComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
M
@if (auth.openedBy(); as admin) {
visibility You are in mailbox {{ auth.email() }} as administrator
}
` }) export class ShellComponent { readonly auth = inject(AuthService); readonly state = inject(MailStateService); readonly theme = inject(ThemeService); private api = inject(ApiService); private dialogs = inject(DialogService); private toasts = inject(ToastService); private router = inject(Router); readonly themes = THEMES; readonly menuOpen = signal(false); query = ''; readonly links = [ { path: '/contacts', label: 'Contacts', icon: 'contacts' }, { path: '/blocked', label: 'Blocked senders', icon: 'block' }, { path: '/settings', label: 'User panel', icon: 'person' } ]; readonly folderActions: RowAction[] = [ { value: 'rename', label: 'Rename', icon: 'drive_file_rename_outline' }, { value: 'delete', label: 'Delete folder', icon: 'delete', danger: true } ]; readonly systemFolders = computed(() => this.state.folders().filter(f => f.system)); readonly ownFolders = computed(() => this.state.folders().filter(f => !f.system)); readonly used = computed(() => { const p = this.state.profile(); return p ? `${formatBytes(p.usedBytes)} / ${formatBytes(p.quotaBytes)}` : ''; }); readonly usedPercent = computed(() => { const p = this.state.profile(); return p && p.quotaBytes ? Math.min(100, Math.round(p.usedBytes / p.quotaBytes * 100)) : 0; }); label = folderLabel; icon = folderIcon; constructor() { this.state.refreshFolders(); this.state.refreshProfile(); this.state.refreshBoxes(); const timer = setInterval(() => { this.state.refreshFolders(); this.state.refreshBoxes(); }, 30000); inject(DestroyRef).onDestroy(() => clearInterval(timer)); this.router.events.pipe(filter(e => e instanceof NavigationEnd)).subscribe(() => { const q = this.router.parseUrl(this.router.url).queryParams['q']; this.query = typeof q === 'string' ? q : ''; }); } /** Drafts: how many; Sent, Trash: nothing; others: unread. */ badge(folder: Folder): number { if (folder.name === 'Drafts') return folder.messageCount; if (folder.name === 'Sent' || folder.name === 'Trash') return 0; return folder.unreadCount; } search(): void { const match = this.router.url.match(/^\/folder\/(\d+)/); const folderId = match ? Number(match[1]) : this.state.inbox()?.id; if (!folderId) return; const q = this.query.trim(); this.router.navigate(['/folder', folderId], { queryParams: q ? { q } : {} }); } createFolder(): void { this.dialogs.prompt('New folder', 'Folder name', '', 'Create').subscribe(name => { if (!name) return; this.api.createFolder(name).subscribe({ next: folder => { this.state.refreshFolders(); this.toasts.ok(`Folder "${folder.name}" created`); this.router.navigate(['/folder', folder.id]); }, error: e => this.toasts.error(errorText(e)) }); }); } onFolderAction(action: string, folder: Folder): void { if (action === 'rename') { this.dialogs.prompt('Rename folder', 'Folder name', folder.name, 'Rename').subscribe(name => { if (!name || name === folder.name) return; this.api.renameFolder(folder.id, name).subscribe({ next: () => { this.state.refreshFolders(); this.toasts.ok('Folder renamed'); }, error: e => this.toasts.error(errorText(e)) }); }); } else if (action === 'delete') { this.dialogs.confirm(`Delete "${folder.name}"?`, 'Its messages go to the Trash.', 'Delete', true).subscribe(ok => { if (ok === null) return; this.api.deleteFolder(folder.id).subscribe({ next: () => { this.state.refreshFolders(); this.toasts.ok('Folder deleted'); if (this.router.url.startsWith(`/folder/${folder.id}`)) this.router.navigate(['/']); }, error: e => this.toasts.error(errorText(e)) }); }); } } /** Another mailbox of this login: its session, full reload — fresh folders and profile. */ switchTo(b: MailboxEntry): void { if (b.email === this.auth.email()) { this.router.navigate(['/']); return; } this.api.mailboxSession(b.email).subscribe({ next: session => { this.state.reset(); this.auth.switchTo(session); location.assign('/'); }, error: e => this.toasts.error(errorText(e)) }); } pick(email: string): void { const b = this.state.boxes().find(x => x.email === email); if (b) this.switchTo(b); } backToAdmin(): void { this.auth.backToAdmin(); location.assign('/settings'); } logout(): void { this.state.reset(); this.auth.logout(); } }