feat: server image

This commit is contained in:
2026-04-29 18:54:08 +02:00
parent 3d81c34159
commit e1ac1d1bc0
27 changed files with 1340 additions and 615 deletions

View File

@@ -3,11 +3,74 @@
<section>
<h4 class="text-sm font-semibold text-foreground mb-3">Room Settings</h4>
@if (!isAdmin()) {
<p class="text-xs text-muted-foreground mb-3">
You are viewing this server's settings as a non-admin. Only the server owner can make changes.
</p>
<p class="text-xs text-muted-foreground mb-3">You are viewing this server's details without server-management permission.</p>
}
<div class="space-y-4">
<div class="rounded-lg border border-border bg-secondary/40 p-4">
<div class="flex items-center gap-3">
<div class="grid h-14 w-14 shrink-0 place-items-center overflow-hidden rounded-lg bg-secondary text-base font-semibold text-foreground">
@if (serverData()?.icon) {
<img
[src]="serverData()!.icon"
[alt]="serverData()!.name + ' icon'"
class="h-full w-full object-cover"
/>
} @else {
<ng-icon
name="lucideImage"
class="h-6 w-6 text-muted-foreground"
/>
}
</div>
<div class="min-w-0 flex-1">
<p class="text-sm font-medium text-foreground">Server Image</p>
<p class="text-xs text-muted-foreground">Synced to members and shown in server discovery.</p>
@if (iconError()) {
<p class="mt-1 text-xs text-destructive">{{ iconError() }}</p>
}
</div>
@if (canManageIcon()) {
<div class="flex shrink-0 items-center gap-2">
<label
for="server-icon-upload"
class="grid h-9 w-9 cursor-pointer place-items-center rounded-lg border border-border bg-card text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground"
title="Upload image"
aria-label="Upload server image"
>
<ng-icon
name="lucideUpload"
class="h-4 w-4"
/>
</label>
<input
id="server-icon-upload"
type="file"
accept="image/*"
class="sr-only"
(change)="onServerIconSelected($event)"
/>
@if (serverData()?.icon) {
<button
type="button"
class="grid h-9 w-9 place-items-center rounded-lg border border-border bg-card text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground"
title="Remove image"
aria-label="Remove server image"
(click)="removeServerIcon()"
>
<ng-icon
name="lucideX"
class="h-4 w-4"
/>
</button>
}
</div>
}
</div>
</div>
<div>
<label
for="room-name"
@@ -191,21 +254,23 @@
{{ saveSuccess() === 'server' ? 'Saved!' : 'Save Settings' }}
</button>
<!-- Danger Zone -->
<div class="pt-4 border-t border-border">
<h4 class="text-sm font-medium text-destructive mb-3">Danger Zone</h4>
<button
(click)="confirmDeleteRoom()"
type="button"
class="w-full px-4 py-2 bg-destructive/10 text-destructive border border-destructive/20 rounded-lg hover:bg-destructive/20 transition-colors flex items-center justify-center gap-2 text-sm"
>
<ng-icon
name="lucideTrash2"
class="w-4 h-4"
/>
Delete Room
</button>
</div>
@if (canDeleteServer()) {
<!-- Danger Zone -->
<div class="pt-4 border-t border-border">
<h4 class="text-sm font-medium text-destructive mb-3">Danger Zone</h4>
<button
(click)="confirmDeleteRoom()"
type="button"
class="w-full px-4 py-2 bg-destructive/10 text-destructive border border-destructive/20 rounded-lg hover:bg-destructive/20 transition-colors flex items-center justify-center gap-2 text-sm"
>
<ng-icon
name="lucideTrash2"
class="w-4 h-4"
/>
Delete Room
</button>
</div>
}
}
</div>

View File

