fix: improve plugins functionality with server management
This commit is contained in:
@@ -103,11 +103,11 @@
|
||||
<div class="flex min-w-0 items-start gap-3">
|
||||
<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"
|
||||
/>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="h-full w-full bg-cover bg-center bg-no-repeat"
|
||||
[style.backgroundImage]="'url(' + server.icon + ')'"
|
||||
></div>
|
||||
} @else {
|
||||
{{ server.name[0] || '?' }}
|
||||
}
|
||||
@@ -297,6 +297,96 @@
|
||||
</app-confirm-dialog>
|
||||
}
|
||||
|
||||
@if (pluginConsentDialog(); as dialog) {
|
||||
<div
|
||||
class="fixed inset-0 z-50 bg-black/50"
|
||||
role="presentation"
|
||||
></div>
|
||||
<section
|
||||
class="fixed left-1/2 top-1/2 z-[51] flex max-h-[min(42rem,calc(100vh-2rem))] w-[min(34rem,calc(100vw-2rem))] -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-lg border border-border bg-card text-foreground shadow-2xl"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="join-plugin-consent-title"
|
||||
>
|
||||
<header class="border-b border-border p-4">
|
||||
<p class="text-sm text-muted-foreground">Plugin downloads</p>
|
||||
<h2
|
||||
id="join-plugin-consent-title"
|
||||
class="mt-1 text-lg font-semibold"
|
||||
>
|
||||
{{ dialog.server.name }} uses plugins
|
||||
</h2>
|
||||
</header>
|
||||
|
||||
<div class="grid min-h-0 gap-4 overflow-auto p-4">
|
||||
@if (dialog.required.length > 0) {
|
||||
<section class="grid gap-2">
|
||||
<h3 class="text-sm font-semibold">Required before joining</h3>
|
||||
@for (requirement of dialog.required; track requirement.pluginId) {
|
||||
<div class="rounded-lg border border-border bg-background/50 px-3 py-2">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0">
|
||||
<p class="truncate text-sm font-semibold">{{ requirement.manifest?.title || requirement.pluginId }}</p>
|
||||
@if (requirement.reason) {
|
||||
<p class="mt-1 text-xs text-muted-foreground">{{ requirement.reason }}</p>
|
||||
}
|
||||
</div>
|
||||
<span class="shrink-0 rounded-full bg-primary/10 px-2 py-0.5 text-xs font-semibold text-primary">Required</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
}
|
||||
|
||||
@if (dialog.optional.length > 0) {
|
||||
<section class="grid gap-2">
|
||||
<h3 class="text-sm font-semibold">Optional plugins</h3>
|
||||
@for (requirement of dialog.optional; track requirement.pluginId) {
|
||||
<label class="flex items-start gap-3 rounded-lg border border-border bg-background/50 px-3 py-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="mt-1 h-4 w-4 rounded border-border bg-secondary"
|
||||
[checked]="selectedOptionalPluginIds().has(requirement.pluginId)"
|
||||
[disabled]="pluginConsentBusy()"
|
||||
(change)="toggleOptionalPluginInstall(requirement.pluginId, $any($event.target).checked)"
|
||||
/>
|
||||
<span class="min-w-0 flex-1">
|
||||
<span class="block truncate text-sm font-semibold">{{ requirement.manifest?.title || requirement.pluginId }}</span>
|
||||
@if (requirement.reason) {
|
||||
<span class="mt-1 block text-xs text-muted-foreground">{{ requirement.reason }}</span>
|
||||
}
|
||||
</span>
|
||||
</label>
|
||||
}
|
||||
</section>
|
||||
}
|
||||
|
||||
@if (pluginConsentError()) {
|
||||
<p class="rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">{{ pluginConsentError() }}</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
<footer class="flex justify-end gap-2 border-t border-border p-4">
|
||||
<button
|
||||
type="button"
|
||||
(click)="closePluginConsentDialog()"
|
||||
[disabled]="pluginConsentBusy()"
|
||||
class="inline-flex min-h-8 items-center justify-center rounded-lg border border-border bg-card px-3 py-1.5 text-sm font-semibold transition-colors hover:bg-secondary disabled:cursor-not-allowed disabled:opacity-55"
|
||||
>
|
||||
Cancel join
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
(click)="confirmPluginConsent()"
|
||||
[disabled]="pluginConsentBusy()"
|
||||
class="inline-flex min-h-8 items-center justify-center rounded-lg border border-primary bg-primary px-3 py-1.5 text-sm font-semibold text-primary-foreground transition-colors hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-55"
|
||||
>
|
||||
{{ pluginConsentBusy() ? 'Downloading' : dialog.required.length > 0 ? 'Accept and join' : 'Join' }}
|
||||
</button>
|
||||
</footer>
|
||||
</section>
|
||||
}
|
||||
|
||||
<!-- Create Server Dialog -->
|
||||
@if (showCreateDialog()) {
|
||||
<div
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user