feat: server image
This commit is contained in:
@@ -6,8 +6,16 @@
|
||||
>
|
||||
@if (panelMode() === 'channels') {
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="grid h-9 w-9 place-items-center rounded-md bg-secondary text-sm font-semibold text-foreground">
|
||||
{{ currentRoom()?.name?.charAt(0)?.toUpperCase() || '#' }}
|
||||
<div class="grid h-9 w-9 place-items-center overflow-hidden rounded-md bg-secondary text-sm font-semibold text-foreground">
|
||||
@if (currentRoom()?.icon) {
|
||||
<img
|
||||
[src]="currentRoom()!.icon"
|
||||
[alt]="currentRoom()!.name + ' icon'"
|
||||
class="h-full w-full object-cover"
|
||||
/>
|
||||
} @else {
|
||||
{{ currentRoom()?.name?.charAt(0)?.toUpperCase() || '#' }}
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="min-w-0 flex-1">
|
||||
|
||||
@@ -43,8 +43,8 @@
|
||||
<div class="h-full w-full overflow-hidden rounded-[inherit]">
|
||||
@if (room.icon) {
|
||||
<img
|
||||
[ngSrc]="room.icon"
|
||||
[alt]="room.name"
|
||||
[src]="room.icon"
|
||||
[alt]="room.name + ' icon'"
|
||||
class="h-full w-full object-cover"
|
||||
/>
|
||||
} @else {
|
||||
|
||||
@@ -1,31 +1,13 @@
|
||||
/* eslint-disable @typescript-eslint/member-ordering */
|
||||
import {
|
||||
Component,
|
||||
DestroyRef,
|
||||
Type,
|
||||
computed,
|
||||
effect,
|
||||
inject,
|
||||
signal
|
||||
} from '@angular/core';
|
||||
import { Component, DestroyRef, Type, computed, effect, inject, signal } from '@angular/core';
|
||||
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
||||
import { CommonModule, NgOptimizedImage } from '@angular/common';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { NavigationEnd, Router } from '@angular/router';
|
||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
import { lucidePlus } from '@ng-icons/lucide';
|
||||
import {
|
||||
EMPTY,
|
||||
Subject,
|
||||
catchError,
|
||||
filter,
|
||||
firstValueFrom,
|
||||
from,
|
||||
map,
|
||||
switchMap,
|
||||
tap
|
||||
} from 'rxjs';
|
||||
import { EMPTY, Subject, catchError, filter, firstValueFrom, from, map, switchMap, tap } from 'rxjs';
|
||||
|
||||
import { Room, User } from '../../../shared-kernel';
|
||||
import { UserBarComponent } from '../../../domains/authentication/feature/user-bar/user-bar.component';
|
||||
@@ -38,11 +20,7 @@ import { NotificationsFacade } from '../../../domains/notifications';
|
||||
import { type ServerInfo, ServerDirectoryFacade } from '../../../domains/server-directory';
|
||||
import { ThemeNodeDirective } from '../../../domains/theme';
|
||||
import { hasRoomBanForUser } from '../../../domains/access-control';
|
||||
import {
|
||||
ConfirmDialogComponent,
|
||||
ContextMenuComponent,
|
||||
LeaveServerDialogComponent
|
||||
} from '../../../shared';
|
||||
import { ConfirmDialogComponent, ContextMenuComponent, LeaveServerDialogComponent } from '../../../shared';
|
||||
|
||||
@Component({
|
||||
selector: 'app-servers-rail',
|
||||
@@ -54,7 +32,6 @@ import {
|
||||
ConfirmDialogComponent,
|
||||
ContextMenuComponent,
|
||||
LeaveServerDialogComponent,
|
||||
NgOptimizedImage,
|
||||
ThemeNodeDirective,
|
||||
UserBarComponent
|
||||
],
|
||||
@@ -166,8 +143,7 @@ export class ServersRailComponent {
|
||||
}
|
||||
|
||||
initial(name?: string): string {
|
||||
if (!name)
|
||||
return '?';
|
||||
if (!name) return '?';
|
||||
|
||||
const ch = name.trim()[0]?.toUpperCase();
|
||||
|
||||
@@ -219,8 +195,7 @@ export class ServersRailComponent {
|
||||
confirmPasswordJoin(): void {
|
||||
const room = this.passwordPromptRoom();
|
||||
|
||||
if (!room)
|
||||
return;
|
||||
if (!room) return;
|
||||
|
||||
this.joinPasswordError.set(null);
|
||||
this.savedRoomJoinRequests.next({ room, password: this.joinPassword() });
|
||||
@@ -260,8 +235,7 @@ export class ServersRailComponent {
|
||||
confirmLeave(result: { nextOwnerKey?: string }): void {
|
||||
const ctx = this.contextRoom();
|
||||
|
||||
if (!ctx)
|
||||
return;
|
||||
if (!ctx) return;
|
||||
|
||||
const isCurrentRoom = this.currentRoom()?.id === ctx.id;
|
||||
|
||||
@@ -364,8 +338,7 @@ export class ServersRailComponent {
|
||||
const currentUserId = localStorage.getItem('metoyou_currentUserId');
|
||||
const currentUser = this.currentUser();
|
||||
|
||||
if (!currentUserId)
|
||||
return EMPTY;
|
||||
if (!currentUserId) return EMPTY;
|
||||
|
||||
this.joinPasswordError.set(null);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -316,7 +316,9 @@
|
||||
@case ('server') {
|
||||
<app-server-settings
|
||||
[server]="selectedServer()"
|
||||
[isAdmin]="isSelectedServerOwner()"
|
||||
[isAdmin]="canManageSelectedServerSettings()"
|
||||
[canManageIcon]="canManageSelectedServerIcon()"
|
||||
[canDeleteServer]="isSelectedServerOwner()"
|
||||
/>
|
||||
}
|
||||
@case ('serverPlugins') {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user