Compare commits
1 Commits
54e8b9a5e4
...
v1.0.159
| Author | SHA1 | Date | |
|---|---|---|---|
| 232a9ea8ea |
@@ -267,11 +267,22 @@ export class App implements OnInit, OnDestroy {
|
||||
|
||||
if (!currentUserId) {
|
||||
if (!this.isPublicRoute(currentUrl)) {
|
||||
this.router.navigate(['/login'], {
|
||||
queryParams: {
|
||||
returnUrl: currentUrl
|
||||
}
|
||||
}).catch(() => {});
|
||||
// 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());
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user