fix: improve plugins functionality with server management

This commit is contained in:
2026-04-29 20:33:54 +02:00
parent b8f6d58d99
commit fa2cca6fa4
82 changed files with 1708 additions and 303 deletions

View File

@@ -1,30 +1,77 @@
/* 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 { Room, User } from '../../../../shared-kernel';
import {
selectSearchResults,
selectIsSearching,
selectRoomsError,
selectSavedRooms
} from '../../../../store/rooms/rooms.selectors';
import {
Room,
User,
type PluginRequirementSummary
} 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';
import { PluginRequirementService, PluginStoreService } from '../../../plugins';
interface JoinPluginConsentDialog {
optional: PluginRequirementSummary[];
password?: string;
required: PluginRequirementSummary[];
server: ServerInfo;
}
@Component({
selector: 'app-server-search',
standalone: true,
imports: [CommonModule, FormsModule, NgIcon, ConfirmDialogComponent, LeaveServerDialogComponent, UserSearchListComponent],
imports: [
CommonModule,
FormsModule,
NgIcon,
ConfirmDialogComponent,
LeaveServerDialogComponent,
UserSearchListComponent
],
viewProviders: [
provideIcons({
lucideSearch,
@@ -49,6 +96,8 @@ export class ServerSearchComponent implements OnInit {
private db = inject(DatabaseService);
private serverDirectory = inject(ServerDirectoryFacade);
private webrtc = inject(RealtimeSessionFacade);
private pluginRequirements = inject(PluginRequirementService);
private pluginStore = inject(PluginStoreService);
private searchSubject = new Subject<string>();
private banLookupRequestVersion = 0;
@@ -69,6 +118,10 @@ export class ServerSearchComponent implements OnInit {
joinErrorMessage = signal<string | null>(null);
joinedServerMenuId = signal<string | null>(null);
leaveDialogRoom = signal<Room | null>(null);
pluginConsentDialog = signal<JoinPluginConsentDialog | null>(null);
selectedOptionalPluginIds = signal<Set<string>>(new Set());
pluginConsentBusy = signal(false);
pluginConsentError = signal<string | null>(null);
// Create dialog state
showCreateDialog = signal(false);
@@ -138,7 +191,8 @@ 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');
@@ -244,10 +298,65 @@ export class ServerSearchComponent implements OnInit {
this.joinPasswordError.set(null);
}
closePluginConsentDialog(): void {
if (this.pluginConsentBusy()) {
return;
}
this.pluginConsentDialog.set(null);
this.selectedOptionalPluginIds.set(new Set());
this.pluginConsentError.set(null);
}
toggleOptionalPluginInstall(pluginId: string, checked: boolean): void {
this.selectedOptionalPluginIds.update((selectedIds) => {
const nextIds = new Set(selectedIds);
if (checked) {
nextIds.add(pluginId);
} else {
nextIds.delete(pluginId);
}
return nextIds;
});
}
async confirmPluginConsent(): Promise<void> {
const dialog = this.pluginConsentDialog();
if (!dialog) {
return;
}
const selectedOptionalIds = this.selectedOptionalPluginIds();
const acceptedRequirements = dialog.required.concat(
dialog.optional.filter((requirement) => selectedOptionalIds.has(requirement.pluginId))
);
this.pluginConsentBusy.set(true);
this.pluginConsentError.set(null);
try {
await this.attemptJoinServer(dialog.server, dialog.password, {
acceptedRequirements,
skipPluginConsent: true
});
this.pluginConsentDialog.set(null);
this.selectedOptionalPluginIds.set(new Set());
} catch (error) {
this.pluginConsentError.set(error instanceof Error ? error.message : 'Unable to install server plugins');
} finally {
this.pluginConsentBusy.set(false);
}
}
async confirmPasswordJoin(): Promise<void> {
const server = this.passwordPromptServer();
if (!server) return;
if (!server)
return;
await this.attemptJoinServer(server, this.joinPassword());
}
@@ -259,7 +368,8 @@ 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;
}
@@ -302,7 +412,11 @@ export class ServerSearchComponent implements OnInit {
};
}
private async attemptJoinServer(server: ServerInfo, password?: string): Promise<void> {
private async attemptJoinServer(
server: ServerInfo,
password?: string,
options: { acceptedRequirements?: PluginRequirementSummary[]; skipPluginConsent?: boolean } = {}
): Promise<void> {
const currentUserId = localStorage.getItem('metoyou_currentUserId');
const currentUser = this.currentUser();
@@ -315,6 +429,16 @@ export class ServerSearchComponent implements OnInit {
this.joinPasswordError.set(null);
try {
if (options.skipPluginConsent !== true) {
const consentDialog = await this.buildPluginConsentDialog(server, password);
if (consentDialog) {
this.pluginConsentDialog.set(consentDialog);
this.selectedOptionalPluginIds.set(new Set(consentDialog.optional.map((requirement) => requirement.pluginId)));
return;
}
}
const response = await firstValueFrom(
this.serverDirectory.requestJoin(
{
@@ -351,6 +475,11 @@ export class ServerSearchComponent implements OnInit {
};
this.closePasswordDialog();
if (options.acceptedRequirements?.length) {
await this.pluginStore.installServerRequirementsLocally(resolvedServer.id, options.acceptedRequirements, { activate: true });
}
this.store.dispatch(
RoomsActions.joinRoom({
roomId: resolvedServer.id,
@@ -378,9 +507,40 @@ export class ServerSearchComponent implements OnInit {
}
this.joinErrorMessage.set(message);
if (options.skipPluginConsent) {
throw new Error(message);
}
}
}
private async buildPluginConsentDialog(server: ServerInfo, password?: string): Promise<JoinPluginConsentDialog | null> {
const apiBaseUrl = this.serverDirectory.getApiBaseUrl({
sourceId: server.sourceId,
sourceUrl: server.sourceUrl
});
const snapshot = await firstValueFrom(this.pluginRequirements.getSnapshot(apiBaseUrl, server.id));
const installedPluginIds = await this.pluginStore.getLocalServerInstalledPluginIds(server.id);
const installableRequirements = snapshot.requirements
.filter((requirement) => !installedPluginIds.has(requirement.pluginId))
.filter((requirement) => !!requirement.manifest || !!requirement.installUrl);
const required = installableRequirements.filter((requirement) => requirement.status === 'required');
const optional = installableRequirements.filter(
(requirement) => requirement.status === 'optional' || requirement.status === 'recommended'
);
if (required.length === 0 && optional.length === 0) {
return null;
}
return {
optional,
password,
required,
server
};
}
private async requestMissingServerIcons(servers: ServerInfo[], currentUser: User | null): Promise<void> {
if (!currentUser) {
return;
@@ -415,6 +575,7 @@ export class ServerSearchComponent implements OnInit {
description: currentUser.description,
profileUpdatedAt: currentUser.profileUpdatedAt
});
this.webrtc.sendRawMessageToSignalUrl(wsUrl, {
type: 'server_icon_sync_request',
serverId: server.id,
@@ -444,7 +605,8 @@ export class ServerSearchComponent implements OnInit {
})
);
if (requestVersion !== this.banLookupRequestVersion) return;
if (requestVersion !== this.banLookupRequestVersion)
return;
this.bannedServerLookup.set(Object.fromEntries(entries));
}
@@ -453,7 +615,8 @@ 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);