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: `
@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();
}
}