diff --git a/toju-app/src/app/app.ts b/toju-app/src/app/app.ts index 6403d6c..1de568f 100644 --- a/toju-app/src/app/app.ts +++ b/toju-app/src/app/app.ts @@ -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()); diff --git a/toju-app/src/app/domains/direct-call/application/services/direct-call.service.ts b/toju-app/src/app/domains/direct-call/application/services/direct-call.service.ts index da2e3f0..16fbde8 100644 --- a/toju-app/src/app/domains/direct-call/application/services/direct-call.service.ts +++ b/toju-app/src/app/domains/direct-call/application/services/direct-call.service.ts @@ -210,7 +210,7 @@ export class DirectCallService { return; } - await this.openCallView(callId); + await this.router.navigate(['/call', callId]); } async openMobileCallOverlay(callId: string): Promise { diff --git a/toju-app/src/app/domains/plugins/application/services/plugin-store.service.ts b/toju-app/src/app/domains/plugins/application/services/plugin-store.service.ts index 1ce26be..9df9891 100644 --- a/toju-app/src/app/domains/plugins/application/services/plugin-store.service.ts +++ b/toju-app/src/app/domains/plugins/application/services/plugin-store.service.ts @@ -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///blob/...`) or + * the raw redirector (`github.com///raw/...`). Both forms must be + * mapped to `raw.githubusercontent.com` for `` 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; diff --git a/toju-app/src/app/domains/plugins/feature/plugin-store/plugin-store.component.html b/toju-app/src/app/domains/plugins/feature/plugin-store/plugin-store.component.html index 3da3da2..26debe4 100644 --- a/toju-app/src/app/domains/plugins/feature/plugin-store/plugin-store.component.html +++ b/toju-app/src/app/domains/plugins/feature/plugin-store/plugin-store.component.html @@ -257,11 +257,13 @@ @for (plugin of filteredPlugins(); track trackPlugin($index, plugin)) {
- @if (plugin.imageUrl) { + @if (plugin.imageUrl && !hasBrokenImage(plugin)) { } @else { diff --git a/toju-app/src/app/domains/plugins/feature/plugin-store/plugin-store.component.ts b/toju-app/src/app/domains/plugins/feature/plugin-store/plugin-store.component.ts index afdf246..ce20593 100644 --- a/toju-app/src/app/domains/plugins/feature/plugin-store/plugin-store.component.ts +++ b/toju-app/src/app/domains/plugins/feature/plugin-store/plugin-store.component.ts @@ -155,6 +155,7 @@ export class PluginStoreComponent implements OnInit { readonly serverInstallOptional = signal(false); readonly serverInstallError = signal(null); readonly serverInstallBusy = signal(false); + readonly brokenImageKeys = signal>(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 { diff --git a/toju-app/src/app/domains/server-directory/feature/server-search/server-search.component.html b/toju-app/src/app/domains/server-directory/feature/server-search/server-search.component.html index a033599..5ac3792 100644 --- a/toju-app/src/app/domains/server-directory/feature/server-search/server-search.component.html +++ b/toju-app/src/app/domains/server-directory/feature/server-search/server-search.component.html @@ -22,17 +22,32 @@

Search

- + @if (!currentUser()) { + + } @else { + + }
diff --git a/toju-app/src/app/domains/server-directory/feature/server-search/server-search.component.ts b/toju-app/src/app/domains/server-directory/feature/server-search/server-search.component.ts index 5b74d3f..5b6ee27 100644 --- a/toju-app/src/app/domains/server-directory/feature/server-search/server-search.component.ts +++ b/toju-app/src/app/domains/server-directory/feature/server-search/server-search.component.ts @@ -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 diff --git a/toju-app/src/app/features/direct-call/private-call.component.ts b/toju-app/src/app/features/direct-call/private-call.component.ts index 0831859..0494f37 100644 --- a/toju-app/src/app/features/direct-call/private-call.component.ts +++ b/toju-app/src/app/features/direct-call/private-call.component.ts @@ -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 { diff --git a/toju-app/src/index.html b/toju-app/src/index.html index 2cdb25b..dc54d25 100644 --- a/toju-app/src/index.html +++ b/toju-app/src/index.html @@ -10,7 +10,7 @@ />