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

@@ -18,6 +18,8 @@ export interface ServerInfo {
ownerPublicKey?: string;
userCount: number;
maxUsers: number;
icon?: string;
iconUpdatedAt?: number;
hasPassword?: boolean;
isPrivate: boolean;
tags?: string[];

View File

@@ -101,8 +101,16 @@
(dblclick)="openServerCard(server)"
>
<div class="flex min-w-0 items-start gap-3">
<div class="grid h-10 w-10 shrink-0 place-items-center rounded-lg bg-secondary text-sm font-semibold text-foreground">
{{ server.name[0] || '?' }}
<div class="grid h-10 w-10 shrink-0 place-items-center overflow-hidden rounded-lg bg-secondary text-sm font-semibold text-foreground">
@if (server.icon) {
<img
[src]="server.icon"
[alt]="server.name + ' icon'"
class="h-full w-full object-cover"
/>
} @else {
{{ server.name[0] || '?' }}
}
</div>
<div class="min-w-0 flex-1">

View File

@@ -1,64 +1,30 @@
/* eslint-disable @typescript-eslint/member-ordering */
import {
Component,
effect,
inject,
OnInit,
signal
} from '@angular/core';
import { Component, effect, inject, OnInit, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Router } from '@angular/router';
import { Store } from '@ngrx/store';
import {
debounceTime,
distinctUntilChanged,
firstValueFrom,
Subject
} from 'rxjs';
import { debounceTime, distinctUntilChanged, firstValueFrom, Subject } from 'rxjs';
import { NgIcon, provideIcons } from '@ng-icons/core';
import {
lucideSearch,
lucideUsers,
lucideLock,
lucideGlobe,
lucidePlus,
lucideSettings,
lucideChevronDown
} from '@ng-icons/lucide';
import { lucideSearch, lucideUsers, lucideLock, lucideGlobe, lucidePlus, lucideSettings, lucideChevronDown } from '@ng-icons/lucide';
import { RoomsActions } from '../../../../store/rooms/rooms.actions';
import {
selectSearchResults,
selectIsSearching,
selectRoomsError,
selectSavedRooms
} from '../../../../store/rooms/rooms.selectors';
import { selectSearchResults, selectIsSearching, selectRoomsError, selectSavedRooms } from '../../../../store/rooms/rooms.selectors';
import { Room, User } from '../../../../shared-kernel';
import { SettingsModalService } from '../../../../core/services/settings-modal.service';
import { DatabaseService } from '../../../../infrastructure/persistence';
import { type ServerInfo } from '../../domain/models/server-directory.model';
import { ServerDirectoryFacade } from '../../application/facades/server-directory.facade';
import { selectCurrentUser } from '../../../../store/users/users.selectors';
import {
ConfirmDialogComponent,
LeaveServerDialogComponent,
type LeaveServerDialogResult
} from '../../../../shared';
import { ConfirmDialogComponent, LeaveServerDialogComponent, type LeaveServerDialogResult } from '../../../../shared';
import { hasRoomBanForUser } from '../../../access-control';
import { UserSearchListComponent } from '../../../direct-message/feature/user-search-list/user-search-list.component';
import { RealtimeSessionFacade } from '../../../../core/realtime';
@Component({
selector: 'app-server-search',
standalone: true,
imports: [
CommonModule,
FormsModule,
NgIcon,
ConfirmDialogComponent,
LeaveServerDialogComponent,
UserSearchListComponent
],
imports: [CommonModule, FormsModule, NgIcon, ConfirmDialogComponent, LeaveServerDialogComponent, UserSearchListComponent],
viewProviders: [
provideIcons({
lucideSearch,
@@ -82,6 +48,7 @@ export class ServerSearchComponent implements OnInit {
private settingsModal = inject(SettingsModalService);
private db = inject(DatabaseService);
private serverDirectory = inject(ServerDirectoryFacade);
private webrtc = inject(RealtimeSessionFacade);
private searchSubject = new Subject<string>();
private banLookupRequestVersion = 0;
@@ -118,6 +85,7 @@ export class ServerSearchComponent implements OnInit {
const currentUser = this.currentUser();
void this.refreshBannedLookup(servers, currentUser ?? null);
void this.requestMissingServerIcons(servers, currentUser ?? null);
});
}
@@ -170,8 +138,7 @@ export class ServerSearchComponent implements OnInit {
/** Submit the new server creation form and dispatch the create action. */
createServer(): void {
if (!this.newServerName())
return;
if (!this.newServerName()) return;
const currentUserId = localStorage.getItem('metoyou_currentUserId');
@@ -225,7 +192,7 @@ export class ServerSearchComponent implements OnInit {
toggleJoinedServerMenu(event: Event, server: ServerInfo): void {
event.stopPropagation();
this.joinedServerMenuId.update((currentId) => currentId === server.id ? null : server.id);
this.joinedServerMenuId.update((currentId) => (currentId === server.id ? null : server.id));
}
closeJoinedServerMenu(): void {
@@ -255,10 +222,12 @@ export class ServerSearchComponent implements OnInit {
return;
}
this.store.dispatch(RoomsActions.forgetRoom({
roomId: room.id,
nextOwnerKey: result.nextOwnerKey
}));
this.store.dispatch(
RoomsActions.forgetRoom({
roomId: room.id,
nextOwnerKey: result.nextOwnerKey
})
);
this.leaveDialogRoom.set(null);
}
@@ -278,8 +247,7 @@ export class ServerSearchComponent implements OnInit {
async confirmPasswordJoin(): Promise<void> {
const server = this.passwordPromptServer();
if (!server)
return;
if (!server) return;
await this.attemptJoinServer(server, this.joinPassword());
}
@@ -291,8 +259,7 @@ export class ServerSearchComponent implements OnInit {
getServerUserCount(server: ServerInfo): number {
const candidate = server as ServerInfo & { currentUsers?: number };
if (typeof server.userCount === 'number')
return server.userCount;
if (typeof server.userCount === 'number') return server.userCount;
return typeof candidate.currentUsers === 'number' ? candidate.currentUsers : 0;
}
@@ -304,9 +271,7 @@ export class ServerSearchComponent implements OnInit {
getServerOwnerLabel(server: ServerInfo): string {
const joinedRoom = this.joinedRoomForServer(server);
const ownerKey = server.ownerId || joinedRoom?.hostId || '';
const ownerMember = joinedRoom?.members?.find((member) =>
member.id === ownerKey || member.oderId === ownerKey
);
const ownerMember = joinedRoom?.members?.find((member) => member.id === ownerKey || member.oderId === ownerKey);
return server.ownerName || ownerMember?.displayName || server.ownerId || joinedRoom?.hostId || 'Unknown owner';
}
@@ -324,6 +289,8 @@ export class ServerSearchComponent implements OnInit {
hostName: room.hostId || 'Unknown',
userCount: room.userCount ?? 0,
maxUsers: room.maxUsers ?? 50,
icon: room.icon,
iconUpdatedAt: room.iconUpdatedAt,
hasPassword: typeof room.hasPassword === 'boolean' ? room.hasPassword : !!room.password,
isPrivate: room.isPrivate,
channels: room.channels,
@@ -348,32 +315,37 @@ export class ServerSearchComponent implements OnInit {
this.joinPasswordError.set(null);
try {
const response = await firstValueFrom(this.serverDirectory.requestJoin({
roomId: server.id,
userId: currentUserId,
userPublicKey: currentUser?.oderId || currentUserId,
displayName: currentUser?.displayName || 'Anonymous',
password: password?.trim() || undefined
}, {
sourceId: server.sourceId,
sourceUrl: server.sourceUrl
}));
const resolvedSource = this.serverDirectory.normaliseRoomSignalSource({
sourceId: response.server.sourceId ?? server.sourceId,
sourceName: response.server.sourceName ?? server.sourceName,
sourceUrl: response.server.sourceUrl ?? server.sourceUrl,
signalingUrl: response.signalingUrl,
fallbackName: response.server.sourceName ?? server.sourceName ?? server.name
}, {
ensureEndpoint: true
});
const response = await firstValueFrom(
this.serverDirectory.requestJoin(
{
roomId: server.id,
userId: currentUserId,
userPublicKey: currentUser?.oderId || currentUserId,
displayName: currentUser?.displayName || 'Anonymous',
password: password?.trim() || undefined
},
{
sourceId: server.sourceId,
sourceUrl: server.sourceUrl
}
)
);
const resolvedSource = this.serverDirectory.normaliseRoomSignalSource(
{
sourceId: response.server.sourceId ?? server.sourceId,
sourceName: response.server.sourceName ?? server.sourceName,
sourceUrl: response.server.sourceUrl ?? server.sourceUrl,
signalingUrl: response.signalingUrl,
fallbackName: response.server.sourceName ?? server.sourceName ?? server.name
},
{
ensureEndpoint: true
}
);
const resolvedServer = {
...server,
...response.server,
channels:
Array.isArray(response.server.channels) && response.server.channels.length > 0
? response.server.channels
: server.channels,
channels: Array.isArray(response.server.channels) && response.server.channels.length > 0 ? response.server.channels : server.channels,
...resolvedSource,
signalingUrl: response.signalingUrl
};
@@ -409,6 +381,53 @@ export class ServerSearchComponent implements OnInit {
}
}
private async requestMissingServerIcons(servers: ServerInfo[], currentUser: User | null): Promise<void> {
if (!currentUser) {
return;
}
for (const server of servers) {
if (server.icon) {
continue;
}
const selector = this.serverDirectory.buildRoomSignalSelector(
{
sourceId: server.sourceId,
sourceName: server.sourceName,
sourceUrl: server.sourceUrl,
fallbackName: server.sourceName ?? server.name
},
{
ensureEndpoint: !!server.sourceUrl
}
);
if (!selector) {
continue;
}
const wsUrl = this.serverDirectory.getWebSocketUrl(selector);
try {
await firstValueFrom(this.webrtc.connectToSignalingServer(wsUrl));
this.webrtc.identify(currentUser.oderId || currentUser.id, currentUser.displayName || 'User', wsUrl, {
description: currentUser.description,
profileUpdatedAt: currentUser.profileUpdatedAt
});
this.webrtc.joinRoom(server.id, currentUser.oderId || currentUser.id, wsUrl);
this.webrtc.sendRawMessage({
type: 'server_icon_sync_request',
serverId: server.id,
iconUpdatedAt: 0
});
window.setTimeout(() => this.webrtc.leaveRoom(server.id), 15_000);
} catch {
/* discovery icons are best-effort */
}
}
}
private async refreshBannedLookup(servers: ServerInfo[], currentUser: User | null): Promise<void> {
const requestVersion = ++this.banLookupRequestVersion;
@@ -427,8 +446,7 @@ export class ServerSearchComponent implements OnInit {
})
);
if (requestVersion !== this.banLookupRequestVersion)
return;
if (requestVersion !== this.banLookupRequestVersion) return;
this.bannedServerLookup.set(Object.fromEntries(entries));
}
@@ -437,8 +455,7 @@ export class ServerSearchComponent implements OnInit {
const currentUser = this.currentUser();
const currentUserId = localStorage.getItem('metoyou_currentUserId');
if (!currentUser && !currentUserId)
return false;
if (!currentUser && !currentUserId) return false;
const bans = await this.db.getBansForRoom(server.id);