perf: Optimizing the image loading
Does no longer load all klipy images through image proxy from signal server. Improves loading performance.
This commit is contained in:
@@ -109,7 +109,7 @@ sequenceDiagram
|
|||||||
|
|
||||||
## GIF integration
|
## GIF integration
|
||||||
|
|
||||||
`KlipyService` checks availability on the active server, then proxies search requests through the server API. Images are rendered via an image proxy endpoint to avoid mixed-content issues.
|
`KlipyService` checks availability on the active server, then proxies search requests through the server API. Rendered remote images now attempt a direct load first and only fall back to the image proxy after the browser reports a load failure, which is the practical approximation of a CORS or mixed-content fallback path in the renderer.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
graph LR
|
graph LR
|
||||||
|
|||||||
@@ -135,6 +135,10 @@ export class KlipyService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildRenderableImageUrl(url: string): string {
|
buildRenderableImageUrl(url: string): string {
|
||||||
|
return this.normalizeMediaUrl(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
buildImageProxyUrl(url: string): string {
|
||||||
const trimmed = this.normalizeMediaUrl(url);
|
const trimmed = this.normalizeMediaUrl(url);
|
||||||
|
|
||||||
if (!trimmed)
|
if (!trimmed)
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import {
|
||||||
|
Directive,
|
||||||
|
HostBinding,
|
||||||
|
HostListener,
|
||||||
|
effect,
|
||||||
|
inject,
|
||||||
|
input,
|
||||||
|
signal
|
||||||
|
} from '@angular/core';
|
||||||
|
import { KlipyService } from '../application/klipy.service';
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: 'img[appChatImageProxyFallback]',
|
||||||
|
standalone: true
|
||||||
|
})
|
||||||
|
export class ChatImageProxyFallbackDirective {
|
||||||
|
readonly sourceUrl = input('', { alias: 'appChatImageProxyFallback' });
|
||||||
|
|
||||||
|
private readonly klipy = inject(KlipyService);
|
||||||
|
private readonly renderedSource = signal('');
|
||||||
|
private hasAppliedProxyFallback = false;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
effect(() => {
|
||||||
|
this.hasAppliedProxyFallback = false;
|
||||||
|
this.renderedSource.set(this.klipy.buildRenderableImageUrl(this.sourceUrl()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostBinding('src')
|
||||||
|
get src(): string {
|
||||||
|
return this.renderedSource();
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener('error')
|
||||||
|
handleError(): void {
|
||||||
|
if (this.hasAppliedProxyFallback) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const proxyUrl = this.klipy.buildImageProxyUrl(this.sourceUrl());
|
||||||
|
|
||||||
|
if (!proxyUrl || proxyUrl === this.renderedSource()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hasAppliedProxyFallback = true;
|
||||||
|
this.renderedSource.set(proxyUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -206,7 +206,7 @@
|
|||||||
<div class="group flex max-w-sm items-center gap-3 rounded-xl border border-border bg-secondary/60 px-2.5 py-2">
|
<div class="group flex max-w-sm items-center gap-3 rounded-xl border border-border bg-secondary/60 px-2.5 py-2">
|
||||||
<div class="relative h-12 w-12 overflow-hidden rounded-lg bg-secondary/80">
|
<div class="relative h-12 w-12 overflow-hidden rounded-lg bg-secondary/80">
|
||||||
<img
|
<img
|
||||||
[src]="getPendingKlipyGifPreviewUrl()"
|
[appChatImageProxyFallback]="pendingKlipyGif()!.previewUrl || pendingKlipyGif()!.url"
|
||||||
[alt]="pendingKlipyGif()!.title || 'KLIPY GIF'"
|
[alt]="pendingKlipyGif()!.title || 'KLIPY GIF'"
|
||||||
class="h-full w-full object-cover"
|
class="h-full w-full object-cover"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import type { ClipboardFilePayload } from '../../../../../../core/platform/elect
|
|||||||
import { ElectronBridgeService } from '../../../../../../core/platform/electron/electron-bridge.service';
|
import { ElectronBridgeService } from '../../../../../../core/platform/electron/electron-bridge.service';
|
||||||
import { KlipyGif, KlipyService } from '../../../../application/klipy.service';
|
import { KlipyGif, KlipyService } from '../../../../application/klipy.service';
|
||||||
import { Message } from '../../../../../../shared-kernel';
|
import { Message } from '../../../../../../shared-kernel';
|
||||||
|
import { ChatImageProxyFallbackDirective } from '../../../chat-image-proxy-fallback.directive';
|
||||||
import { TypingIndicatorComponent } from '../../../typing-indicator/typing-indicator.component';
|
import { TypingIndicatorComponent } from '../../../typing-indicator/typing-indicator.component';
|
||||||
import { ChatMarkdownService } from '../../services/chat-markdown.service';
|
import { ChatMarkdownService } from '../../services/chat-markdown.service';
|
||||||
import { ChatMessageComposerSubmitEvent } from '../../models/chat-messages.models';
|
import { ChatMessageComposerSubmitEvent } from '../../models/chat-messages.models';
|
||||||
@@ -40,6 +41,7 @@ const DEFAULT_TEXTAREA_HEIGHT = 62;
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
NgIcon,
|
NgIcon,
|
||||||
|
ChatImageProxyFallbackDirective,
|
||||||
TypingIndicatorComponent
|
TypingIndicatorComponent
|
||||||
],
|
],
|
||||||
viewProviders: [
|
viewProviders: [
|
||||||
@@ -231,12 +233,6 @@ export class ChatMessageComposerComponent implements AfterViewInit, OnDestroy {
|
|||||||
requestAnimationFrame(() => this.messageInputRef?.nativeElement.focus());
|
requestAnimationFrame(() => this.messageInputRef?.nativeElement.focus());
|
||||||
}
|
}
|
||||||
|
|
||||||
getPendingKlipyGifPreviewUrl(): string {
|
|
||||||
const gif = this.pendingKlipyGif();
|
|
||||||
|
|
||||||
return gif ? this.klipy.buildRenderableImageUrl(gif.previewUrl || gif.url) : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
formatBytes(bytes: number): string {
|
formatBytes(bytes: number): string {
|
||||||
const units = [
|
const units = [
|
||||||
'B',
|
'B',
|
||||||
|
|||||||
@@ -98,7 +98,7 @@
|
|||||||
>
|
>
|
||||||
<div class="relative mt-2 inline-block overflow-hidden rounded-md border border-border/60 bg-secondary/20">
|
<div class="relative mt-2 inline-block overflow-hidden rounded-md border border-border/60 bg-secondary/20">
|
||||||
<img
|
<img
|
||||||
[src]="getMarkdownImageSource(node.url)"
|
[appChatImageProxyFallback]="node.url"
|
||||||
[alt]="node.alt || 'Shared image'"
|
[alt]="node.alt || 'Shared image'"
|
||||||
class="block max-h-80 max-w-full w-auto"
|
class="block max-h-80 max-w-full w-auto"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import {
|
|||||||
ChatVideoPlayerComponent,
|
ChatVideoPlayerComponent,
|
||||||
UserAvatarComponent
|
UserAvatarComponent
|
||||||
} from '../../../../../../shared';
|
} from '../../../../../../shared';
|
||||||
|
import { ChatImageProxyFallbackDirective } from '../../../chat-image-proxy-fallback.directive';
|
||||||
import {
|
import {
|
||||||
ChatMessageDeleteEvent,
|
ChatMessageDeleteEvent,
|
||||||
ChatMessageEditEvent,
|
ChatMessageEditEvent,
|
||||||
@@ -102,6 +103,7 @@ interface ChatMessageAttachmentViewModel extends Attachment {
|
|||||||
ChatVideoPlayerComponent,
|
ChatVideoPlayerComponent,
|
||||||
RemarkModule,
|
RemarkModule,
|
||||||
MermaidComponent,
|
MermaidComponent,
|
||||||
|
ChatImageProxyFallbackDirective,
|
||||||
UserAvatarComponent
|
UserAvatarComponent
|
||||||
],
|
],
|
||||||
viewProviders: [
|
viewProviders: [
|
||||||
@@ -318,10 +320,6 @@ export class ChatMessageItemComponent {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getMarkdownImageSource(url?: string): string {
|
|
||||||
return url ? this.klipy.buildRenderableImageUrl(url) : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
getMermaidCode(code?: string): string {
|
getMermaidCode(code?: string): string {
|
||||||
return (code ?? '').replace(MERMAID_LINE_BREAK_PATTERN, '\n').trim();
|
return (code ?? '').replace(MERMAID_LINE_BREAK_PATTERN, '\n').trim();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,7 +93,7 @@
|
|||||||
[style.aspect-ratio]="gifAspectRatio(gif)"
|
[style.aspect-ratio]="gifAspectRatio(gif)"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
[src]="gifPreviewUrl(gif)"
|
[appChatImageProxyFallback]="gif.previewUrl || gif.url"
|
||||||
[alt]="gif.title || 'KLIPY GIF'"
|
[alt]="gif.title || 'KLIPY GIF'"
|
||||||
class="h-full w-full object-cover transition-transform duration-200 group-hover:scale-[1.03]"
|
class="h-full w-full object-cover transition-transform duration-200 group-hover:scale-[1.03]"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
lucideX
|
lucideX
|
||||||
} from '@ng-icons/lucide';
|
} from '@ng-icons/lucide';
|
||||||
import { KlipyGif, KlipyService } from '../../application/klipy.service';
|
import { KlipyGif, KlipyService } from '../../application/klipy.service';
|
||||||
|
import { ChatImageProxyFallbackDirective } from '../chat-image-proxy-fallback.directive';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-klipy-gif-picker',
|
selector: 'app-klipy-gif-picker',
|
||||||
@@ -28,7 +29,8 @@ import { KlipyGif, KlipyService } from '../../application/klipy.service';
|
|||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
NgIcon
|
NgIcon,
|
||||||
|
ChatImageProxyFallbackDirective
|
||||||
],
|
],
|
||||||
viewProviders: [
|
viewProviders: [
|
||||||
provideIcons({
|
provideIcons({
|
||||||
@@ -112,10 +114,6 @@ export class KlipyGifPickerComponent implements OnInit, AfterViewInit, OnDestroy
|
|||||||
return '1 / 1';
|
return '1 / 1';
|
||||||
}
|
}
|
||||||
|
|
||||||
gifPreviewUrl(gif: KlipyGif): string {
|
|
||||||
return this.klipy.buildRenderableImageUrl(gif.previewUrl || gif.url);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async loadResults(reset: boolean): Promise<void> {
|
private async loadResults(reset: boolean): Promise<void> {
|
||||||
if (reset) {
|
if (reset) {
|
||||||
this.currentPage = 1;
|
this.currentPage = 1;
|
||||||
|
|||||||
Reference in New Issue
Block a user