fix: browser bug with plugins, and improve joining
This commit is contained in:
@@ -323,7 +323,7 @@
|
||||
<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="grid gap-3 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>
|
||||
@@ -333,6 +333,48 @@
|
||||
</div>
|
||||
<span class="shrink-0 rounded-full bg-primary/10 px-2 py-0.5 text-xs font-semibold text-primary">Required</span>
|
||||
</div>
|
||||
|
||||
@if (requirement.manifest?.capabilities; as capabilities) {
|
||||
<details class="rounded-md border border-border bg-secondary/40 px-2 py-1.5 text-xs text-muted-foreground">
|
||||
<summary class="cursor-pointer font-semibold text-foreground">Capabilities</summary>
|
||||
<div class="mt-2 flex flex-wrap gap-1.5">
|
||||
@for (capability of capabilities; track capability) {
|
||||
<span class="rounded-full bg-background px-2 py-0.5 font-mono text-[11px] text-muted-foreground">{{ capability }}</span>
|
||||
}
|
||||
</div>
|
||||
</details>
|
||||
}
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
@if (getPluginSourceUrl(requirement)) {
|
||||
<button
|
||||
type="button"
|
||||
(click)="openPluginSource(requirement)"
|
||||
class="inline-flex min-h-8 items-center justify-center gap-2 rounded-lg border border-border bg-card px-3 py-1.5 text-xs font-semibold transition-colors hover:bg-secondary"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideExternalLink"
|
||||
class="h-3.5 w-3.5"
|
||||
/>
|
||||
Source
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (hasPluginReadme(requirement)) {
|
||||
<button
|
||||
type="button"
|
||||
(click)="openPluginConsentReadme(requirement)"
|
||||
[disabled]="pluginConsentReadmeLoadingId() === requirement.pluginId"
|
||||
class="inline-flex min-h-8 items-center justify-center gap-2 rounded-lg border border-border bg-card px-3 py-1.5 text-xs font-semibold transition-colors hover:bg-secondary disabled:cursor-not-allowed disabled:opacity-55"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideFileText"
|
||||
class="h-3.5 w-3.5"
|
||||
/>
|
||||
{{ pluginConsentReadmeLoadingId() === requirement.pluginId ? 'Loading' : 'Readme' }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
@@ -342,25 +384,73 @@
|
||||
<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>
|
||||
<div class="grid gap-3 rounded-lg border border-border bg-background/50 px-3 py-2">
|
||||
<label class="flex items-start gap-3">
|
||||
<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>
|
||||
|
||||
@if (requirement.manifest?.capabilities; as capabilities) {
|
||||
<details class="rounded-md border border-border bg-secondary/40 px-2 py-1.5 text-xs text-muted-foreground">
|
||||
<summary class="cursor-pointer font-semibold text-foreground">Capabilities</summary>
|
||||
<div class="mt-2 flex flex-wrap gap-1.5">
|
||||
@for (capability of capabilities; track capability) {
|
||||
<span class="rounded-full bg-background px-2 py-0.5 font-mono text-[11px] text-muted-foreground">{{ capability }}</span>
|
||||
}
|
||||
</div>
|
||||
</details>
|
||||
}
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
@if (getPluginSourceUrl(requirement)) {
|
||||
<button
|
||||
type="button"
|
||||
(click)="openPluginSource(requirement)"
|
||||
class="inline-flex min-h-8 items-center justify-center gap-2 rounded-lg border border-border bg-card px-3 py-1.5 text-xs font-semibold transition-colors hover:bg-secondary"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideExternalLink"
|
||||
class="h-3.5 w-3.5"
|
||||
/>
|
||||
Source
|
||||
</button>
|
||||
}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
@if (hasPluginReadme(requirement)) {
|
||||
<button
|
||||
type="button"
|
||||
(click)="openPluginConsentReadme(requirement)"
|
||||
[disabled]="pluginConsentReadmeLoadingId() === requirement.pluginId"
|
||||
class="inline-flex min-h-8 items-center justify-center gap-2 rounded-lg border border-border bg-card px-3 py-1.5 text-xs font-semibold transition-colors hover:bg-secondary disabled:cursor-not-allowed disabled:opacity-55"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideFileText"
|
||||
class="h-3.5 w-3.5"
|
||||
/>
|
||||
{{ pluginConsentReadmeLoadingId() === requirement.pluginId ? 'Loading' : 'Readme' }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
}
|
||||
|
||||
@if (pluginConsentReadmeError()) {
|
||||
<p class="rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">{{ pluginConsentReadmeError() }}</p>
|
||||
}
|
||||
|
||||
@if (pluginConsentError()) {
|
||||
<p class="rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">{{ pluginConsentError() }}</p>
|
||||
}
|
||||
@@ -385,6 +475,46 @@
|
||||
</button>
|
||||
</footer>
|
||||
</section>
|
||||
|
||||
@if (pluginConsentReadme(); as readme) {
|
||||
<div
|
||||
class="fixed inset-0 z-[52] bg-black/60"
|
||||
role="presentation"
|
||||
(click)="closePluginConsentReadme()"
|
||||
></div>
|
||||
<section
|
||||
class="fixed left-1/2 top-1/2 z-[53] flex max-h-[min(44rem,calc(100vh-2rem))] w-[min(44rem,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-readme-title"
|
||||
>
|
||||
<header class="flex items-start justify-between gap-3 border-b border-border p-4">
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm text-muted-foreground">Plugin readme</p>
|
||||
<h2
|
||||
id="join-plugin-readme-title"
|
||||
class="mt-1 truncate text-lg font-semibold"
|
||||
>
|
||||
{{ readme.title }}
|
||||
</h2>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
(click)="closePluginConsentReadme()"
|
||||
class="grid h-8 w-8 shrink-0 place-items-center rounded-lg border border-border text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground"
|
||||
title="Close readme"
|
||||
>
|
||||
X
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div
|
||||
class="min-h-0 overflow-auto p-4 text-sm leading-6 [&_a]:text-primary [&_blockquote]:border-l-2 [&_blockquote]:border-border [&_blockquote]:pl-3 [&_code]:rounded [&_code]:bg-background [&_code]:px-1 [&_h1]:mb-2 [&_h1]:text-xl [&_h1]:font-semibold [&_h2]:mb-2 [&_h2]:mt-4 [&_h2]:text-lg [&_h2]:font-semibold [&_h3]:mb-1 [&_h3]:mt-3 [&_h3]:font-semibold [&_li]:ml-5 [&_ol]:list-decimal [&_p]:mb-3 [&_pre]:mb-3 [&_pre]:overflow-auto [&_pre]:rounded-lg [&_pre]:bg-background [&_pre]:p-3 [&_ul]:list-disc"
|
||||
>
|
||||
<app-chat-message-markdown [content]="readme.markdown" />
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
|
||||
<!-- Create Server Dialog -->
|
||||
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
} from 'rxjs';
|
||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
import {
|
||||
lucideExternalLink,
|
||||
lucideFileText,
|
||||
lucideSearch,
|
||||
lucideUsers,
|
||||
lucideLock,
|
||||
@@ -39,6 +41,7 @@ import {
|
||||
User,
|
||||
type PluginRequirementSummary
|
||||
} from '../../../../shared-kernel';
|
||||
import { ExternalLinkService } from '../../../../core/platform';
|
||||
import { SettingsModalService } from '../../../../core/services/settings-modal.service';
|
||||
import { DatabaseService } from '../../../../infrastructure/persistence';
|
||||
import { type ServerInfo } from '../../domain/models/server-directory.model';
|
||||
@@ -49,10 +52,15 @@ import {
|
||||
LeaveServerDialogComponent,
|
||||
type LeaveServerDialogResult
|
||||
} from '../../../../shared';
|
||||
import { ChatMessageMarkdownComponent } from '../../../chat';
|
||||
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';
|
||||
import {
|
||||
PluginRequirementService,
|
||||
PluginStoreService,
|
||||
type PluginStoreReadme
|
||||
} from '../../../plugins';
|
||||
|
||||
interface JoinPluginConsentDialog {
|
||||
optional: PluginRequirementSummary[];
|
||||
@@ -68,12 +76,15 @@ interface JoinPluginConsentDialog {
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
NgIcon,
|
||||
ChatMessageMarkdownComponent,
|
||||
ConfirmDialogComponent,
|
||||
LeaveServerDialogComponent,
|
||||
UserSearchListComponent
|
||||
],
|
||||
viewProviders: [
|
||||
provideIcons({
|
||||
lucideExternalLink,
|
||||
lucideFileText,
|
||||
lucideSearch,
|
||||
lucideUsers,
|
||||
lucideLock,
|
||||
@@ -94,6 +105,7 @@ export class ServerSearchComponent implements OnInit {
|
||||
private router = inject(Router);
|
||||
private settingsModal = inject(SettingsModalService);
|
||||
private db = inject(DatabaseService);
|
||||
private externalLinks = inject(ExternalLinkService);
|
||||
private serverDirectory = inject(ServerDirectoryFacade);
|
||||
private webrtc = inject(RealtimeSessionFacade);
|
||||
private pluginRequirements = inject(PluginRequirementService);
|
||||
@@ -122,6 +134,9 @@ export class ServerSearchComponent implements OnInit {
|
||||
selectedOptionalPluginIds = signal<Set<string>>(new Set());
|
||||
pluginConsentBusy = signal(false);
|
||||
pluginConsentError = signal<string | null>(null);
|
||||
pluginConsentReadme = signal<PluginStoreReadme | null>(null);
|
||||
pluginConsentReadmeLoadingId = signal<string | null>(null);
|
||||
pluginConsentReadmeError = signal<string | null>(null);
|
||||
|
||||
// Create dialog state
|
||||
showCreateDialog = signal(false);
|
||||
@@ -306,6 +321,7 @@ export class ServerSearchComponent implements OnInit {
|
||||
this.pluginConsentDialog.set(null);
|
||||
this.selectedOptionalPluginIds.set(new Set());
|
||||
this.pluginConsentError.set(null);
|
||||
this.closePluginConsentReadme();
|
||||
}
|
||||
|
||||
toggleOptionalPluginInstall(pluginId: string, checked: boolean): void {
|
||||
@@ -345,6 +361,7 @@ export class ServerSearchComponent implements OnInit {
|
||||
|
||||
this.pluginConsentDialog.set(null);
|
||||
this.selectedOptionalPluginIds.set(new Set());
|
||||
this.closePluginConsentReadme();
|
||||
} catch (error) {
|
||||
this.pluginConsentError.set(error instanceof Error ? error.message : 'Unable to install server plugins');
|
||||
} finally {
|
||||
@@ -352,6 +369,45 @@ export class ServerSearchComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
async openPluginConsentReadme(requirement: PluginRequirementSummary): Promise<void> {
|
||||
this.pluginConsentReadmeError.set(null);
|
||||
this.pluginConsentReadmeLoadingId.set(requirement.pluginId);
|
||||
|
||||
try {
|
||||
const readme = await this.pluginStore.loadRequirementReadme(requirement);
|
||||
|
||||
this.pluginConsentReadme.set(readme);
|
||||
} catch (error) {
|
||||
this.pluginConsentReadmeError.set(error instanceof Error ? error.message : 'Unable to load plugin readme');
|
||||
} finally {
|
||||
this.pluginConsentReadmeLoadingId.set(null);
|
||||
}
|
||||
}
|
||||
|
||||
closePluginConsentReadme(): void {
|
||||
this.pluginConsentReadme.set(null);
|
||||
this.pluginConsentReadmeError.set(null);
|
||||
this.pluginConsentReadmeLoadingId.set(null);
|
||||
}
|
||||
|
||||
openPluginSource(requirement: PluginRequirementSummary): void {
|
||||
const sourceUrl = this.getPluginSourceUrl(requirement);
|
||||
|
||||
if (sourceUrl) {
|
||||
this.externalLinks.open(sourceUrl);
|
||||
}
|
||||
}
|
||||
|
||||
getPluginSourceUrl(requirement: PluginRequirementSummary): string | null {
|
||||
const candidate = requirement.manifest?.homepage ?? requirement.sourceUrl ?? requirement.installUrl ?? requirement.manifest?.bugs ?? null;
|
||||
|
||||
return candidate?.startsWith('http://') || candidate?.startsWith('https://') ? candidate : null;
|
||||
}
|
||||
|
||||
hasPluginReadme(requirement: PluginRequirementSummary): boolean {
|
||||
return !!requirement.manifest?.readme;
|
||||
}
|
||||
|
||||
async confirmPasswordJoin(): Promise<void> {
|
||||
const server = this.passwordPromptServer();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user