test: Ensure tests work after latest changes
All checks were successful
Queue Release Build / prepare (push) Successful in 17s
Deploy Web Apps / deploy (push) Successful in 7m20s
Queue Release Build / build-windows (push) Successful in 25m4s
Queue Release Build / build-linux (push) Successful in 33m59s
Queue Release Build / finalize (push) Successful in 41s
All checks were successful
Queue Release Build / prepare (push) Successful in 17s
Deploy Web Apps / deploy (push) Successful in 7m20s
Queue Release Build / build-windows (push) Successful in 25m4s
Queue Release Build / build-linux (push) Successful in 33m59s
Queue Release Build / finalize (push) Successful in 41s
This commit is contained in:
@@ -210,7 +210,7 @@ export class DirectCallService {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.openCallView(callId);
|
||||
await this.router.navigate(['/call', callId]);
|
||||
}
|
||||
|
||||
async openMobileCallOverlay(callId: string): Promise<void> {
|
||||
|
||||
@@ -1107,7 +1107,7 @@ function parsePluginEntry(sourceUrl: string, sourceTitle: string, value: unknown
|
||||
githubUrl: resolveOptionalUrl(sourceUrl, readGithubUrl(value)),
|
||||
homepageUrl: resolveOptionalUrl(sourceUrl, readString(value, 'homepage', 'homepageUrl', 'website')),
|
||||
id,
|
||||
imageUrl: resolveOptionalUrl(sourceUrl, readString(value, 'image', 'imageUrl', 'icon', 'iconUrl', 'banner')),
|
||||
imageUrl: normalizeImageUrl(resolveOptionalUrl(sourceUrl, readString(value, 'image', 'imageUrl', 'icon', 'iconUrl', 'banner'))),
|
||||
installUrl: resolveOptionalUrl(sourceUrl, readString(value, 'install', 'installUrl', 'manifest', 'manifestUrl')),
|
||||
readmeUrl: resolveOptionalUrl(sourceUrl, readString(value, 'readme', 'readmeUrl')),
|
||||
scope: readPluginInstallScope(value),
|
||||
@@ -1300,6 +1300,44 @@ function normalizeOptionalSourceUrl(rawUrl: string): string | undefined {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrites human-friendly GitHub URLs so the browser can load the underlying
|
||||
* binary asset. Users typically paste links copied from the GitHub web UI which
|
||||
* point at the rendered HTML preview (`github.com/<owner>/<repo>/blob/...`) or
|
||||
* the raw redirector (`github.com/<owner>/<repo>/raw/...`). Both forms must be
|
||||
* mapped to `raw.githubusercontent.com` for `<img>` tags to work.
|
||||
*/
|
||||
function normalizeImageUrl(rawUrl: string | undefined): string | undefined {
|
||||
if (!rawUrl) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let url: URL;
|
||||
|
||||
try {
|
||||
url = new URL(rawUrl);
|
||||
} catch {
|
||||
return rawUrl;
|
||||
}
|
||||
|
||||
if (url.hostname !== 'github.com' && url.hostname !== 'www.github.com') {
|
||||
return rawUrl;
|
||||
}
|
||||
|
||||
const segments = url.pathname.split('/').filter(Boolean);
|
||||
const kindIndex = segments.findIndex((segment) => segment === 'blob' || segment === 'raw');
|
||||
|
||||
if (kindIndex < 2 || kindIndex >= segments.length - 1) {
|
||||
return rawUrl;
|
||||
}
|
||||
|
||||
const owner = segments[0];
|
||||
const repo = segments[1];
|
||||
const ref = segments.slice(kindIndex + 1).join('/');
|
||||
|
||||
return `https://raw.githubusercontent.com/${owner}/${repo}/${ref}${url.search}`;
|
||||
}
|
||||
|
||||
function resolveOptionalUrl(sourceUrl: string, rawUrl?: string): string | undefined {
|
||||
if (!rawUrl) {
|
||||
return undefined;
|
||||
|
||||
@@ -257,11 +257,13 @@
|
||||
@for (plugin of filteredPlugins(); track trackPlugin($index, plugin)) {
|
||||
<article class="grid min-w-0 overflow-hidden rounded-lg border border-border bg-background sm:grid-cols-[5.5rem_minmax(0,1fr)]">
|
||||
<div class="grid min-h-24 place-items-center bg-secondary text-muted-foreground sm:min-h-full">
|
||||
@if (plugin.imageUrl) {
|
||||
@if (plugin.imageUrl && !hasBrokenImage(plugin)) {
|
||||
<img
|
||||
[src]="plugin.imageUrl"
|
||||
[alt]="plugin.title"
|
||||
(error)="hideBrokenImage($event)"
|
||||
(error)="hideBrokenImage($event, plugin)"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
class="h-full w-full object-cover"
|
||||
/>
|
||||
} @else {
|
||||
|
||||
@@ -155,6 +155,7 @@ export class PluginStoreComponent implements OnInit {
|
||||
readonly serverInstallOptional = signal(false);
|
||||
readonly serverInstallError = signal<string | null>(null);
|
||||
readonly serverInstallBusy = signal(false);
|
||||
readonly brokenImageKeys = signal<Set<string>>(new Set());
|
||||
|
||||
private destroyed = false;
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
@@ -530,12 +531,26 @@ export class PluginStoreComponent implements OnInit {
|
||||
return `${plugin.sourceUrl}:${plugin.id}`;
|
||||
}
|
||||
|
||||
hideBrokenImage(event: Event): void {
|
||||
hideBrokenImage(event: Event, plugin: PluginStoreEntry): void {
|
||||
const image = event.target as HTMLImageElement | null;
|
||||
|
||||
if (image) {
|
||||
image.hidden = true;
|
||||
}
|
||||
|
||||
const key = this.imageKey(plugin);
|
||||
const next = new Set(this.brokenImageKeys());
|
||||
|
||||
next.add(key);
|
||||
this.brokenImageKeys.set(next);
|
||||
}
|
||||
|
||||
hasBrokenImage(plugin: PluginStoreEntry): boolean {
|
||||
return this.brokenImageKeys().has(this.imageKey(plugin));
|
||||
}
|
||||
|
||||
private imageKey(plugin: PluginStoreEntry): string {
|
||||
return `${plugin.sourceUrl}:${plugin.id}:${plugin.imageUrl ?? ''}`;
|
||||
}
|
||||
|
||||
trackServer(index: number, server: Room): string {
|
||||
|
||||
@@ -22,17 +22,32 @@
|
||||
|
||||
<h1 class="min-w-0 flex-1 truncate text-center text-base font-semibold text-foreground">Search</h1>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Settings"
|
||||
class="grid h-11 w-11 shrink-0 place-items-center rounded-lg border border-border bg-secondary text-muted-foreground transition-colors hover:bg-secondary/80"
|
||||
(click)="openSettings()"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideSettings"
|
||||
class="h-5 w-5"
|
||||
/>
|
||||
</button>
|
||||
@if (!currentUser()) {
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Log in"
|
||||
class="inline-flex h-11 shrink-0 items-center justify-center gap-1.5 rounded-lg bg-primary px-3 text-sm font-semibold text-primary-foreground transition-colors hover:bg-primary/90"
|
||||
(click)="goLogin()"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideLogIn"
|
||||
class="h-5 w-5"
|
||||
/>
|
||||
<span>Log in</span>
|
||||
</button>
|
||||
} @else {
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Settings"
|
||||
class="grid h-11 w-11 shrink-0 place-items-center rounded-lg border border-border bg-secondary text-muted-foreground transition-colors hover:bg-secondary/80"
|
||||
(click)="openSettings()"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideSettings"
|
||||
class="h-5 w-5"
|
||||
/>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
|
||||
@@ -27,7 +27,8 @@ import {
|
||||
lucideGlobe,
|
||||
lucidePlus,
|
||||
lucideSettings,
|
||||
lucideChevronDown
|
||||
lucideChevronDown,
|
||||
lucideLogIn
|
||||
} from '@ng-icons/lucide';
|
||||
|
||||
import { RoomsActions } from '../../../../store/rooms/rooms.actions';
|
||||
@@ -94,7 +95,8 @@ interface JoinPluginConsentDialog {
|
||||
lucideGlobe,
|
||||
lucidePlus,
|
||||
lucideSettings,
|
||||
lucideChevronDown
|
||||
lucideChevronDown,
|
||||
lucideLogIn
|
||||
})
|
||||
],
|
||||
templateUrl: './server-search.component.html'
|
||||
@@ -246,6 +248,11 @@ export class ServerSearchComponent implements OnInit {
|
||||
this.settingsModal.open('network');
|
||||
}
|
||||
|
||||
/** Navigate to the login screen, preserving the search route as the return URL. */
|
||||
goLogin(): void {
|
||||
this.router.navigate(['/login'], { queryParams: { returnUrl: '/search' } });
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate back from the Search page to the chat-room view (server rail + current server).
|
||||
* Prefers the current room; falls back to the first saved room. No-op when the user has not
|
||||
|
||||
Reference in New Issue
Block a user