@@ -12,9 +12,12 @@ import { NgIcon, provideIcons } from '@ng-icons/core';
import { Store } from '@ngrx/store';
import {
lucideCheck,
lucideImage,
lucideTrash2,
lucideLock,
lucideUnlock
lucideUnlock,
lucideUpload,
lucideX
} from '@ng-icons/lucide';
import { Room } from '../../../../shared-kernel';
@@ -34,9 +37,12 @@ import { SettingsModalService } from '../../../../core/services/settings-modal.s
viewProviders: [
provideIcons({
lucideCheck,
lucideImage,
lucideTrash2,
lucideLock,
lucideUnlock
lucideUnlock,
lucideUpload,
lucideX
})
],
templateUrl: './server-settings.component.html'
@@ -49,6 +55,10 @@ export class ServerSettingsComponent {
server = input<Room | null>(null);
/** Whether the current user is admin of this server. */
isAdmin = input(false);
/** Whether the current user can manage this server's icon. */
canManageIcon = input(false);
/** Whether the current user can delete this server. */
canDeleteServer = input(false);
roomName = '';
roomDescription = '';
@@ -59,6 +69,7 @@ export class ServerSettingsComponent {
roomPassword = '';
maxUsers = 0;
showDeleteConfirm = signal(false);
iconError = signal<string | null>(null);
saveSuccess = signal<string | null>(null);
private saveTimeout: ReturnType<typeof setTimeout> | null = null;
@@ -170,6 +181,64 @@ export class ServerSettingsComponent {
this.modal.navigate('network');
}
onServerIconSelected(event: Event): void {
const inputElement = event.target as HTMLInputElement;
const file = inputElement.files?.[0];
inputElement.value = '';
if (!file || !this.canManageIcon()) {
return;
}
if (!file.type.startsWith('image/')) {
this.iconError.set('Choose an image file.');
return;
}
if (file.size > 512 * 1024) {
this.iconError.set('Choose an image smaller than 512 KB.');
return;
}
const reader = new FileReader();
reader.onload = () => {
const room = this.server();
const icon = typeof reader.result === 'string' ? reader.result : '';
if (!room || !icon) {
this.iconError.set('Could not read that image.');
return;
}
this.iconError.set(null);
this.store.dispatch(RoomsActions.updateServerIcon({
roomId: room.id,
icon
}));
this.showSaveSuccess('icon');
};
reader.onerror = () => this.iconError.set('Could not read that image.');
reader.readAsDataURL(file);
}
removeServerIcon(): void {
const room = this.server();
if (!room || !this.canManageIcon()) {
return;
}
this.iconError.set(null);
this.store.dispatch(RoomsActions.updateServerIcon({
roomId: room.id,
icon: ''
}));
this.showSaveSuccess('icon');
}
private showSaveSuccess(key: string): void {
this.saveSuccess.set(key);

View File

@@ -316,7 +316,9 @@
@case ('server') {
<app-server-settings
[server]="selectedServer()"
[isAdmin]="isSelectedServerOwner()"
[isAdmin]="canManageSelectedServerSettings()"
[canManageIcon]="canManageSelectedServerIcon()"
[canDeleteServer]="isSelectedServerOwner()"
/>
}
@case ('serverPlugins') {

View File

@@ -165,6 +165,7 @@ export class SettingsModalComponent {
resolveRoomPermission(viewedRoom, user, 'manageServer') ||
resolveRoomPermission(viewedRoom, user, 'manageRoles') ||
resolveRoomPermission(viewedRoom, user, 'manageChannels') ||
resolveRoomPermission(viewedRoom, user, 'manageIcon') ||
resolveRoomPermission(viewedRoom, user, 'manageBans') ||
resolveRoomPermission(viewedRoom, user, 'kickMembers') ||
resolveRoomPermission(viewedRoom, user, 'banMembers')
@@ -208,6 +209,7 @@ export class SettingsModalComponent {
resolveRoomPermission(server, user, 'manageServer') ||
resolveRoomPermission(server, user, 'manageRoles') ||
resolveRoomPermission(server, user, 'manageChannels') ||
resolveRoomPermission(server, user, 'manageIcon') ||
resolveRoomPermission(server, user, 'manageBans') ||
resolveRoomPermission(server, user, 'kickMembers') ||
resolveRoomPermission(server, user, 'banMembers'))
@@ -252,6 +254,20 @@ export class SettingsModalComponent {
return this.selectedServerRole() === 'host';
});
canManageSelectedServerSettings = computed(() => {
const server = this.selectedServer();
const user = this.currentUser();
return !!server && !!user && (resolveLegacyRole(server, user) === 'host' || resolveRoomPermission(server, user, 'manageServer'));
});
canManageSelectedServerIcon = computed(() => {
const server = this.selectedServer();
const user = this.currentUser();
return !!server && !!user && (resolveLegacyRole(server, user) === 'host' || resolveRoomPermission(server, user, 'manageIcon'));
});
isSelectedServerCurrent = computed(() => {
const selectedServerId = this.selectedServerId();
const currentRoomId = this.currentRoom()?.id ?? null;