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

This commit is contained in:
2026-05-19 00:52:28 +02:00
parent 54e8b9a5e4
commit 232a9ea8ea
9 changed files with 114 additions and 26 deletions

View File

@@ -267,12 +267,23 @@ export class App implements OnInit, OnDestroy {
if (!currentUserId) {
if (!this.isPublicRoute(currentUrl)) {
// On mobile, new/unauthenticated visitors landing on the app root or
// /search should stay on /search (which already exposes a login CTA).
// The login form has no mobile chrome / back button, so dropping new
// users straight onto it leaves them with no way to navigate away.
const currentPath = this.getRoutePath(currentUrl);
const isSearchLanding = currentPath === '/' || currentPath === '/search';
if (this.isMobile() && isSearchLanding) {
this.router.navigate(['/search'], { replaceUrl: true }).catch(() => {});
} else {
this.router.navigate(['/login'], {
queryParams: {
returnUrl: currentUrl
}
}).catch(() => {});
}
}
} else {
this.store.dispatch(UsersActions.loadCurrentUser());
this.store.dispatch(RoomsActions.loadRooms());

View File

@@ -210,7 +210,7 @@ export class DirectCallService {
return;
}
await this.openCallView(callId);
await this.router.navigate(['/call', callId]);
}
async openMobileCallOverlay(callId: string): Promise<void> {

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -22,6 +22,20 @@
<h1 class="min-w-0 flex-1 truncate text-center text-base font-semibold text-foreground">Search</h1>
@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"
@@ -33,6 +47,7 @@
class="h-5 w-5"
/>
</button>
}
</div>
<div class="flex flex-row items-center gap-2">

View File

@@ -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

View File

@@ -309,7 +309,7 @@ export class PrivateCallComponent {
this.untrackLocalMic();
if (!this.overlayMode()) {
void this.router.navigate(['/pm', session.conversationId]);
void this.router.navigate(['/dm', session.conversationId]);
}
}
@@ -325,7 +325,7 @@ export class PrivateCallComponent {
return;
}
void this.router.navigate(['/pm', session.conversationId]);
void this.router.navigate(['/dm', session.conversationId]);
}
onMobileCallSlideChange(event: Event): void {

View File

@@ -10,7 +10,7 @@
/>
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob:; script-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob: http: https:; script-src-elem 'self' 'unsafe-inline' 'unsafe-eval' data: blob: http: https:; connect-src 'self' blob: ws: wss: http: https:; media-src 'self' blob: file:; img-src 'self' data: blob: http: https:; frame-src https://www.youtube-nocookie.com https://open.spotify.com https://w.soundcloud.com;"
content="default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob:; script-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob: http: https:; script-src-elem 'self' 'unsafe-inline' 'unsafe-eval' data: blob: http: https:; connect-src 'self' blob: ws: wss: http: https:; media-src 'self' blob: file:; img-src 'self' data: blob: file: http: https:; frame-src https://www.youtube-nocookie.com https://open.spotify.com https://w.soundcloud.com;"
/>
<link
rel="icon"