Compare commits
1 Commits
54e8b9a5e4
...
v1.0.159
| Author | SHA1 | Date | |
|---|---|---|---|
| 232a9ea8ea |
@@ -267,12 +267,23 @@ export class App implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
if (!currentUserId) {
|
if (!currentUserId) {
|
||||||
if (!this.isPublicRoute(currentUrl)) {
|
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'], {
|
this.router.navigate(['/login'], {
|
||||||
queryParams: {
|
queryParams: {
|
||||||
returnUrl: currentUrl
|
returnUrl: currentUrl
|
||||||
}
|
}
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.store.dispatch(UsersActions.loadCurrentUser());
|
this.store.dispatch(UsersActions.loadCurrentUser());
|
||||||
this.store.dispatch(RoomsActions.loadRooms());
|
this.store.dispatch(RoomsActions.loadRooms());
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ export class DirectCallService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.openCallView(callId);
|
await this.router.navigate(['/call', callId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async openMobileCallOverlay(callId: string): Promise<void> {
|
async openMobileCallOverlay(callId: string): Promise<void> {
|
||||||
|
|||||||
@@ -1107,7 +1107,7 @@ function parsePluginEntry(sourceUrl: string, sourceTitle: string, value: unknown
|
|||||||
githubUrl: resolveOptionalUrl(sourceUrl, readGithubUrl(value)),
|
githubUrl: resolveOptionalUrl(sourceUrl, readGithubUrl(value)),
|
||||||
homepageUrl: resolveOptionalUrl(sourceUrl, readString(value, 'homepage', 'homepageUrl', 'website')),
|
homepageUrl: resolveOptionalUrl(sourceUrl, readString(value, 'homepage', 'homepageUrl', 'website')),
|
||||||
id,
|
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')),
|
installUrl: resolveOptionalUrl(sourceUrl, readString(value, 'install', 'installUrl', 'manifest', 'manifestUrl')),
|
||||||
readmeUrl: resolveOptionalUrl(sourceUrl, readString(value, 'readme', 'readmeUrl')),
|
readmeUrl: resolveOptionalUrl(sourceUrl, readString(value, 'readme', 'readmeUrl')),
|
||||||
scope: readPluginInstallScope(value),
|
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 {
|
function resolveOptionalUrl(sourceUrl: string, rawUrl?: string): string | undefined {
|
||||||
if (!rawUrl) {
|
if (!rawUrl) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
@@ -257,11 +257,13 @@
|
|||||||
@for (plugin of filteredPlugins(); track trackPlugin($index, plugin)) {
|
@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)]">
|
<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">
|
<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
|
<img
|
||||||
[src]="plugin.imageUrl"
|
[src]="plugin.imageUrl"
|
||||||
[alt]="plugin.title"
|
[alt]="plugin.title"
|
||||||
(error)="hideBrokenImage($event)"
|
(error)="hideBrokenImage($event, plugin)"
|
||||||
|
loading="lazy"
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
class="h-full w-full object-cover"
|
class="h-full w-full object-cover"
|
||||||
/>
|
/>
|
||||||
} @else {
|
} @else {
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ export class PluginStoreComponent implements OnInit {
|
|||||||
readonly serverInstallOptional = signal(false);
|
readonly serverInstallOptional = signal(false);
|
||||||
readonly serverInstallError = signal<string | null>(null);
|
readonly serverInstallError = signal<string | null>(null);
|
||||||
readonly serverInstallBusy = signal(false);
|
readonly serverInstallBusy = signal(false);
|
||||||
|
readonly brokenImageKeys = signal<Set<string>>(new Set());
|
||||||
|
|
||||||
private destroyed = false;
|
private destroyed = false;
|
||||||
private readonly destroyRef = inject(DestroyRef);
|
private readonly destroyRef = inject(DestroyRef);
|
||||||
@@ -530,12 +531,26 @@ export class PluginStoreComponent implements OnInit {
|
|||||||
return `${plugin.sourceUrl}:${plugin.id}`;
|
return `${plugin.sourceUrl}:${plugin.id}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
hideBrokenImage(event: Event): void {
|
hideBrokenImage(event: Event, plugin: PluginStoreEntry): void {
|
||||||
const image = event.target as HTMLImageElement | null;
|
const image = event.target as HTMLImageElement | null;
|
||||||
|
|
||||||
if (image) {
|
if (image) {
|
||||||
image.hidden = true;
|
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 {
|
trackServer(index: number, server: Room): string {
|
||||||
|
|||||||
@@ -22,6 +22,20 @@
|
|||||||
|
|
||||||
<h1 class="min-w-0 flex-1 truncate text-center text-base font-semibold text-foreground">Search</h1>
|
<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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="Settings"
|
aria-label="Settings"
|
||||||
@@ -33,6 +47,7 @@
|
|||||||
class="h-5 w-5"
|
class="h-5 w-5"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-row items-center gap-2">
|
<div class="flex flex-row items-center gap-2">
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ import {
|
|||||||
lucideGlobe,
|
lucideGlobe,
|
||||||
lucidePlus,
|
lucidePlus,
|
||||||
lucideSettings,
|
lucideSettings,
|
||||||
lucideChevronDown
|
lucideChevronDown,
|
||||||
|
lucideLogIn
|
||||||
} from '@ng-icons/lucide';
|
} from '@ng-icons/lucide';
|
||||||
|
|
||||||
import { RoomsActions } from '../../../../store/rooms/rooms.actions';
|
import { RoomsActions } from '../../../../store/rooms/rooms.actions';
|
||||||
@@ -94,7 +95,8 @@ interface JoinPluginConsentDialog {
|
|||||||
lucideGlobe,
|
lucideGlobe,
|
||||||
lucidePlus,
|
lucidePlus,
|
||||||
lucideSettings,
|
lucideSettings,
|
||||||
lucideChevronDown
|
lucideChevronDown,
|
||||||
|
lucideLogIn
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
templateUrl: './server-search.component.html'
|
templateUrl: './server-search.component.html'
|
||||||
@@ -246,6 +248,11 @@ export class ServerSearchComponent implements OnInit {
|
|||||||
this.settingsModal.open('network');
|
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).
|
* 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
|
* 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();
|
this.untrackLocalMic();
|
||||||
|
|
||||||
if (!this.overlayMode()) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
void this.router.navigate(['/pm', session.conversationId]);
|
void this.router.navigate(['/dm', session.conversationId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMobileCallSlideChange(event: Event): void {
|
onMobileCallSlideChange(event: Event): void {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
/>
|
/>
|
||||||
<meta
|
<meta
|
||||||
http-equiv="Content-Security-Policy"
|
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
|
<link
|
||||||
rel="icon"
|
rel="icon"
|
||||||
|
|||||||
Reference in New Issue
Block a user