Files
Toju/toju-app/src/app/features/settings/settings-modal/settings-modal.component.ts
2026-04-02 03:18:37 +02:00

409 lines
12 KiB
TypeScript

/* eslint-disable @typescript-eslint/member-ordering */
import {
Component,
inject,
signal,
computed,
effect,
untracked,
HostListener,
viewChild
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Store } from '@ngrx/store';
import { NgIcon, provideIcons } from '@ng-icons/core';
import {
lucideX,
lucideBug,
lucideBell,
lucideDownload,
lucideGlobe,
lucideAudioLines,
lucidePalette,
lucideSettings,
lucideUsers,
lucideBan,
lucideShield
} from '@ng-icons/lucide';
import { SettingsModalService, SettingsPage } from '../../../core/services/settings-modal.service';
import { RealtimeSessionFacade } from '../../../core/realtime';
import { selectSavedRooms, selectCurrentRoom } from '../../../store/rooms/rooms.selectors';
import { selectCurrentUser } from '../../../store/users/users.selectors';
import { Room, UserRole } from '../../../shared-kernel';
import { NotificationsSettingsComponent } from '../../../domains/notifications/feature/settings/notifications-settings.component';
import { resolveLegacyRole, resolveRoomPermission } from '../../../domains/access-control';
import { GeneralSettingsComponent } from './general-settings/general-settings.component';
import { NetworkSettingsComponent } from './network-settings/network-settings.component';
import { VoiceSettingsComponent } from './voice-settings/voice-settings.component';
import { ServerSettingsComponent } from './server-settings/server-settings.component';
import { MembersSettingsComponent } from './members-settings/members-settings.component';
import { BansSettingsComponent } from './bans-settings/bans-settings.component';
import { PermissionsSettingsComponent } from './permissions-settings/permissions-settings.component';
import { DebuggingSettingsComponent } from './debugging-settings/debugging-settings.component';
import { UpdatesSettingsComponent } from './updates-settings/updates-settings.component';
import { THIRD_PARTY_LICENSES, type ThirdPartyLicense } from './third-party-licenses';
import { ThemeLibraryService, ThemeService } from '../../../domains/theme';
@Component({
selector: 'app-settings-modal',
standalone: true,
imports: [
CommonModule,
FormsModule,
NgIcon,
GeneralSettingsComponent,
NetworkSettingsComponent,
NotificationsSettingsComponent,
VoiceSettingsComponent,
UpdatesSettingsComponent,
DebuggingSettingsComponent,
ServerSettingsComponent,
MembersSettingsComponent,
BansSettingsComponent,
PermissionsSettingsComponent
],
viewProviders: [
provideIcons({
lucideX,
lucideBug,
lucideBell,
lucideDownload,
lucideGlobe,
lucideAudioLines,
lucidePalette,
lucideSettings,
lucideUsers,
lucideBan,
lucideShield
})
],
templateUrl: './settings-modal.component.html'
})
export class SettingsModalComponent {
readonly modal = inject(SettingsModalService);
private store = inject(Store);
private webrtc = inject(RealtimeSessionFacade);
private theme = inject(ThemeService);
private themeLibrary = inject(ThemeLibraryService);
readonly thirdPartyLicenses: readonly ThirdPartyLicense[] = THIRD_PARTY_LICENSES;
private lastRequestedServerId: string | null = null;
private permissionsComponent = viewChild<PermissionsSettingsComponent>('permissionsComp');
savedRooms = this.store.selectSignal(selectSavedRooms);
currentRoom = this.store.selectSignal(selectCurrentRoom);
currentUser = this.store.selectSignal(selectCurrentUser);
isOpen = this.modal.isOpen;
activePage = this.modal.activePage;
themeStudioFullscreen = this.modal.themeStudioFullscreen;
themeStudioMinimized = this.modal.themeStudioMinimized;
isThemeStudioFullscreen = computed(() => this.activePage() === 'theme' && this.themeStudioFullscreen());
activeThemeName = this.theme.activeThemeName;
savedThemesAvailable = this.themeLibrary.isAvailable;
savedThemes = this.themeLibrary.entries;
savedThemesBusy = this.themeLibrary.isBusy;
selectedSavedTheme = this.themeLibrary.selectedEntry;
readonly globalPages: { id: SettingsPage; label: string; icon: string }[] = [
{ id: 'general',
label: 'General',
icon: 'lucideSettings' },
{ id: 'theme',
label: 'Theme Studio',
icon: 'lucidePalette' },
{ id: 'network',
label: 'Network',
icon: 'lucideGlobe' },
{ id: 'notifications',
label: 'Notifications',
icon: 'lucideBell' },
{ id: 'voice',
label: 'Voice & Audio',
icon: 'lucideAudioLines' },
{ id: 'updates',
label: 'Updates',
icon: 'lucideDownload' },
{ id: 'debugging',
label: 'Debugging',
icon: 'lucideBug' }
];
readonly serverPages: { id: SettingsPage; label: string; icon: string }[] = [
{ id: 'server',
label: 'Server',
icon: 'lucideSettings' },
{ id: 'members',
label: 'Members',
icon: 'lucideUsers' },
{ id: 'bans',
label: 'Bans',
icon: 'lucideBan' },
{ id: 'permissions',
label: 'Permissions',
icon: 'lucideShield' }
];
manageableRooms = computed<Room[]>(() => {
const user = this.currentUser();
if (!user)
return [];
return this.savedRooms().filter((room) => {
const viewedRoom = this.currentRoom()?.id === room.id ? this.currentRoom() ?? room : room;
const role = resolveLegacyRole(viewedRoom, user);
return role === 'host'
|| resolveRoomPermission(viewedRoom, user, 'manageServer')
|| resolveRoomPermission(viewedRoom, user, 'manageRoles')
|| resolveRoomPermission(viewedRoom, user, 'manageChannels')
|| resolveRoomPermission(viewedRoom, user, 'manageBans')
|| resolveRoomPermission(viewedRoom, user, 'kickMembers')
|| resolveRoomPermission(viewedRoom, user, 'banMembers');
});
});
selectedServerId = signal<string | null>(null);
selectedServer = computed<Room | null>(() => {
const id = this.selectedServerId();
if (!id)
return null;
return this.manageableRooms().find((room) => room.id === id) ?? null;
});
showServerTabs = computed(() => {
return this.manageableRooms().length > 0 && !!this.selectedServerId();
});
selectedServerRole = computed<UserRole | null>(() => {
const server = this.selectedServer();
const user = this.currentUser();
if (!server || !user)
return null;
return resolveLegacyRole(this.currentRoom()?.id === server.id ? this.currentRoom() ?? server : server, user);
});
canAccessSelectedServer = computed(() => {
const server = this.selectedServer();
const user = this.currentUser();
return !!server && !!user && (
resolveLegacyRole(server, user) === 'host'
|| resolveRoomPermission(server, user, 'manageServer')
|| resolveRoomPermission(server, user, 'manageRoles')
|| resolveRoomPermission(server, user, 'manageChannels')
|| resolveRoomPermission(server, user, 'manageBans')
|| resolveRoomPermission(server, user, 'kickMembers')
|| resolveRoomPermission(server, user, 'banMembers')
);
});
canManageSelectedMembers = computed(() => {
const server = this.selectedServer();
const user = this.currentUser();
return !!server && !!user && (
resolveLegacyRole(server, user) === 'host'
|| resolveRoomPermission(server, user, 'manageRoles')
|| resolveRoomPermission(server, user, 'kickMembers')
|| resolveRoomPermission(server, user, 'banMembers')
);
});
canManageSelectedBans = computed(() => {
const server = this.selectedServer();
const user = this.currentUser();
return !!server && !!user && (
resolveLegacyRole(server, user) === 'host'
|| resolveRoomPermission(server, user, 'manageBans')
);
});
canManageSelectedPermissions = computed(() => {
const server = this.selectedServer();
const user = this.currentUser();
return !!server && !!user && (
resolveLegacyRole(server, user) === 'host'
|| resolveRoomPermission(server, user, 'manageRoles')
|| resolveRoomPermission(server, user, 'manageServer')
);
});
isSelectedServerOwner = computed(() => {
return this.selectedServerRole() === 'host';
});
animating = signal(false);
showThirdPartyLicenses = signal(false);
constructor() {
effect(() => {
if (!this.isOpen()) {
this.lastRequestedServerId = null;
return;
}
const rooms = this.manageableRooms();
const targetId = this.modal.targetServerId();
const currentRoomId = this.currentRoom()?.id ?? null;
const selectedId = this.selectedServerId();
const hasSelected = !!selectedId && rooms.some((room) => room.id === selectedId);
if (!hasSelected) {
const fallbackId = [targetId, currentRoomId].find((candidateId) =>
!!candidateId && rooms.some((room) => room.id === candidateId)
) ?? rooms[0]?.id ?? null;
this.selectedServerId.set(fallbackId);
}
this.animating.set(true);
});
effect(() => {
if (!this.isOpen() || this.activePage() !== 'theme' || !this.savedThemesAvailable()) {
return;
}
untracked(() => {
void this.refreshSavedThemes();
});
});
effect(() => {
const server = this.selectedServer();
if (server) {
const permsComp = this.permissionsComponent();
if (permsComp) {
permsComp.loadPermissions(server);
}
}
});
effect(() => {
if (!this.isOpen())
return;
const serverId = this.selectedServerId();
if (!serverId || this.lastRequestedServerId === serverId)
return;
this.lastRequestedServerId = serverId;
for (const peerId of this.webrtc.getConnectedPeers()) {
try {
this.webrtc.sendToPeer(peerId, {
type: 'server-state-request',
roomId: serverId
});
} catch {
/* peer may have disconnected */
}
}
});
}
@HostListener('document:keydown.escape')
onEscapeKey(): void {
if (this.showThirdPartyLicenses()) {
this.closeThirdPartyLicenses();
return;
}
if (this.isThemeStudioFullscreen()) {
this.modal.minimizeThemeStudio();
return;
}
if (this.isOpen()) {
this.close();
}
}
close(): void {
this.showThirdPartyLicenses.set(false);
this.animating.set(false);
setTimeout(() => this.modal.close(), 200);
}
openThirdPartyLicenses(): void {
this.showThirdPartyLicenses.set(true);
}
closeThirdPartyLicenses(): void {
this.showThirdPartyLicenses.set(false);
}
navigate(page: SettingsPage): void {
this.modal.navigate(page);
}
openThemeStudio(): void {
this.modal.openThemeStudio();
}
async refreshSavedThemes(): Promise<void> {
await this.themeLibrary.refresh();
this.syncSavedThemeSelectionToActiveTheme();
}
async onSavedThemeSelect(event: Event): Promise<void> {
const select = event.target as HTMLSelectElement;
const fileName = select.value || null;
this.themeLibrary.select(fileName);
if (!fileName) {
return;
}
const applied = await this.themeLibrary.useSelectedTheme();
if (!applied) {
this.syncSavedThemeSelectionToActiveTheme();
}
}
async editSelectedSavedTheme(): Promise<void> {
const opened = await this.themeLibrary.openSelectedThemeInDraft();
if (opened) {
this.modal.openThemeStudio();
}
}
onBackdropClick(): void {
this.close();
}
onServerSelect(event: Event): void {
const select = event.target as HTMLSelectElement;
this.selectedServerId.set(select.value || null);
}
restoreDefaultTheme(): void {
this.theme.resetToDefault('button');
this.syncSavedThemeSelectionToActiveTheme();
this.navigate('theme');
}
private syncSavedThemeSelectionToActiveTheme(): void {
const matchingTheme = this.savedThemes().find((entry) => entry.isValid && entry.themeName === this.activeThemeName()) ?? null;
this.themeLibrary.select(matchingTheme?.fileName ?? null);
}
}